본문 바로가기

JPA/값 타입

값 타입 컬렉션

우리가 지금까지 배운 값 타입을 컬렉션으로 사용할 수도 있다.
하지만 명확한 단점때문에 사용하는 상황이 한정돼있다.
이번 글에서는 값 타입 컬렉션에 대해서 알아보자!

github 전체코드 주소(branch: valueTypeCollection)

(이번 글은 굉장히 복잡하다. 읽다가 헷갈리는 부분은 JPA/값 타입 카테고리의 글들을 참고해보자!!!)


Student Entity

값 타입 컬렉션은 값을 하나 이상 저장하기 위해 사용한다.
Student Entity가 값타입 컬렉션으로 favoriteSubjects와 addressHistory를 가지고 있다.
그렇다면 이 값 타입 컬렉션은 Student 테이블에 저장될까?
당연히 아니다. 값 타입 컬렉션은 별도의 테이블에 저장된다.
(Entity와 값 타입 컬렉션은 일 대 다 관계를 맺게 된다.
Student Entity는 1이 되고, 값 타입 컬렉션은 다가 된다.
관계형 DB에서는 1:N 관계를 한 테이블 내에서 풀어내는 것이 불가능하고,
FK를 통한 참조로 이를 풀어내야 하므로 다른 테이블에 저장되어야 한다.)

 

코드를 통해서 살펴보자!

 

Address.java

package hellojpa;

import lombok.Getter;

import javax.persistence.Embeddable;
import java.util.Objects;

@Getter
@Embeddable
public class Address {

    private String city;

    private String street;

    private String zipcode;

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

    public Address() {
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(city, street, zipcode);
    }
}

 

Student.java

package hellojpa;

import lombok.Data;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Entity
@Data
public class Student {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int age;

    @ElementCollection
    @CollectionTable(name = "FAVORITE_SUBJECT"
            , joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "SUBJECT_NAME")
    private Set<String> favoriteSubjects = new HashSet<>();

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

`favoriteSubjects`와 `addressHistory`라는 두 개의 값 타입 컬렉션이 존재한다.
그리고 이 값 타입 컬렉션들은 별도의 테이블에 저장된다고 했다.
그 별도의 테이블을 설정하기 위한 어노테이션이 @CollectionTable이다.
이 어노테이션으로 값 타입을 저장할 테이블명과 JoinColumn명을 설정해준다.
만들어지는 테이블을 보면 다음과 같다.
(@ElementCollection은 그냥 값 타입 컬렉션을 사용할 때 넣어주면 된다.)

 

DB 결과

또한 값 타입 컬렉션과 매핑되는 테이블의 컬럼며을 설정하고싶을 수 있다.
이 때 @Embedded 타입 컬렉션의 경우 컬럼명을 설정하고싶다면,
@AttributeOverrides 어노테이션을 사용하면 된다.
그 외의 primitive type과 wrapper class, String 등의 기본값 타입의 컬렉션은
@Column 어노테이션을 통해 컬럼명을 설정해줄 수 있다.

(위 결과를 보면 favorite_subject 테이블의 외래키가 아닌 값을 저장하는 컬럼명이
@Column에 의해 SUBJECT_NAME으로 설정된 것을 확인할 수 있다.)

값 타입 컬렉션 CRUD

값 타입 컬렉션을 저장하는 코드를 살펴보자!!

 

JpaMain.java - 값 타입 컬렉션 저장 코드

// 값 타입 컬렉션 저장 코드
Student student = new Student();
student.setName("code-mania");

student.getFavoriteSubjects().add("수학");
student.getFavoriteSubjects().add("영어");
student.getFavoriteSubjects().add("역사");

student.getAddressHistory().add(new Address("서울특별시", "세종대로", "10000"));
student.getAddressHistory().add(new Address("인천광역시", "부평대로", "60000"));

em.persist(student);

값 타입 컬렉션은 DB에서는 별도의 테이블에 저장된다.
하지만 객체에서는 Student Entity에 속해있다.
따라서 Student Entity가 저장될 때 저장되고, 삭제될 때 삭제된다.
즉, Student Entity의 라이프사이클과 값 타입 컬렉션의 라이프사이클은 일치한다.

DB 결과

 

JpaMain.java - 값 타입 컬렉션 조회 코드

// 값 타입 컬렉션 조회 코드
System.out.println("============================");
Student findStudent = em.find(Student.class, student.getId());
List<Address> addressHistory = findStudent.getAddressHistory();
for (Address address : addressHistory) {
	System.out.println("address.getCity() = " + address.getCity());
}

Set<String> favoriteSubjects = findStudent.getFavoriteSubjects();
for (String favoriteSubject : favoriteSubjects) {
	System.out.println("favoriteSubject = " + favoriteSubject);
}

로그(Address 부분)

조회 코드를 보면 처음에 Student Table만 조회하고,
실제로 사용하는 시점에 Address Table을 조회하는 것을 볼 수 있다.
이는 값 타입 컬렉션이 조회 시 지연로딩을 사용하기때문이다.
(조회전략은 @ElementCollection 어노테이션에서 fetch 옵션을 통해 설정 가능하다.
하지만 이전 글들에서 배웠듯이 대부분의 경우 지연전략을 사용하는 것이 좋다.)

 

JpaMain.java - 값 타입 컬렉션 수정 코드

//값 타입 컬렉션 수정 코드
favoriteSubjects.remove("역사");
favoriteSubjects.add("과학");

findStudent.getAddressHistory().remove(new Address("old1", "street", "10000"));
findStudent.getAddressHistory().add(new Address("newCity1", "street", "10000"));

우리가 값 타입 컬렉션의 generic 타입으로 사용하고 있는 클래스는 String과 Address이다.
그리고 해당 클래스들의 객체는 수정불가능한 불변객체로 설계되어있다.
따라서 수정할 때 Setter를 통해서 수정하는 것은 불가능하고,
remove 메서드를 통해 객체를 제거 후 add 메서드로 추가하게 된다.

이 때 사용하는 class에(지금의 경우: Address, String) equals와 hash 메서드가
올바르게 정의되어있지 않으면 remove 메서드가 제대로 동작하지 않는다.
(이해가 안 되면 collection과 hash의 관계에 대해서 공부해보자!)

이렇게 수정코드를 작성하고 로그를 확인해보면 우리는 하나 이상한 점을 찾을 수 있다.

favoriteSubjects remove 및 add 로그
addressHistory의 remove 및 add 로그

delete문이 나가는 것을 잘 보면 favoriteSubjects를  remove 및 add했을 때는
하나만 delete하고, 하나만 insert했다.
(하나라고 확신할 수 있는 경우는 Set에는 동일한 데이터가 중복저장되지 않기때문이다.)

하지만 addressHistory의 경우 모두 delete 후 addressHistory의 요소들을 모두 insert했다.

이는 값 타입 컬렉션이 어떤 타입이냐에 따라서 차이가 생긴다.
favoriteSubjects의 경우 Set 타입이고, addressHistory의 경우 List 타입이다.
Set은 중복되지 않는 성질이 있고, List는 중복되어도 상관이 없다.
JPA가 이러한 성질을 고려하여 delete 및 insert문을 작성한 것이다.
여기서 주의할 부분은 List를 수정할 때마다 전부 delete하고 전부 insert하므로
 List를 사용하면 성능이 엄청나게 나빠질 수 있다는 것이다.

값 타입 컬렉션의 제약사항

일단 값 타입 컬렉션은 다음과 같은 제약사항이 있다.

1. 식별자가 없어서 값을 변경 후 추적이 어렵다.
2. 값 타입 컬렉션의 타입으로 List같은 타입을 사용하면 성능이 저하된다.

그러면 위 단점들을 고려했을 때 언제 값 타입을 사용해야 적절할까?

먼저 식별자가 필요하다면 값 타입이 아닌 엔티티로 만들어야 한다.
(식별자가 필요한 경우: 식별자를 통해서 데이터를 지속적으로 추적 및 관리해야 하는 경우)

또한 List는 최악의 성능을 자랑하기때문에 Set을 사용해야 한다.
Set은 중복되지 않는 성질이 있다.
그리고 이 성질을 DB에도  PK를 통해 적용시키는 것이 좋다.
즉, DB를 설계할 때 값 타입 컬렉션과 매핑되는 테이블의 모든 필드를 PK로 묶는 것이다.
참고로 모든 필드를 PK로 묶으면 null과 중복 저장이 불가능해진다.
(DB와 객체에서 모두 중복저장이 방지되므로 프로젝트를 안정적으로 만들어준다.
DB설계와 객체설계가 일치해야 더 안정적인 프로젝트가 되며,
Set을 사용해도 DB에서 PK로 묶어내지 않아 중복될 수도 있다면 좋은 구조는 아니라고 생각한다.)

 

따라서 위 단점들을 고려해 최종적으로 이렇게 정리된다.
식별자가 필요없고,  중복이 없어 모든 필드를 PK로 묶을 수 있으며, null이 없는 경우에만
값 타입 컬렉션을 사용하는 것이 바람직하다.
그 외의 경우에는 값 타입 컬렉션을 Entity로 승격시켜서 일대다 관계로 풀어내는 것이 좋다.
다음 시간에는 일대다 관계로 어떻게 풀어내는지 살펴보자~~

 

참고강의: 배달의 민족 개발팀장 김영한 강사님의 JPA 강의

'JPA > 값 타입' 카테고리의 다른 글

값 타입 컬렉션을 엔티티로 승격시키기  (0) 2021.09.06
값 타입의 비교  (0) 2021.08.31
임베디드 타입과 불변 객체  (0) 2021.08.28
임베디드 타입  (0) 2021.08.28
값 타입  (0) 2021.08.28