강의

멘토링

커뮤니티

인프런 커뮤니티 질문&답변

spring님의 프로필 이미지
spring

작성한 질문수

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

엔티티의 자연키 지정

@NaturalIdCache에 대한 보충 설명 및 사용법 공유

작성

·

302

·

수정됨

9

'25. 엔티티의 자연키 지정' 영상의 후반부에 적용한 @NaturalIdCache에 대해 추가로 학습한 내용이, 저처럼 해당 애노테이션을 처음 접한 분들에게 도움이 될 것 같아서 글을 작성합니다. 강의에서 오해가 있을 수 있는 부분, 그리고 자연키에 캐시를 적용하는 방법을 정리해 보았습니다.

 

강의 내용과 실제 동작의 차이점

강의에서는 “같은 트랜잭션 안에서 같은 아이디 값을 가지고 여러 번 조회 시 Persistence Context 에 캐시된 값을 꺼내오는 것 처럼. @NaturalIdCache를 적용하면 이것도 영속 컨텍스트에 캐싱이 된다.”고 말씀하셨습니다.

 

해당 내용에 대한 이해를 돕기 위해 Hibernate의 두 가지 캐시에 대해 간단히 짚고 넘어가겠습니다.

  • 1차 캐시 (First-Level Cache): 세션(영속성 컨텍스트) 범위의 캐시입니다. 같은 트랜잭션 안에서만 유효하며, 트랜잭션이 끝나면 사라집니다. Spring Data JPA에서는 기본적으로 @Id 에 대한 조회를 1차 캐시합니다.

     

  • 2차 캐시 (Second-Level Cache): 세션 팩토리 범위의 캐시로, 여러 세션에서 데이터를 공유할 수 있습니다. 적용하려면 별도의 의존성 추가 및 캐시 관련 설정(@Cache 등)이 필요합니다.

 

따라서, "같은 트랜잭션 안에서 캐시된 값을 꺼내온다."는 말은 세션 범위의 1차 캐시로 해석됩니다. 하지만 제가 직접 테스트해 본 결과, @NaturalIdCache는 1차 캐시가 아닌 2차 캐시와 관련이 있었으며, 1차 캐시를 적용하기 위해서는 다른 방법이 필요했습니다.

 

테스트를 통한 확인

자연키에 대한 1차 캐시 동작을 확인하기 위해, 강의에서 적용한 Member 엔티티의 @NaturalIdCache 를 제거하고, 자연키(Email)에 @NaturalId만 적용한 상황에서 두 가지 방식으로 테스트를 진행했습니다.

 

테스트1: findByEmail 메서드를 사용한 조회

Java

@Test
void NaturalIdFirstLevelCache() {
    Member member = Member.register(createMemberRegisterRequest(), createPasswordEncoder());
    memberRepository.save(member);

    entityManager.flush();
    entityManager.clear();
    System.out.println("회원 저장 및 persistence context 초기화 완료");

    // 같은 email(Natural ID)로 두 번 조회
    Member findMember1 = memberRepository.findByEmail(member.getEmail()).get();
    Member findMember2 = memberRepository.findByEmail(member.getEmail()).get();
    assertThat(findMember1).isSameAs(findMember2);
}
  • Spring Data의 쿼리 메서드를 사용하여 이메일로 조회하는 findByEmail 메서드를 만들고, 한 트랜잭션에서 같은 회원을 두 번 조회했습니다. 자연키에 대한 1차 캐시가 동작한다면, SELECT 쿼리는 한 번만 실행되어야 합니다.

  • 결과는 SELECT 쿼리가 두 번 실행되었습니다. 즉, 자연키에 대한 1차 캐시가 동작하지 않았습니다.

 

테스트2: Hibernate의 자연키 관련 API를 사용한 조회

@NaturalId를 다루는 글들을 찾아본 결과 Hibernate가 제공하는 자연키 관련 API가 있다는 것을 확인했고, 이를 적용하기 위해 커스텀 리포지토리를 구현했습니다.

Java

@Repository
@RequiredArgsConstructor
public class CustomizedMemberRepositoryImpl implements CustomizedMemberRepository {
    private final EntityManager entityManager;

    @Override
    public Optional<Member> findByNaturalId(Email naturalId) {
        return entityManager.unwrap(Session.class)
            .bySimpleNaturalId(Member.class)
            .loadOptional(naturalId);
    }
}

 

그리고, 테스트 1과 같은 방식으로 테스트를 진행하였습니다.


@Test
void NaturalIdApi() {
    Member member = Member.register(createMemberRegisterRequest(), createPasswordEncoder());
    memberRepository.save(member);

    entityManager.flush();
    entityManager.clear();
    System.out.println("회원 저장 및 persistence context 초기화 완료");

    Member findMember1 = memberRepository.findByNaturalId(member.getEmail()).get();
    Member findMember2 = memberRepository.findByNaturalId(member.getEmail()).get();
    assertThat(findMember1).isSameAs(findMember2);
}
  • 결과는 SELECT 쿼리가 한 번만 실행되었습니다. 이를 통해 자연키에 대한 1차 캐시는 @NaturalIdCache 애노테이션과 무관하게, 전용 API를 사용해야만 동작하는 것을 확인했습니다.

 

@NaturalIdCache의 용도

@NaturalIdCache Javadoc에는 다음과 같은 설명이 있습니다.

Specifies that mappings from the natural id values of the annotated entity to the corresponding entity id values should be cached in the shared second-level cache.

…중략

This annotation is usually used in combination with Cache, since a round trip may only be avoided if the entity itself is also available in the cache.

대략 “natural id와 상응하는 id에 대한 매핑을 2차 캐시에 저장하는 애노테이션이고, 엔티티가 캐시되어있어야 하기 때문에 일반적으로 Cache와 함께 사용된다.”라고 해석됩니다. 즉, 1차 캐시가 아닌 2차 캐시를 위한 애노테이션입니다.

 

정리

2차 캐시 관련 설정 및 테스트를 마저 진행한 후 최종 정리한 내용은 다음과 같습니다.

 

자연키의 1차 캐시

  • @NaturalIdCache 애노테이션과 관련 없습니다. 자연키에 @NaturalId만 붙이면 됩니다.

  • 반드시 Hibernate Session의 bySimpleNaturalId() 같은 전용 API를 사용해야 적용됩니다.

 

자연키의 2차 캐시

  • @Cache와 @NaturalIdCache를 함께 사용해야 동작합니다.

  • @Cache만 사용 시 @Id로 조회할 때만 2차 캐시가 동작합니다.

  • @NaturalIdCache만 사용 시 자연키와 ID에 대한 매핑 정보는 캐시 히트되는 걸 확인했지만, ID와 엔티티에 대한 캐시가 없어서 캐시가 적용되지 않았습니다.

     

  • @Cache와 @NaturalIdCache 모두 사용 시 ID를 통한 조회와 자연키를 통한 조회 모두 2차 캐시가 적용됩니다.

     


참고 자료

 

글의 오류나 부족한 내용을 알고 계신 분은 코멘트를 달아주시면 감사하겠습니다.

답변 2

5

토비님의 프로필 이미지
토비
지식공유자

안녕하세요.

제가 미처 확인하지 못했던 부분까지 꼼꼼하게 체크해주셔서 감사합니다.

저도 @NaturalIdCache를 적용한 테스트를 만들어보고 2차 캐시가 세팅되어 있지 않아도 1차 캐시처럼 동작하기도 한다는 것을 확인했던 것 같은데, 뭔가 착각이 있었나 보네요. 강의에서는 간략하게만 언급하고 넘어갔던 부분이었지만 혼란을 드릴 수 있을 것 같아서 저도 좀 더 확인해보고 부가 설명을 드려야할 것 같습니다.

관련 내용을 모두 검토하고 테스트로 검증까지 하신 것을 공유해주셔서 정말 감사합니다.

시간이 될 때 다시 확인해보고 말씀드릴게요.

토비님의 프로필 이미지
토비
지식공유자

흠.. 전에 기본 스프링부트 기본 프로젝트 구조에서 2차 캐시 설정 없이도 예제를 만들어서 캐싱이 잘 되는 걸 확인했었는데, 지금은 안 되네요. 제가 뭘 착각을 했던 것 같습니다.

흠. 강의 노트에라도 설명을 남겨놔야겠네요.

spring님의 프로필 이미지
spring
질문자

빠르게 확인해주셔서 감사합니다. 말씀대로 노트에 설명을 남겨두면 다른 수강생 분들에게 도움이 될 것 같아요. 부트 강의도 그렇고 이번 강의도 너무 재밌게 잘 듣고 있습니다. 좋은 강의 만들어주셔서 정말 감사합니다.

0

안녕하세요, 인프런 AI 인턴입니다. spring님, @NaturalIdCache에 대한 상세한 설명과 사용법을 공유해 주셔서 감사합니다. 이 주제는 Hibernate 캐시에 대한 깊은 이해를 필요로 하며, 쉽게 헷갈릴 수 있는 부분이 많습니다. 이를 보완하기 위한 질문과 답변 링크를 찾아 보았습니다. 다만, 이번 주제와 정확히 일치하는 기존 질문 답변을 찾지 못했습니다. 대신 유사한 캐시 사용법에 대한 설명을 제공할 수 있는 자료를 소개합니다.

관련 질문과 유사한 답변 자료:

  1. 테스트에서의 @Transactional 사용에 대해 질문이 있습니다.
    • 이 질문은 테스트 환경에서의 캐시와 트랜잭션 관리에 대한 내용으로, 많은 Hibernate 및 JPA 사용자들에게 중요한 정보를 제공합니다. 이는 @NaturalIdCache 나 다른 유사한 캐시를 사용하면서 만날 수 있는 문제를 해결하는 데 도움을 줄 수 있습니다.

추가적인 설명

  • 1차 캐시 (First-Level Cache): 은 Hibernate Session 혹은 영속성 컨텍스트 단위로 관리되며, 트랜잭션 내에서만 유효합니다. 동일한 영속성 컨텍스트 내에서 두 번의 같은 엔티티 조회에 대해 데이터베이스 쿼리가 발생하지 않는 것을 보장합니다.
  • 2차 캐시 (Second-Level Cache): 는 피어세스 단위의 캐시로써 서로 다른 세션 사이에서 데이터를 공유할 수 있습니다. 이는 다수의 세션들 간의 데이터 공유가 필요한 대규모 애플리케이션에서 주로 사용됩니다.

@NaturalIdCache는 주로 @Cache와 함께 사용되며, ID뿐만 아니라 자연 키를 사용하는 조회에도 효과적입니다. 자연 키를 사용하는 경우에는 Hibernate가 제공하는 전용 API (예: bySimpleNaturalId())를 사용하는 것이 유리합니다.

보충 자료로 추천드린 링크들과 함께 귀하의 테스트 결과를 참고하면, @NaturalIdCache 적극적으로 활용하는 방법에 대해 더 명확하게 이해하실 수 있을 것입니다. 공유해 주신 테스트 코드 또한 다른 학습자들에게 매우 유익할 것입니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

spring님의 프로필 이미지
spring

작성한 질문수

질문하기