JPA 와 컬렉션에 대해서 알아보자

JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하고 다음 경우에 이 컬렉션을 사용할 수 있다.
1. @OneToMany, @ManyToMany 를 사용해서 일대다나 다대다 엔티티를 매핑할 때
2. @ElementCollection을 사용해서 값 타입을 하나 이상 보관할 때

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

JPA 명세에는 자바 컬렉션 인터페이스에 대한 특별한 언급이 없다. 따라서 JPA구현체에 따라서 제공하는 기능이 조금씩 다를 수 있는데 여기서는 하이버네이트 기준으로 설명한다.

JPA와 컬렉션

@Entity
@Data
public class Team {
  @Id
  private String id;

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

Team 은 Members 컬렉션을 필드로 가지고 있다. 다음 코드로 Team을 영속 상태로 만들어보자.

Team team = new Team();
team.setId("test");
System.out.println(team.getMembers().getClass());
entityManager.persist(team);
System.out.println(team.getMembers().getClass());

출력 결과는 다음과 같다.

class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag

출력 결과를 보면 원래 ArrayList 타입이었던 컬렉션이 엔티티를 영속 상태로 만든 직후에 하이버네이트가 제공하는 PersistentBag 타입으로 변경 되었다. 하이버네이트가 제공하는 내장 컬렉션은 원본 컬렉션을 감싸고 있어서 래퍼 컬렉션으로도 부른다.
하이버네이트는 이런 특징 때문에 컬렉션을 사용할 때 다음 처럼 즉시 초기화해서 사용하는 것을 권장한다.

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

Collection, List

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

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

중복을 허용한다고 가정하므로 객체를 추가하는 add() 메소드는 내부에서 어떠한 비교도 하지 않고 항상 true를 반환한다. 같은 엔티티가 있는지 찾거나 삭제할 때는 equals() 메서드를 사용한다.

List<Comment> commentList = new ArrayList<Comment>();
boolean result = commentList.add(data); //항상 true

commentList.contains(comment); //equals 비교
commentList.remove(comment); //equals 비교

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

Set

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

@OneToMany
@JoinColumn
private Set<SetChild> set = new HashSet<SetChild>();

HashSet은 중복을 허용하지 않으므로 add() 메서드로 객체를 추가할 때 마다 equals() 메서드로 같은 객체가 있는지 비교한다. 같은 객체가 없으면 추가하고 true로 반환, 이미 있어서 추가에 실패하면 false 로 반환한다. HashSet은 해시 알고리즘을 사용하므로 hashCode() 도 함께 사용한다.

Set<Comment> commentList = new HashSet<Comment>();
boolean result = commentList.add(data); //hashCode + equals() 비교

commentList.contains(comment); //hashCode + equals() 비교
commentList.remove(comment); //hashCode + equals() 비교

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

List + @OrderColumn

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

@Entity
@Data
public class Board {
  @Id @GeneratedValue
  private Long id;

  private String title;

  @OneToMany(mappedBy = "board")
  @OrderColumn(name = "POSITION")
  private List<Comment> comments = new ArrayList<Comment>();
}

@Entity
@Data
public class Comment {

  @Id @GeneratedValue
  private Long id;

  private String comment;

  @ManyToOne
  @JoinColumn(name = "BOARD_ID")
  private Board board;
}

Board.comments에 List 인터페이스를 사용하고 @OrderColumn을 추가했다. 따라서 Board.comments는 순서가 있는 컬렉션으로 인식된다. 자바가 제공하는 List 컬렉션은 내부에 위치 값을 가지고 있다. 따라서 다음 코드처럼 List의 위치 값을 활용할 수 있다.

list.add(1, data); //1번위치에 data를 저장 
list.get(10); //10 번 위치에 있는 값 조회

순서가 있는 컬렉션은 데이터베이스에 순서 값도 함께 관리해 준다. 여기서는 @OrderColumn의 name속성에 POSITION이라는 값을 주었다. JPA는 List의 위치 값을 테이블의 POSITiON 컬럼에 보관한다. 그런데 Board.comments 컬렉션은 Board 엔티티에 있지만 테이블의 일대다 관계의 특성상 위치 값은 다(N) 쪽에 저장해야 한다.

@OrderColumn을 사용해서 List의 위치 값을 보관하면 편리할 것 같지만 실무에서 사용하기에는 단점이 너무 많다. 따라서 @OrderColumn을 매핑하지 말고 개발자가 직접 POSITION 값을 관리 하거나 @OrderBy를 사용하길 권장한다.

출처 : 자바 ORM 표준 JPA 프로그래밍 (김영한)