지나공 : 지식을 나누는 공간
JPA 7-2 : 값 타입 컬렉션 본문
여기서 알아볼 내용은,
- 값 타입 컬렉션 사용하는 예제 : 저장, 조회, 수정 등
- 값 타입 컬렉션 주의사항
- 실무에서의 사용법 팁
- 값 타입 컬렉션은 언제 쓰는지
- JPA 타입 정리
문제상황
Member처럼 컬렉션을 가지면 1대 다 관계를 유지하지만 DB 테이블은 특성 상 한 객체가 컬렉션을 가지는 것을 구현하기 어렵다. 그래서 Member에 Favorite Food가 있다면 이걸 별도의 테이블로 보관해야 한다. 그래야 관리할 수 있다.
일단 매핑을 해보자.
값 타입 매핑은 @ElementCollection으로 해야 하고, 테이블명 정하는 건 CollectionTable에서 name을 통해 할 수 있다. MEMBER_ID를 가지고 JOIN하겠다는 의미이다.
값 타입 컬렉션
값 타입을 하나 이상 저장할 때 사용하고, 컬렉션들은 1대 다의 관계이므로 같은 테이블에 지정할 수 없고 별도의 테이블로 풀어내야 한다. 컬렉션을 저장하기 위한 별도의 테이블 말이다.
위의 문제상황 그림과 같이 테이블을 설계했다고 하자.
Member 안에는
List<Address> addressHistory;
Set<String> favoriteFoods;
Address homeAddress;
가 있다.
[값 타입 저장 예제]
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000")); //(1)
member.getFavoriteFoods().add("치킨"); //(2)
member.getFavoriteFoods().add("족발"); //(3)
member.getFavoriteFoods().add("피자"); //(4)
member.getAddressHistory().add(new Address("old1", "street1", "10000")); //(5)
member.getAddressHistory().add(new Address("old2", "street1", "10000")); //(6)
em.persist(member);
insert into Member(city, street, zipcode, USERNAME, MEMBER_ID); // (1)에 의해 실행된 쿼리
insert into Address(MEMBER_ID, city, street, zipcode); 두 번 // (5), (6)에 의한
insert into Favorite_Food(MEMBER_ID, FOOD_NAME); 세 번 // (2),(3),(4)에 의한 쿼리
마지막 줄에서 member만 persist했는데 다 저장이 된다.
값 타입들은 따로 persist를 할 필요 없이 member에 생명주기가 종속적이기 때문에 member에서 바뀌면 자동으로 업데이트가 된다. 일대다 연관관계에서 cascade랑 비슷하다.
즉, 값 타입 컬렉션은 영속성전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 생각하면 된다.
[값 타입 조회 예제]
Member findMember = em.find(Member.class, member.getId());
실행된 쿼리를 보면 member에 대한 것들만 select문이 나갔다.
값 타입들은 기본적으로 지연로딩이니까.
따라서 address중에 city만 가져와서 찍어보면,
Member findMember = findMember.getAddressHistory();
for(Address address : addressHistory){
System.out.println("address = " + address.getCity();
}
그제서야 address 테이블에 대한 select 쿼리문이 나간다. 결국 지연로딩이라는 의미.
[값 타입 수정 예제]
아래처럼 코드를 작성하면 될까?
findMember.getHomeAddress().setCity("newCity");
실행하면 절대 안된다. 왜 그런지는 이전 포스팅에 적어놨다.
그래서 수정하고 싶다면 아래 코드처럼 새로 생성해서 넣어야 한다.
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipCode());
Set<String>인 favoriteFoods를 수정하는 방법은, 딱히 방법이 없다.
String이라는 자바 기본 타입이라서 생성자로 새 객체를 생성할 수도 없고. 따라서 삭제한 뒤에 추가한다.
이 타입은 업데이트 할 수가 없다.
//치킨 -> 한식
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
여기서도 알 수 있듯이, 마치 영속성 전이가 되는 것처럼 컬렉션의 값만 변경하고 commit()을 하면 DB에도 쿼리가 나간다. 값 타입 컬렉션들은 Member한테 모두 속해서 Member만 라이프사이클이 관리되는 것이다.
이번엔 List<Address>의 한 주소 객체를 통째로 바꿔보자. old1 주소를 바꾸자.
이것도 값 타입이기 때문에 일단 오브젝트를 찾아서 통째로 갈아 끼워야 한다. 아래 처럼 한다.
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity", "street", "10000"));
그런데 중요한 점..!
Set<String>인 favoriteFoods()에 대해 실습한 건 쿼리문이 delete 1회, insert1회로 제대로 되어 있는데,
List<Address> 인 addressHistory()에 대해서 실습한 건 쿼리문이 전체 컬렉션 delete -> insert 2회 로 되어있다.
결론적으로는 수정이 잘 되긴했지만 실행된 쿼리문이 예상과 다르다. 마치 delete 1회, insert 1회가 나갈 것 같은데.
왜 그럴까?
값 타입의 제약 사항 때문이다.
값 타입은 엔티티와 다르게 식별자 개념이 없어서, 값을 변경하면 추적이 어렵다.
따라서 값 타입 컬렉션에 변경사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
@OrderColumn 이런 걸 사용해도 되지만 이것도 오류가 많다...
따라서 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다.
null 입력할 수 없고 중복 저장도 안되게 해야 한다.
실무 팁 )
상황에 따라 값 타입 컬렉션 대신에 일대다 관계로 만드는 걸 고려하는 것이 대안이 될 수 있다.
일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 이용하자.
영속성전이(cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션인 것 처럼 사용하기!
예 ) Address -> AddressEntity로 만들기
그러면 엔티티로서 자체적으로 pk를 가지게 되니까 이걸 가지고 가능해지는 부분이 생긴다.
그럼 값 타입은 언제 써요?
진짜 간단한 것일 때. 정말 단순한 거일 때 사용하는 거고 이런 거 아닌 이상 웬만하면 엔티티로.
정리
[엔티티 타입 특징]
- 식별자가 있다.
- 생명 주기를 관리할 수 있다.
- 공유할 수 있다.
[값 타입 특징]
- 식별자가 없다.
- 생명주기를 자기가 관리하지 못하고 속한 엔티티에 의존한다.
- 공유하지 않는 게 안전하다. 하고 싶다면 복사를 해라
- 불변 객체로 만드는 것이 안전하다.
값 타입은 정말로 값 타입이라고 판단될 때만 사용하기.
엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들지 말기.
식별자가 필요하고, 지속적으로 값을 추적하거나 변경해야 한다면 그건 값 타입이 아니라 엔티티다.
김영한님의 자바 ORM 표준 강의를 듣고 정리한 내용입니다.
'Tech > JPA' 카테고리의 다른 글
JPA 8 -1 : 프로젝션, 페이징 (0) | 2020.12.13 |
---|---|
JPA 8 : JPQL 기본 문법과 쿼리 API (0) | 2020.12.09 |
JPA 7-1 : 값 타입과 불변 객체, 값 타입 비교 (2) | 2020.11.10 |
JPA 7 : 임베디드 타입, JPA 데이터 타입 분류 (0) | 2020.11.09 |
JPA 6 : 영속성 전이(CASCADE)란? (2) | 2020.11.01 |