영한님의 스프링 부트와 JPA 활용 1편 강의를 들으면서 id 생성 전략이 IDENTITY 일 때는 INSERT 쿼리가 나가는데, AUTO일 때는 왜 안 나가는지 궁금해 관련 내용을 정리해 보게 되었습니다.
먼저 사용하는 Member, MemberService, MemberRepository의 코드는 아래와 같습니다.
Member
package jpashop.jpashop.domain;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@Entity
public class Member {
@Id @GeneratedValue()
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();
}
MemberRepository
package jpashop.jpashop.repository;
import jpashop.jpashop.domain.Member;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Repository
public class MemberRepository {
@PersistenceContext
private EntityManager em;
public void save(Member member){
em.persist(member);
}
public Member findOne(Long id){
return em.find(Member.class, id);
}
public List<Member> findAll(){
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
public List<Member> findByName(String name){
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
}
}
MemberService
package jpashop.jpashop.service;
import jpashop.jpashop.domain.Member;
import jpashop.jpashop.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Transactional(readOnly = true)
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepository;
/**
*
* 회원 가입
*/
@Transactional()
public Long join(Member member){
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
//EXCEPTION
List<Member> findMembers = memberRepository.findByName(member.getName());
if(!findMembers.isEmpty()){
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
//회원 전체 조회
public List<Member> findMembers(){
return memberRepository.findAll();
}
//회원 한건 조회
public Member findOne(Long id){
return memberRepository.findOne(id);
}
}
회원가입을 테스트하는 위의 테스트 메서드를 실행했을 때 JPA는 동일 트랜잭션 내에서 영속 엔티티의 동일성을 보장하기 때문에 당연히 테스트 코드는 성공합니다.
위의 캡처에서는 안 나와있지만 테스트 클래스에 @Transactional이 붙어있는 상태이고, 트랜잭션이 생성된 상태에서 memberServaice.join()을 호출하므로 memberService의 트랜잭션은 이미 부모 트랜잭션이 존재하기 때문에 새롭게 트랜잭션을 생성하지 않고 부모 트랜잭션에 합류합니다. 즉, memberService와 테스트 메서드가 동일 트랜잭션에 속하게 됩니다.
이제 로그를 확인해 보겠습니다.
memberService.join() -> memberRepository.save() -> em.persist(member)
순으로 호출하는데, INSERT 쿼리가 나가지 않습니다.
참고로 INSERT 쿼리가 나가지 않았지만 테스트가 성공한 이유는 findOne() 호출했을 때 JPA는 먼저 영속성 컨텍스트에 같은 엔티티가 존재하는지 확인하고 존재한다면 영속성 컨텍스트에 존재하는 엔티티를 반환해 주기 때문입니다.
이유는 JPA의 영속성 컨텍스트는 아래의 네 가지 경우일 때만 플러시 됩니다.
그리고 또 한 가지, 플러시 하지 않아도 쿼리가 나가는 경우가 있습니다.
id 생성 전략이 IDENTITY 일 때입니다.
Member의 id 생성 전략을 IDENTITY로 바꾸고 테스트를 실행하면 persist() 호출 시점에 바로 쿼리가 나갑니다.
JPA의 영속성 컨텍스트 내부 1차 캐시는 KEY, VALUE 형태로 엔티티를 저장하는데 이때 엔티티의 pk 값이 KEY가 됩니다. 즉 여기서는 Member.id가 KEY로 저장됩니다.
em.persist()로 엔티티를 영속화하면 해당 엔티티는 영속성 컨텍스트 내부 1차 캐시에 저장이 되어야 하는데, IDENTITY 전략은 id 생성을 DB에 위임하기 때문에 DB에 INSERT 쿼리가 실제 나가야지만 ID 값을 알 수 있습니다. (IDENTITY 전략은 AUTO_INCREMENT로 동작하기 때문)
그래서 원래 JPA는 쿼리를 SQL 쓰기 지연 저장소에 저장해두고 강제 플러시 되거나 영속성 컨텍스트가 초기화되거나 트랜잭션 커밋 시점 등에 쿼리를 DB에 한 번에 보내게 되는데, id 생성 전략이 IDENTITY 일 때는 엔티티를 영속화할 때 id 값을 알아야 하므로 즉시 INSERT 쿼리가 나갑니다.
AUTO 전략은 @GeneratedValue의 기본값입니다.
방언에 따라 (TABLE, IDENTITY, SEQUENCE) 세 개 중 하나가 자동으로 지정됩니다. (MYSQL = IDENTITY.. )
현재 프로젝트에서(h2 DB)는 AUTO로 할 경우 SEQUENCE 전략이 선택됩니다.
em.persist()로 member를 영속화할 때 id 생성을 위해서 DB가 관리하는 SEQUENCE OBJECT에서 id 값을 가져와야 합니다.
SEQUENCE 전략은 굳이 INSERT 쿼리가 DB에 나가지 않아도 id 값을 알 수 있으므로 AUTO 일 땐 INSERT 쿼리가 나가지 않습니다.