[JPA] 자바 ORM 표준 JPA 프로그래밍 14장

xyzw·2023년 8월 28일
0

Spring

목록 보기
19/22

컬렉션과 부가 기능

  • 컬렉션
  • 컨버터: 엔티티의 데이터를 변환해서 데이터베이스에 저장한다.
  • 리스너: 엔티티에서 발생한 이벤트를 처리한다.
  • 엔티티 그래프: 엔티티를 조회할 때 연관된 엔티티들을 선택해서 함께 조회한다.

컬렉션

JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하고,

  1. @OneToMany, @ManyToMany를 사용해서 일대다나 다대다 엔티티 관계를 매핑할 때
  2. @ElementCollection을 사용해서 값 타입을 하나 이상 보관할 때

이 컬렉션을 사용할 수 있다.

자바 컬렉션 인터페이스의 특징

  • Collection: 자바가 제공하는 최상위 컬렉션이다. 하이버네이트는 중복을 허용하고 순서를 보장하지 않는다고 가정한다.
  • Set: 중복을 허용하지 않는 컬렉션이다. 순서를 보장하지 않는다.
  • List: 순서가 있는 컬렉션이다. 순서를 보장하고 중복을 허용한다.
  • Map: Key, Value 구조로 되어있는 특수한 컬렉션이다.

JPA와 컬렉션

하이버네이트는 컬렉션을 효율적으로 관리하기 위해
엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용한다.

하이버네이트가 제공하는 내장 컬렉션은 원본 컬렉션을 감싸고 있어서 래퍼 컬렉션으로도 부른다.

하이버네이트는 컬렉션을 사용할 때 다음처럼 즉시 초기화해서 사용하는 것을 권장한다.

Collection<Member> members = new ArrayList<Member>();

Collection, List

Collection, List는 중복을 허용하는 컬렉션이고 PersistentBag을 래퍼 컬렉션으로 사용한다. 이 인터페이스는 ArrayList로 초기화하면 된다.

그리고 이 인터페이스는 엔티티를 추가할 대 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 된다. 따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다.

Set

Set은 중복을 허용하지 않는 컬렉션이다. 하이버네이트는 PersistentSet을 컬렉션 래퍼로 사용한다.
이 인터페이스는 HashSet으로 초기화하면 된다.

Set은 엔티티를 추가할 때 중복된 엔티티가 있는지 비교해야 한다. 따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.

List + @OrderColumn

List 인터페이스에 @OrderColumn을 추가하면 순서가 있는 특수한 컬렉션으로 인식한다. 즉 데이터베이스에 순서 값을 저장해서 조회할 때 사용한다.
하이버네이트는 내부 컬렉션인 PersistentList를 사용한다.

@OrderColumn을 실무에서 사용하기에는 단점이 많다. 따라서 @OrderColumn을 매핑하지 말고 개발자가 직접 POSITION 값을 관리하거나 @OrderBy를 사용하는 게 좋다.

@OrderBy

@OrderColumn이 데이터베이스에 순서용 컬럼을 매핑해서 관리했다면 @OrderBy는 데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬한다. 따라서 순서용 컬럼을 매핑하지 않아도 된다.
그리고 @OrderBy는 모든 컬렉션에 사용할 수 있다.

Converter

컨버터를 사용하면 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다.

예를 들어 회원의 VIP 여부를 자바의 boolean 타입을 사용하고 싶다고 하자. JPA를 사용하면 자바의 boolean 타입은 방언에 따라 다르지만 데이터베이스에 저장될 때 0 또는 1인 숫자로 저장된다. 그런데 데이터베이스에 숫자 대신 문자 Y 또는 N 으로 저장하고 싶다면 컨버터를 사용하면 된다.

CREATE TABLE MEMBER (
	ID VARCHAR(255) NOT NULL,
    USERNAME VARCHAR(255),
    VIP VARCHAR(1) NOT NULL,
    PRIMARY KEY (ID)
)

매핑할 테이블은 VIP 컬럼을 VARCHAR(1)로 지정하여 문자 Y, N을 입력할 것이다.

@Entity
public class Member {
	
    @Id
    private String id;
    private String username;
    
    @Convert(converter=BooleanToYNConverter.class)
    private boolean vip;
    
    //Getter, Setter
    ...
}

회원 엔티티의 vip 필드는 boolean 타입이다. @Convert를 적용해서 데이터베이스에 저장되기 직전에 BooleanToYNConverter 컨버터가 동작하도록 했다.

@Converter
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {

	@Override
    public String convertToDatabaseColumn(Boolean attribute) {
    	return (attribute != null && attribute) ? "Y" : "N";
    }
    
    @Override
    public Boolean convertToEntityAttribute(String dbData) {
    	return "Y".equals(dbData);
    }
}

컨버터 클래스는 @Converter 어노테이션을 사용하고 AttributeConverter 인터페이스를 구현해야 한다. 그리고 제네릭에 현재 타입과 변환할 타입을 지정해야 한다.

AttributeConverter 인터페이스에는 구현해야 할 메소드가 두 개 있다.

  • convertToDatabaseColumn(): 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환한다.
  • convertToEntityAttribute(): 데이터베이스에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환한다.

컨버터는 클래스 레벨에도 설정할 수 있다. 단, 이때는 attributeName 속성을 사용해서 어떤 필드에 컨버터를 적용할지 명시해야 한다.

글로벌 설정

모든 Boolean 타입에 컨버터를 적용하려면 @Converter(autoApply = true) 옵션을 적용하면 된다.

@Converter 속성

  • converter: 사용할 컨버터를 지정한다.
  • attributeName: 컨버터를 적용할 필드를 지정한다.
  • disableConversion: 글로벌 컨버터나 상속받은 컨버터를 사용하지 않는다. (기본값: false)

리스너

JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.

이벤트 종류

  • PostLoad: 엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후
  • PrePersist: persist() 메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출된다. 식별자 생성 전략을 사용한 경우 엔티티에 식별자는 아직 존재하지 않는다. 새로운 인스턴스를 merge할 때도 수행된다.
  • PreUpdate: flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출된다.
  • PreRemove: remove() 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출된다. 또한 삭제 명령어로 영속성 전이가 일어날 때도 호출된다, orphanRemoval에 대해서는 flush나 commit 시에 호출된다.
  • PostPersist: flush나 commit을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출된다. 식별자가 항상 존재한다. 참고로 식별자 생성 전략이 IDENTITY면 persist()를 호출한 직후에 바로 PostPersist가 호출된다.
  • PostUpdate: flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출된다.
  • PostRemove: flush나 commit을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출된다.

이벤트 적용 위치

이벤트는 엔티티에서 직접 받거나 별도의 리스너를 등록해서 받을 수 있다.

  • 엔티티에 직접 적용
  • 별도의 리스너 등록
  • 기본 리스너 사용

엔티티 그래프

엔티티를 조회할 때 연관된 엔티티들을 함께 조회하려면 다음처럼 글로벌 fetch 옵션을 FetchType.EAGER로 설정한다.

@Entity
class Order {
	@ManyToOne(fetch=FetchType.EAGER)
    Member member;
    ...
}

또는 다음처럼 JPQL에서 페치 조인을 사용하면 된다.

select o from Order o join fetch o.member

글로벌 fetch 옵션은 애플리케이션 전체에 영향을 주고 변경할 수 없는 단점이 있다. 그래서 일반적으로 글로벌 fetch 옵션은 Fetch.LAZY를 사용하고, 엔티티를 조회할 때 연관된 엔티티를 함께 조회할 필요가 있으면 JPQL의 페치 조인을 사용한다.

그런데 페치 조인을 사용하면 같은 JPQL을 중복해서 작성하는 경우가 많다.
엔티티 그래프 기능을 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티를 선택할 수 있다.
따라서 JPQL은 데이터를 조회하는 기능만 수행하면 되고 연관된 엔티티를 함께 조회하는 기능은 엔티티 그래프를 사용하면 된다.

엔티티 그래프는 정적으로 정의하는 Named 엔티티 그래프와 동적으로 정의하는 엔티티 그래프가 있다.

Named 엔티티 그래프

@NamedEntityGraph로 정의한다.

  • name: 엔티티 그래프의 이름을 정의한다.
  • attributeNodes: 함께 조회할 속성을 선택한다. 이때 @NamedAttributeNode를 사용하고 그 값으로 함께 조회할 속성을 선택하면 된다.

동적 엔티티 그래프

엔티티 그래프를 동적으로 구성하려면 createEntityGraph() 메소드를 사용하면 도니다.

0개의 댓글