[인프런 김영한] JPA - 값 타입 컬렉션

2021. 2. 14. 16:26프로그래밍 언어/Spring Framework

[인프런 김영한] JPA - 값 타입과 불변 객체


해당 글은 인프런 김영한강사님의 영상을 보고 정리한 글입니다.

Spring Boot, Spring Data JPA를 사용해 실습하였습니다.

김영한 인프런 : www.inflearn.com/users/@yh

 

인프런 - 김영한의 강의들을 만나보세요.

우아한형제들 개발 팀장 (전: 카카오, SK플래닛) 저서: 자바 ORM 표준 JPA 프로그래밍

www.inflearn.com


▣ 값타입 컬렉션

 * 값타입을 컬렉션에 담아서 사용하는 방법

 * 값타입을 하나 이상 저장할 떄 사용.

  - @ElementCollection, @CollectionTable 사용한다.

 * 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.

 * 컬렉션을 저장하기 위한 별도의 테이블이 필요함. 

 

 

 

 

 

 

▣ 실습 예제

◈ 등록

Member

@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @Embedded
    private Address homeAddress;

    @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<>();

}

 

 

Addrress

@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;

    // 기본 생성자 필수
    public Address() {
    }

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}

 

 

실행

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

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

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

memberRepository.save(member);

// 실행결과
Hibernate: 
    insert 
    into
        member
        (city, street, zipcode, username, member_id) 
    values
        (?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        address
        (member_id, city, street, zipcode) 
    values
        (?, ?, ?, ?)
Hibernate: 
    insert 
    into
        address
        (member_id, city, street, zipcode) 
    values
        (?, ?, ?, ?)
Hibernate: 
    insert 
    into
        favorite_food
        (member_id, food_name) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        favorite_food
        (member_id, food_name) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        favorite_food
        (member_id, food_name) 
    values
        (?, ?)

| 값타입 컬렉션을 따로 save하지 않았는데 다른 테이블인데도 불구하고 함께 저장이 되었다는것을 볼 수 있습니다.

왜냐하면 값타입이기 때문에 본인에 대한 LifeCycle이 없으며 Member에게 종속되어 있습니다.

크게보면 username도 값타입, FAVORITE_FOOD도 값타입이라고 볼 수 있습니다.

 

 

 

 

 조회

실행

memberRepository.save(member);
memberRepository.flush();

System.out.println("===========================");
Member member1 = memberRepository.findById(member.getId()).get();

 

결과

===========================
Hibernate: 
    select
        member0_.member_id as member_i1_2_0_,
        member0_.city as city2_2_0_,
        member0_.street as street3_2_0_,
        member0_.zipcode as zipcode4_2_0_,
        member0_.username as username5_2_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?

| 쿼리를 확인해 보면 member 컬럼만 가지고 오게 되며 이말은 LAZY 전략을 기본적으로 사용합니다.

Embedded로 설정된 Address는 member에 소속된 값타입이기 때문에 함께 조회됩니다.

 

@ElementCollection는 LAZY전략으로 기본 Default되어 있습니다.

 

 

 

 수정

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

| 위 처럼 값타입을 set으로 변경하면 update문은 잘 실행되나 사이트 이펙트가 발생할 요소이기 때문에 이럴때는 

아래처럼 Address를 새로운 값타입으로 넣어준다.

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

 

* Collection을 다룰 때 equals를 오버라이드 하는게 의미가 있다.

 

 

 

 

 

▣ 값타입 걸렉션의 제약사항

 * 값타입은 Entity와 다르게 식별자 개념이 없다.

 * 값은 변경하면 추적이 어렵다.

 * 값타입 컬렉션에 변경 사항이 발생하면, 주인 Entity와 연관된 모든 데이터를 삭제하고, 값타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.

 * 값타입 걸렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야함. Not Null, 중복 저장 X

 

 

> 복잡하다면 이런 부분은 다르게 풀어야 합니다.

▣ 값 타입 컬렉션 대안

 * 실무에서는 상황에 따라 값타입 컬렉션 ㄷ대신에 일대다 고나계를 고려

 * 일대다 관계를 위한 Entity를 만들고, 여기에서 값 타입을 사용

 * 영속성전이(CASCADE) + 고아 객체 제거를 사용해서 값타입 컬렉션 처럼 사용

 

AddressEntiity

@Entity
@Table(name = "ADDRESS")
public class AddressEntity {

    @Id
    @GeneratedValue
    private Long id;

    private Address address;
}

 

Member 수정

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

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();

 

▣ 값타입 언제 쓸까요?

-> 진짜 단순할 때 

 - 치킨, 피자를 select box로 만들어서 사용할 떄 

 

 

 


▣ 정리

* Entity 타입의 특징

 - 식별자 O(ID)

 -  생명주기 관리

 - 공유

* 값타입의 특징

 - 식별자 X (ID)

 - 생명주기를 Entity에 의존

 - 공유하지 않는 것이 안전 (복사해서 사용)

 - 불변 객체로 만드는 것이 안전

 

- Entity와 값타입을 혼동해서 Entity를 값타입으로 만들면 X

- 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그건 값타입이 아닌 Entity