본문 바로가기
JPA

[JPA] 값 타입 컬렉션

by byeongoo 2020. 4. 26.

1. 값 타입 컬렉션

이전 포스팅에서 설명한 값 타입을 컬렉션으로 사용하는 방법에 대해서 알아보겠습니다.

  • 값 타입을 하나 이상 저장할 때 사용
  • @ElementCollection, @CollectionTable 사용
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요

Member가 favoriteFoods와 addressHistory라는 값 타입 컬렉션을 가지고 있을 때 RDB에서는 favoriteFoods, addressHistory라는 별도의 테이블로 빼서 관리를 합니다. 값 타입을 묶어서 하나의 PK를 만들어냅니다.

여기에 식별자 ID같은 개념을 넣어서 걔를 PK로 쓰게되면, 얘는 값타입이 아니라 ENTITY가 됩니다.

값타입은 값들만 저장하고 PK로 구성하면 됩니다.

 

@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는 String이라는 값 하나이고 내가 정의한게 아니니까 컬럼명을 지정할 수 있습니다. 임베디드 타입은 테이블의 컬럼으로 들어가고, 타입 컬렉션은 새로운 테이블 생성되는 차이가 있습니다. 컬렉션들은 일대다 개념이기 때문에 한테이블에 넣을 방법이 없습니다. 그래서 일대다로 풀어서 별도의 테이블로 만들어 냅니다.

2. 값 타입 컬렉션 변경 예제

Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "zipcode");

member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");

member.getAddressHistory().add(new Address("old1", "street", "zipcode");
member.getAddressHistory().add(new Address("old2", "street", "zipcode");

em.persist(member);

한가지 흥미로운점은 값 타입 컬렉션을 persist 안했는데 자동으로 persist 된걸 확인할 수 있습니다. 이것은 값 타입이기 때문입니다. 값 타입 컬렉션도 본인 스스로의 라이프사이클이 없습니다. member에 소속되어 있습니다. 값 타입들은 멤버에서 값이 바뀌면 자동으로 업데이트됩니다.

 

DB에서 조회해기위해, em.flush(), emclear() 후 member를 조회해보겠습니다.

member.getAddressHistory().add(new Address("old1", "street", "zipcode");
member.getAddressHistory().add(new Address("old2", "street", "zipcode");

em.persist(member);
em.flush();
em.clear();

System.out.println("================== START ===================");
Member findMember = em.find(Member.class, member.getId());

콘솔창에 찍힌 메세지를 보면 member만 가지고 옵니다. 그 말은 이 컬렉션들은 지연로딩입니다.

이제 값 타입을 수정해보겠습니다. homeCity에서 newCity로 바꿔보겠습니다.

//homeCity -> newCity
findMember.getHomeAddress().setCity("newCity");

값 타입이라는 것은 이뮤터블해야합니다. 기대한대로 업데이트문이 나갑니다. 그런데 값타입은 사이드 이펙트가 생깁니다. 이런식으로 변경하면 절대 안됩니다.

 

업데이트를 위해서 값을 새로 넣어야합니다. 

//homeCity -> newCity
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode());

값타입이라는거 자체가 추적도 안되기 떄문에 이런식으로 교체를 해줘야합니다. 이제 값타입 컬렉션을 업데이트 해보겠습니다. favoriteFoods는 단순 스트링이라 remove로 치킨을 지우고 새로 add해서 한식을 넣어야합니다.

findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");

컬렉션의 값만 변경해도 jpa가 뭐가 변경됐는지 보고 db에 반영합니다. 이제 주소를 바꿔보겠습니다. 대부분 컬렉션을 찾을때 equals를 활용합니다.

findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity", "street", "10000"));

이렇게 넣으면 기존에 있던거를 지워줍니다. equals와 hash를 제대로 넣어놔야합니다. 그리고 새로 add해주면 됩니다.

지금 보면 MEMBER_ID를 기준으로 ADDRESS테이블을 통째로 지웁니다. 그리고 데이터를 INSERT를 2번합니다.

테이블에 있는 데이터를 완전히 갈아낍니다. 한식 치킨에서는 됐는데 여기서는 안됩니다. 결론적으로는 원하는데로 데이터가 수정되었습니다. 값 타입은 엔티티와 다르게 식별자 개념이 없습니다. 값을 변경하면 추적이 어렵습니다.

값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. 결론적으로는 이걸 쓰면 안됩니다.

테이블 보면 이거 컬럼 4개만 있는데 변경되면 추적이 안됩니다. OrderColumn 넣고하면 어떻게든 풀 수있습니다. 다 지우고 하는게 아니라 update 쿼리가 나갑니다.

@OrderColumn(name = "address_history_order")
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();

컬렉션 순서 값이 들어갑니다. pk를 member_id와 address_history_order 2개로 잡습니다. 그런데 이것도 위험합니다. 의도하지 않게 동작하는 경우도 많습니다. 결론적으로는 이렇게 복잡하게 쓰려면 완전히 다르게 풀어야합니다.

 

[값 타입 컬렉션의 제약 사항]

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 함: null 입력X, 중복 저장X

 

[값 타입 컬렉션 대안]

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
  • 속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬 렉션 처럼 사용
    • EX) AddressEntity
@Entity
public class AddressEntity{

    @Id
    @GenerateValue
    private Long id;
    
    private Address address;
    
    public Long getId(){
        return id;
    }
    
    public void setId(Long id){
        this.id = id;
    }

}

member에서 AddressEntity로 매핑합니다.

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistroy = new ArrayList<>();
member.getAddressHistory().add(new AddressEntity("old1", "street", "10000"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "10000"));

 

값 타입 컬렉션을 쓸때가 있기는합니다. 진짜 단순한, 예를 들어 셀렉트 박스에 사용할 때 입니다. 이런 단순하고 값이 바뀌어도 업데이트 할 필요가 없을 때 사용합니다.

3. 정리

엔티티 타입의 특징

  • 식별자O
  • 생명 주기 관리
  • 공유

값 타입의 특징

  • 식별자X
  • 생명 주기를 엔티티에 의존
  • 공유하지 않는 것이 안전(복사해서 사용)
  • 불변 객체로 만드는 것이 안전

REFERENCE

https://www.inflearn.com/course/ORM-JPA-Basic

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 서버 데이터베이스 프레임워크 및 라이브러리 프로그래밍 언어 서비스 개발 Java JPA Spring Data JPA 온라인 강의 ORM, JPA, 자바, java, 우아한형제들

www.inflearn.com