스프링 데이터 JPA : 구현체 확인

    이 게시글은 인프런 영한님의 강의를 복습하며 작성한 글입니다.


    스프링 데이터 JPA 클래스 관계도

    스프링 데이터 JPA는 다음과 같이 JpaRepository 인터페이스를 제공해주고, 이 인터페이스를 상속받은 인터페이스를 사용하게 되면 JPA를 좀 더 편리하게 사용할 수 있게 된다.

     

    JpaRepository 인터페이스의 클래스 다이어그램은 위에서 확인할 수 있다.

    • JpaRepository의 패키지는 "org.springframework.data.jpa.repository"으로 되어있다. JpaRepository는 JPA 패키지에 있기 때문에 JPA에서만 활용할 수 있다는 것을 알 수 있다. 
    • PagingAndSortingRepository, CrudRepository 등등은 "org.springframework.data"에 있는 것을 알 수 있다. 이것은 JPA 뿐만 아니라 다른 형태에서도 사용할 수 있다는 것을 알 수 있다. 예를 들면 MyBatis 등등이 될 수 있다. 

     


    스프링 데이터 JPA 구현체 확인

    스프링 데이터 JPA 구현체를 확인하기 위해서는 JpaRepository 인터페이스로 들어간 후, 옆에 있는 화살표를 클릭해서 관련된 인터페이스 + 구현체를 살펴보면 된다. 

    JpaRepository의 구현체는 'SimpleJpaRepository'가 된다. 

    SimpleJpaRepository는 내부적으로 @Repository, @Transactional 어노테이션을 가진다.

    • @Repository 어노테이션
      1. @ComponentScan의 대상이 된다.
      2. JPA에서 발생하는 Exception을 Spring에서 사용하는 Exception으로 바꿔준다.
    • @Transactional(readOnly = true) 
      1. 읽기 전용이기 때문에 em.flush()를 생략한다. (JPA 15.4.2 책 읽기 전용 쿼리의 성능 최적화 참고)

    이런 이유로 인해 스프링 빈으로 따로 등록하지 않아도 되고, JpaRepository 인터페이스를 상속받은 인터페이스에서 @Transactional이 없어도 트랜잭션이 사용가능한 것이다. 

     


    조회 메서드 → ReadOnly로만 동작

    SimpleJpaRepository는 내부적으로 조회 메서드들은 따로 @Transactional 어노테이션을 가지지 않는다. 즉, 클래스 레벨의 @Transactional을 함께 공유한다는 뜻이다. 이 메서드들은 읽기 전용으로만 동작한다. 

     


    저장 메서드 → @Transactional(readonly = false) 확인

    저장 메서드는 내부적으로 @Transactional을 가진다. @Transactional의 readonly 옵션의 default 값은 false다. 따라서 이 메서드를 사용하게 되면, 트랜잭션이 수정된 것을 반영해준다. 그런데 Save 메서드에는 한 가지 특이한 점이 있다. 조건문으로 갈린다는 점이다.

    • Entity가 새로운 객체라면 → em.persist
    • Entity가 새로운 객체가 아니라면 → em.merge

    를 한다는 점이다. persist와 merge는 다음과 같이 동작한다.

    • em.persist
      1. insert 쿼리를 바로 보낸다.
    • em.merge 
      1. select 쿼리로 동일한 PK를 가지는 Entity를 DB에서 불러온다.
      2. DB에서 불러온 Entity가 있으면 덮어쓰고 Insert 쿼리를 보내고, 없으면 그냥 그대로 insert 쿼리를 보낸다. 

    em.merge는 이 두 가지 동작 때문에 거의 사용되지 않는다. 왜냐하면 조회 쿼리가 한번 더 나가서 네트워크 비용이 증가하는 단점이 있고, 덮어쓰게 되면 그것 자체로도 큰 문제가 될 수 있기 때문이다. 

    또한, Merge를 하게 되면 지금 가지고 있는 객체를 덮어쓰는 것이기 때문에 DB에 위와 같이 null 값이 들어가는 경우가 발생할 수 있다. 이런 경우를 방지하게 위해 주로 객체를 불러와 값을 수정해서 Update 쿼리를 하는 것이 추천된다. 

     


    저장 메서드는 어떻게 새로운 엔티티를 판단하는가?

    저장 메서드를 보면 isNew라는 메서드를 이용해서 entity가 새로운 값인지를 살펴본다. 

    • PK가 객체(Long, Integer, 등등) : null이면 새로운 엔티티
    • PK가 자바 기본타입(long, int, 등등) : 0이면 새로운 엔티티 
    • Persistable 인터페이스를 구현해서 판단 로직 변경 가능 

    새로운 엔티티를 판단하는 EntityManager의 기본 전략은 다음과 같다. 위 전략을 살펴보면 기본적으로 JPA가 제공하는 @GeneratedValue를 사용하는 사람들이라면 큰 문제가 없다는 것을 인지할 수 있다. 

     

    @GeneratedValue는 엔티티 객체가 처음에 영속성 컨텍스트로 들어가는 시점에 PK 값을 객체에 저장해준다. 그런데 기본적으로 엔티티 객체를 생성하는 시점에 PK 값은 어떤 값도 선언하지 않고, 기본적으로 Long 타입을 사용한다. 따라서 객체이며 어떤 값도 선언하지 않았기 때문에 null이라는 값이 될 것임이 보장된다. 이 때문에 기존에 사용하던 방식으로 하면 항상 em.persist가 보장되었던 것이다. 

     

     문제가 되는 경우는 PK값을 @GenerateValue를 쓰는 것이 아니라 개발자가 직접 채번할 때 문제가 발생한다. 직접 채번을 한다면 객체를 생성하는 시점에 어떤 값이 들어갈 것이다. 예를 들어 UUID로 개발자가 PK값을 채번한다고 하면, 객체가 만들어지는 순간 UUID가 들어가있을 것이다. UUID는 String 형태로 바꾸어서 넣는다.

    이 때 String은 null 값도 아니고 0도 아니기 때문에 엔티티매니저는 이 객체를 DB에 있는 객체라고 판단을 한다. 따라서, merge 쿼리가 동작할 수 밖에 없는 상황이 보장된다. 

    댓글

    Designed by JB FACTORY