이 글은 3달 전쯤? 단순한 계기로 노션에 작성해둔 글을 수정해서 올려놓은 글입니다. 글 내용도 갑작스럽게 끝나는 느낌이 있어서 수정을 하는 와중에도 이렇게까지 다룰만한 내용인가, 올려놔도 되나? 하는 의구심도 들었는데요.  

 

그냥 이 사람은 그렇게 이해한 상태이구나 정도의 가벼운 마음으로 봐주시길 바랍니다. ㅎㅎ 

 

 

JPA에서는 특정 Column을 별도의 영역에 정의하는 2가지를 방법을 제공한다.

여러 column(field)을 하나의 embedded type으로 정의하여 객체에 포함하는 방법이고 다른 하나는 별도의 추상 클래스를 만들어 변수를 정의하고 @MappedSuperclass로 상속시키는 방법이다.

 

언급된 이 2가지 방식은 어떠한 차이가 있고 특정 column을 정의할 땐 어느 방법을 선택해야 할까?

 

??? : 이건 그냥 상속을 사용하는 것과 조합(위임)을 사용하는 것의 차이에요. 개발을 할 때에는 조합 방식을 쓰는 게 좋아요.

  • 이 질문에 대한 답변으로 가끔 나오는 내용이다. 이것만으로 사용 이유를 시원스럽게(?) 이해할 수 있을까?

 

우선 왜 저런 이야기가 나오는지 짚어보자.

 

 

 

객체지향 관점에서는 "상속보다는 조합을 사용하자"라는 이야기가 정설로 통한다.

이는

  • super type과 sub type이 강한 결합성을 가짐으로써 캡슐화가 깨지고 속한 객체들이 변경의 여파를 받게 된다는 것
  • 한 객체에서 정의된 상태나 행위에 대해서 관리하거나 확인해야 할 포인트(class)가 늘어난다는 것
  • sub type에서 메서드를 재정의하는 경우에 잘못된 이해로 특정 로직만 추가하고 super type의 메서드를 호출하고 있을 때 오동작할 수 있다는 것
  • super type의 메서드가 변경되어서 sub type의 메서드가 깨지거나 오동작할 수 있는 것

등의 문제점들을 기반으로 한다.

 

그렇기에 super type의 기능 혹은 다른 객체의 기능을 사용할 것이라면 이를 상속받는 것이 아니라 상태 값 즉 인스턴스 변수를 통해 기능을 사용하는 것을 추천하게 되는 것이다.

 

그럼 모든 column을 정의할 때 embedded type으로 묶어서 정의하는 것이 가장 좋을 것일까? 

  • createdDate, updatedDate, creator, updator와 같은 운영 상의 이유를 포함하는 Base column을 적용할 때에도 이 방식이 좋은 것일까?

위의 내용들을 이해했다면, 상속 방식 즉. MappedSuperclass를 사용해도 무관할 것 같은 상황이 있다고 느끼리라 생각한다.

 

 

 

우선 위에서 언급된 문제점을 바탕으로 생각해보자.

첫 번째는 위에서 나온 단점들은 객체의 캡슐화가 깨지는 것과 관리 포인트가 늘어난다는 점을 제외하고는 객체의 행위와 관련된 문제점들이라는 것을 알 수 있다. 즉 일반적으로 entity의 상태 변수를 상속하여서 발생하는 상황들은 아니다.

 

 

두 번째는 추상 클래스가 올바르게 사용되는 시점은 해당 type의 "공통된 속성"을 한 곳에 모아 관리할 때이며, 엔티티는 객체이기도 하지만, 데이터 영역에 접근하는 Adapter(JDBC API로 구현된 DAO, Repository)의 DTO. 즉 특정한 데이터 홀더라고 봐야 한다고 생각한다는 점이다. 나는 그렇게 생각한다.

 

여기서 말하고 싶은 것은 엔티티는 객체 지향 관점에서의 객체와는 어느 정도 거리가 있는 녀석이라는 것이다.

 

특히 entity의 공통된 속성들(Create, UpdateDate, creator, updator)의 경우 어떤 개념(객체)에 묶이는 것도 아니고 일반적으로 변경될만한 속성도 아니기에 (선택적으로 사용되는 경우에도 문제없다) 변경의 여파는 거의 발생하지 않을 것이다. 즉 각각의 엔티티의 상태로 조합하여 사용하는 것과 별반 차이가 없다.

 

이러한 데이터는 유연성을 고려할 필요도 없다. 사용하거나 하지 않거나 필요에 따라서 계층적으로 분리하여 사용할 수 있다.

 

 

 

이번엔 JPA Spec 기준으로 생각해보자.

첫 번째로 @MapperdSuperClass는 Spec에 적힌 대로 공통 정보를 상속하는 것에 목적을 두고 만들어진 방법이다.

부가적으로 내부에 정의된 상태를 별도로 나타내지 않고 상속받은 엔티티에서만 나타내기 때문에 결과적으로 완전한 상태의 엔티티를 바라보게 된다.

[2.11.2] Typically, the purpose of such a mapped superclass is to define state and mapping information that is common to multiple entity classes.

 

 

두 번째는 MapperdSuperClass를 사용하는 방식이 embedded type에 비해서 매우 간단하다는 점이다. 운영과 관련된 column의 경우 해당 방식을 통해 쉽게 적용하고 사용할 수 있다.

@MappedSuperclass 
public abstract class BaseEntity { 
    private LocalDate createDate; 
    private LocalDate updateDate; 
} 

@Entity 
@NoArgConstructor(access = AccessLevel.PROTECTED) 
public class PostEntity extend BaseEntity { //... }

 

위와 같은 상황에서 extend BaseEntity이라는 구문만을 추가함으로써 (상속의 간편함) 사용할 수 있으며, 자동으로 값을 생성하여 매핑해주는 @EntityListeners(AuditingEntityListener.class)와 같은 기능도 제공받을 수 있다.

 

 

세 번째로는 JPQL을 사용할 때인데, 엔티티에서 embedded type까지 쿼리 하기 위해서 type 명을 명시해야 하는 불편함이 존재한다.

 

이 부분은 영한님이 답변해주신 이 링크를 참고해보는 게 좋을 것 같다.

https://www.inflearn.com/questions/18578

 

 

갑자기 결론?!

 

결론

이 글에서 언급된 column들은 운영이나 통계 등의 도메인 외부의 요소를 위한 것들로 객체의 상태 관점보다는 데이터 그 자체로써 사용되는 의미가 짙다고 생각된다.

 

embedded type은 특정 의미(도메인, 비즈니스 성격)를 가진 column들이 중복적으로 정의되어 코드 수정 시 불편함이나 해당 값을 다루는 메서드를 한 곳에 응집시키기 위해 사용되는 방법이다. 대부분의 상황에서 이 방식을 고려하는 것이 좋겠지만 단순한 데이터를 다룰 때에는 단순한 방식을 선택하여 처리하는 것이 더 나은 경우도 있다. 

 

위에서 언급된 상황에서는 embedded type을 사용하는 것보단 MapperdSuperClass를 사용하는 것이 좋다고 생각한다. 

 

자바 ORM 표준 JPA 프로그래밍 서적을 학습하면서, 정리하고 있는 내용입니다.

 

 

EntityManagerFactory

사용자의 요청이 들어옴에 따라 EntityManager를 생성하는 책임을 가지고 있다.

 

JPA를 동작시키기 위한 내부 객체들이 이 Factory를 통해 생성되고, 구현체에 따라선 커낵션 풀도 별도로 구성하므로 생성 비용이 크다고 한다.

 

여러 EntityManagerFactory를 생성하여 공유할 수 있으나. 하나의 객체만 생성해서 전 영역에 공유하는 것이 권장된다. (configuration lite mode를 사용하면 여러 개를 생성하고 공유할 수 있을 것 같다.)

 

 

 

EntityManager (Persistence Context)

엔티티 정보를 저장하고 있는 환경. 요청 결과가 DB에 반영되기 이전에 위치하는 논리적인 영역을 의미한다. 가상의 데이터베이스 영역이라고 봐도 무방하다.

 

사용자의 요청을 받아 Connection을 통해 DB와 로컬 트랜잭션을 맺으며, 데이터를 관리하고 영속적으로 반영하는 혹은 삭제하는 책임을 가지는 객체이다.

 

JPA의 대부분의 기능은 해당 객체가 지원한다. 커넥션과 밀접한 관계를 가지기에 스레드 간의 공유, 재사용이 어렵다.

멀티스레드에 안전하지 않은 상태가 된다는 것 아닐까?

 

 

 

Entity의 Life Cycle

  • 비영속 (new, transient) : 영속성 컨텍스트와 관계가 없는 새로 생성된 상태

      // 비영속 상태의 Entity. 즉 entityManager가 모르고 있는 Entity이다.
      Entity entity = new Entity();
      entity.setXxxA("test");
      entity.setXxxB("value");
  • 영속 (managed) : 영속성 컨텍스트에게 관리되는 상태 (환경 안에 정보가 있는 상태)

    JPA는 기본적으로 트랜잭션 안에서 데이터를 변경하여야 한다.

      // 요청에 따른 entityManager(Persistence Context) 생성
      EntityManager entityManager = entityManagerFactory.createEntityManager();
    
      // 영속성 컨택스트에 대한 글로벌 트랜잭션 관리 시작
      EntityTransaction transaction = entityManager.getTransaction();
      transaction.begin();
    
      try {
    
              // 비영속 상태의 Entity.
              Entity entity = new Entity();
              entity.setXxxA("test");
              entity.setXxxB("value");
    
              // 영속성 컨택스트에 Entity 정보를 저장. 즉 entityManager가 알고있는 Entity이다.
              entityManager.persist(entity);
    
              // transcation 결과 반영 - (DB 저장 X, 영속성 컨텍스트에만 반영)
              transaction.commit();
      } catch (RuntimeException exception) {
              // 위 로직 실패 시 결과 반영 X -> 원복
              transaction.rollback(); 
      } finally {
              // 성공, 실패 상관없이 transaction 종료
              transaction.close();
      }
  • 준영속 (detached) : 영속성 컨텍스트에 관리되다가 이후 분리된 상태

    영속성 컨텍스트가 지원하는 기능을 사용할 수 없기에 비영속 상태와 유사하다. 다른 점은 식별자 값(id)이 존재한다는 점이다.

      // 특정 회원 엔티티를 영속성 컨텍스트에서 분리한다.
      // 쉽게 말하면 해당 엔티티를 영속성 컨텍스트에 존재하는 1차 캐시에서 삭제한다.
      entityManager.detach(entity);
    
      // 해당 영속성 컨텍스트에서 관리되는 모든 엔티티를 준영속화한다.
      // 쉽게 말하면 영속성 컨텍스트의 1차 캐시를 초기화한다.
      emtityManager.clear();
    
      // 영속성 컨택스트를 종료시킨다.
      entityManager.close();
  • 삭제 (removed) : 엔티티가 삭제된 상태

      // 해당 객체를 삭제. 즉 DB에 저장된 정보를 삭제하는 요청을 전송한다. 
      entityManager.remove(entity);

    준영속과 삭제의 다른 점은 Entity가 DB에 반영되기 전이냐 이후이냐의 차이이다.

 

 

 

JPA Internal Flow

 

 

 

Persistence Context의 특징

  • 영속성 컨텍스트와 식별자 값

    영속성 컨텍스트는 @ID로 매핑된 값을 통해 엔티티를 구분한다. 즉 해당 값이 무조건 존재해야 하며, 없는 경우 예외가 발생한다.

  • 데이터베이스 저장 시점

    영속성 컨텍스트에 저장, 관리되는 엔티티는 flush()가 호출된 시점에 실제 DB에 반영된다.

  • 엔티티 관리 방식의 장점 - 아래

 

 

 

JPA 엔티티 관리 방식의 장점

  • 1차 캐시

    영속성 컨텍스트에서 관리되는 데이터를 우선적으로 조회하는 것을 의미한다.

    사용자의 요청이 종료된다면 해당 영역도 제거되기에 하나의 비즈니스 로직에서만 이점을 제공한다.

     

    전역적으로 관리되는 캐시는 2차 캐시라고 부른다.

     

     

    해당 정보가 컨텍스트 안에 존재하지 않는다면,

    • DB에서 해당 정보를 조회한 후 (SELECT)

    • 쿼리 결과를 이용하여 엔티티를 생성한 다음

    • 컨텍스트 내부에 엔티티를 저장하고 반환한다.

    • 1차 캐시는 내부적으로 Map을 통해 관리되며, @Id로 매핑된 값을 Key로 가진다. 이 값은 DB의 PK 값인 경우도 존재한다.*

       

  • 동일성 보장

    1차 캐시를 이용하여 반복 가능한 읽기 등급의 격리 수준을 Application 수준에서 제공한다. 즉 같은 트랜잭션 내에 여러 번 조회 쿼리를 진행하더라도 동일한 결과를 반환받는 것을 이야기한다.

     

    이때에는 동일성 비교가 가능하다. → Map에서 동일한 엔티티를 제공한다.

     

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

    한 트랜잭션 내에서 여러 엔티티를 등록할 때 각각의 엔티티에 대한 Insert Query를 DB에 바로 요청하지 않고, 일종의 버퍼처럼 컨택스트 내부에 존재하는 SQL 저장소에 Query를 쌓아놓다가 1번에 반영할 수 있다.

     

    entityManager.commit();

     

    큰 오버헤드를 가지는 i/o 처리를 최소화할 수 있다.

     

  • 변경 감지

    영속성 컨텍스트에게 관리되고 있는 엔티티 정보에 대해서 Service logic에서 setter 등을 통해 수정을 가할 경우, 그 변경 결과가 트랜잭션을 커밋하는 시점에 영속성 컨텍스트에게 감지되고 최종적으로 Update Query가 DB로 전송되어 최신 정보로 변경하게 된다.

     

    엔티티 정보와 엔티티 스냅샷 정보를 비교하여 감지한다.

     

    스냅샷 정보는 DB에서 값을 읽어와 캐시에 저장한 시점의 엔티티 정보를 복사하여 저장해둔 것이다. 이것을 트랜잭션이 커밋된 시점에서 엔티티 정보와 비교하여 변경된 사항들에 대해서 Update Query를 생성하는 것이다.

     

    엔티티 삭제도 동일 메커니즘을 공유하고 트랜잭션 커밋 시 Delete Query를 전송한다.

     

     

     

Flush?

영속성 컨텍스트의 변경 내용들을 데이터베이스에 반영시키는 기능이다.

 

영속성 컨텍스트에 저장된 정보를 비우는 것은 아니다.

 

 

Flush 발생 시 Flow는

  • 엔티티에 대한 변경을 감지하고
  • 추가, 수정, 삭제된 엔티티 정보에 대한 Query를 내부 SQL 저장소에 등록한 뒤
  • 해당 정보를 DB에 전송한다.

이렇게 이루어진다.

 

 

Flush를 발생시키는 방법으로는

  • em.flush()를 통한 직접 호출

  • 트랜잭션 commit() 을 통한 자동 호출

  • JPQL으로 만들어진 Query 실행 시 자동 호출

    영속성 컨텍스트에만 관리되고 있는 영속 상태의 엔티티들은 사실상 DB에 존재하지 않으므로 JPQL을 통한 Query 요청 시 결과에서 이 것들을 확인할 수 없게 된다. 이를 방지하기 위해 미리 Flush()를 호출하여 DB에 반영한 뒤 정상적인 결과를 제공하게 된다.

가 있다.

 

 

Flush 모드 옵션

  • FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 Flush를 실행한다.
  • FlushModeType.COMMIT : 커밋할 때만 Flush를 실행한다.

'Programming' 카테고리의 다른 글

Open Authorization Framework 2.0?  (0) 2021.03.01
Live Study_Week 14. Generic  (0) 2021.02.28
객체 지향 디자인의 핵심 개념 : 책임 [ GRASP ]  (4) 2021.02.11
Java의 Reflection API와 성능 이슈?  (0) 2021.02.02
G1 GC  (0) 2021.01.26

+ Recent posts