BackEnd/JPA

[JPA] 스프링 데이터 JPA 분석

샤아이인 2022. 5. 6.

내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.

 

1. 스프링 데이터 JPA 구현체 분석

이번시간에는 Spring Data JPA가 어떻게 동작하는지 내부적인 구현체를 분석해보자!

 

SimpleJpaRepository가 핵심 구현체 이다!

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> ... {
    
    @Transactional
    public <S extends T> S save(S entity) {
        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        } 
    }
    ... 
}
  • @Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환
    • 영속성 계층에 있는 예외들은 JDBC, JPA 등등 각각 Exception이 다르다. 이를 Spring의 예외로 통일시켜주는 것 이다. 따라서 Controller 나 Service에 Exception이 전달될 때 JPA, JDBC의 Exception이 아닌! Spring의 Exception이 전달되게 된다.
    • 하부 기술을 JDBC 에서 JPA로 바꿔도 Exception을 처리하는 과정이 동일하다.

 

  • @Transactional 트랜잭션 적용
    • Spring Data JPA에서는 JPA의 모든 변경은 트랜잭션 안에서 동작한다.
    • 스프링 데이터 JPA는 변경(등록, 수정, 삭제) 메서드를 트랜잭션 처리한다.
    • 서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작
    • 서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용
    • 그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했음(사실은 트랜잭션이 리포지토리 계층에 걸려있는 것임)

 

  • @Transactional(readOnly = true)
    데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 flush를 생략해서 약간의 성능 향상을 얻을 수 있다! => 변경 감지 기능이 생략된다.

 

매우 중요!!! *save() 메서드*

- 새로운 엔티티면 저장( persist )

- 새로운 엔티티가 아니면 병합( merge ), 참고로 merge를 데이터 수정용으로 사용하면 안된다! merge는 비영속 상태의 Entity를 다시 영속화 시킬 때 사용한다.

 

2. 새로운 엔티티를 구별하는 방법

바로 위 글에서 새로운 Entity이면 persist를 하도록 save 메서드가 구현되어 있었다.

 

▶ 새로운 엔티티를 판단하는 기본 전략은 다음과 같다.

- 식별자가 객체(Integer, Long)일 때 null 로 판단

- 식별자가 자바 기본 타입(Primitive type: int, long...)일 때 0 으로 판단

- Persistable 인터페이스를 구현해서 판단 로직 변경 가능

public interface Persistable<ID> {
      ID getId();
      boolean isNew();
}

 

1. Item 엔티티 만들기

@Getter
@Entity
public class Item {

    @Id @GeneratedValue
    private Long id;
}

 

2. ItemRepository 만들기

public interface ItemRepository extends JpaRepository<Item, Long> {
}

 

3. Test 작성

@SpringBootTest
class ItemRepositoryTest {

    @Autowired ItemRepository itemRepository;

    @Test
    public void save_test() {
        // given
        Item item = new Item();
        itemRepository.save(item);
    }
}

테스트 에서는 새로운 Item을 생성한 후, save의 인자로 전달하고 있다.

 

전달받는 save의 구현체는 다음과 같다.

@Transactional
@Override
public <S extends T> S save(S entity) {

    Assert.notNull(entity, "Entity must not be null.");

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

이때 Item 에는 Long id 에 null값이 들어있는 상태이다. 아직 DB에 저장된적이 없기 때문에 당연하다.

 

디버깅을 통해 확인해 보자.

id가 null이기 때문에 새로운 객체로 판단하여 em.persist()에 전달된다.

persist가 된 이후에 entity의 Id값을 보면 다음과 같이 1로 할당된것을 확인할 수 있다.

 

문제는 사용자가 식별자 값을 직접 지정하기위해 @GenerateValue를 생략할때 발생한다.

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item {

    @Id
    private Long id;
    
    public Item(Long id) {
        this.id = id;
    }
}

 

JPA에서 식별자 생성시 @GenerateValue 없이 @Id 만 사용해서 직접 할당하는 방식이면, 이미 식별자 값이 있는 상태로 save() 를 호출한다. 따라서 이 경우 merge() 가 호출된다.

@Test
public void save_test() {
    // given
    Item item = new Item(7L); // 이미 id값이 있는 item
    itemRepository.save(item);
}

디버깅을 해보면 다음과 같다.

merge()가 호출되는것을 확인할 수 있다. 문제는 merge는 DB에 값이 있을것이라 가정하고 동작한다.

 

merge() 는 우선 DB를 호출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지하므로 매우 비효율 적이다.

즉, select 쿼리가 나간 이후에 다시 insert 쿼리가 나가게 된다. 쿼리가 2번 나가게 된다.

또한 merge는 Entity의 모든값을 교체하기 때문에 좋지 못하다.

 

따라서 이런경우 Persistable 를 사용해서 새로운 엔티티 확인 여부를 직접 구현하게는 효과적이다.

@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<Long> {

    @Id
    private Long id;

    @CreatedDate
    private LocalDateTime createdDate;

    public Item(Long id) {
        this.id = id;
    }

    @Override
    public Long getId() { // Persistable 로 인해 추가된 부분
        return null;
    }

    @Override
    public boolean isNew() { // Persistable 로 인해 추가된 부분
        return createdDate == null;
    }
}

참고로 위 코드처럼 등록시간( @CreatedDate )을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다.

(@CreatedDate에 값이 없으면, 즉 createdDate == null 이면 새로운 엔티티로 판단)

 

이후 테스트코드를 돌려보면 save에서 persist 되는것을 확인할 수 있다.

'BackEnd > JPA' 카테고리의 다른 글

[JPA] QueryDSL 기본문법 - 1  (0) 2022.05.13
[JPA] 나머지 기능들  (0) 2022.05.07
[JPA] 확장 기능  (0) 2022.05.06
[JPA] 쿼리 메소드 기능 - 4  (0) 2022.05.04
[JPA] 쿼리 메소드 기능 - 3  (0) 2022.05.04

댓글