30. 값 타입(1)

30. 값 타입(1)

JPA의 데이터 타입을 가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다. 엔티티 타입은 @Entity로 정의하는 객체고, 값 타입은 int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다. 엔티티 타입은 식별자를 통해 지속해서 추적할 수 있지만, 값 타입은 식별자가 없고 숫자나 문자같은 속성만 있으므로 추적할 수 없다. 예를 들어 회원 엔티티라는 것은 그 회원의 키나 나이 값을 변경해도 같은 회원이다. 심지어 그 회원의 모든 데이터를 변경해도 식별자만 유지하면 같은 회원으로 인식할 수 있다. 반면에 숫자 값 100을 200으로 변경하면 완전히 다른 값으로 대체된다. 비유하자면 엔티티 타입은 살아 있는 생물이고 값 타입은 단순한 수치 정보다.

값 타입은 다음 3가지로 나눌 수 있다.

  • 기본값 타입(basic value type)
    - 자바 기본 타입(예 : int, double)
    - 래퍼 클래스(예 : Integer)
    - String
  • 임베디드 타입(embedded type)(복합 값 타입)
  • 컬렉션 값 타입(collection value type)

기본값 타입은 String, int처럼 자바가 제공하는 기본 데이터 타입이고 임베디드 타입은 JPA에서 사용자가 직접 정의한 값 타입이다. 마지막으로 컬렉션 값 타입은 하나 이상의 값 타입을 저장할 때 사용한다. 기본값 타입부터 순서대로 알아보자.

1. 기본값 타입

가장 단순한 기본값 타입을 알아보자.

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    private int age;
    ...
}

위의 예제의 Member에서 String, int가 값 타입이다. Member 엔티티는 id라는 식별자 값도 가지고 생명주기도 있지만 값 타입인 name, age 속성은 식별자 값도 없고 생명주기도 회원 엔티티에 의존한다. 따라서 회원 엔티티 인스턴스를 제거하면 name, age 값도 제거된다. 그리고 값 타입은 공유하면 안 된다. 예를 들어 다른 회원 엔티티의 이름을 변경한다고 해서 나의 이름까지 변경된느 것은 상상하기도 싫을 것이다.

어쩌면 너무 당연한 내용을 설명하고 있어서 약간 당황스러울 것이다. 다음으로 자바에서 제공하는 기본값 타입이 아닌 직접 값 타입을 정의해보자.

자바에서 int, double 같은 기본 타입(primitive type)은 절대 공유되지 않는다. 예를 들어 a = b 코드는 b의 값을 복사해서 a에 입력한다. 물론 Integer처럼 래퍼 클래스나 String 같은 특수한 클래스도 있다. 이것들을 객체지만 자바언어에서 기본 타입처럼 사용할 수 있게 지원하므로 기본값 타입으로 정의했다.

2. 임베디드 타입(복합 값 타입)

새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 임베디드 타입(embedded type)이라 한다. 중요한 것은 직접 정의한 임베디드 타입도 int, String처럼 값 타입이라는 것이다. 예제를 통해 임베디드 타입을 자세히 알아보자.

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    
    // 근무 기간
    @Temporal(TemporalType.DATE) java.util.Date startDate;
    @Temporal(TemporalType.DATE) java.util.Date endDate;
    
    // 집 주소 표현
    private String city;
    private String street;
    private String zipcode;
    // ...
}

위의 예제는 평범한 회원 엔티티다. 누군가에게 이 엔티티를 설명하려면 이렇게 이야기할 것이다.

  • 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다.

이런 설명은 단순히 정보를 풀어둔 것 뿐이다. 그리고 근무 시작일과 우편번호는 서로 아무 관련이 없다. 이것보단 다음처럼 설명하는 것이 더 명확하다.

  • 회원 엔티티는 이름, 근무 기간, 집 주소를 가진다.

회원이 상세한 데이터를 그대로 가지고 있는 것은 객체지향적이지 않으며 응집력만 떨어뜨린다. 대신에 근무 기간, 주소 같은 타입이 있다면 코드가 더 명확해질 것이다. [근무기간, 집 주소]를 가지도록 임베디드 타입을 사용해보자.

  • 값 타입 적용 회원 엔티티
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    
    @Embedded Period workPeriod;     // 근무 기간
    @Embedded Address homeAddress;   // 집 주소
    // ...
}
  • 기간 임베디드 타입
@Embeddable
public class Period {

    @Temporal(TemporalType.DATE) java.util.Date startDate;
    @Temporal(TemporalType.DATE) java.util.Date endDate;
    // ..
    
    public boolean isWork(Date date) {
        // .. 값 타입을 위한 메소드를 정의할 수 있다.
    }
}
  • 주소 임베디드 타입
@Embeddable 
public class Address {

    @Column(name = "city")    // 매핑할 컬럼 정의 가능
    private String city;
    private String street;
    private String zipcode;
    // ..
}

위의 예제(값 타입 적용 회원 엔티티)를 보면 회원 엔티티가 더욱 의미 있고 응집력 있게 변한 것을 알 수 있다.

  • 위의 예제(기간 임베디드 타입)를 보면 startDate, endDate를 합해서 Period(기간) 클래스를 만들었다.
  • 위의 예제(주소 임베디드 타입)를 보면 city, street, zipcode를 합해서 Address(주소) 클래스를 만들었다.

새로 정의한 값 타입들은 재사용할 수 있고 응집도도 아주 높다. 또한 위의 예제(기간 임베디드 타입)의 Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드도 만들 수 있다.

임베디드 타입을 사용하려면 다음 2가지 어노테이션이 필요하다. 참고로 둘 중 하나는 생략해도 된다.

  • @Embeddable : 값 타입을 정의하는 곳에 표시
  • @Embedded : 값 타입을 사용하는 곳에 표시

그리고 임베디드 타입은 기본 생성자가 필수다.

임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 타입의 관계를 UML로 표현하면 컴포지션(composition) 관계가 된다(위의 그림).

하이버네이트는 임베디드 타입을 컴포넌트(components)라 한다.

1. 임베디드 타입과 테이블 매핑

임베디드 타입을 데이터베이스 테이블에 어떻게 매핑하는지 아래 그림을 통해 알아보자.

임베디드 타입은 엔티티의 값일 뿐이다. 따라서 값이 속한 엔티티의 테이블에 매핑한다. 예제에서 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.

임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다. 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.

ORM을 사용하지 않고 개발하면 테이블 컬럼과 객체 필드를 대부분 1:1로 매핑한다. 주소나 근무 기간 같은 값 타입 클래스를 만들어서 더 객체지향적으로 개발하고 싶어도 SQL을 직접 다루면 테이블 하나에 클래스 하나를 매핑하는 것도 고단한 작업인데 테이블 하나에 여러 클래스를 매핑하는 것은 상상하기도 싫을 것이다. 이런 지루한 반복 작업은 JPA에 맡기고 더 세밀한 객체지향 모델을 설계하는데 집중하자.

임베디드 타입과 UML
UML에서 임베디드 값 타입은 아래 그림처럼 기본타입처럼 단순하게 표현하는 것이 편리하다.

2. 임베디드 타입과 연관관계

임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다. JPA 표준 명세가 제공하는 아래 예제 코드와 그림으로 임베디드 타입의 연관관계를 알아보자.

엔티티는 공유될 수 있으므로 참조한다고 표현하고, 값 타입은 특정 주인에 소속되고 논리적인 개념상 공유되지 않으므로 포함한다고 표현했다.

@Entity
public class Member {

    @Embedded Address address;           // 임베디드 타입 포함
    @Embedded PhoneNumber phoneNumber;   // 임베디드 타입 포함
    // ...
}

@Embeddable
public class Address {
    String street;
    String city;
    String state;
    @Embedded Zipcode zipcode;           // 임베디드 타입 포함
}

@Embeddable
public class ZipCode {
    String zip;
    String plusFour;
}

@Embeddable
public class PhoneNumber {
    String areaCode;
    String localNumber;
    @ManyToOne PhoneServiceProvider provider;    // 엔티티 참조
    ...
}

@Entity
public class PhoneServiceProvider {
    @Id String name;
    ...
}

위의 예제를 보면 값 타입인 Address가 값 타입인 Zipcode를 포함하고, 값 타입인 PhoneNumber가 엔티티 타입인 PhoneServiceProvider를 참조한다.

3. @AttributeOverride : 속성 재정의

임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride를 사용하면 된다. 예를 들어 회원에게 주소가 하나 더 필요하면 어떻게 해야 할까?

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    
    @Embedded Address homeAddress;
    @Embedded Address companyAddress;
}

위의 예제의 Member 엔티티를 보면 집 주소에 회사 주소를 하나 더 추가했다. 문제는 테이블에 매핑되는 컬럼명이 중복되는 것이다. 이때는 아래 예제와 같이 @AttributeOverrides를 사용해서 매핑정보를 재정의해야 한다.

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    
    @Embedded Address homeAddress;
    
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name="city", column=@Column(name
            = "COMPANY_CITY")},
        @AttributeOverride(name="street", column=@Column(name
            = "COMPANY_STREET")},
        @AttributeOverrdie(name="zipcode", column=@Column(name
            = "COMPANY_ZIPCODE")}
    })
    Address companyAddress;
}    

아래 예제에 생성된 테이블을 보면 재정의한대로 변경되어 있다.

CREATE TABLE MEMBER (
    COMPANY_CITY varchar(255),
    COMPANY_STREET varchar(255),
    COMPANY_ZIPCODE varchar(255),
    city varchar(255),
    street varchar(255),
    zipcode varchar(255),
    ...
)

@AttributeOverride를 사용하면 어노테이션을 너무 많이 사용해서 엔티티 코드가 지저분해진다. 다행히도 한 엔티티에 같은 임베디드 타입을 중복해서 사용하는 일은 많지 않다.

@AttributeOverrdies는 엔티티에 설정해야 한다. 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 설정해야 한다.

4. 임베디드 타입과 null

임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.

member.setAddress(null);    // null 입력
em.persist(member);

회원 테이블의 주소와 관련된 CITY, STREET, ZIPCODE 컬럼 값은 모두 null이 된다.

참고

  • 자바 ORM 표준 JPA 프로그래밍

좋은 웹페이지 즐겨찾기