JPA의 데이터 타입을 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다. 엔티티 타입은 @Entity로 정의하는 객체이고, 값 타입은 int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다. 엔티티 타입은 식별자를 통해 지속해서 추적할 수 있지만, 값 타입은 식별자가 없고 숫자나 문자같은 속성만 있으므로 추적할 수 없다. 값 타입은 다음 3가지로 나눌 수 있다.
- 기본값 타입
- 임베디드 타입(복합 값 타입)
- 컬렉션 값 타입
기본값 타입
@Entity
public class Member {
.
@Id @GeneratedValue
private Long id;
//기본값 타입
private String name;
private int age;
...
}
위의 엔티티에서 String, int가 값타입이다. 엔티티는 id라는 식별자 값도 가지고 생명주기도 있지만 값 타입인 name, age 속성은 식별자 값도 없고 생명주기도 엔티티에 의존한다. 따라서 엔티티 인스턴스를 제거하면 name, age 값도 제거된다. 또한 값 타입은 공유하면 안 된다.
임베디드 타입(복합 값 타입)
새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 임베디드 타입이라 한다. 중요한 것은 직접 정의한 임베디드 타입도 int, String 처럼 값 타입이라는 것이다.
//값 타입 적용 회원 엔티티
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded
Period workPeriod;
@Embedded
Adderess homeAddress;
//...
}
//기간 임베디드 타입
@Embeddable
public class Period {
@Temporal(TemporalType.DATE)
Date startDate;
@Temporal(TemporalType.DATE)
Date endDate;
//..
public boolean isWork(Date date) {
//.. 값 타입을 위한 메소드를 정의할 수 있다.
}
}
//주소 임베디드 타입
@Embeddable
public class Address {
@Column(name="city") // 매핑할 컬럼 정의 가능
private String city;
private String street;
private String zipcode;
//..
}
정의한 값 타입들은 재사용할 수 있고 응집도도 아주 높다. 또한 해당 값 타입만 사용하는 의미 있는 메소드도 만들 수 있다. 임베디드 타입을 사용하려면 2가지 어노테이션이 필요하다. 참고로 둘 중 하나는 생략해도 된다.
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
또한 임베디드 타입은 기본 생성자가 필수이다. 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 타입의 관계를 UML로 표현하면 컴포지션 관계가 된다.
임베디드 타입은 엔티티의 값일 뿐이다. 따라서 값이 속한 엔티티의 테이블에 매핑한다. 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다. 잘 설계한 ORM 어플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.
임베디드 타입은 또한 값 타입을 포함하거나 엔티티를 참조할 수 있다.
@Entity
public class Member {
@Embedded
Adderss 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
pubic class PhoneServiceProvider {
@Id
String name;
//...
}
임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride를 사용하면 된다. 속성 재정의를 너무 많이 사용하면 엔티티 코드가 지저분해지지만 다행히 한 엔티티에 같은 임베디드 타입을 중복해서 사용하는 일은 많지 않다.
@Entity
public class Member {
@Id @GeneratedValue
private Lond id;
private String name;
@Embedded
Address homeAddress;
// 컬럼명 중복을 막기위해 속성 재정의
@Embedded
@AttributeOverrides({
@AttrivuteOverride(name="city", column=@Column(name = "COMPANY_CITY")),
@AttrivuteOverride(name="street", column=@Column(name = "COMPANY_STREET")),
@AttrivuteOverride(name="zipcode", column=@Column(name = "COMPANY_ZIPCODE"))
})
Adderess companyAddress;
}
또한 임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.
값 타입과 불변 객체
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다. 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다. 회원2에 새로운 주소를 할당하려고 회원1의 주소를 그대로 참조해서 사용한다면 회원2의 주소만 변경되는 것이 아니라 회원 1의 주소도 변경되어 버린다. 두 회원이 같은 address 인스턴스를 참조하기 때문이다. 이렇듯 뭔가를 수정했는데 전혀 예상치 못한 곳에서 문제가 발생하는 것을 부작용(side effect)이라 한다.
값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다 대신에 값(인스턴스)을 복사해서 사용해야 한다. 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다. 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입 이라는 것이다. 자바는 기본 타입에 값을 대입하면 값을 복사해서 전달한다. 하지만 자바는 객체에 값을 대입하면 항상 참조 값을 전달한다. 물론 객체를 대입할 때마다 인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있지만, 문제는 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다는 것이다.
값 타입은 부작용 걱정 없이 사용할 수 있어야 한다. 부작용이 일어나면 값 타입이라 할 수 없다. 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다. 따라서 값 타입은 될 수 있으면 불변객체로 설계해야 한다. 한 번 만들면 절대 변경할 수 없는 객체를 불변 객체라 한다. 불변 객체의 값은 조회할 수 있지만 수정할 수 없다. 불변 객체도 결국은 객체이기 때문에 인스턴스의 참조 값 공유를 피할 수 없다. 하지만 참조 값을 공유해도 인스턴스의 값을 수정할 수 없으므로 부작용이 발생하지 않는다. 불변 객체를 구현하는 가장 간단한 방법으로 생성자로만 값을 설정하고 수정자를 만들지 않으면 된다. 불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.
@Embeddable
public class Address {
private String city;
protected Address() {} //JPA에서 기본 생성자는 필수다.
//생성자로 초기 값을 설정한다.
public Address(String city) {this.city = city}
//접근자 (Getter)는 노출한다.
public String getCity() {
return city;
}
//수정자 (Setter)는 만들지 않는다. (불변객체)
}
값 타입 컬렉션
값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 어노테이션을 사용하면 된다.
@Entity
public class Member {
.
@Id @GeneratedValue
private Long id;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOODS",
joinComumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name="FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<String>();
@ElementCollection
@CollectionTable(name = "ADDRESS"
, joinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<Adddress>();
//...
}
@Embeddable
public class Address {
@Column
private String city;
private String street;
private String zipcode;
//...
}
favoriteFoods는 기본값 타입인 String을 컬렉션으로 가진다. 이것을 데이터베이스 테이블로 매핑해야 하는데 관계형 데이터베이스의 테이블은 컬럼안에 컬렉션을 포함할 수 없다. 따라서 별도의 테이블을 추가하고 @CollectionTable를 사용해서 추가한 테이블을 매핑해야한다. 그리고 값으로 사용되는 컬럼이 하나면 @Column을 사용해서 컬럼명을 지정할 수 있다.
addressHistory는 인베디드 타입인 Address 타입인 Address를 컬렉션으로 가진다. 이것도 마찬가지로 별도의 테이블을 사용해야 한다. 그리고 테이블 매핑정보는 @AttributeOverride를 사용해서 재정의할 수 있다.
값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다. 따라서 엔티티의 컬렉션에 값을 넣고 엔티티만 영속화해도 값 타입도 함께 저장한다. 값 타입 컬렉션도 조회할 때 패치 전략을 선택할 수 있는데 LAZY가 기본이다. 컬렉션을 수정하려면 기존의 값을 제거하고 새로운 값 타입을 add해야한다.
값 타입은 식별자라는 개념이 없고 단순한 값들의 모음이므로 값을 변경해버리면 데이터베이스에 저장된 원본 데이터를 찾기는 어렵다. 특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 데이터베이스에서 찾고 값을 변경하면 된다. 하지만 값 타입 컬렉션에 보관된 값 타입들은 변도의 테이블에 보관되므로 여기에 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다는 문제가 있다. 따라서 실무에서 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다. 추가로 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 따라서 데이터베이스 기본 키 제약 조건으로 인해 컬럼에 null을 입력할 수 없고, 같은 값을 중복해서 저장할 수 없는 제약도 있다. 따라서 새로운 엔티티를 만들어서 일대다 관계로 설정하고 영속성 전이 + 고아 객체 제거 기능을 적용하면 값 타입 컬렉션 처럼 사용할 수 있다.
'JPA' 카테고리의 다른 글
[JPA] 객체지향 쿼리 언어(2) : QueryDSL (0) | 2020.10.05 |
---|---|
[JPA] 객체지향 쿼리 언어(1) : JPQL (0) | 2020.09.30 |
[JPA] 프록시와 연관관계 관리 (0) | 2020.09.22 |
[JPA] 고급 매핑 (0) | 2020.09.15 |
[JPA] 연관관계 매핑 (0) | 2020.09.10 |