inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

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

엔티티의 자연키 지정

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

358

spring

작성한 질문수 1

11

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

 

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

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

 

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

 

따라서, "같은 트랜잭션 안에서 캐시된 값을 꺼내온다."는 말은 세션 범위의 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);
}

 

테스트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);
}

 

@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차 캐시

 

자연키의 2차 캐시


참고 자료

 

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

java spring spring-boot jpa 리팩터링 ddd

답변 2

5

토비

안녕하세요.

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

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

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

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

3

토비

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

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

1

spring

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

0

인프런 AI 인턴

안녕하세요, 인프런 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 적극적으로 활용하는 방법에 대해 더 명확하게 이해하실 수 있을 것입니다. 공유해 주신 테스트 코드 또한 다른 학습자들에게 매우 유익할 것입니다.

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

N+1 관련해서 질문있습니다.

0

46

3

도메인 모델에서 관계와 규칙을 구분하는 방법

0

61

2

헥사고날 아키텍처에서의 배치, 시큐리티, 비동기 이벤트 처리는 어떻게 하나요?

0

140

2

어댑터에서 도메인에 직접 의존하는 경우에 대해

0

136

2

Member 도메인이 PasswordEncoder를 받는 구조 질문 있습니다.

0

118

2

MemberService와 EmailSender 책임 분리에 대한 질문

0

111

2

NonNullApi를 NullMarked로 대체하라고 합니다.

0

133

2

39. 문서와 코드 다듬기 updateInfo 테스트 질문 있습니다.

0

82

2

Repository Adapter 설계에 대해 피드백을 부탁드립니다

0

112

2

헥사고날 part2 강의 출시 예정일 문의 드립니다.

0

264

2

PT 문의사항

0

104

1

초기 어플리케이션 구동 시 compose.yml 파싱 오류

0

154

2

애플리케이션의 JPA 리턴과 도메인 모델

0

131

2

애그리거트 루트의 하위 도메인들의 depth가 깊어질 때 문의

0

140

2

페이징 처리를 해야한다면 어떻게 해야할까요?

0

198

2

애그리거트의 repository

0

128

2

Domain Expert가 정확히 어떤 역할을 하는 사람인가요?

0

239

1

회원 애플리케이션 서비스 테스트 (1)

0

110

2

정적 팩토리 메서드 관련 질문드립니다!

0

112

2

spotbug + @NonNullApi 로만 Null 방어가 될까요?

0

132

2

required 포트에 관해서

0

96

2

혹시 다음 편은 언제쯤 오픈할까요?

0

173

2

서비스 단위 테스트 코드 작성

0

104

2

domain 모듈에 entity를 정의한다고 했을때

0

106

2