지나공 : 지식을 나누는 공간

JPA 7-2 : 값 타입 컬렉션 본문

Tech/JPA

JPA 7-2 : 값 타입 컬렉션

해리리_ 2020. 12. 7. 22:51

여기서 알아볼 내용은,

 

  • 값 타입 컬렉션 사용하는 예제 : 저장, 조회, 수정 등
  • 값 타입 컬렉션 주의사항
  • 실무에서의 사용법 팁
  • 값 타입 컬렉션은 언제 쓰는지
  • 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");

실행하면 절대 안된다. 왜 그런지는 이전 포스팅에 적어놨다.

eocoding.tistory.com/35

 

JPA 7-1 : 값 타입과 불변 객체, 값 타입 비교

(JPA7에 이어지는 내용) 이번에 알아볼 내용은, 값 타입의 공유 참조와 그 부작용 공유 참조 시 부작용 피하기 객체 타입의 한계 불변 객체 값 타입의 비교 값 타입 공유 참조 임베디드 타입과 같

eocoding.tistory.com

그래서 수정하고 싶다면 아래 코드처럼 새로 생성해서 넣어야 한다.

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 표준 강의를 듣고 정리한 내용입니다.

728x90
Comments