JPA에 관한 질문들

JPA에 관한 질문들

  • 프로젝트에 JPA를 사용하셨는데 다른 사람의 리포지토리를 보면 아시겠지만 MyBatis를 사용한 경우가 많았다. MyBatis를 사용하지 않은 이유는? JPA가 갖는 이점은?

우선 저밖에 JPA를 사용하지 않은 것 같아 조금 당황스럽기도 했다. 그러나 주니어 개발자가 가장 많이 성장하고 많은 것을 배울 수 있는 건 JPA를 학습하는 일이라고 생각한다.

먼저 JPA를 통해 웹 애플리케이션을 설계하다보면 자연스레 객체지향적 설계에 대해 고민하게 된다. 어떻게 하면 JPA를 JPA답게 사용할까 고민하고 공부하는 그 자체가 객체지향 프로그래밍에 도움이 된다.

MyBatis는 ORM이 아니다. 물론 쿼리와 로직을 분리해낼 수 있는 템플릿 역할을 수행해주긴 하지만 도메인 객체의 설계가 바뀔 때마다 복잡한 설정의 변경 과정이 수반되어야 한다. 결국 쿼리를 미리 짜놓고 객체를 설계하는, 프로그램의 전체적인 개발 과정이 쿼리에 강하게 종속되는 문제를 절대 해결할 수 없다. JPA는 그런 부담감에서 개발자를 해결시켜주기 때문에 쿼리를 미리 생각해놓고 객체를 설계하는, 객체 지향이 아닌 쿼리 지향 프로그래밍을 해야하는 상황을 막아준다.

  • 꼬리질문: JPA가 객체지향적 사고에 도움이 된다고 했는데, JPA를 통한 객체지향적 설계를 가능케 해주는 기능들에 대해 설명해 보아라.

    • 도메인 객체간에 상속관계를 설정할 수 있다. @MappedSuperclass@Inheritance 애노테이션을 사용할 수 있고, 이런 상속 관계를 테이블에 매핑하는데 있어 TABLE_PER_CLASS나 JOINED 와 같은 전략을 설정할 수도 있다.
    • 도메인 객체의 속성을 별도의 값 타입으로 분리해낼 수 있다. @Embedded@Embeddable 을 통해 가능해지는 것이다. 이번에 핵데이를 참가하며 영속성 관리를 위해 마이바티스를 사용한 참가자들을 보니 대부분의 도메인 객체 필드들이 String 아니면 Integer였다. 그룹지어 객체 단위로 관리할 수 있는 속성들이 있음에도 마이바티스로 매핑해줘야 하는 부담이 있어 별도의 객체로 빼지 않은 것이다.
    • @Embeddable 객체를 선언하면 JPA는 이를 별도의 테이블로 관리한다. PK는 없고, 자신을 갖고 있는 객체의 ID를 조인컬럼으로 관리하게 된다.
  • JPA가 데이터를 저장하고 조회하는 과정을 논해 보세요.

Spring Data JPA의 규약에 따라 개발자는 repository 인터페이스를 선언한다. 선언된 리포지토리 인터페이스는 런타임 시에 Spring 의 ApplicationContext에 의해서 프록시 객체로 구현되어 스프링 빈으로 관리되게 된다.

개발자는 @Service@Controller 어디에서도 리포지토리를 호출할 수 있지만 베스트 프랙티스는 서비스 클래스에서 리포지토리를 호출하는 것이다.

repository bean에 정의된 메소드가 호출되면 영속성 콘텍스트가 만들어진다. 영속성 콘텍스트는 요청 1개(스레드 1개)에 하나씩 만들어지고 한번 쓰면 내다버린다. 1회용이다.

  • 꼬리질문: 한번 쓰고 내다버리면 오버헤드가 너무 큰 것 아닌가? 연산 비용이 너무 많이 소요될 거 같은데?
    • 그에 대한 고려를 해 Spring Data JPA에서는 EntityManagerFactory라는 객체를 두고 있다.
    • 공장을 세우는 건 비용이 많이 들지만 공장에서 제품을 찍어내는 것은 비용이 그리 많이 들지 않는다.

영속성 콘텍스트가 요청을 접수한다. 영속성 콘텍스트는 영속성 콘텍스트 공장으로부터 전달받은 datasource를 통해 DB에 객체 정보를 질의하고, ResultSet이 돌아오면 갖고 있던 객체 정보를 통해 객체를 생성해 돌려준다. 유념할 것은 이 객체는 새로 만들어진 객체라는 점이다. 이 때 객체의 상태는 detached 상태다.

  • 꼬리질문: 객체의 상태란 무슨 말인가? detached라면 어디서 떨어졌다는 말인지?
    • 영속성 콘텍스트의 세션에서 분리되었다는 말이다.
    • JPA에서 도메인 객체는 세 가지 상태를 갖는다 : MANAGED, DETACHED, TRANSIENT이다.
    • TRANSIENT는 아직 저장되지 않은 신삥 객체이므로 신경쓸게 별로 없지만 JPA를 처음 학습할 때 가장 구분하기 어려운 개념이 MANAGED와 DETACHED이다.
    • MANAGED 객체는 영속성 콘텍스트가 접수한 객체이며 지금 관리중인 객체 이다. 이 객체에 대해선 dirty check나 lazy fetch와 같은 영속성 콘텍스트가 제공해주는 편의 기능을 모두 사용할 수 있다.
    • DETACHED 객체는 영속성 콘텍스트가 접수한 적은 있지만 지금 관리하고 있지는 않은 객체 이다. 이 객체에 대해서는 JPA가 제공해주는 편의 기능을 하나도 사용할 수 없음에 유의해야 한다. 만약 이 객체가 관리하고 있는 field 중 lazy fetch하는 필드가 있다면 detached 상태에서는 어떠한 수를 써서도 이 필드를 가져올 수 없다.

조회에 대해서는 그렇게 혼동되는 개념이 없는데 저장은 조금 헷갈리는 개념 있을 수 있다. 가장 헷갈리는 개념은 EntityManager.persist() 가 호출된다고 해서 바로 데이터가 DB에 작성되는 것이 아니다.

JPA 환경에서 모든 쿼리는 쓰기 지연 저장소에 저장된다. 모든 쿼리를 저장해놨다가 EntityManager.flush() 가 호출되거나 모종의 사유로 엔티티 매니저 즉 영속성 콘텍스트가 문을 닫으면 그때 비로소 DB에 컨택해 트랜잭션을 시작하는 것이다.

  • 꼬리질문: 그러면 Spring Data JPA에서 영속성 콘텍스트는 언제 열리고 언제 닫히나?

    • 기본적으로 Spring Data JPA에서는 트랜잭션을 repository bean 안으로 고립시켜놓고 사용한다.
    • 이렇게 하면 영속성 콘텍스트에 대해 학습이 부족한 개발자들도 영속성 콘텍스트를 마치 JCF 다루듯이 편하게 쓸 수 있고, 쓰기 지연 등 JPA의 특성에 대한 이해가 없어도 쉽게 사용할 수 있을 것 같다.
    • 그러나 JPA에 대한 이해를 갖춘 상태에서 이렇게 트랜잭션을 고립시키는 것은 옳지 않을 수도 있다.
    • 가장 흔히 쓰이는 방법은 서비스 클래스에 repository를 주입받고 이 repository를 사용하는 메소드에 대해 @Transactional 애노테이션을 붙여주는 것이다.
    • 이렇게 되면 서비스 빈이 Spring AOP에 의해 트랜잭션 부가 기능이 추가되어 트랜잭션의 범위가 리포지토리를 침범해 메소드 전역으로 확대되게 된다.
    • 이 메소드 안에서 repository.findById() 등을 불러 객체를 조회하게 되면 이 객체는 DETACHED가 아닌 MANAGED 상태가 된다.
    • 이 객체가 return되어 메소드의 실행이 종료되기 전까지 JPA가 제공하는 쓰기지연, dirty check와 같은 부가 기능들을 그대로 사용할 수 있고 lazy fetch의 이점도 누릴 수 있다.
  • 꼬리질문: Transaction이 지연되어 실행된다고 했는데 그러면 내부적으로 dirty read와 같은 현상이 재현되는 것 아닌가?

    • 그렇지 않다. 왜냐하면 JPA는 객체를 조회할때도 먼저 DB에 질의하는 것이 아니라 자신이 보관하고 있는 객체 스냅샷 저장소를 먼저 들여다보기 때문이다.
    • 영속성 콘텍스트 공장에서는 JPA가 관리할 객체 (@Entity가 선언된 모든 객체)를 분석한 후 각 객체마다 하니씩 해시테이블의 형태로 객체를 보관할 수 있는 스냅샷 저장소를 만든다.
    • 이 저장소에는 영속성 콘텍스트가 열려있는 동안 저장되거나 잘의된 모든 객체들이 들어있다.
    • 따라서 특정 객체에 대한 요청이 들어오면 이 저장소에 만약 같은 PK나 @Id를 갖는 객체가 들어온다면, 이미 변경된 상태의 객체가 리턴되기 때문에 dirty read의 가능성은 없다.
    • 그리고 한 메소드 안에서 수정과 조회를 같이 수행할 일이 거의 없다. 어차피 영속성 콘텍스트는 자신이 죽을 때 Transaction을 모두 수행하기 때문에 새롭게 메소드에 진입한다거나 하는 경우에는 전혀 해당사항이 없다.