강의를 들으며 생각 정리 + "자바 ORM 표준 JPA 프로그래밍" 책 참고
스프링 데이터 JPA 구현체 분석
스프링 데이터 JPA는 리포지토리 사용시 자동으로 구현체를 생성해준다. 어떤 구현체를 생성해 주는 것일까?
-> 스프링 데이터 JPA가 제공하는 공통 인터페이스의 구현체는 SimpleJpaRepository이다.
<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 예외를 스프링이 추상화한 예외로 변환한다.
-> 리포지토리 기술을 JPA에서 Jdbc Template 등의 다른 기술로 바꾸더라도 기존 비즈니스 로직에 영향이 최소화된다.
@Transactional 적용: JPA의 모든 변경은 트랜잭션 안에서 동작한다.
스프링 데이터 JPA는 CURD를 트랜잭션 처리한다. 그래서 스프링 데이터 JPA는 사용 계층에서 트랜잭션이 없어도 변경이 가능하다.
save() 메서드
대표적으로 save() 메서드를 표시했는데 로직은 다음과 같다.
1. 새로운 엔티티면 저장(persist)
2. 새로운 엔티티가 아니면 병합(merge)
새로운 엔티티를 구별하는 방법
앞서 스프링 데이터 JPA의 save() 메서드에 대해 알아보았다.
새로운 엔티티인지에 따라 persist나 merge를 호출하는 방식인데, 그렇다면 새로운 엔티티를 구별하는 방법은 무엇일까?
save() 메서드를 다시 확인해보자.
<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);
}
}
...
}
entityInformation.isNew(entity) 부분이 바로 새로운 엔티티를 판단하는 부분이다.
<새로운 엔티티를 판단하는 기본 전략>
1. 식별자가 객체일 때(ex. Long), 식별자의 null 여부로 판단 - 식별자가 null이면 새로운 엔티티
2. 식별자가 자바 기본 타입일 때(ex. long), 식별자가 0인지 아닌지 판단 - 식별자가 0이면 새로운 엔티티
3. Persistable 인터페이스를 구현해서 판단 로직 변경 가능
다음 예시를 보자.
</entity/Item>
@Entity
@Getter
public class Item {
@Id @GeneratedValue
private Long id;
}
</repository/ItemRepository>
public interface ItemRepository extends JpaRepository<Item, Long> {
}
엔티티의 식별자는 Long 타입이고, @GeneratedValue를 사용한다. 스프링 데이터 JPA 리포지토리를 사용하는 상황이다.
테스트
@Test
public void save() {
Item item = new Item();
itemRepository.save(item);
}
@GeneratedValue는 엔티티가 영속성 컨텍스트에 persist 되었을 때, 식별자를 부여한다.
+) 참고: 현재 자동 생성 전략이 AUTO(default)인데, H2 데이터베이스의 경우 AUTO를 시퀀스 전략으로 가져간다.
즉, entityInformation.isNew(entity) 단계에서 entity의 식별자가 아직 null이기 때문에, persist 메서드를 호출한다.
+) 만약 식별자가 long id; 라면 entity의 식별자가 0이기 때문에 이 역시 persist 메서드를 호출하게 된다.
그럼 이미 식별자 값이 지정되어 있는 경우는 어떨까?
</entity/Item - 수정>
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item {
@Id
private String id;
public Item(String id) {
this.id = id;
}
}
@GeneratedValue를 사용하지 않았다.
테스트
@Test
public void save() {
Item item = new Item("A");
itemRepository.save(item);
}
이렇게 이미 식별자가 "A"로 지정되어 있기 때문에, entityInformation.isNew(entity)는 merge 메서드를 호출하게 된다.
이렇게 식별자를 직접 할당하면 merge를 호출하게 되는데 merge는 우선 DB를 호출해서 값을 확인하고(select 쿼리), DB에 값이 없으면 새로운 엔티티로 인지하므로 매우 비효율적이다.
따라서 이 경우는 Persistable를 사용해서 새로운 엔티티 확인 여부(isNew)를 직접 구현하는게 효과적이다.
</entity/Item - 수정>
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {
@Id
private String id;
@CreatedDate
private LocalDateTime createdDate;
public Item(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public boolean isNew() {
return createdDate == null;
}
}
스프링 데이터가 제공하는 @CreatedDate를 사용한다. 엔티티 생성 시간은 엔티티가 persist 되기 직전 측정되기 때문에, 이 값이 아직 생성되지 않은, 즉 createdDate == null 인 경우에 새로운 엔티티라고 판단하도록 설정하는 것이다.
<정리>
1. @GeneratedValue를 사용해 식별자 자동 생성 전략을 사용한다면 스프링 데이터 JPA의 save() 자유롭게 사용
2. 식별자 직접 생성 전략을 사용한다면 Persistable 인터페이스를 사용해서 isNew() 직접 구현
-> 결론은 새로운 엔티티는 merge가 아닌 persist로 영속화 하는 것이 효과적이라는 것이다.
'java > jpa' 카테고리의 다른 글
[JPA] CommandAcceptanceException: Error executing DDL (0) | 2021.06.10 |
---|---|
[Spring Data JPA] 나머지 기능들 (0) | 2021.05.31 |
[Spring Data JPA] 확장 기능 (0) | 2021.05.30 |
[Spring Data JPA] 쿼리 메소드 기능 (0) | 2021.05.24 |
[Spring Data JPA] 공통 인터페이스 기능 (0) | 2021.05.23 |