자바 ORM 표준 JPA 프로그래밍 - 기본편 챕터 14 정리

정종일·2023년 6월 30일
0

Spring

목록 보기
18/18

JPA가 지원하는 컬렉션의 종류와 중요한 부가 기능들을 알아보자.

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

1. 컬렉션


자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하고 다음 경우에 이 컬렉션을 사용할 수 있다

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

자바 컬렉션 인터페이스의 특징은 아래와 같다

  1. Collection : 자바가 제공하는 최상위 컬렉션. 하이버네이트는 중복 허용, 순서를 보장하지 않는다고 가정
  2. Set : 중복을 허용하지 않는 컬렉션. 순서를 보장하지 않음
  3. List : 순서가 있는 컬렉션. 순서를 보장하고 중복을 허용
  4. Map : key, value 구조로 되어있는 특수한 컬렉션
☑️ Map은 복잡한 매핑에 비해 활용도가 떨어지고 다른 컬렉션을 사용해도 충분하다. Map은 @MapKey 어노테이션으로 매핑 가능하다

1. JPA와 컬렉션


하이버네이트는 엔티티를 영속상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용한다.

@Entity
public class Team {

		@Id
		private String id;

		@OneToMany
		@JoinColumn
		private Collection<Member> members = new ArrayList<Member>();
		...
}

Team을 영속상태로 만들면 ArrayList타입이었던 컬렉션이 하이버네이트가 제공하는 PersistentBag 타입으로 변경된다. 하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속상태로 만들 때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 이 내장 컬렉션을 사용하도록 참조를 변경한다. 이런 특징때문에 컬렉션을 사용할 때 즉시 초기화해서 사용하는 것을 권장한다.

하이버네이트 내장 컬렉션과 특징

2. Collection, List


Collection, List 는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 된다. 따라서 엔티티를 추가해도 지연로딩된 컬렉션을 초기화 하지 않는다.

3. Set


이 인터페이스는 HashSet으로 초기화하면 된다.

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

4. List + @OrderColumn


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

순서가 있는 컬렉션은 데이터베이스에 순서 값도 함께 관리한다.

@Entity
public class Board {
		...
		@OneToMany(mappedBy = "board")
		@OrderColumn(name = "POSITION")
		private List<Comment> comments = new ArrayList<Comment>();
		...
}

comment 컬렉션은 board 엔티티에 있지만 순서 값은 comment 엔티티 안에 위치한다.

@OrderColumn은 편리할 것 같지만 실무에서 사용하기엔 단점이 많다. 따라서 개발자가 직접 값을 관리하거나 다음에서 설명하는 @OrderBy 를 사용하길 권장한다.

OrderColumn 단점


  1. Board 엔티티에서 매핑하므로 Comment는 POSITION 값을 알 수 없다. 그래서 Comment를 INSERT할 때는 POSITION 값이 저장되지 않는다. 또한 Board.comment의 위치값이므로 이 값을 사용해서 UPADTE하는 SQL이 추가로 발생한다.
  2. List를 변경하면 연관된 많은 위치 값을 변경해야 한다. 중간 값이 사라지면 그 뒤에 값들이 Shift되기 때문에 UPDATE 쿼리가 N번 추가로 발생한다.
  3. 중간에 POSITION 값이 없으면 조회한 List에는 Null이 보관된다. 따라서 컬렉션을 순회할 때 NullPointException이 발생한다

5. @OrderBy


@OrderColumn 이 데이터베이스에 순서용 컬럼을 매핑해서 관리했다면 @OrderBy 는 데이터베이스의 Order by 절을 사용해서 컬렉션을 정렬한다. 따라서 순서용 컬럼을 매핑하지 않아도 된다.

@Entity
public class Board {
		...
		@OneToMany(mappedBy = "board")
		@OrderBy("username desc. id asc")
		private List<Comment> comments = new ArrayList<Comment>();
		...
}

개인적으로 굳이 무조건 정렬을 넣을 필요가 있나 싶다..

☑️ 하이버네이트는 Set에 `@OrderBy` 적용 시 `HashSet`이 아닌 `LinkedHashSet`을 사용한다.

2. @Converter


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

@Entity
public class Member {
		...
		@Converter(converter = BooleanToYNConverter.class)
		private boolean vip;
		...
}

컨버터 클래스는 인터페이스를 구현해야 한다. 그리고 제네릭에 현재 타입과 변환할 타입을 지정해야 한다.

public interface AttributeConverter<X, Y> {
		public Y convertToDatabaseColumn ( X attribute);
		public X convertToEntityAttribute (Y dbData);
}
  • convertToDatabaseColumn : 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환
  • convertToEntityAttribute : 데이터베이스에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환

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

@Entity
@Convert(converter = BooleanToYNConverter.class, attributeName = "vip")
public class Member {
		...
		private boolean vip;
}

1. 글로벌 설정


모든 boolean 타입에 컨버터를 적용하려면

@Converter(autoApply = true)
public class BooleanToYNConverter implements AttributeConverter<X, Y> {
		@Override
		...
}

위와 같이 글로벌 설정을 하면 엔티티에 지정을 하지 않아도 모든 Boolean 타입에 대해 자동으로 컨버터가 적용된다.

Converter 속성

3. 리스너


모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모두 로그로 남겨야 하는 요구사항이 있다고 가정한다면 어플리케이션 삭제 로직을 하나씩 찾아서 로그를 남기는 것은 비효율적이다. JPA 리스너 기능을 사용하면 엔티티의 생명 주기에 따른 이벤트를 처리할 수 있다.

1. 이벤트의 종류


  1. PostLoad : 엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후(2차 캐시에 저장되어 있어도 호출)

  2. PrePersist : persist() 메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출. 식별자 생성 전략을 사용한 경우, 엔티티에 식별자는 아직 존재하지 않는다. 새로운 인스턴스를 merge할 때도 수행된다.

  3. PreUpdate : flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출.

  4. PreRemove : remove() 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출. 또한 삭제 명령어로 영속성 전이가 일어날 때도 호출된다. orphanRemoval에 대해서는 flush나 commit시에 호출된다.

  5. PostPersist : flush나 commit을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출. 식별자가 항상 존재한다.

    PostPersist는 식별자 생성 전략이 IDENTITY면 식별자를 생성하기 위해 persist()를 호출하면서 데이터베이스에 해당 엔티티를 저장하므로 이 때는 persist()를 호출한 직후에 바로 PostPersist가 호출된다.

  6. PostUpdate : flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출

  7. PostRemove : flush나 commit을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출

2. 이벤트 적용 위치


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

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

엔티티에 직접 적용

@Entity
public class Duck {

    @Id @GeneratedValue
    public Long id;

    private String name;
   
    @PrePersist
    public void prePersist() {
        System.out.println("Duck.prePersist id=" + id);
    }
		@PostPersist
    public void postPersist() {
        System.out.println("Duck.postPersist id=" + id);
    }
    @PostLoad
    public void postLoad() {
        System.out.println("Duck.postLoad");
    }
    @PreRemove
    public void preRemove() {
        System.out.println("Duck.preRemove");
    }
    @PostRemove
    public void postRemove() {
        System.out.println("Duck.postRemove");
    }
    ...
}

엔티티에 이벤트가 발생할 때마다 어노테이션으로 지정한 메소드가 실행된다.

별도의 리스너 등록

@Entity
@EntityListeners(DuckListener.class)
public class Duck {...}

public class DuckListener {

    @PrePersist
    // 특정 타입이 확실하면 특정 타입을 받을 수 있다.
    private void prePersist(Object obj) {
        System.out.println("DuckListener.prePersist obj = [" + obj + "]");
    }
    @PostPersist
    // 특정 타입이 확실하면 특정 타입을 받을 수 있다.
    private void postPersist(Object obj) {
        System.out.println("DuckListener.postPersist obj = [" + obj + "]");
    }
}

리스너는 대상 엔티티를 파라미터로 받을 수 있다. 반환 타입은 void로 설정해야 한다.

기본 리스너 사용

모든 엔티티의 이벤트를 처리하려면 META-INF/orm.xml에 기본 리스너로 등록하면 된다.

여러 리스너를 등록했을 때 이벤트 호출 순서는 다음과 같다

  1. 기본 리스너
  2. 부모 클래스 리스너
  3. 리스너
  4. 엔티티

더 세밀한 설정

  • javax.persistence.ExcludeDefaultListeners : 기본 리스너 무시
  • javax.persistence.ExcludeSuperclassListeners : 상위 클래스 이벤트 리스너 무시
  • 기타 어노테이션 적용 코드
@Entity
@EntityListeners(DuckListener.class)
@ExcludeDefaultListeners
@ExcludeSuperclassListeners
public class Duck extends BaseEntity {...}

이벤트를 잘 활용하면 대부분의 엔티티에 공통으로 적용하는 등록 일자, 수정 일자 처리와 해당 엔티티를 누가 등록/수정했는지에 대한 기록을 리스너 하나로 처리할 수 있다.

4. 엔티티 그래프


엔티티 그래프 기능은 엔티티 조회 시점에 연관된 엔티티들을 함께 조회하는 기능이다.

1. Named Entity Graph


주문을 조회할 때 연관된 회원도 함께 조회하는 엔티티 그래프

@NamedEntityGraph(name = "Order.withMember", attributeNodes = {
    @NamedAttributeNode("member")
})
@Entity
@Table(name = "ORDERS")
public class Order {
    
    @Id @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id'

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;  //  주문 회원

    ...
}

Named 엔티티 그래프는 @NamedEntityGraph로 정의

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

Order.member가 지연 로딩으로 설정되어있지만, 엔티티 그래프에서 함께 조회할 속성으로 member를 선택했으므로 이 엔티티 그래프를 사용하면 Order를 조회할 때 연관된 member도 함께 조회할 수 있다.

둘 이상 정의하려면 @NamedEntityGraphs를 사용하면 된다.

profile
제어할 수 없는 것에 의지하지 말자

0개의 댓글