-
9. 값 타입 (2)Back-End/JPA 2022. 6. 10. 14:06
5. 값 타입 컬렉션 (Collection Value Type)
값 타입을 하나 이상 저장하려면 컬렉션에 저장하면 된다.
이때 2가지 어노테이션이 필요하다.
@ElementCollection
: 값 타입 컬렉션을 사용하는 속성에 표시한다.@CollectionTable
: 컬렉션을 위한 추가 테이블을 매핑한다. 생략 가능하며, 기본값으로 {엔티티 이름}_{컬렉션 속성 이름} 테이블과 매핑한다.
위의 다이어그램에서 favoriteFoods는 기본값 타입 컬렉션이고, addressHistory는 임베디드 타입 컬렉션이다.
관계형 데이터베이스 테이블의 컬럼 안에 컬렉션을 포함할 수 없기 때문에, 두 가지 값 타입 컬렉션 모두 별도의 테이블을 추가하고, 추가한 테이블을 매핑해야 한다.
favoriteFoods 와 addressHistory 값 타입 컬렉션은 아래와 같이 구현할 수 있다.
예제 코드)
@Entity public class Member { ... @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<>(); ... }
favoriteFoods 처럼 값으로 사용되는 컬럼이 하나인 경우, @Column을 사용해서 컬럼명을 지정할 수 있다.
5.1. 값 타입 컬렉션 사용
✔️ 등록
Member member = new Member(); // 임베디드 값 타입 member.setHomeAddress(new Address("통영", "몽돌해수욕장", "660-123")); // 기본값 타입 컬렉션 member.getFavoriteFoods().add("짬뽕"); member.getFavoriteFoods().add("짜장"); member.getFavoriteFoods().add("탕수육"); // 임베디드 값 타입 컬렉션 member.getAddressHistory().add(new Address("서울", "강남", "123-123")); member.getAddressHistory().add(new Address("서울", "강북", "000-000")); em.persist(member);
마지막에 member 엔티티만 영속화하면, JPA 가 member 엔티티의 값 타입도 함께 저장한다.
실제 데이터베이스에 실행되는 INSERT SQL은 6개이고, 각각은 다음과 같다.
- member: INSERT SQL 1번
- member.homeAddress: 컬렉션이 아닌 임베디드 값 타입이므로 회원테이블을 저장하는 SQL에 포함된다.
- member.favoriteFoods: INSERT SQL 3번
- member.addressHistory: INSERT SQL 2번
📌 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
✔️ 조회
값 타입 컬렉션도 조회할 때 페치 전략을 선택할 수 있고, 기본값은 LAZY 다.
@ElementCollection(fetch = FetchType.LAZY)
Member member = em.find(Member.class, 1L); Address homeAddress = member.getHomeAddress(); Set<String> favoriteFoods = member.getFavoriteFoods(); // LAZY /** * SQL * SELECT MEMBER_ID, FOOD_NAME * FROM FAVORITE_FOODS * WHERE MEMBER_ID = 1 */ for (String favoriteFood : favoriteFoods) { System.out.println("favoriteFood = " + favoriteFood); } List<Address> addressHistory = member.getAddressHistory(); // LAZY /** * SQL * SELECT MEMBER_ID, CITY, STREET, ZIPCODE * FROM ADDRESS * WHERE MEMBER_ID = 1 */ addressHistory.get(0);
위의 예제를 실행할 때 실행되는 SELECT SQL은 다음과 같다.
- member: 회원만 조회한다. 임베디드 값 타입인 homeAddress도 함께 조회한다. SELECT SQL을 1번 호출한다.
- member.homeAddress: 회원을 조회할 때 같이 조회해둔다.
- member.favoriteFoods: LAZY로 설정해서, 실제 컬렉션을 사용할 때 SELECT SQL을 1번 호출한다.
- member.addressHistory: LAZY로 설정해서, 실제 컬렉션을 사용할 때 SELECT SQL을 1번 호출한다.
✔️ 수정
Member member = em.find(Member.class, 1L); // 임베디드 값 타입 수정 member.setHomeAddress("새로운도시", "신도시1", "123456"); // 기본값 타입 컬렉션 수정 Set<String> favoriteFoods = member.getFavoriteFoods(); favoriteFoods.remove("탕수육"); favoriteFoods.add("치킨"); // 임베디드 값 타입 컬렉션 수정 List<Address> addressHistory = member.getAddressHistory(); addressHistory.remove(new Address("서울", "기존 주소", "123-123")); addressHistory.add(new Address("새로운도시", "새로운 주소", "123-456"));
임베디드 값 타입 수정
- MEMBER 테이블만 UPDATE 한다. 사실 Member 엔티티를 수정하는 것과 같다.
기본값 타입 컬렉션 수정- 탕수육을 치킨으로 변경하려면, 탕수육을 제거하고 치킨을 추가해야 한다.
- 자바의 String 타입은 수정할 수 없다.
임베디드 값 타입 컬렉션 수정- 값 타입은 불변해야 한다.
- 따라서 컬렉션에서 기존 주소를 삭제하고 새로운 주소를 등록해야 한다.
- 참고로 값 타입은 equals, hashCode를 꼭 구현해야 한다.
5.2. 제약사항
엔티티에 소속된 값 타입의 경우, 값이 변경되어도 소속된 엔티티를 데이터베이스에 찾고 값을 변경하면 된다.
그러나 값 타입 컬렉션의 경우, 보관된 값 타입들이 별도의 테이블에 보관된다. 따라서 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다.이런 문제로 인해, JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체 있는 모든 값을 데이터베이스에 다시 저장한다.
따라서 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.
참고) 일대다 관계로 설정하기
더보기값 타입 컬렉션을 사용하는 대신에 새로운 엔티티를 생성하여 일대다 관계로 설정해보자.
추가적으로 이 엔티티에 영속성 전이 + 고아 객체 제거 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.
@Entity public class Member { ... @OneToMany(casecade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "MEMBER_ID") private List<AddressEntity> addressHistory = new ArrayList<AddressEntity>(); } @Entity public class AddressEntity { @Id @GeneratedValue private Long id; @Embedded Address address; ... }
그리고 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 이로 인해 (기본 키 제약조건), 컬럼에 null을 입력할 수 없고 같은 값을 중복해서 저장할 수 없다는 제약이 있다.
6. 정리
엔티티 타입 (Entity Type) 값 타입 (Value Type) 식별자 식별자가 있다. 식별자가 없다. 생명 주기 생명 주기가 있다. 생명 주기를 엔티티에 의존한다. 공유 공유할 수 있다. 공유하지 않는 것이 안전하다. 값 타입은 정말 값 타입이라 판단될 때만 사용해야 한다.
식별자가 필요하고 지속해서 값을 추적하고 구분하고 변경해야 한다면, 그것은 값 타입이 아닌 엔티티다.
728x90'Back-End > JPA' 카테고리의 다른 글
9. 값 타입 (1) (0) 2022.06.09 8. 프록시와 연관관계 관리 (0) 2022.06.07 7. 고급 매핑 (0) 2022.05.03 6. 다양한 연관관계 매핑 (0) 2022.04.30 5. 연관관계 매핑 기초 (0) 2022.04.23