JPA 프로그래밍

Ch14. 컬렉션 부가기능

developer-soyun 2025. 2. 2. 17:36

14.1 컬렉션

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

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

14.1.1 JPA와 컬릭션

하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들 때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 이 내장 컬렉션을 사용하도록 참조를 변경한다

  • 하이버네이트는 이런 특징 때문에 다음처럼 즉시 초기화해서 사용하는 것을 권장한다
Collection<Member> members = new ArrayList<Memeber>();

 

14.1.2 Collection, List

@Entity
public class Parent{
	@Id @GeneratedValue
    private Long id;
    
    @OneToMany
    @JoinColumn
    private Collection<CollectionChild> collection = new ArrayList<CollectionChild>();
    
    @OneTomany
    @JoinColumn
    private List<ListChild> list = new ArrayList<ListChild>();
}
  • Collection, List 는 중복을 허용한다고 가정하므로 객체를 추가하는 add() 메서드는 내부에서 어떤 비교도 하지 않고 항상 true를 반환한다
    • 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다
  • 같은 엔티티가 있는지 찾거나 삭제할 때는 equals() 메서드를 사용한다

 

14.1.3 Set

@Entity
public class Parent{
	@OneToMany
    @JoinColumn
    private Set<SetChild> set = new HashSet<SetChild>();
}
  • HashSet은 중복을 허용하지 않으므로 add() 메서드로 객체를 추가할 때 마다 equals() 메서드로 같은 객체가 있는지 비교한다
    • 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다
  • HashSet은 해시 알고리즘을 사용하므로 hashcode()로 함께 사용해서 비교한다

 

14.1.4 List + @OrderColumn

@Entity
public class Board{
	@OneToMany(mappedBy = "board")
    @OrderColumn(name  "POSITIO")
    private List<Comment> comments = new ArrayList<Comment>();
}
  • List 인터페이스에 @OrderColumn을 추가하면 순서가 있는 특수한 컬렉션으로 인식한다.
  • 순서가 있다는 의미는 데이터베이스에 순서 값을 저장해서 조회할 때 사용한다는 의미이다.
  • 순서가 있는 컬렉션은 데이터베이스에 순서 값도 함께 관리한다
  • @OrderColumn은 실무에서 잘 사용하지 않는다

14.1.5 @OrderBy

@Entity
public class Team{
	@OneToMany(mappedBy = "team")
    @OrderBy("username decs, id asc")
    private Set<Member> members = new HashSet<Member>();
}
  • @OrderColumn이 데이터베이스에 순서용 컬럼을 매핑해서 관리했다면 @OrderBy는 데이터베이스의 ORDER BY 절을 사용해서 컬렉션을 정렬한다
  • @OrderBy의 값은 JPQL의 order by절처럼 엔티티의 필드를 대상으로 한다

 

14.2 @Converter

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

@Entity
public class Member{
	@Id
    private String id;
    private String username;
    
    @Converty(converter = BooleanToYNConverter.class)
    private boolean vip;
}

회원 엔티티

 

@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);
    }
}

Boolean을 YN으로 바꿔주는 컨버터

 

  • 컨버터 클래스는 @Convert 어노테이션을 사용하고 AttributeConverter 인터페이스를 구현해야한다
  • 제너릭에 현재 타입과 변환할 타입을 지정해야한다
convertToDatabaseColumn(): 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환한다
convertToEntityAttribute(): 데이터베이스에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환한다

 

14.3 리스너

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

 

14.3.1 이벤트 종류

리스너 시점

  1. @PrePersist : 엔티티가 저장되기 전에 실행됨
  2. @PostPersist : 엔티티가 저장된 후 실행됨
  3. @PreUpdate : 엔티티가 업데이트되기 전에 실행됨
  4. @PostUpdate : 엔티티가 업데이트된 후 실행됨
  5. @PreRemove : 엔티티가 삭제되기 전에 실행됨
  6. @PostRemove : 엔티티가 삭제된 후 실행됨
  7. @PostLoad : 엔티티가 조회된 후 실행됨

 

14.3.2 이벤트 적용 위치

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

 

1. 엔티티에 직접 적용 : 엔티티 클래스 내부에 직접 이벤트 리스너 어노테이션을 추가하기

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @PrePersist
    public void beforeSave() {
        System.out.println("User 저장 전 실행!");
    }
}

 

2. 별도의 리스너 등록: 리스너 클래스를 따로 만들어 여러 엔티티에서 재사용할 수 있다

public class UserListener {
    @PrePersist
    public void beforeSave(Object obj) {
        System.out.println("저장 전 실행! (별도 리스너)");
    }
}

@Entity
@EntityListeners(UserListener.class)
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

 

3. 기본 리스너 사용: META-INF/orm.xml 설정 파일을 통해 글로벌 이벤트 핸들링 가능

<entity-mappings>
    <entity-listeners>
        <entity-listener class="com.example.UserListener"/>
    </entity-listeners>
</entity-mappings>

 

 

14.4 엔티티 그래프

엔티티를 조회할 때 연관된 엔티티까지 함께 가져오려면 두 가지 방법이 있다

  1. 글로벌 fetch 옵션을 FetchType.EAGER로 설정
    • 특정 엔티티를 조회할 때 항상 연관된 엔티티도 함께 로드된다
    • 하지만 필요하지 않은 경우에도 데이터를 불러와 불필요한 성능 저하를 초래할 수 있다
  2. JPQL에서 페치 조인(fetch join) 사용
    • 쿼리 실행 시점에 원하는 연관 엔티티를 함께 조회할 수 있다
    • 하지만 같은 JPQL을 반복적으로 작성해야 하는 문제가 발생할 수 있다
    • 이는 JPQL이 단순한 데이터 조회뿐만 아니라 연관된 엔티티 조회까지 담당하기 때문이다

해결책: 엔티티 그래프(Entity Graph) 활용

  • JPQL은 데이터 조회 역할만 수행하고, 연관된 엔티티 조회는 엔티티 그래프를 통해 해결할 수 있다
  • 엔티티 그래프를 사용하면 JPQL의 중복을 줄이고 가독성을 높이며, 유지보수성을 향상시킬 수 있다
  • 또한, 필요한 연관 엔티티만 동적으로 가져올 수 있어 불필요한 데이터 로딩을 방지할 수 있다

결론적으로, JPQL은 데이터 조회에 집중하고, 연관 엔티티 로딩은 엔티티 그래프를 활용하는 것이 더 좋은 설계

 

엔티티 그래프(Entity Graph) 정리

  • Root 엔티티에서 시작하여 연관된 엔티티를 함께 조회할 수 있도록 정의
  • 이미 로딩된 엔티티는 엔티티 그래프와 상관없이 기존 영속성 컨텍스트의 데이터를 사용

fetchgraph vs loadgraph

  • fetchgraph: 지정한 연관 엔티티만 즉시 로딩, 나머지는 지연 로딩(LAZY) 유지
  • loadgraph: 지정한 연관 엔티티는 즉시 로딩, 나머지는 기본 fetch 전략을 따른다 (EAGER면 즉시 로딩)