1. 개요


JPA는 데이터 타입을 크게 두 가지로 분류합니다.

  1. Entity 타입
  2. Value 타입

이 중 Value 타입에 대해 살펴보겠습니다.

2. Value 타입


Value 타입은 값의 변경을 추적할 수 없는 타입을 말합니다. int, double와 같은 Primitive 타입뿐만 아니라, Integer, String과 같은 클래스도 참조는 가지지만 값의 변경을 추적할 수 없기 때문에 마찬가지로 Value 타입에 속합니다. (값의 변경을 추적할 수 없다는 말은 값을 변경할 수 없다는 의미입니다. Integer, String의 값을 변경하면 참조하는 주소가 바뀝니다.)
JPA의 Value 타입은 세 가지로 나눠 생각해볼 수 있습니다.

  1. 기본 값 타입: int, double와 같은 Java의 Primitive 타입과, Integer, String과 같은 래퍼 클래스를 말합니다.
  2. Embedded 타입 (복합 값 타입): 기본 값 타입을 래핑해서 새롭게 만든 클래스 타입을 말합니다.
  3. Collection 값 타입: 위 1, 2번 타입을 List, Set과 같은 Collection 형태로 사용하는 것을 말합니다.

 

위 세 가지 분류를 하나씩 살펴보겠습니다.

3. 기본 값 타입


int, double과 같은 Primitive 타입은 힙 영역에 할당되어 참조를 가지는 것이 아니기 때문에 당연하고, Integer와 String 같은 클래스도 클래스이기 때문에 힙 영역에 할당되어 참조를 가지지만 Immutable(불변) 객체이기 때문에 동일한 참조로 값을 변경할 수 없습니다. 따라서 동일한 주소 참조를 가지로 값의 변경에 대해 추적하는 것이 불가능하기 때문에 Value 타입에 속합니다.

4. Embedded 타입 (복합 값 타입)


Embedded 타입은 값 타입을 여러 개 래핑해서 의미있는 이름으로 새롭게 만든 클래스 타입을 말합니다.

// Embedded 타입 예1
public class Position {
    int x, y;
}

// Embedded 타입 예2 
public class Address {
    String city, street, zipcode;
}

위처럼 만들면 더 객체지향스럽고 재활용하기도 좋습니다.  Member Entity 클래스에 Address 임베디드 타입을 추가해보겠습니다.
@Embedded 애너테이션을 사용해 필드를 정의합니다.

@Entity
public class Member {
    // ... 생략
    
    @Embedded
    private Address address;
}

Address 임베디드 타입은 다음과 같이 작성할 수 있습니다.

@Getter
@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
}

DB 입장에서 보면 Address와 같은 임베디드 타입을 만들어 쓰나, 그냥 기본 값 타입의 필드인 city, street, zipcode를 나열해서 쓰나 Member 테이블에 city, street, zipcode 3개의 컬럼이 만들어지는건 동일합니다. 하지만, DB 입장이 아닌 Java 입장에서 보면 Address로 만들어 사용하면 더 객체지향스럽게 사용이 가능합니다. 다른 곳에서 Address를 재사용할 수 있고, Address를 위한 메서드를 정의할 수 있습니다.

 

그런데 아래와 같이 동일한 임베디드 타입을 2개 가지면 어떻게 될까요?

@Entity
public class Member {
    // ... 생략
    
    @Embedded
    private Address homeAddress; // 집 주소 
    @Embedded
    private Address workAddress; // 직장 주소
}

위와 같이 하나의 Entity에서 동일한 임베디드 타입을 2개 가지게 되면 컬럼명이 중복되기 때문에 예외가 발생합니다.
따라서 @AttributeOverrides 애너테이션을 사용해 속성에 매핑되는 컬럼명을 재정의해줘야 합니다.

@Entity
public class Member {
    // ... 생략
    
    @Embedded
    private Address homeAddress;
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "city", column = @Column(name = "work_city")),
        @AttributeOverride(name = "street", column = @Column(name = "work_street")),
        @AttributeOverride(name = "zipcode", column = @Column(name = "work_zipcode"))
    })
    private Address workAddress;
}

5. Value 타입과 Immutable(불변) 객체 


임베디드 타입은 새롭게 정의한 클래스이기 때문에 참조를 가집니다.
아래의 코드를 살펴보겠습니다.

Address address = new Address("서울", "street", "zipcode");
Member member1 = new Member("홍길동");
member1.setHomeAddress(address);
entityManager.persist(member1);

Member member2 = new Member("김길동");
member2.setHomeAddress(address);
entityManager.persist(member2);

// member1의 도시를 변경하려고 아래와 같이 변경하면 어떻게 될까요?
member1.getHomeAddress().setCity("부산");

member1과 member2 모두 같은 address 참조를 가지기 때문에 위와 같이 수정해버리면 둘 모두에 UPDATE 쿼리를 실행합니다.
따라서, 컴파일러 레벨에서 변경을 막을 방법이 필요하고, 이를 Immutable 객체로 만듦으로서 해결합니다.
즉, 임베디드 타입에 Setter를 만들지 않거나, private으로 둚으로써 생성자를 통해 값을 할당하는 것만 가능하도록 만듭니다.

6. Collection 값 타입


Collection 값 타입은 Java의 Collection에 값 타입(기본 값 타입 또는 임베디드 타입)을 넣어 1:N 개념으로 사용하는 것을 말합니다.
값 타입을 컬렉션으로 사용할 때는 @ElementCollection, @CollectionTable을 사용합니다.

@Entity
public class Member {
    // ... 생략
    
    @ElementCollection
    @CollectionTable(name = "favorite_food", joinColumns = 
        @JoinColumn(name = "member_id")
    )
    private Set<String> favoriteFoods = new HashSet<>();
    
    @ElementCollection
    @CollectionTable(name = "Address", joinColumns =
        @JoinColumn(name = "member_id")
    )
    private List<Address> addressHistory = new ArrayList<>();
}

값 타입을 컬렉션의 형태로 넣으면 1:N 관계가 되기 때문에 별도로 테이블을 생성해줘야 합니다. (한 테이블에 컬렉션을 담을 순 없죠)
위와 같이 작성한 후 코드를 실행해보면 다음과 같이 테이블이 생성됩니다.

[그림 1] 값 타입을 컬렉션으로 작성할 때 생성된 테이블