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 이벤트 종류
- @PrePersist : 엔티티가 저장되기 전에 실행됨
- @PostPersist : 엔티티가 저장된 후 실행됨
- @PreUpdate : 엔티티가 업데이트되기 전에 실행됨
- @PostUpdate : 엔티티가 업데이트된 후 실행됨
- @PreRemove : 엔티티가 삭제되기 전에 실행됨
- @PostRemove : 엔티티가 삭제된 후 실행됨
- @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 엔티티 그래프
엔티티를 조회할 때 연관된 엔티티까지 함께 가져오려면 두 가지 방법이 있다
- 글로벌 fetch 옵션을 FetchType.EAGER로 설정
- 특정 엔티티를 조회할 때 항상 연관된 엔티티도 함께 로드된다
- 하지만 필요하지 않은 경우에도 데이터를 불러와 불필요한 성능 저하를 초래할 수 있다
- JPQL에서 페치 조인(fetch join) 사용
- 쿼리 실행 시점에 원하는 연관 엔티티를 함께 조회할 수 있다
- 하지만 같은 JPQL을 반복적으로 작성해야 하는 문제가 발생할 수 있다
- 이는 JPQL이 단순한 데이터 조회뿐만 아니라 연관된 엔티티 조회까지 담당하기 때문이다
해결책: 엔티티 그래프(Entity Graph) 활용
- JPQL은 데이터 조회 역할만 수행하고, 연관된 엔티티 조회는 엔티티 그래프를 통해 해결할 수 있다
- 엔티티 그래프를 사용하면 JPQL의 중복을 줄이고 가독성을 높이며, 유지보수성을 향상시킬 수 있다
- 또한, 필요한 연관 엔티티만 동적으로 가져올 수 있어 불필요한 데이터 로딩을 방지할 수 있다
결론적으로, JPQL은 데이터 조회에 집중하고, 연관 엔티티 로딩은 엔티티 그래프를 활용하는 것이 더 좋은 설계다
엔티티 그래프(Entity Graph) 정리
- Root 엔티티에서 시작하여 연관된 엔티티를 함께 조회할 수 있도록 정의
- 이미 로딩된 엔티티는 엔티티 그래프와 상관없이 기존 영속성 컨텍스트의 데이터를 사용
fetchgraph vs loadgraph
- fetchgraph: 지정한 연관 엔티티만 즉시 로딩, 나머지는 지연 로딩(LAZY) 유지
- loadgraph: 지정한 연관 엔티티는 즉시 로딩, 나머지는 기본 fetch 전략을 따른다 (EAGER면 즉시 로딩)