[JPA] Java Persist API 내부 동작 방식

최신 트렌드에 맞춰 스프링 부트를 사용해 API 서버를 개발한다면 반드시 같이 사용되는 기술인 Spring Data JPA의 기본이 되는 JPA에 대하여 제가 학습한 지식을 일부 공유하고자 포스팅을 작성합니다. JPA를 사용만 해보신 분들이라면 다음과 같은 단어들 중 한두 개 정도는 들어보셨을거라 생각됩니다. 영속, 비영속, 준영속, 영속성 컨텍스트, 연관관계, 트랜잭션, EntityManager 등등 JPA라는 기술 하나에서만 엄청 많은 단어들이 나올 수 있습니다. Spring Data JPA는 이 JPA라는 기술을 사용하기 편리하게 거의 대부분을 추상화해둔 기술입니다. 그만큼 JPA가 어떻게 동작하는지 모르고 Spring Data JPA를 사용하게 되면 성능문제 등 여러가지 문제를 조우할 수 있습니다.

⒈ ORM 이란?

ORM(Object Relational Mapping)은 이름 그대로 객체와 관계형 데이터베이스를 매핑해주는 기술로 애플리케이션과 JDBC 사이에서 동작합니다. JPA는 ORM중 하나로 JAVA언어의 ORM 기술 표준이고, 구현체로는 대부분 하이버네이트를 사용합니다.

2. 영속성 컨텍스트

JPA의 모든 데이터는 영속성 컨텍스트에서 관리됩니다. “데이터를 영구 저장하는 환경” 이라는 뜻.

EntityManager.persist(entity);

위의 코드처럼 엔티티 매니저를 통해 영속성 컨텍스트에 접근합니다. 눈에 보이지 않는 논리적인 개념으로 아래항목에서 그림으로 알아보겠습니다.

3. “그래서 영속이 뭔가요?”

설명은 멤버 객체(아이디값만 가지고있음)로 예시를 들어드립니다.

엔티티의 생명주기에는 크게 4가지 항목이 있습니다.

+) 그림에서는 엔티티매니저를 영속성 컨텍스트라고 설명하지만 위에서 언급한것처럼 엔티티매니저를 통해 영속성 컨텍스트에 접근하는 논리적인 개념입니다. 예시를 이해하기 쉽게하기 위해 하나로 구분하였으니 참고하여 읽어주시기 바랍니다.

– 비영속(new)

영속성 컨텍스트와 전혀 상관없이 객체가 새롭게 생성된 상태

//멤버 객체가 새로 생성된 상태
Member member = new Member();
member.setId("member1");

– 영속

객체가 영속성 컨텍스트에서 관리되고 있는 상태

+) 이 글에서는 @PersistenceContext 같은 애노테이션 및 스프링 동작구조(DI 컨테이너, 빈, 컴포넌트 등)에 대해서는 설명드리지 않습니다. JPA의 동작구조에만 집중하기 위함이니 궁금하신분은 따로 스프링 및 JPA 이론을 찾아보시기 바랍니다.

//EntityManager 는 관례로 보통 em이라는 이름을 사용합니다.
@PersistenceContext
EntityManager em;

em.persist(member); //멤버를 영속성 컨텍스트에 영속

– 준영속 및 삭제

영속성 컨텍스트에서 관리되던 엔티티를 영속성 컨텍스트와 분리한 것을 준영속 상태라고 합니다.

em.detach(member);

영속성 컨텍스트에서 객체를 영구히 삭제하는 경우를 삭제 상태라고 합니다.

em.remove(member);

준영속 상태와 삭제 상태가 되면 영속성 컨텍스트는 더이상 엔티티를 추적하지 않습니다.

4. “3번만 봐서는 영속성 컨텍스트의 이점을 모르겠어요”

영속성 컨텍스트의 이점으로는 1차 캐시, 스냅샷, 동일성 보장, SQL 쓰기 지연, 변경 감지, 지연 로딩이 있습니다.

+) 전부 다 말씀드리고싶지만 그러면 글이 너무 길어지고 장황해져서 1차캐시, 변경 감지, 스냅샷 정도만 설명드리도록 하겠습니다.

– 1차 캐시

영속성 컨텍스트 내부에는 1차 캐시라는 임시의 캐시 저장소가 존재합니다. 코드와 그림을 먼저 예시로 보겠습니다.

Member member = new Member();
member.setId("member1");

em.persist(member);

Member findMember = em.find(Member.class, "member1"); //조회

만약 위와같은 코드가 있을 때 어떤느낌 인가요?

새로운 멤버객체를 생성한다. -> 영속상태로 만든다 -> 어딘가에서 member1 아이디를 조회한다.

그럼 위에서 언급하는 어딘가는 무엇일까요? 단순히 영속성 컨텍스트 어딘가에서 조회하겠구나 생각은 할 수 있겠지만 영속성 컨텍스트는 큰 틀 입니다. 결국 위에서 언급한 어딘가는 1차캐시 및 DB가 됩니다.

member1이라는 아이디가 1차캐시에 등록되어 있을때는 1차캐시에서 바로 조회해오게 됩니다.

이 글의 예시에서는 member1 객체를 만들어 직접 영속성 컨텍스트에 영속시키고 조회해오는 방식을 배워보았습니다.

그렇다면 만약 db에는 member2라는 아이디의 인스턴스가 존재하지만 1차캐시에는 존재하지 않는 상황을 가정해보겠습니다.

단순히 조회만 하게되면 어떻게 될까요?

Member findMember2 = em.find(Member.class, "member2");

1차캐시에 해당하는 아이디가 존재하지 않을경우 DB에서 조회 후 1차캐시에 저장 후에 조회해오게 됩니다.

p6spy 라이브러리나 application.properties에서 쿼리 trace 설정을 통해 실제로 발생하는 쿼리를 보게되면 1차캐시에 존재하는 엔티티를 조회할때는 select 쿼리가 발생되지 않지만 member2 인스턴스처럼 1차캐시에 존재하지 않는 엔티티를 조회할때는 쿼리가 발생하는것을 확인할 수 있습니다.

조회 동작구조를 요약해보면 1차캐시에 조회하려는 엔티티가 있는지 확인, 없다면? DB에서 조회 후 1차캐시에 등록 이후 반환 으로 알 수 있었습니다.

– 변경 감지(Dirty Checking)

JPA에서 값을 변경할 때는 엔티티를 조회해와서 엔티티가 가지고 있는 필드값만 바꾸면 자동으로 update 쿼리가 DB에 반영됩니다. 영속상태의 데이터는 영속성 컨텍스트의 위에서 관리된다고 말씀드렸습니다. JPA가 변경되는 엔티티 내용을 추적하여 최종적으로 변경된 내용을 마지막에 DB에 쿼리를 날려 적용시키는 방식입니다.

JPA를 조금 사용해보신분들은 엔티티의 값을 변경할 때 em.update(entity); 와 같은 업데이트 코드없이 조회해온 엔티티의 필드값을 member.setId(“member123”);처럼 바꿔주면 업데이트가 되는걸 보신적이 있을겁니다.

– 스냅샷

그래서 변경감지가 어떻게 동작하는가? 라는 의문을 제기한다면 1차캐시의 특징 중 하나인 스냅샷 때문이라고 볼 수 있습니다. 아래 그림으로 예시를 설명드리겠습니다.

위 그림처럼 1차캐시에 데이터를 등록해둘때 엔티티에 대한 초기상태의 스냅샷을 저장하고있습니다.

만약 엔티티가 변경되면 초기상태의 스냅샷과 상태를 비교하여 달라진 부분에 대한 업데이트 쿼리가 최종적으로 DB에 반영되게 됩니다.

## 마치며

기술을 배울 때 중요하다고 생각되는 것 중 하나가 무언가를 배울 때 “그래서 그 기술을 왜쓰는데?”, “그래서 그게 어떻게 동작하는건데?” 같은 질문을 자신에게 던져보는 것 입니다. 검색하면 나오는 것을 그대로 쓰는 것과 해당 기술을 이해하고 쓰는 것은 완전히 다르다고 생각합니다. JPA 기술의 경우 지연 로딩을 설정하게 되면 조금만 조인이 걸려있어도 N + 1 쿼리문제가 발생하게 되는데 동작구조를 모르면 이런 문제가 발생했을 시 쿼리 성능최적화를 할 수 없습니다.

JPA는 잘못쓰면 성능이 떨어지는 기술이 될 수 있지만 이해하고 사용한다면 개발 향상성이 매우 크게 올라가는 기술입니다.

이 글에서 작성된 내용은 JPA의 매우 기본적인 이론만 담고 있으며, JPA 전체 내용중 5%도 되지 않습니다.

분명히 러닝커브가 높은 기술이지만 그렇기에 파고들며 배우는 재미가 있을거라 생각합니다.


[참고 문헌]

[1] 자바 ORM 표준 JPA 프로그래밍 – 김영한
[2] 인프런 : 김영한 강사님 – 자바 ORM 표준 JPA 프로그래밍 – 기본편