본 포스팅은 인프러의 JPA 기본편을 수강하고 정리하는 내용입니다.
값 타입 컬렉션
은 값 타입을 컬렉션에 담아서 쓰는 것을 말한다. 문제는 DB에 넣을때가 문제가 된다. 기본적으로 값 타입 컬렉션을 DB에 넣는 구조를 지원하지 않는다. 결론적으로 이러한 컬렉션들을 별도의 테이블로 뽑아야한다. (FAVORITE_FOOD 테이블, ADDRESS 테이블)
- 값 타입을 하나 이상 저장할 때 사용
@ElementCollection
,@CollectionTable
사용- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
- DB안에 한 테이블에 컬렉션을 넣을 수 있는 방법은 없다.
- 일대다로 풀어서 컬렉션을 저장하기 위한 별도의 테이블이 필요함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
// 기간 Period
@Embedded
private Period workPeriod;
// 주소
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "MEMBER_ID")
)
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns =
@JoinColumn(name = "MEMBER_ID")
)
private List<Address> addressHistory = new ArrayList<>();
...
}
값 타입 컬렉션 사용
1) 값 타입 저장 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "1000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressHistory().add(new Address("old1", "street", "1000"));
member.getAddressHistory().add(new Address("old2", "street", "1000"));
em.persist(member);
tx.commit();
2) 값 타입 조회 예제
- 값 타입 컬렉션도 지연 로딩 전략 사용
- 엔티티 테이블만 가져오고 값 타입 컬렉션은 프록시로 가져온다.
- 나중에 실제 값 타입 객체(FAVORITE_FOOD 테이블, ADDRESS 가 호출될때 select 쿼리를 호출한다)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "1000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressHistory().add(new Address("old1", "street", "1000"));
member.getAddressHistory().add(new Address("old2", "street", "1000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("===== START =====");
Member findMember = em.find(Member.class, member.getId());
List<Address> addressHistory = findMember.getAddressHistory();
for(Address address : addressHistory) {
System.out.println("address = " + address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
tx.commit();
3) 값 타입 수정 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "1000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressHistory().add(new Address("old1", "street", "1000"));
member.getAddressHistory().add(new Address("old2", "street", "1000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("===== START =====");
Member findMember = em.find(Member.class, member.getId());
// homeCity => newCity
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
// 치킨 -> 한식 (String 자체가 값 타입이기에 제거하고 새로 추가해줘야함, 컬렉션의 값만 변경해도 JPA가 알아서 바꿔준다.)
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
// MEMBER_ID에 해당하는 값들을 전부 삭제하고 기존의 old2와 newCity1 두 개를 새로 insert수행한다.
// 마치 cascade에 고아 객체 제거 기능이 포함된 것처럼 동작한다.
findMember.getAddressHistory().remove(new Address("old1", "street", "1000")); // 내부에서 equals() 로 값을 찾아 삭제
findMember.getAddressHistory().add(new Address("newCity1", "street", "1000"));
tx.commit();
- 참고: 값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
- 값 타입은 별도의 라이프 사이클을 가지지 않고 엔티티에 의존한다. 엔티티의 값을 바뀌거나 하면 자동으로 업데이트 된다. (일대다 연관관계에서 cascade all 넣고 orphan true로 넣은것이랑 같다고 보면 된다)
값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 값은 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 값 타입 컬렉션에서 하나를 지우고 새로 하나를 추가한다면 기존에 있는 것들을 전부 삭제 기존의 남은 것들을 전부 insert쿼리로 추가한다.
@OrderColumn(name = "address_history_order")
을 사용하여 컬렉션 순서 값이 들어가서 해결할 수 있긴하다. 하지만 이것도 엄청 위험하다. 원하는대로 의도하지 동작하는 것도 되게 많고 컬렉션이 0, 1, 2, 3인데 중간에 2번을 빼먹으면 0, 1하고 2번이 null로 들어오기도 한다.1 2 3 4 5 6
@OrderColumn(name = "address_history_order") @ElementCollection @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID") ) private List<Address> addressHistory = new ArrayList<>();
- 결론적으로 이렇게 복잡하게 쓸거면 완전히 다르게 풀어야한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함:
null 입력X
,중복 저장X
값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
- 값 타입을 엔티티로 승격한다
- 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
- 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
- EX) AddressEntity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {
}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(city, address.city) &&
Objects.equals(street, address.street) &&
Objects.equals(zipcode, address.zipcode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
...
}
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
private Address address;
public AddressEntity() {
}
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
...
}
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
// 기간 Period
@Embedded
private Period workPeriod;
// 주소
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "MEMBER_ID")
)
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@OneToMany(cascade = ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
...
}
public class JpaMain {
public static void main(String[] args) {
...
try {
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "1000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressHistory().add(new AddressEntity("old1", "street", "1000"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "1000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("===== START =====");
Member findMember = em.find(Member.class, member.getId());
// MEMBER_ID에 해당하는 값들을 전부 삭제하고 기존의 old2와 newCity1 두 개를 새로 insert수행한다.
// 마치 cascade에 고아 객체 제거 기능이 포함된 것처럼 동작한다.
findMember.getAddressHistory().remove(new AddressEntity("old1", "street", "1000")); // 내부에서 equals() 로 값을 찾아 삭제
findMember.getAddressHistory().add(new AddressEntity("newCity1", "street", "1000"));
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
값 타입 컬렉션은 언제쓰는가?
예를 들어, 셀렉트 박스에 치킨이나 피자를 멀티로(다중) 체크할 수 있다. 이렇게 추적할 필요가 없고 값이 바껴도 업데이트칠 필요가 없을때 값 타입컬렉션을 쓴다.
그게 아닌 이상은 왠만하면 엔티티다. 주소 이력도 엔티티다. 주소 이력만 조회할때 다 엔티티로 해야한다. 값을 변경하지 않는다 해도 DB쿼리 자체를 그쪽에서 시작해서 가져와야 하는 것들은 다 엔티티로 보면된다. 주소가 다 사라져도 이력이 남아야할때가 있을텐데 그럴때도 엔티티를 써야한다. 물론 정말 단순한건 값 타입을 써도 된다.
정리
- 엔티티 타입의 특징
- 식별자O
- 생명 주기 관리
- 공유
- 값 타입의 특징
- 식별자X
- 생명 주기를 엔티티에 의존
- 공유하지 않는 것이 안전(복사해서 사용)
- 불변 객체로 만드는 것이 안전
값 타입은 정말 값 타입이라 판단될 때만 사용해야 한다.
엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안된다. 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티다.