
자바 ORM 표준 JPA 프로그래밍 11장 웹 애플리케이션 제작을 학습하다가 궁금한 점이 생겼다. 관련 정보를 찾아보다 보니 유익한 점이 많은 것 같아 포스팅하게 되었다.
책을 보면 회원 가입 로직은 다음과 같이 구현되어 있다. (주제와 관련없는 로직은 제거했다.)
public Long join(Member member) {
memberRepository.save(member);
return member.getId();
}
member 객체는 매개변수로 주어졌을 뿐인데 id가 어떻게 자동으로 저장되는 것인지 궁금했다. 실제로 기존에 동아리에서 개발하던 코드를 보면 save() 메서드를 호출할 때 대부분 아래와 같은 형태로 호출했다.
Member member = memberRepository.save(memberRequest);
나는 당연히 영속화되는 객체와 매개변수로 주어지는 객체는 별개의 것이라고 여겨왔고, 매개변수로 주어지는 객체에는 id 값이 비어있을 것이라고 생각했다.
책의 내용은 내가 무의식적으로 여기던 정보와 상이했기에 관련 정보를 찾아보게 되었다.
이 문제를 풀기 위해서는 영속성 컨텍스트에 대한 배경 지식이 필요하다.
영속성 컨텍스트에는 1차 캐시가 존재하는데, 영속화된 엔티티 정보들이 해당 메모리 공간에 기록되어 있다. 여기에는 각 엔티티의 식별자와 정보 등이 기록되는데, 주의해서 봐야 할 것은 엔티티 정보가 참조로 기록된다는 점이다.
em.persist(member)를 호출하면 엔티티 매니저는 기존에 저장되어 있던 member 객체를 영속성 컨텍스트(1차 캐시)에 참조로써 저장한다. 그리고 만약 해당 엔티티 클래스에 @GeneratedValue가 적용되어 있다면 새로 기록할 ID 값을 기존 member 객체에 주입한다. 이는 영속성 컨텍스트가 엔티티 정보를 참조로써 유지하기에 가능한 일이다.
결론적으로 em.persist(member)를 호출하면 member 객체에는 persist 전에는 없던 ID 값이 주입된다. 그래서 책의 내용처럼 반환 객체 없이 기존 매개변수 객체에도 getId()가 정상적으로 호출될 수 있는 것이다.
앞에서 동아리 개발 코드에서는 아래와 같이 호출한다고 언급했다.
Member member = memberRepository.save(memberRequest);
여기서 이런 의문이 들었다.
"ID 값은 기존 객체에도 저장되는데, 굳이 새로운 객체에 반환값을 저장할 필요가 있을까?"
실제로 반환 객체를 사용하지 않고도 save()를 호출하는 것만으로 영속화는 이루어지고 ID 값은 주입된다. 하지만 여기에는 함정이 있다.
Spring Data JPA의 save() 메서드는 다음과 같이 정의되어 있다.
@Transactional
public T save(T entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
해석하면 다음과 같다.
em.persist()를 호출하여 영속화한다.em.merge()를 호출하여 병합한다.여기서 문제는 2번 케이스다. 준영속 엔티티를 save하려고 하면 엔티티 매니저는 persist()가 아니라 merge()를 호출한다. 그런데 merge()는 persist()와 다르게 매개변수로 전달한 객체와 반환되는 객체가 다르다.
merge()가 호출되면 기존에 DB(또는 1차 캐시)에 저장되어 있던 동일한 식별자를 가진 엔티티와 병합된다. 기존 엔티티에 새로 save를 시도하는 엔티티 정보가 덮어씌워지는 것이다.
결국 2번 케이스의 경우 매개변수로 들어온 객체를 영속화하여 반환하는 것이 아니라 기존에 저장되어 있던 엔티티를 최신화하여 반환하는 것이 된다. 이 경우에는 아래와 같이 반환 객체를 사용해야만 정상적인 로직 수행이 가능해진다.
Member member = memberRepository.save(memberRequest);
지금까지 학습한 내용은 전부 이해가 되었는데, 문득 책에서는 왜 그런 코드를 작성한 것인지 궁금해졌다.
아래는 책 내용 중 MemberRepository.save()의 구현부이다.
public void save(Member member) {
em.persist(member);
}
잘 보면 위에서 정의했던 Spring Data JPA의 save() 메서드와 구조가 다르다. 해당 라이브러리에서는 persist와 merge의 기능을 동시에 수행한 반면 책에서는 persist의 기능만을 수행하고 있다. 이 경우에는 매개변수로 전달된 객체가 곧 영속성 엔티티가 된다. 만약 영속화된 엔티티를 반환하더라도 매개변수로 주어진 객체와 동일하다고 할 수 있는 것이다. 그래서 (반환의 의미가 없기에) 아무 것도 반환하지 않고 있다.
하지만 책에서는 다른 구조의 save() 메서드가 한 군데 등장한다. ItemRepository.save()인데, 구현부는 아래와 같다.
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.merge(item);
}
}
위 코드는 반환 값이 없다는 점을 제외하면 Spring Data JPA의 메서드와 동일한 구조로 동작한다는 것을 알 수 있다.
사실 save() 메서드에서 persist와 merge를 동시에 수행해야 한다던가 하는 기준은 없다. 하지만 Spring Data JPA에서는 persist를 더 포괄적인 용도(준영속 엔티티도 지원)로 사용할 수 있도록 save() 메서드에 두 기능을 합친 것으로 보인다. 따라서 책처럼 메서드를 정의하는 것이 틀린 것도 아니고, 그렇게 정의했으니 반환 값을 사용하지 않는 것도 문제가 되지 않는다. 다만 책에서는 "이렇게 정의하는 방법도 있다"는 정도로 언급한 것 같다.
Spring Data JPA를 사용한다면 save() 메서드는 persist뿐만 아니라 merge의 기능도 동시에 수행하기 때문에 혹시 모를 상황을 대비하여 항상 반환되는 엔티티를 사용하는 것을 추천한다.
https://kkambi.tistory.com/134
https://stackoverflow.com/questions/8625150/why-use-returned-instance-after-save-on-spring-data-jpa-repository