개발자꿈나무
값 타입 컬렉션 본문
자바의 컬렉션처럼 값 타입도 컬렉션을 사용할 수 있다.
회원의 좋아하는 음식들과 이전 주소 이력을 저장하고자 한다면 Set과 List를 이용해서 저장할 수 있다.
단, 테이블에는 컬렉션 타입이 들어갈 수 없으므로 별도의 컬렉션 테이블을 만들어서 관리해야 한다.
//주소 이력
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
//좋아하는 음식
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
- 값 타입을 하나 이상 저장하고자 할 때 사용한다.
- @ElementCollection 어노테이션을 사용하여야 하며, 별도의 테이블로 만들어주는 것이므로 @CollectionTable을 이용해서 테이블 이름과 조인할 컬럼을 지정해줘야 한다.
- 위에서 간략하게 설명했듯이 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없으며, 컬렉션을 저장하기 위한 별도의 테이블이 필요하므로 @CollectionTable을 사용해야 한다.
* favoriteFoods 같은 경우에는 값으로 사용되는 컬럼이 하나이므로 @Column을 이용해서 별도의 이름을 저장할 수 있다.
* addressHistory도 컬럼명을 변경하고 싶으면 @AttributeOverride를 사용하면 된다.
실행해보면 address와 favorite_food라는 테이블이 생긴 것을 확인할 수 있다.
★ 값 타입 컬렉션 사용
⭐︎ 컬렉션 등록
//저장
Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("서울시", "구로구", "1000"));
member.getFavoriteFoods().add("스파게티");
member.getFavoriteFoods().add("짜장면");
member.getFavoriteFoods().add("볶음밥");
member.getAddressHistory().add(new Address("서울", "강남", "2000"));
em.persist(member);
컬렉션에 값을 저장하고자 할 때는 member를 먼저 생성하고 음식들과 주소를 저장해주면 되는데 눈여겨볼 점은 마지막에 member 엔티티만 영속화했다는 것이다. 값 타입 컬렉션도 결국 값일 뿐이기에 member 엔티티를 영속화하면 값 타입도 자동으로 저장이 되는 것이다.
실행했을 때 쿼리문을 살펴보면 먼저 insert Member 쿼리가 실행되고 이후에 Address 테이블에 1번, Favorite_Food 테이블에 총 3번 실행이 된다. -> em.persist(member) 한 번 호출로 총 5번의 insert 쿼리가 실행되는 것이다.
* 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
⭐︎ 컬렉션 조회
//조회
Member findMember = em.find(Member.class, member.getId());
Set<String> favoriteFoods = findMember.getFavoriteFoods(); //LAZY
for(String favoriteFood : favoriteFoods) { //DB조회
System.out.println("favoriteFood : " + favoriteFood);
}
List<Address> addressHistory = findMember.getAddressHistory(); //LAZY
System.out.println("addressHistory(City) : " + addressHistory.get(0).getCity()); //DB조회
값 타입 컬렉션의 페치 전략은 기본값이 LAZY이다. 처음 findMember를 조회해올 때는 member 테이블에 있는 값만 조회가 된다.
이후 favoriteFood의 값들을 실제로 사용하고자 할 때 데이터베이스에 접근해 값을 가져온다. AddressHistory도 마찬가지
⭐︎ 컬렉션 수정
//수정
Member updateMember = em.find(Member.class, member.getId());
Address address = updateMember.getHomeAddress();
member.setHomeAddress(new Address("newCity", address.getStreet(), address.getZipcode()));
updateMember.getFavoriteFoods().remove("짜장면");
updateMember.getFavoriteFoods().add("짬뽕");
//equals, hashCode의 중요성이 여기서 나옴. 잘못 구현되면 안 지워짐
updateMember.getAddressHistory().remove(new Address("서울시", "강남", "2000"));
updateMember.getAddressHistory().add(new Address("부산시", "금정구", "5100"));
임베디드 타입인 address에서 도시만 수정하고 싶을 때 address.setCity()를 하면 절대 안된다. 저번 포스팅에서 정리한 대로 값 타입은 참조를 공유하기 때문인데 직접 대입을 하게 되면 다른 부분들까지 함께 수정이 될 수 있으므로 setter를 아예 없애거나 private으로 만들어 불변 객체로 만들어둔 상태이다. 수정하고 싶을 때는 아예 새로운 Address 객체를 생성해야하며 도시만 바꾸고 나머지는 그대로 두고싶다고 하면 address.getStreet()을 불러오면 된다.
좋아하는 음식은 자바의 String 타입을 사용하고 있는데 String 타입은 별도의 수정이 불가능하므로, 현재 회원의 음식들 중 삭제하고 싶은 대상을 제거하고 새로 추가하면 된다.
주소 이력도 마찬가지로 제거하고 추가해주면 되는데, 임베디드 값 타입의 경우에는 equals를 이용해서 완전히 일치하는 값 타입을 찾아서 제거를 한다. 따라서 remove() 안의 정보가 다르면 안되며 equals와 hashCode를 꼭 제대로 구현해줘야 일치하는 값 타입을 찾아서 삭제할 수 있다.
여기서 한 가지 주의해야 할 점이 있는데 addressHistory를 삭제하고 추가하는 과정을 살펴보겠다. 내가 하고자 한건 기존에 저장되어 있는 2개의 데이터 중 하나의 데이터를 삭제하고 새로운 도시명의 데이터를 추가하는 것인데 데이터베이스에 들어있는 결과값은 내가 원하는 결과가 나왔지만 쿼리문을 보면 아예 해당하는 멤버의 address를 전부 지워버리고, insert 쿼리가 2개가 나갔다.
★ 값 타입 컬렉션의 제약사항
값 타입은 엔티티와 다르게 식별자가 존재하지 않는다. 따라서 값을 변경하게 되면 추적이 쉽지 않다. Address 테이블의 구성을 보면 MEMBER_ID, CITY, STREET, ZIPCODE 컬럼을 가지는데 어느것 하나도 PK로 사용하기에 적당하지 않다. 따라서 모든 컬럼들을 묶어서 복합 키로 기본키를 가지는데 이렇게 되면 값을 수정한다고 해도 추적이 쉽지 않으므로 JPA는 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다. 현재 AddressHistory에는 총 2개의 값이 있으므로 insert 쿼리가 2번 나가게 된 것이다.
-> 이는 좋은 방법이 아니며, Address 테이블 같은 경우에는 컬렉션을 사용하는 것보다는 차라리 하나의 엔티티로 만들어 일대다 관계로 관리하는 것이 바람직할 수 있다.
@Entity
public class AddressEntity {
@Id @GeneratedValue
@Column(name = "ADRESS_ID")
private Long id;
public AddressEntity() {
}
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
private Address address;
}
//Member 엔티티
//컬렉션 대신 일대다 관계로 매핑
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
AddressEntity에 생성자를 만들고, 멤버 엔티티에서 영속성 전이와 고아 객체 제거 기능을 설정해 일대다 관계를 매핑해주면 된다. 수정하는 똑같은 로직을 실행했을 때 더이상 delete, insert 쿼리가 나가는게 아니라 Update 쿼리가 나가게 된다.
★ 값 타입 총정리
- 엔티티 타입의 특징
- 식벽자가 존재한다
- 생명 주기를 관리할 수 있다
- 참조 값을 공유할 수 있다 - 값 타입의 특징
- 식벽자가 없다
- 생명 주기를 엔티티에 의존한다
- 공유하지 않는 것이 안전하며 대신 값을 복사해서 사용해야 하나, 부작용이 발생할 수 있으므로 불변 객체로 만드는 것이 안전하다
'자바 > JPA' 카테고리의 다른 글
JPQL - 기본 문법과 기능 (0) | 2023.08.30 |
---|---|
객체지향 쿼리 언어 (0) | 2023.08.29 |
값 타입과 불변 객체 (0) | 2023.08.23 |
갑 타입 - 임베디드 타입 (0) | 2023.08.23 |
값 타입 - 기본 값 (0) | 2023.08.23 |