잡동사니

JPA 추전 강좌, 자바 ORM 표준 JPA 프로그래밍 - 기본편 내용 정리 및 후기 본문

IT/JPA

JPA 추전 강좌, 자바 ORM 표준 JPA 프로그래밍 - 기본편 내용 정리 및 후기

yeTi 2019. 12. 13. 17:26

인프런의 자바 ORM 표준 JPA 프로그래밍 - 기본편 - 김영한 을 수강하면서 내용을 정리하고 간단한 후기를 남깁니다.

공감

개발자는 SQL 맵퍼다.
객체지향 설계를 할수록 맵핑작업만 늘어난다.
계층형 설계를 하더라도 결과물에 대한 신뢰를 할 수가 없다. (진한 의미의 계층 분할이 어렵다.)

가지고 있던 고민

객체지향 설계를 할때 필요없는 시점의 데이터도 조회하여야 하는가?
예) 회원 정보가 다른 정보와 연관관계를 가지고 있을때 회원 정보만 필요해도 다른 정보도 조회해서 모리에 가지고 있어야 하는가 - Proxy 활용

얻은 정보

객체의 정보를 update 할때는 객체의 정보만 변경해주면 알아서 DB에 반영된다. - Dirty Checking

JPQL

SQL을 추상화하여 객체기반 쿼리이다.

EntityManager

자바 Persistence API 중 하나로 JPA 2.0 스펙을 따르고, 영속성 컨텍스트와 상호작용하는 인터페이스로 엔티티의 추가, 수정, 삭제, 조회를 한다.

엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하지만(thread-safe), 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안된다.

위의 그림과 같이 EntityManagerFactory는 고객의 요청에 따라 EntityManager를 생성하고 EntityManager는 내부적으로 커넥션을 사용하여 데이터를 사용한다.

구성 환경에 따라 엔티티 메니저는 영속성 컨텍스트와 1:1 혹은 N:1의 관계를 가진다.

Persistance Context (영속성 컨텍스트)

어플리케이션과 DB사이에 존재하는 계층
1차 캐시와 쓰기지연 SQL 저장소로 구성된다.
persist할때 1차 캐시에 엔티티를 저장하고 쓰기 지연 SQL 저장소에 SQL을 저장한 후 커밋 시점에 DB에 저장한다.

영속 상태 : 엔티티가 영속 컨텍스트에 존재하는 상태
준영속 상태 : 엔티티가 영속 컨텍스트에 존재하다가 제거된 상태

변경 감지(Dirty Checking)
영속성 컨텍스트내에 있는 1차 캐시에 엔티티를 가지고 있고 조회시점에 스냅샷을 만들어 놓는다.
이는 추후 데이터베이스에 반영하기전에 스냅샷과 엔티티를 비교하기 위해서 이다.

flush를 수행하면 변경사항이 존재할 경우 쓰기 지연 SQL 저장소에 업데이트 쿼리를 생성한다.

DDL 생성

create, create-drop, update는 로컬에서만 사용하자.
가능하면 개발, 테스트, 운영 서버에는 해당 옵션을 사용하지 말자.
서비스용 DB계정에는 테이블의 DDL 권한을 빼서 사용하는게 좋다.

권장하는 식별자 전략

  • Long형
  • 시퀀스 or auto_increment
  • UUID
  • 자연키(도메인에서 활용하는 데이터 중 하나)는 사용하지 말것을 권장
  • 주문테이블의 경우 회원아이디와 제품아이디를 복합키로 테이블을 설계할 수 있다. 하지만 이런 경우에도 비즈니스적으로 무의미한 값을 PK를 가질 것을 추천한다. 왜냐하면 JPA를 활용하기에 더 편하고 서비스의 변경에 더 유연하게 대응가능하기 때문이다.

IDENTITY 전략의 특징

일반적으로 영속성 컨텍스트에서 엔티티를 보관하고 있다가 커밋시점에 쓰기 지연 SQL 저장소에 있던 쿼리를 DB에 전송한다.

하지만 IDENTITY 전략을 사용하면 DB에 데이터를 넣기 전까지는 id를 알 수 없어 영속성 컨텍스트에서 엔티티를 관리할 수 없는 이슈가 있기 때문에 persist하는 시점에 DB에 쿼리를 전송한다.

SEQUENCE 전략의 특징

영속성 컨텍스트에서 엔티티를 관리하기 위하여 persist시 시퀀스에서 값을 가져온다.

하지만 SEQUENCE 전략을 사용하면 시퀀스를 조회하고 INSERT를 하기 때문에 DB와 통신을 두번해야하는 이슈가 발생하기 한다.
따라서 옵션으로 allocationSize 를 설정하여 서비스에서 시퀀스를 호출할 때 allocationSize만큼 DB에 올려놓고 서비스가 메모리상에서 아이디 번호를 해당 범위내에서 직접 사용하는 방식으로 활용할 수 있다.

TABLE 전략의 특징

SEQUENCE 전략과 동일하다.

설계 방식의 변환 필요

DB 스키마에 종속된 데이터 중심의 설계에서 객체 중심의 설계로 변환이 필요하다.
JPA를 배우다보면 JPA자체가 어렵다기보다는 객체지향적 사고를 하는것이 어려운 경우가 많다.

책 추천
객체지향의 사실과 오해(역할, 책임, 협력 관점에서 본 객체지향) - 조영호
오브젝트(코드로 이해하는 객체지향 설계) - 조영호

단방향 연관관계
선수 - 팀

  • 다대일 연관관계

    • 선수가 소속팀을 가지는 경우

      @ManyToOne
      @JoinColumn(name = "디비 컬럼")
  • 일대다 연관관계

    • 소속팀이 선수를 가지는 경우

      @OneToMany
      @JoinColumn(name = "디비 컬럼")
    • 사용하지 말자.

      • 객체관점에서는 팀에서 소속선수를 찾는것이 맞다.
      • 팀 엔티티의 변경에 선수의 정보가 변경되는것이 성능 이슈나 운영상황에서 버그가 생길 수 있다.
      • 객체지향 설계에서 손해를 보더라도 참조키가 있는 엔티티가 연관관계를 가지는것을 추천한다.
  • 다대다 연관관계

    • JPA에서 공식적으로 지원하지만 실무에서 사용하지 말자.
    • 생성되는 쿼리도 이상하고 이런경우 보통 부가정보도 관리해야하는데 관리할 수 없다.
    • 이런 구조는 일대다, 다대일을 활용하여 푸는것을 추천한다.

선수 - 사물함

  • 일대일 연관관계
    • 다대일 연관관계와 개념이나 사용법은 동일함
    • 어노테이션만 ManyToOne 대신 OneToOne을 사용하면 됨

양방향 연관관계

  • 다대일 연관관계

    • 선수가 소속팀을 가진 상태에서 소속팀이 선수를 가지는 경우

      @OneToMany(mappedBy = "필드명")
    • 이때 선수는 연관관계의 주인이 되고 연관관계의 주인은 참조 값의 변경사항이 DB에 반영된다. 소속팀에서도 선수를 조회할 수 있지만 선수에 대한 변경이 DB에 반영되지 않는다는 것이다.
      DB에 반영하는 기준으로 보면 주인에만 값을 설정하면 되지만 객체관점에서 보면 참조하는 쪽(팀)에도 값을 설정하여 동기화하는것이 좋다.
      이 때 두 객체에 각각 값을 설정하는 것보다 두 값을 모두 설정할 수 있는 함수를 활용하는것을 추천하고 연관 관계 편의 메소드를 만드는 것을 추천한다.

  • 일대다 연관관계

    • JPA가 공식적으로 일대다 연관관계를 지원하지 않는다.

    • 컬럼을 조인하여 추가, 갱신을 막는 방식으로 기능 활용할 수 있다.

      @JoinColumn(name = "디비 컬럼", insertable = false, updatable = false)
    • 단방향이든 양방향이든 일대다 연관관계는 사용하지 말것을 추천한다.

선수 - 사물함

  • 일대일 연관관계
    • 다대일 연관관계와 개념이나 사용법은 동일하다.
    • 어노테이션만 ManyToOne 대신 OneToOne을 사용하면 된다.
    • 참조 테이블과 엔티티의 주인이 다를 경우 (FK는 사물함에 있지만 객체 연관관계를 선수에서 가져가는 경우) 지연 로딩기능이 무의미하다. 선수 엔티티에서 사물함의 존재여부를 알려면 어차피 쿼리를 해야하기 때문에 값을 조회해버린다.

정리
설계시점에는 단방향 연관관계만으로도 충분하며 이후 그래프 탐색기능이 필요한 경우에 양방향 연관관계를 추가하여 사용하는것이 좋다.

연관관계의 주인을 FK를 가진 엔티티가 가지는 것이 좋고 양방향 연관관계를 사용시 연관관계 편의 메소드를 만들어서 사용하자.

일대다 연관관계는 주인으로 사용하지 말자.

상속관계 매핑

객체의 상속관계를 DB에 매핑하는 전략으로 세가지 전략이 있다.

조인 전략

  • DB의 수퍼클래스-서브클래스 모델링을 활용하는 전략

  • 테이블을 정규화하여 설계할 수 있다.

  • 데이터 공간을 효율적으로 사용할 수 있는 반면 조회시 조인이 필요하기 때문에 쿼리가 복잡해지고 성능이 저하될 수도 있다.

  • 활용

    • 부모 클래스

      @Inheritance(strategy = InheritanceType.JOINED)
      @DiscriminatorColumn(name = "DTYPE")
    • 자식 클래스

      @DiscriminatorValue("value1")

단일 테이블 전략

  • 하나의 테이블에 모든 컬럼을 추가하는 방식이다.
  • 테이블 설계가 단순하며 일반적으로 성능이 좋다.
  • 데이터 저장의 효율성이 떨어진다.
  • 데이터양의 임계치가 넘어가면 성능이 떨어진다.
  • 활용
    • 조인 전략과 동일하고 상속 타입만 변경하면 된다. InheritanceType.SINGLE_TABLE

구현 클래스마다 테이블 전략

  • 사용하면 안된다.

  • DB 전문가와 ORM 전문가 모두 싫어하는 스타일의 설계가 된다.

  • 각 클래스에 부모가 가지는 정보를 각각 가지는 전략이다.

  • 특정 클래스의 정보를 조회할때는 이점이 있으나 부모클래스로 다형성을 활용할 경우 모든 자식 테이블을 조회해봐야 한다.

  • 활용

    • 부모 클래스

      @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
      추상 클래스로 선언

기본적으로는 조인 전략을 활용하고 이후 데이터가 억 단위 혹은 하루에 백만건이상 데이터가 쌓이는 시점에 설계를 변경하자.

김영하씨의 경우는 단일 테이블 전략을 활용하고 상세 정보를 JSON으로 관리하는 방식도 쓴다고 한다.

MappedSuperclass

엔티티에서 공통으로 사용할 속성들을 정의하여 활용할 수 있는 객체이다.

자식 클래스에서 상속받아 활용가능하며 부모 클래스는 추상 클래스로 선언하는것을 추천한다.

상속맵핑 관계가 아니고 엔티티도 아니다.

프록시

엔티티를 조회할때 getReference()로 요쳥하면 실제 엔티티가 아니라 엔티티를 상속받은 프록시 객체를 반환한다.

프록시에 실제 엔티티가 설정되는 시점은 엔티티의 필드를 요청하는 시점이다.

프록시가 엔티티를 초기화할때 영속성 컨텍스트에 조회를 요청하면 영속성 컨텍스트가 해당 엔티티를 초기화해주는 방식으로 동작하여 참조값이 변경되지 않는다.

프록시와 엔티티를 ==연산하면 false를 반환한다.

JPA는 한 트랜젝션안에서는 엔티티의 동일성을 보장하기 때문에 엔티티나 프록시 중 먼저 선언되는 형식으로 맞춘다.

  • 만일 프록시를 먼저 호출한 후 엔티티를 호출하면 프록시로 반환한다.
  • 영속성 컨텍스트에 엔티티가 있는 상황에서는 프록시가 아닌 엔티티가 반환된다.

프록시를 가진 상태에서 엔티티가 준영속상태로 변경되면 이후 프록시를 활용할때 LazyInitializationException이 발생한다.

지연로딩과 즉시로딩

실무에서는 지연로딩만 사용하자!!

즉시로딩은 상상하지 못한 쿼리를 만들 수 있고 N+1이슈를 만든다.

@ManyToOne이나 @OneToOne을 사용하면 기본이 즉시로딩이므로 주의하자!!!

지연로딩을 사용할 경우 연관관계의 엔티티들을 한번에 조회하고 싶으면 Fetch Join이나 엔티티 그래프를 활용하자.

사용법은 @ManyToOne(fetch = FetchType.LAZY) 즉시로딩은 EAGER다.

영속성 전이(CASCADE)

부모-자식 관계에서 자식의 데이터를 부모에 종속적으로 관리할 수 있도록 하는 기능이다.

예를 들어 부모 엔티티와 자식 엔티티를 생성 후 둘다 persist를 해야지 저장이 되는데 영속성 전이를 설정한 경우 부모 엔티티만 persist를 해도 자식 엔티티까지 저장된다.

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) 로 사용할 수 있으며 옵션은 ALL, PERSIST 를 사용하기를 권장한다.

자식 엔티티가 한 부모와 연관관계를 가질때만 사용하자. 자식 엔티티가 여러 부모와 연관관계를 가질 때 사용하게되면 의도치 않는 동작으로 운영에 어려움을 겪을 수 있다.

고아 객체

고아 객체란 부모-자식간 관계가 끊어진 객체이다.

고아 객체에 대한 삭제 기능을 제공하는데 이는 컬랙션에서 제거된 객체를 DB에서도 지워주는 기능이다. 부모 객체를 삭제해도 해당 부모를 참조하던 자식 객체들도 DB에서 제거된다.

따라서 영속성 전이(CASCADE)와 같이 자식이 한 부모와 연관관계를 가지고 있을때만 사용하는것을 추천한다.

@OneToOne, @OneToMany 에서만 사용 가능하다. @OneToMany(mappedBy = "parent", orphanRemoval = true)처럼 사용할 수 있다.

영속성 전이 + 고아 객체를 모두 사용하면

영속성 전이에 의해서 자식 엔티티에 대해서 persist하지 않아도 DB에 관리되고, 고아 객체의 삭제기능으로 자식 객체의 삭제가 DB에 반영되기 때문에 부모 엔티티에 의해서 자식 엔티티의 생명주기가 관리된다.

이는 자식 엔티티의 repository를 만들지 않고도 부모 엔티티에 의해 생명주기를 관리할 수 있으며, 이는 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할때 유용하다.

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)로 사용할 수 있다.

값 타입

기본 값 타입에서는 래퍼 클래스(Integer, Double, ...)나 String같은 특수 클래스는 공유 가능하지만 설정한 값을 변경할 수 없다.

값 타입의 비교는 동일성 비교와 동등성 비교가 있는데 동일성 비교는 참조값을 비교하는 것이고 동등성 비교는 사용하는 값을 비교하는 것이다.

동등성 비교를 할 경우에는 equals()를 재정의하여 사용해야하고 이 때 해시코드도 재정의 해줘야 한다.

임베디드 타입

임베디드 타입은 다수의 값 타입을 가진 복합 값 타입이다.

임베디드 타입은 값 타입이기 때문에 생명주기를 엔티티와 동일하게 가져간다.

테이블 매핑에 영향을 주지 않고 객체지향적으로 엔티티를 설계할 수 있기 때문에 객체의 재사용성을 가져갈 수 있고 응집도를 높일 수 있으며 도메인의 공통 용어를 생성할 수 있다.

사용법은 선언한 객체에 @Embeddable을 선언하고 활용하는 필드에 @Embedded를 선언한다. 둘 곳중에 한 곳만 선언에도 활용 가능하나 둘 다 선언하는것을 추천한다.

임베디드 타입에도 엔티티를 선언해서 사용할 수 있다.

한 엔티티에 다수의 동일한 임베디드 타입을 사용하면 컬럼명이 중복된다는 오류가 발생한다. 이때는 @AttributeOverride를 활용하여 컬럼명을 재정의해서 사용해야한다.

사용법

@Embedded  
@AttributeOverrides({
    @AttributeOverride(name = "city", column = @Column(name = "sender_city")),
    @AttributeOverride(name = "street", column = @Column(name = "sender_street")),
    @AttributeOverride(name = "zip_code", column = @Column(name = "sender_zip_code"))  
 })  
 private Address address;

임베디드 타입 선언시 null로 정의하면 임베디드 타입에 정의된 내부 변수들도 모두 null로 정의된다.

임베디드 타입과 같은 값 타입은 공유 참조 문제가 발생할 수 있다. 공유 참조란 두 엔티티가 동일한 값 타입을 참조할 경우 한쪽 엔티티의 값 타입으류변경할 때 다른 쪽 엔티티의 값도 바뀌는 현상을 말한다.

따라서 값 타입은 불변 객체로 만들어야 한다.

불변(Immutable) 객체

불변 객체란 생성자로 값을 생성한 뒤에는 값을 변경할 수 없는 객체이다. 보통 Setter를 만들지 않는 방법으로 구현한다.

값 타입을 두 엔티티가 참조할 경우 발생하는 Side effect는 추적하기가 매우 힘들기 때문에 원천적으로 차단하기 위한 방식이다.

값을 변경하고 싶은 경우에도 객체를 새로 생성해야하는 불편함이 있지만 이를 버그와 trade off한다고 보면 된다 .

값 타입 컬렉션

값 타입으로 생성한 컬렉션이다.

값 타입 컬렉션을 엔티티에서 사용하면 기본적으로 지연 로딩 전략을 사용한다.

임베디드 타입과 다르게 데이터가 저장될 실제 테이블을 가져야 한다. 이는 데이터베이스에는 컬렉션을 동일한 테이블에 저장할 수 없기 때문이다.

@ElementCollection
@CollectionTable(name = "favorite_food", joinColumns = @JoinColumn(name = "MEMBER_ID")
)
private Set<String> favoriteFoods = new HashSet<>();

@ElementCollection
@CollectionTable(name = "address", joinColumns = @JoinColumn(name = "MEMBER_ID")
)
@Column(name = "food_name")
private List<Address> address = new ArrayList<>();

값 타입을 불변 객체로 활용하는 컨셉에 이어서 해당 컬렉션의 데이터를 수정하고 싶은 경우에는 객체를 교체하는 방식으로 사용해야 한다.

값 타입은 엔티티와는 다르게 식별자의 개념이 없으므로 값의 변경에 대한 추척이 어렵다. 따라서 값의 변경이 일어날 때 주인 엔티티와 연관된 모든 데이터를 삭제하고 현재 가지고 있는 모든 값을 다시 입력한다. 결과적으로는 의도한대로 동작하나 과정에서 Insert 쿼리가 다수 발생한다.

값 타입 컬렉션을 활용할 때는 명시적으로 모든 값으로 PK를 만들어줘야 한다.

실무에서는 일대다 관계를 고려하자. 일대다관계에 영속성 전이와 고아 객테 삭제 기능을 사용하면 동일하게 활용 할 수 있다.

@Entity
@Table(name = "address")
public class AddressEntity {
    @Id @GenerateValue
    private Long id;

    private Address address;
}

값 타입 컬렉션은 정말 심플한 데이터에 대해서만 사용하고 식별자를 활용하여 데이터를 추적가능 해야하는 구조에는 엔티티 타입으로 사용하자.

JPQL

객체지향 SQL로 SQL를 랩핑하여 객체를 사용하여 쿼리할 수 있도록 한다.

ANSI 표준 SQL 문법은 모두 지원한다.

기본 문법

em.createQuery("select m from Member m where m.username like '%kim%'", Member.class).getResultList();

Alias를 사용하는걸 추천한다.

이는 kim이 포함된 회원을 조회해서 Member 객체로 반환하라는 의미이다.

쿼리를 생성할 때 타입을 명시하면 TypedQuery<Member>, 타입을 명시하지 않으면 Query로 사용할 수 있다.

결과를 가져올 때 둘 이상의 결과를 가져오려면 getResultList(), 하나의 결과를 가져오려면 getSingleResult()를 사용하면 된다. 단, getSingleResult()를 사용하면 NoResultException이나 NonUniqueResultException이 발생할 수 있다.

파라메타를 바인딩 할 때는 :username이나 ?1과 같이 사용할 수 있고 setParameter("username", "홍길동")이나 setParameter(1, "홍길동")로 맵핑할 수 있다. 실무에서는 이름으로 바인딩하는걸 추천한다. (위치로 바인딩을 하면 조건 추가, 삭제시 버그가 발생할 수 있기 때문이다.)

결과를 프로젝션할 때는 Query 타입으로 조회, Object[]로 조회, new 생성자로 조회하는 세가지 방법이 있다. Query 타입으로 조회하면 Object[]로 형변환을 해서 사용해야하고 Object[]로 조회하면 배열 형태로 값을 빼서 사용해야하는 불편함이 있어서 new 생성자로 조회하는것을 추천한다.

페이징

페이징을 setFirstResultsetMaxResults로 SQL의 offset, limit 처럼 사용할 수 있다.

String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
    .setFirstResult(10)
    .setMaxResults(20)
    .getResultList();

조인

조인은 INNER JOIN, LEFT OUTER JOIN, 세타조인을 지원한다. JPA 2.1부터는 ON절을 지원하여 필터 조건을 걸거나 세타조인을 LEFT OUTER JOIN형식(하이버네이트 5.1부터 지원)으로 사용할 수 있다.

서브쿼리는 (NOT) EXIST, ALL, ANY, SOME, (NOT) IN함수를 지원한다. 서브쿼리는 JPA에서 공식적으로는 WHERE, HAVING만 가능하나 하이버네이트에서는 SELECT도 지원한다.

coalesce, nullif

coalescenullif 함수도 제공하는데 각각 null일 때 치환하거나 같이 같을때 치환하는 기능을 한다.

기본 함수

JPQL에서 제공하는 기본 함수는 CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LEGNTH, LOCATE, ABS, SQRT, MOD, SIZE, INDEX가 있다. 그리고 DB에서 제공하는 함수를 정의하여 사용할 수 있는데 Dialect를 상속받은 클래스에 정의를 하고 설정에 해당 Dialect를 보도록 합니다. 사용은 function('group_concat', m.username)처럼 한다.

경로 표현식

경로표현식이란 .을 찍어 객체 그래프를 탐색하는것을 말한다.

경로 표현식에는 상태 필드연관 필드가 있는데 연관 필드에는 단일 값 연관 필드컬렉션 값 연관 필드가 있다.

상태 필드는 값을 저장하기 위한 필드로 경로 탐색의 끝이다.

연관 필드는 연관 관계를 가지는 필드로 묵시적 조인을 사용한다. 단일 값 연관 필드는 엔티티가 대상으로 탐색이 가능하고 컬렉션 값 연관 필드는 컬렉션이 대상으로 탐색이 불가능 하다.

연관 필드를 사용할 때는 명시적 조인을 하는걸 추천한다. 왜냐하면 운영상 JPQL이 쿼리와 동일한 형태의 조인을 가지기 때문에 파악이 용이하고 컬렉션 값 연관 필드로 탐색이 가능하기 때문이다.

패치 조인

패치 조인은 SQL에서 제공하는 조인 기능이 아니고 JPQL에서 지원하는 가능이다. 이는 연관 관계의 데이터도 조회해서 컬렉션으로 가지는 것을 말한다. fetch = FetchType.EAGER와 동일한 효과를 가지는데 차이점은 개발자가 선택할 수 있다는 것이다.

실무에서는 fetch = FetchType.LAZY로 설정하고 fetch 조인을 하는것을 추천한다.

일대다 관계에서 패치 조인을 하게되면 결과 데이터가 늘어난거처럼 보이는 현상이 있을 수 있다. 이 때는 distinct를 활용하여 엔티티의 중복을 제거하고 결과를 받을 수 있다.

select distinct t from Team t join fetch t.members

패치 조인과 일반 조인은 조건문에서 조인이 발생하는것은 동일하나 결과 반환시 선택한 엔티티만 주느냐 조인한 엔티티도 주느냐의 차이가 있다.

패치 조인을 한 엔티티에는 별칭을 주지 않는것을 추천 한다. 왜냐하면 JPA사상이 연관관계에 있는 데이터는 모두 가지고 있는것으로 하는데 조건문을 활용해서 일부만 가지고 있을 경우 영속성 컨텍스트에서 의도한대로 관리가 안될 수도 있기 때문이다.

패치 조인은 하나의 대상만 할 수 있다. 왜냐하면 다수의 엔티티를 조인 할 경우 데이터 정합성이 안 맞을 수 있기 때문이다.

일대다 관계(컬렉션)에서 패치 조인시 페이징 API가 동작하지 않는다. 왜냐하면 일대다 관계에서의 조인은 데이터가 증가할 수 있는데 이 때 페이징을 하면 의도하지 않게 페이징이 될 수 있기 때문이다.
하이버네이트에서 해줄 수도 있지만 사용하지 말자.

그렇다면 일대다 관계(컬렉션)에서 페이징은 어떤식으로 조회해야하는가?
팀을 페이징하여 조회하고 멤버의 경우는 @BatchSize(size = 100)를 사용하여 컬렉션의 지연 로딩시 IN 쿼리로 한번에 다수의 엔티티를 조회하도록 하면 N+1문제를 해결할 수 있다.

Property에 @BatchSize대신 hibernate.default_batch_fetch_size 속성으로 설정해도 된다.

다형성 쿼리

TYPETREAT가 있다.

TYPE은 조회 대상을 특정 자식으로 한정할때 사용한다.

[JPQL]
select i from Item i
where type(i) IN (Book, Movie)
[SQL]
select i from Item i
where i.DTYPE in ('B', 'M')

TREAT은 자바의 타입 캐스팅의 개념과 동일하다.

[JPQL]
select i from Item i
where treat(i as Book).auther = 'kim'
[SQL]
select i from Item i
where i.DTYPE = 'B' and i.auther = 'kim'

엔티티 직접 사용

함수 활용

[JPQL]
select count(m) from Member
[SQL]
select count(m.id) from Member

조건문 활용

[JPQL]
select m from Member where m = :member
[SQL]
select * from Member where member_id = ?

Named 쿼리

미리 정의해서 이름을 부여해두고 사용하는 JPQL로 정적 쿼리이다.

어플리케이션 로딩시점에 초기화 후 재사용을 하여 실행중 JPA가 쿼리를 만드는 코스트를 제거할 수 있고 어플리케이션이 로딩시점에 쿼리를 검증할 수 있는 장점이 있다.

사용법은 어노테이션으로 정의할 수 있고 XML에 정의할 수 있다.

어노테이션 정의 예제

@Entity
@NamedQuery(
    name = ""Member.findByUsername),
    query = "select m from Member m where m.username = :username")
...
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username", "회원1")
    .getResultList();

XML 정의 예제

[META-INF/persistence.xml]
<persistence-unit name="jpabook">
    <mapping-file>META-INF/ormMember.xml</mapping-file>
...
[META-INF/ormMember.xml]
<named-query name="Member.findByUsername">
    <query><![CDATA[
        select m
        from Member m
        where m.username = :username
    ]]></query>
</named-query>

XML의 정의가 항상 우선권을 가진다.

Spring-data-jpa를 활용하게 되면 @Query를 사용하여 Repository에 Named 쿼리를 정의하여 사용한다.

벌크 연산

Entity를 활용하지 않고 쿼리로 DB의 데이터를 변경하는 기능을 JPA에서 제공한다.

예제

int resultCount = em.createQUery("update Member m set m.age = 20")
    .executeUpdate();

JPA에서는 Update와 Delete를 지원하고 하이버네이트에서 Select-Insert를 지원한다.

벌크 연산시 주의사항은 벌크 연산은 영속성 컨텍스트를 거치지 않기 때문에 DB에 변경된 값과 영속성 컨텍스트에서 엔티티가 가지고 있는 값이 다를 수 있다.

따라서 벌크 연산을 엔티티를 불러오기 전에 하거나 엔티티를 불러온 상태에서 벌크 연산을 수행하면 이후에 영속성 컨텍스트를 지워줘야 한다.

spring-data-jpa에서는 @Modifying을 사용하면 영속성 컨텍스트를 지워준다.

Criteria

JPQL이 객체로 쿼리를 할 수 있다는 장점이 있지만 쿼리를 String 형태로 만들어야하기 때문에 컴파일 단계에서 오류가 발생하지 않는 불편함이 있고, 동적 쿼리 생성시 복잡도가 증가하여 SQL의 문법적인 버그가 발생할 가능성이 높다.

이에 코드로 쿼리를 관리하고 동적으로 생성할 수 있는 Criteria를 JPA에서 공식으로 자원한다. (JPQL빌더 역할)

하지만 사용이 복잡하여 실제로 사용하기가 힘들다는 단점이 있다.

// Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);

// 루트 클래스, 조회를 시작할 클래스
Root<Member> m = query.from(Member.class);

// 쿼리 생성
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();

QueryDSL

Criteria와 목적은 동일하다.

하지만 Criteria보다 사용이 간편하기 때문에 실무사용에 권장한다.

JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;

List<Member> list = query.select(m).from(m).where(m.age.gt(18)).orderBy(m.name.desc()).fetch();

네이티브 SQL

DB 종속적인 SQL을 직접 사용할하도록 지원한다.

String sql = "select * from member";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();

JDBC 직접 사용, SpringJdbcTemplate 사용

JDBC를 작접 사용하거나 SpringJdbcTemplate, Mybatis와도 같이 사용할 수 있다. 다만 주의할 점은 JPA가 관리하는 대상들이 아니므로 쿼리전에 flush를 호출하여 영속성 컨텍스와 DB를 맞춰줘야 한다.

후기

JPA 공부하지 않은 상태에서 spring-data-jpa를 사용하던 입장이었습니다.

그 동안 JPA를 사용하면서 궁금하던 부분들을 말씀해주셔서 많은 부분들에 대한 의구심이 해소 됐고 이해하기 쉽게 라이브 코딩으로 설명해주셔서 이해가 잘 된거 같습니다.

다음 프로젝트에서는 보다 잘 설계한 서비스를 만들 수 있겠다는 생각도 듭니다.

JPA의 원리가 궁금하신 분들이나 입문자분들께 강추합니다~!!

Comments