읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 자바 ORM 표준 JPA 프로그래밍 책을 공부하며 정리한 글입니다.
 

자바 ORM 표준 JPA 프로그래밍 - 교보문고

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임 | ★ 이 책에서 다루는 내용 ★■ JPA 기초 이론과 핵심 원리■ JPA로 도메인 모델을 설계하는 과정을 예제 중심으로

www.kyobobook.co.kr

한 줄 요약 : 엔티티 매니저는 엔티티를 저장하는 가상의 DB이다.

이 글을 작성하게 된 이유

JPA를 공부하긴 했지만 너무 수박 겉핥기로 공부를 한 것 같다는 생각이 들어서 책을 구매해서 시작했습니다. 그래서 공부하면서 다음에 헷갈릴 만한 내용이거나 정리해두면 좋겠다 싶은 내용들을 정리하려고 작성한 글입니다.


JPA의 코드는 크게 3 부분으로 나뉘어 있습니다.

1. 엔티티 매니저 설정

2. 트랜잭션 관리

3. 비즈니스 로직

 

우리는 그중에서 엔티티 매니저를 알아보려고 합니다.

 

JPA가 제공하는 기능

1. 엔티티와 테이블을 매핑하는 설계 부분

2. 매핑한 엔티티를 실제 사용하는 부분

 

이번에는 매핑한 엔티티를 엔티티 매니저를 통해 어떻게 사용하는지 알아보고자 합니다.

 

엔티티 매니저는 엔티티를 CRUD(저장, 조회, 수정, 삭제)를 하는 등 엔티티와 관련된 모든 일을 처리합니다.

 

이름 그대로 엔티티를 저장하는 가상의 DB로 생각하면 됩니다.

 

DB를 하나만 사용하는 애플리케이션은 일반적으로 EntityManagerFactory를 하나만 생성합니다.

 

EntityManagerFactory emf = Persistence.createEnetityManagerFactory("해당 이름");

EntityManager em = emf.createEnetityManager();

 

위와 같은 방식으로 작성하면 생성된 EntityManagerFactory를 통해 필요할 때마다 엔티티 매니저를 생산할 수 있습니다.

 

여기서 중요한 점은 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유를 해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 공유는 절대 안 됩니다.

 

이렇게 생성된 엔티티 매니저는 DB 연결이 필요하기 전까지 커넥션을 얻지 않습니다.

 

하이버네이트를 포함한 JPA 구현체들은 엔티티 매니저 팩토리를 생성할 때 커넥션 풀도 함께 만드는데, J2SE 환경에서 사용하는 방법입니다.

 

보통 해당 정보는 properties에 작성합니다.(xml 파일일 수도 있고, yaml일 수도 있습니다. 중요한 건 정보!!)

위의 예는 MYSQL DB 설정할 때 사용하던 방식입니다.


이런 정보들을 통해 생성된 엔티티는 영속성 컨텍스트(persistence context)라는 곳에 저장합니다.

 

영속성 컨텍스트를 해석하자면 '엔티티를 영구 저장하는 환경'이라는 뜻입니다.

 

엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리합니다.

 

영속성 컨텍스트에서 관리하기 위해서 엔티티는 4가지 상태가 존재합니다.

 

1. 비영속(new / transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태

2. 영속(managed) : 영속성 컨텍스트에 저장된 상태

3. 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태

4. 삭제(removed) : 삭제된 상태

 

자바 ORM 표준 JPA 프로그래밍 p.93

위의 그림이 엔티티의 생명주기입니다.

 

저는 처음에 이해가 되지 않아서 비슷한 점이 그리 많지는 않지만 깃처럼 생각했습니다.

 

persist()라는 메서드를 통해 new 상태에서 managed 형태로 변하는 걸

 

git에서 add라는 명령어를 통해 staging area에서 관리하듯이

 

엔티티를 영속성 컨텍스트(entityManager)가 관리한다고 생각했습니다.


먼저 영속성 컨텍스트가 관리하며 생기는 각 상태에 대해서 정리해보겠습니다.

 

비영속(new / transient) : 엔티티 객체를 생성한 순간으로 순수한 객체 상태이며 아직 저장하지 않았습니다.

 

em.persist() 호출 전으로 서로 관련 없이 따로 존재하고 있는 상태입니다.

영속(managed) : 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장한 상태입니다.

 

영속성 컨텍스트가 관리하는 엔티티를 영속 상태라고 합니다.

 

영속 상태가 되었다는 뜻은 영속성 컨텍스트가 엔티티를 관리한다는 뜻입니다.

 

그리고 em.find()나 JPQL을 사용해서 조회한 엔티티도 영속성 컨텍스트가 관리하는 영속 상태입니다.

 

준영속 : 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 됩니다.

 

특정 엔티티를 준영속 상태로 만들려면 em.detach()를 호출하면 됩니다.

 

em.close()를 호출해서 영속성 컨텍스트를 닫거나 em.clear()를 호출해서 영속성 컨텍스트를 초기화해도 영속성 컨텍스트가 관리하던 영속 상태의 엔티티는 준영속 상태가 됩니다.

 

삭제 : em.remove(엔티티)를 통해 엔티티를 영속성 컨텍스트와 DB에서 삭제합니다.

 

 

영속성 컨텍스트(entityManager)의 형태에 대해서 먼저 정리해보겠습니다.

모든 내용을 적지는 못했지만 해당 형태가 영속성 컨텍스트에서 이루어지는 CRUD라고 생각합니다.

 

아래 내용부터는 본격적으로 각 내용에 대해서 정리하겠습니다.

 

영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분합니다.

따라서 영속 상태는 식별자 값이 반드시 존재합니다. 식별자 값이 없다면 예외가 발생합니다.

 

JPA는 persist()라는 메서드를 통해 DB에 저장되는 것이 아니라 flush를 통해 DB에 저장됩니다.

 

복잡한 과정을 거치는 것 같아 보여 이걸 왜 쓰나 싶지만 영속성 컨텍스트를 통해 엔티티를 관리하면 여러 장점이 있습니다.

1. 1차 캐시

2. 동일성 보장

3. 트랜잭션을 지원하는 쓰기 지연

4. 변경 감지

5. 지연 로딩

 

그럼 지금부터 영속성 컨텍스트가 왜 필요하고 어떠한 이점이 있는지 엔티티를 CRUD 하면서 그 이유를 알아보겠습니다.

 

엔티티 조회 과정

1. em.find(엔티티 클래스 타입, 엔티티 식별 값); 형태로 엔티티를 조회합니다. 

2. 1차 캐시에서 엔티티 식별 값에 해당하는 엔티티를 찾습니다.

3 - 1. 있다면 캐시 값을 조회해서 반환합니다.

3 - 2. 없다면 DB를 조회합니다.

4. 조회한 데이터로 엔티티 식별 값에 해당하는 엔티티를 생성해서 1차 캐시에 저장합니다.(영속 상태)

5. 조회한 엔티티를 반환합니다.

 

이 엔티티들은 메모리에 있는 1차 캐시에서 바로 불러올 수 있습니다.

그리고 이렇게 1차 캐시에 저장된 엔티티 인스턴스는 엔티티의 동일성을 보장합니다.

 

엔티티 등록 과정

1. persist(엔티티); 형태로 엔티티를 저장합니다.

2. 1차 캐시에 @Id와 Entity를 저장합니다.

2. 쓰기 지연 SQL 저장소에 INSERT SQL문을 저장합니다.(동시에 진행됩니다.)

3. 트랜잭션을 커밋합니다.

4. 엔티티 매니저는 영속성 컨텍스트를 플러시합니다.

5. 영속성 컨텍스트의 변경 내용을 DB에 동기화합니다.

 

등록 쿼리를 아무리 많이 작성해도 트랜잭션을 커밋하지 않는다면 아무런 소용이 없습니다.

어떻게든 커밋 직전에만 DB에 SQL을 전달하면 됩니다.

이것이 트랜잭션을 지원하는 쓰기 지연이 가능한 이유입니다.

 

엔티티 수정

1. 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 flush()가 호출됩니다.

2. 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾습니다.

3. 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보냅니다.

4. 쓰기 지연 저장소의 SQL을 DB에 보냅니다.

5. DB 트랜잭션을 커밋합니다.

 

JPA는 SQL의 update문이 존재하지 않습니다.

최초 상태를 복사해서 저장해둔 스냅샷과 플러시 시점에 엔티티와 비교해서 변경된 엔티티를 찾아서 변경합니다.

 

그리고 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용됩니다.

 

그리고 변경할 때 모든 필드를 이용한 수정 쿼리를 사용합니다.

 

엔티티 삭제 과정

1. em.find(); 를 통해 삭제 대상 엔티티를 조회합니다.

2. em.remove(엔티티); 를 작성하면 해당 엔티티가 삭제됩니다.

 

엔티티가 삭제된다면 영속성 컨텍스트에서 제거됩니다.

 

이렇게 삭제된 엔티티는 재사용하지 말고 GC가 처리하게 두는 게 좋습니다.


플러시

영속성 컨텍스트의 변경 내용을 DB에 반영합니다.

 

플러시를 실행하면 다음 과정을 거칩니다.

1. 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾습니다.

수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록합니다.

2. 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송합니다.(CUD)

 

위 과정을 진행하기 위해 영속성 컨텍스트를 플러시하는 방법은 3가지가 있습니다.

1. em.flush()를 직접 호출합니다.

2. 트랜잭션 커밋 시 플러시가 자동으로 호출됩니다.

3. JPQL 쿼리 실행 시 플러시가 자동으로 호출됩니다.

 

1. em.flush() : 강제로 플러시를 하는 방법인데, 테스트나 다른 프레임워크와 JPA를 사용할 때를 제외하고 거의 사용하지 않습니다.

 

2. 트랜잭션 커밋 시 플러시 자동 호출

DB에 변경 내용을 SQL로 전달하지 않고 트랜잭션만 커밋하면 어떤 데이터도 DB에 반영되지 않습니다.

따라서 트랜잭션을 커밋하기 전에 꼭 플러시를 호출해서 영속성 컨텍스트의 변경 내용을 DB에 반영해야 합니다.

JPA는 이런 문제를 예방하기 위해 트랜잭션을 커밋할 때 플러시를 자동으로 호출합니다.

 

3. JPQL 쿼리 실행 시 플러시 자동 호출

JPQL이나 Criteria 같은 객체지향 쿼리를 호출할 때도 플러시가 실행됩니다.

JPQL 이전에 em.persist()가 존재한다면 영속성 컨텍스트를 플러시해서 변경 내용을 DB에 반영해줍니다.

 

참고로 식별자를 기준으로 조회하는 find() 메서드를 호출할 때는 플러시가 실행되지 않습니다.

 

플러시 모드 옵션

기본으로 FlushModeType.AUTO가 기본값으로 지정되어 있고 원한다면 FlushModeType.COMMIT으로 변경하면 커밋할 때만 플러시됩니다.

 

해당 내용이 플러시가 관련된 내용입니다.

 

가장 중요한 점은 플러시라는 건 영속성 컨텍스트에 보관된 엔티티를 지운다고 생각하면 안 됩니다.

다시 한번 강조하지만 영속성 컨텍스트의 변경 내용을 DB에 동기화하는 것이 플러시입니다.

 

플러시가 이해가 안 되면 이렇게 생각하시면 되지 않을까 싶습니다.

 

MYSQL을 기준으로 설명해보겠습니다.

a라는 DB에 a와 b라는 테이블을 만든다고 가정합니다.

 

그렇다면 a와 b 테이블을 만드는 쿼리를 작성한다고 해서 바로 오른쪽과 같이 DB에 테이블이 만들어지는 것이 아닙니다.

 

해당 쿼리를 실행하기 전까지는 중앙 쿼리문은 그냥 글입니다.

 

저는 이렇게 쿼리가 작성된 상태가 쓰기 지연 SQL 저장소에 저장된 상태와 같다고 생각합니다.

 

그리고 해당 쿼리를 실행하는 순간 테이블이 만들어집니다.

 

이 과정이 flush()하는 과정이라고 생각합니다.

 

flush() 명령이 들어오면 영속성 컨텍스트에서 모든 처리를 끝내고 지연 SQL 저장소에 저장된 정보가 DB로 들어가며 과정이 종료됩니다.


마지막으로 준영속과 관련된 메서드입니다.

 

영속 상태였다가 더는 영속성 컨텍스트가 관리하지 않는 상태를 준영속 상태라고 합니다.

그래서 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없습니다.

준영속 상태는 영속성 컨텍스트로부터 분리된 상태입니다.

 

이렇게 영속 상태의 엔티티를 준영속 상태로 만드는 방법은 크게 3가지입니다.

1. em.detach(엔티티) : 특정 엔티티만 준영속 상태로 전환합니다.

2. em.clear() : 영속성 컨텍스트를 완전히 초기화합니다.

3. em.close() : 영속성 컨텍스트를 종료합니다.

 

1. em.detach(엔티티)를 호출하면 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거됩니다.

 

2. clear()를 호출하게 되면 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만듭니다.

쉽게 말해서 영속성 컨텍스트에 담겨있는 정보를 모두 깨끗하게 비운다는 뜻입니다.

 

3. close()를 호출하게 되면 영속성 컨텍스트가 종료됩니다.

쉽게 말해서 컴퓨터가 꺼지면 컴퓨터를 사용하지 못하는 것처럼 영속성 컨텍스트가 종료되었다고 생각하시면 됩니다.

 

이런 준영속 상태의 특징

1. 거의 비영속 상태에 가깝다.

영속성 컨텍스트가 관리하지 않으므로 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않습니다.

 

2. 식별자 값을 가지고 있습니다.

비영속 상태는 식별자 값이 없을 수도 있지만 준영속 상태는 이미 한 번 영속 상태였으므로 반드시 식별자 값을 가지고 있습니다.

 

3. 지연 로딩을 할 수 없다.

지연 로딩은 실제 객체 대신 프록시 객체를 로딩해주고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법이다.

하지만 준영속 상태는 영속성 컨텍스트가 더는 관리하지 않으므로 지연 로딩 시 문제가 발생한다.

 

위 3가지 방법이 영속 상태의 엔티티를 준영속 상태로 만드는 것이었다면 merge()는 다시 영속 상태로 변경하기 위해 사용합니다.

 

쉽게 말하면 준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환합니다.

 

 

1. 엔티티를 createMember() 메서드의 영속성 컨텍스트 1에서 영속 상태였다가 영속성 컨텍스트1이 종료되면서 준영속 상태가 되었습니다. 따라서 createMember() 메서드는 준영속 상태의 member 엔티티를 반환합니다.

 

2. main() 메서드에서 member.setUsername("")을 호출해서 회원 이름을 변경했지만 준영속 상태인 member 엔티티를 관리하는 영속성 컨텍스트가 더는 존재하지 않으므로 수정 사항을 DB에 반영할 수 없다.

 

3. 준영속 상태의 엔티티를 수정하려면 준영속 상태를 다시 영속 상태로 변경해야 하는데 이때 merge()를 사용한다.

새로운 영속성 컨텍스트 2를 시작하고 em2.merge(member)를 호출해서 준영속 상태의 member 엔티티를 영속성 컨텍스트2가 관리하는 영속 상태로 변경합니다.

영속 상태이므로 트랜잭션을 커밋할 때 수정했던 회원명이 DB에 반영됩니다.

정확히 말하면 member 엔티티가 준영속 상태에서 영속 상태로 변경되는 것은 아니고 mergeMember라는 새로운 영속 상태의 엔티티가 반환됩니다.

 

쉽게 말해서 해당 이름으로 1차 캐시에서 찾을 수 없으니 DB를 찾고, DB에도 없으면 새로운 엔티티를 생성해서 만든다고 생각하면 되지 않을까 싶다.

 

병합은 파라미터로 넘어온 엔티티의 식별자 값으로 영속성 컨텍스트를 조회하고 찾는 엔티티가 없으면 DB에서 조회합니다.

만약 DB에서도 발견하지 못하면 새로운 엔티티를 생성해서 병합합니다.

 

병합은 준영속, 비영속을 신경 쓰지 않습니다. 식별자 값으로 엔티티를 조회할 수 있으면 불러서 병합하고 조회할 수 없으면 새로 생성해서 병합합니다.

 

따라서 병합은 save or update 기능을 수행합니다.


마지막으로 영속성 컨텍스트(entityManger)에 대한 저의 생각은 다음과 같습니다.

 

SQL문을 직접 작성하는 것이 아니라 객체를 사용해서 가상의 DB에 SQL문을 저장합니다.

 

그리고 원할 때 한 번에 처리할 수 있게 만들어줍니다.

 

그리고 1차 캐시를 통해 쉽게 조회할 수 있고, 스냅샷과 비교해 변경 감지로 DB 내용을 변경할 수 있습니다.

 

깃에서 파일을 관리해주듯 영속성 컨텍스트는 엔티티를 관리합니다.

 

그로 인해서 영속성 컨텍스트가 제공하는 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩과 같은 기능들을 사용할 수 있습니다.

 

이상으로 긴 글 마치겠습니다. 감사합니다.