[JPA]영속성 관리(1)

Inung_92·2023년 10월 24일
1

JPA

목록 보기
3/7
post-thumbnail

해당 포스팅의 내용은 김영한 강사님의 자바 ORM 표준 JPA 프로그래밍 책의 내용을 정리하여 작성하였습니다.


영속성이란?

📖영속성의 사전적 정의는 ‘오래 계속되는 성질’이다. 즉, JPA에서 데이터베이스와 관련된 개념으로 데이터의 일관성을 유지하고 지속성을 확보하기 위한 메커니즘을 제공하는 개념이다.

JPA에서의 영속성이 가지는 주요 특징 및 개념은 다음과 같다.

  • 엔티티 객체 : 테이블과 매핑된 객체로 데이터 베이스와 상호작용한다.
  • 영속 컨텍스트 : JPA의 영속성은 엔티티 매니저(EntityManager)를 통해 관리되는데 이를 영속 컨텍스트라고 한다. (추가 설명은 뒤에서…)
  • 변경 감지(Dirty Checking) : 영속 컨텍스트에 등록된 엔티티 객체의 변경 사항을 자동으로 감지하여 데이터베이스에 반영한다.
  • 지연 로딩(Lazy Loading) : 엔티티 객체를 필요한 시점에 로드하여 성능을 최적화 시키고 불필요한 데이터 로드를 방지한다.
  • 트랜잭션 관리 : 데이터의 일관성을 보장하기 위하여 엔티티 객체에 대한 작업은 트랜잭션 내에서 이루어지며 예외 처리를 통한 롤백 등의 적절한 조치가 이루어 질 수 있다.
  • 생명 주기 관리 : 엔티티 객체의 생명 주기를 관리 할 수 있고, 생명 주기 내에서 저장, 수정, 삭제, 조회 등의 동작을 편하게 처리 가능하다.

그렇다면 JPA에서 영속성이 어떻게 관리되어지는지 알아보자.

엔티티 매니저(EntityManager)

엔티티 매니저 팩토리(EntityManager Factory)

📖엔티티 매니저를 만드는 공장으로 비용이 상당이 크다. 비용적 문제로 애플리케이션 전체에서 단 하나의 엔티티 매니저 팩토리만 생성하고 공유해서 사용한다.

생성 코드

// 생성
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpabook");

// persistence-unit 정보 활용
Persistence.createEntityManagerFactory("jpabook");

엔티티 매니저 팩토리는 persistence.xml에서 등록한 persistence-unit의 정보를 바탕으로 생성된다. 엔티티 매니저 팩토리의 비용이 큰 이유는 이전 글에서 설명했지만 DB 커넥션 풀과 기반 객체를 생성하기 때문이다.

다음은 엔티티 매니저 생성에 대해서 알아보자.

엔티티 매니저 생성

엔티티 매니저는 앞서 설명했듯이 엔티티 매니저 팩토리를 통해서 생성되는데 공장에서 생성하기 때문에 매니저를 생성하는 비용은 거의 들지 않는다.

엔티티 매니저의 생성 비용이 적게 드는 이유는 다음과 같다.

  • 엔티티 매니저 팩토리 설정은 애플리케이션 생명 주기 동안 단 한 번만 수행된다. 따라서 설정을 매번 할 필요없이 팩토리에서 매니저를 생성하면 된다.
  • 엔티티 매니저를 별도로 생성 할 경우 여러 스레드에서는 스레드당 설정을 매번 반복하며 생성해야하지만 팩토리를 스레드 간 공유하여 매니저를 생성하면 이런 비용이 감소된다.
  • 엔티티 팩토리 매니저는 메타데이터를 캐싱하고 엔티티 매니저를 생성 할 때 캐싱된 데이터를 활용하여 검색 비용을 줄인다.
  • 마지막으로 엔티티 매니저의 생명 주기를 관리하고, 자원 해제메모리 누수방지 할 수 있다.

이제 엔티티 매니저를 어떻게 생성하는지 코드를 보자.

생성 코드

// 팩토리 생성
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpabook");
// 매니저 생성
EntityManager entityManager = factory.createEntityManager();

엔티티 매니저를 사용 할 때 주의사항은 여러 스레드 간에 절대 공유를 해서는 안된다. 엔티티 매니저 팩토리는 여러 스레드에서 접근하는 것에 안전하므로 공유가 가능하지만 엔티티 매니저는 다음과 같은 이유로 공유를 해서는 안된다.

  • 엔티티 매니저의 작업이 겹침으로 인한 데이터의 일관성 문제 발생
  • 엔티티 객체의 생명 주기 추적에 대한 문제 발생 및 데이터 베이스 동기화 문제 발생
  • 트랜잭션 관리의 복잡성 증가 및 오류 가능성 증가
  • 영속성 컨텍스트의 범위와 트랜잭션의 범위가 일치하지 않는 증상 발생
  • 병목 현상으로 인한 성능 저하

그림을 보면 두 개의 엔티티 매니저가 생성되어 있다. 여기서 첫번째 엔티티 매니저는 커넥션을 사용하지 않고 대기중에 있다. 이 부분에서 알 수 있는 것은 엔티티 매니저는 꼭 필요한 시점에 데이터베이스와 커넥션을 얻어와 통신한다. 또한 트랜 잭션을 시작 할 때 커넥션을 획득한다.

이런 이유로 위에서 설명한 공유 해서는 안되는 이유들이 발생하게 되는 것이다. 이제 영속성의 개념과 엔티티 매니저에 대해서 알아보았으니 엔티티의 생명주기를 알아보자.

엔티티 생명주기

구분

엔티티의 생명주기는 다음과 같이 4가지 상태로 구분된다.

  • 비영속(new/transient) : 영속성 컨텍스트에 저장되지 않은 상태로 전혀 관계가 없는 상태
  • 영속(managed) : 영속성 컨텍스트에 저장된 상태
  • 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed) : 삭제된 상태

위 4가지 상태를 그림으로 표현하면 다음과 같다.

상태별로 어떤 특징이 있는지 알아보자.

비영속

객체를 생성했지만 아직 영속성 컨텍스트와 관련이 없는 상태이다.

// 객체 생성
Member member = new Member();
member.setId("id1");
member.setName("member1");

비영속 상태는 데이터베이스 및 영속성 컨텍스트와 아무련 관련이 없기 때문에 영속 상태로 만들기 위해서는 엔티티 매니저를 통해 저장해야한다.

영속

생성된 엔티티가 엔티티 매니저에 의해 영속성 컨텍스트에 저장된 상태로 영속성 컨텍스트에 의한 관리를 받는다.

...생략
// 엔티티 매지너 생성
EntityManager manager = factory.createEntityManager();

// 엔티티 생성(비영속)
Member member = new Member();
member.setId("id1");
member.setName("member1");

// 엔티티 저장(영속)
manager.persist(member);

영속 상태의 엔티티는 식별자를 가진다는 특징이 있다.

준영속

영속성 컨텍스트에 저장되었던 엔티티가 분리된 상태이다. 준영속 상태의 엔티티는 영속성 컨텍스트의 관리 범위에서 벗어나기 때문에 영속성 컨텍스트의 지원을 받을 수 없다.

...생략
// 엔티티 매지너 생성
EntityManager manager = factory.createEntityManager();

// 엔티티 생성(비영속)
Member member = new Member();
member.setId("id1");
member.setName("member1");

// 엔티티 저장(영속)
manager.persist(member);

// 엔티티 분리(준영속)
manager.detach(member);

위와 같이 직접 detach() 를 통해 분리해주거나 close() 또는 clear()를 호출해서 영속성 컨텍스트를 초기화해도 준영속 상태가 된다.

// 엔티티 매지너 생성
EntityManager manager = factory.createEntityManager();

// 엔티티 생성(비영속)
Member member = new Member();
String id = "id1";
member.setId(id);
member.setName("member1");

// 엔티티 저장(영속)
manager.persist(member);

// 엔티티 분리(준영속)
manager.detach(member);

// 검색
Member findMember = manager.find(Member.class, id);
System.out.println(findMember.getName());

// 결과 : null

위 코드는 준영속 상태로 변경된 엔티티의 정보를 출력하는 예시이다. 여기서 결과가 null이 나오는 이유는 영속성 컨텍스트에서 분리된 상태이기 때문에 해당 엔티티의 정보를 가져올 수 없는 것이다. 자세한 설명은 뒤에서 하겠다.

삭제

엔티티를 영속성 컨텍스트에서 삭제한다.

// 삭제
manager.remove(entity);

엔티티의 생명주기까지 알아보았으니 영속성 컨텍스트에 대한 이야기를 시작하자.

영속성 컨텍스트

개념

📖Persistence Context로 우리말로 해석하자면 ‘엔티티를 영구 저장하는 환경’이라는 의미이다.

영속성 컨텍스트는 논리적인 개념으로 엔티티 매니저 생성 시 하나가 생성되고, 엔티티 매니저를 통해 접근하고 관리되어질 수 있다.

// 엔티티 저장
entityManager.persist(entity);

위 코드는 이제까지 엔티티를 저장한다고 표현했지만 정확하게 이야기하면 엔티티 매니저를 사용해 엔티티를 영속성 컨텍스트에 저장한다고 표현할 수 있다.

영속성 컨텍스트는 여러 엔티티가 동일한 영속성 컨텍스트에 접근하도록 허용하기도 한다. 이 부분에 대해서는 다른 글에서 다루도록 하겠다.

특징

영속성 컨텍스트의 특징은 다음과 같다.

  • 영속성 컨텍스트에 등록된 엔티티를 식별자 값으로 구분한다. 따라서 영속 상태의 엔티티는 반드시 식별자 값이 있어야한다.
  • 영속성 컨텍스트는 트랜잭션을 커밋하는 순간 데이터베이스에 결과를 반영한다. 이것을 플러시(flush)라고 한다.

식별자 값과 플러시에 대해서는 잠시 뒤에 알아보도록 하고, 영속성 컨텍스트가 엔티티를 관리하면 어떤 장점이 있는지 알아보자.

  • 1차 캐시
  • 동일성 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경 감지(dirty checking)
  • 지연 로딩(lazy loading)

이제부터 위 특징들에 대해서 세부적으로 알아보자.

엔티티 조회(동일성과 1차 캐시)

엔티티 조회를 통해 영속성 컨텍스트가 지원하는 동일성과 1차 캐시에 대해서 알아보자.

영속성 컨텍스트는 내부에 캐시를 보유한다. 이것을 1차 캐시라고 부른다. 1차 캐시는 keyvalue의 형태를 가지며 는 @Id로 매핑한 식별자 키이고, 엔티티 인스턴스이다.

1차 캐시의 key는 데이터베이스의 기본 키와 매핑되어 있다. 즉, 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 데이터베이스의 기본 키 값이라고도 할 수 있다.

// 엔티티 조회
Member member = entityManger.find(Member.class, "member1");

위와 같이 엔티티를 조회할 때 사용되는 find() 메소드를 들여다보자.

public <T> T find(Class<T> entityClass, Object primaryKey);

파라미터를 보면 첫번째는 엔티티 클래스이고 두번째는 기본 키 값이다. 파라미터를 통해 1차 캐시에서 엔티티를 찾고 없으면 데이터베이스에서 조회하는 것이다.

아래 순서로 엔티티의 조회가 이루어진다.

  • 1차 캐시에서 조회 대상 엔티티를 찾는다.

  • 1차 캐시에 엔티티가 없다면 데이터베이스에서 조회한다.

위와 같은 원리로 메모리에 있는 1차 캐시에 엔티티가 있다면 데이터베이스와 불필요한 통신이 줄어들게 되어 성능상의 이점을 누릴 수 있는 것이다.

또한, 1차 캐시에서 엔티티를 찾을 때는 key가 같다면 동일한 인스턴스를 반환한다. 이 말은 엔티티의 동일성을 보장한다는 것이다. 아래 예시를 보자.

Member a = entityManager.find(Member.class, "member1");
Member b = entityManager.find(Member.class, "member1");

System.out.println(a == b);

// 결과 : true

a 엔티티와 b 엔티티는 식별자 값이 동일하다. 이 말은 영속성 컨텍스트의 1차 캐시에 저장된 key 값이 동일하기 때문에 엔티티 인스턴스도 동일한 인스턴스를 반환해주는 것이다.

이렇게 엔티티를 조회하는 경우에서 볼 수 있듯이 성능상의 이점과 엔티티의 동일성을 보장한다는 것을 확인 할 수 있다.

엔티티 등록(쓰기 지연)

엔티티 등록을 통해 영속성 컨텍스트가 지원하는 쓰기 지연에 대해서 이해해보자.

EntityManager entityManager = factory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();

// 트랜잭션 시작
transaction.begin();

// 엔티티 저장
entityManager.persist(entity1);
entityManager.persist(entity2);
// 여기까지 SQL 실행 X

// 커밋하는 시점에서 SQL 실행
transaction.commit();

위 코드를 보면 엔티티 매니저는 데이터를 변경 할 경우 트랜잭션을 시작하고 작업을 수행한다. 엔티티를 저장하기 위해 persist() 를 호출하였지만 해당 시점에서는 영속성 컨텍스트에만 엔티티가 저장되고 데이터베이스에 반영되지는 않는다. 이때 반영해야 할 SQL을 쓰기 지연 SQL 저장소에 보관하게 된다. 이후에 트랜잭션이 커밋되는 순간 데이터베이스에 반영할 SQL을 수행한다.

위 그림에서 보이는 것처럼 반영될 SQL을 모아서 한번에 데이터베이스에 보내는 것을 쓰기 지연(transactionnal write-behind)이라고 한다. 트랜잭션을 커밋하면 엔티티 매니저는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업인 플러시(flush)를 수행한다.

그렇다면 쓰기 지연이 가능한 이유에 대해 알아보자.

// 트랜잭션 시작
transaction.begin();
save(a);
save(b);
save(c);

// 커밋
transaction.commit();

위 코드에서 a, b, c 객체를 저장 할 때 각 save() 메소드마다 쿼리를 보내는 것과 트랜잭션이 커밋할 때 한번에 보내는 것에 대한 결과는 동일하다. 그렇기 때문에 쿼리를 그때 그때 보내도 결과는 커밋이 되어야 반영이 된다. 즉, 커밋을 하지 않으면 소용이 없다.

이 말은 어떻게든 커밋이 수행되기 전에만 쿼리문을 데이터베이스에 전달하면 되기 때문에 쓰기 지연이 가능한 것이다. 이렇게 쓰기 지연을 잘 활용하면 쿼리를 한 번에 전달해서 성능 최적화를 노려 볼 수 있다.

엔티티 수정(변경 감지)

엔티티 수정을 통해 영속성 컨텍스트의 변경 감지에 대해서 알아보자.

SQL문을 직접 작성 할 경우 애플리케이션 규모가 커져감에 따라 직접 수정 쿼리를 추가 또는 변경해야하는 일들이 빈번하게 발생한다. 이러한 과정에서 비즈니스 로직을 분석하기 위해 SQL을 계속 확인하게되고, 비즈니스 로직이 SQL에 의존하게 된다.

영속성 컨텍스트는 이러한 불편함을 제거하기 위해 변경 감지를 지원하는데 다음 코드를 보자.

EntityManager entityManager = factory.createEntityManager();
EntityTransaction transaction = entityManeger.getTransaction();

// 트랜잭션 시작
transaction.begin();
// 엔티티 생성
Member member = new Member();
member.setId("id1");
member.setName("member1");
// 엔티티 저장
entitymanager.persist(member);

// 엔티티 변경
member.setName("member2");

// 커밋
transaction.commit();

위 코드에서 update() 와 같은 형태의 메소드가 없다. 하지만 member.setName() 을 통해 영속성 컨텍스트에 저장된 엔티티의 정보가 수정되었다. 이게 가능한 이유는 엔티티의 변경사항을 영속성 컨텍스트가 자동으로 감지하여 데이터베이스에 반영하는 것이다. 이게 바로 변경 감지(dirty checking)이다.

다음 그럼을 보면서 이해해보자.

JPA는 엔티티를 보관할 때 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라고 한다. 플러시 시점에 엔티티와 을 비교해서 변경된 엔티티를 생성하고, 해당 엔티티의 수정사항을 데이터베이스에 반영한다.

이렇게 영속성 컨텍스트가 변경 감지를 적용할 수 있는 대상은 영속 상태의 엔티티만 가능하다. 변경 감지로 인한 이점은 다음과 같다.

  • 모든 필드를 업데이트 하기 때문에 수정쿼리가 항상 같다.
  • 이전에 한번 파싱된 쿼리를 재사용할 수 있다.

하지만 엔티티의 필드가 너무 많거나 저장되는 내용이 크다면 오히려 성능이 저하 될 수 있다. 이럴 때는 동적으로 SQL을 생성하는 전략을 선택한다.

@Entity
@DynamicUpdate //하이버네이트 확장기능
@Table
public class Test{...}

위 어노테이션을 사용하면 수정된 필드만을 사용해서 동적으로 SQL문을 생성한다.

결론적으로 JPA에서는 update() 와 같은 메소드가 없으며, 영속 상태로 등록된 엔티티의 상태를 변경하면 변경 감지를 통해 데이터베이스에 자동으로 반영한다.

엔티티 삭제

엔티티 삭제의 경우 엔티티의 생명주기 4개 중 삭제에 해당하므로 추가적인 설명은 없다. 다만, 여기서 주의해야 할 사항은 영속성 컨텍스트에서 삭제된 엔티티는 재사용하지 않고 자연스럽게 가비지 컬렉션의 대상이 되도록 주는 것이 좋다.

해당 엔티티를 통해 엔티티 매니저의 기능을 활용하는 등의 오류가 발생하기 쉬우며 개발 시 혼동을 초래하기 때문이다.


마무리

영속성 관리에 대한 부분은 JPA에서 가장 중요한 개념 중 하나이기 때문에 해당 포스팅만으로는 전부를 설명 할 수 없기 때문에 다음 글에서 영속성 관리의 나머지 부분을 다루도록 하겠다.

그럼 이만.👊🏽

profile
서핑하는 개발자🏄🏽

0개의 댓글