https://github.com/MoochiPark/jpa/blob/master/chapter09/README.md
[ ▶ 값 타입 ]
JPA의 데이터 타입을 가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다. 엔티티 타입은 @Entity로 정의하는 객체이고, 값 타입은 int, integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다.
엔티티 타입은 식별자를 통해 지속해서 추적할 수 있지만, 값 타입은 추적할 수 없다.
- 기본 값 타입 (basic value type):
- 자바 기본 타입
- 래퍼 클래스
- 임베디드 타입 (embeded type) 혹은 복합 값 타입
- 컬렉션 값 타입 (collection value type)
임베디드 타입은 JPA에서 사용자가 직접 정의한 값 타입이다.
[ ▷ 기본 값 타입 ]
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private int age;
...
}
Member 엔티티라는 id라는 식별자 값도 가지고 생명주기도 있지만 값 타입인 name, age 속성은 식별자 값도 없고 생명주기도 Member 엔티티에 의존한다. 따라서 당연히 회원 엔티티 인스턴스를 제거하면 name. age 값도 제거한다.
[ ▷ 임베디드 타입: 복합 값 타입 ]
새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서 이것을 임베디드 타입이라고 한다. 임베디드 타입도 int, String 처럼 값 타입이다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
// 근무 기간
@Temporal (TemporalType.DATE) Date startDate;
@Temporal (TemporalType.DATE) 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) 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;
...
}
새로 정으한 값 타입들은 재사용할 수 있고 응집도가 매우 높다. 또한 iswork() 처럼 Period만 사용하는 의미있는 메서드도 만들 수 있다.
임베디드 타입을 사용하려면 2가지 어노테이션이 필요하다. 둘중 하나는 생략해도 된다.
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
임베디드 타입은 기본 생성자가 필수다. 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 타입은 컴포지션 구성 관계이다.
[ ▷ 임베디드 타입과 테이블 매핑 ]
임베디드 타입은 엔티티의 값일 뿐이다. 임베디드 타입을 사용하기 전 후의 매핑하는 테이블은 같다. 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다.
[ ▷ 임베디드 타입과 연관관계 ]
임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다.
엔티티는 공유될 수 있으므로 참조한다고 표현하고, 값 타입은 특정 주인에 소속되고 논리적인 개념상으로 공유되지 않으므로 포함한다고 표현한다.
▼ 임베디드 타입과 연관관계
@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 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를 참조할 수 있다.
[ ▷ @AttributeOverride: 속성 재정의 ]
임베디드 타입에 정의한 매핑 정보를 재정의하려먼 엔티티에 @AttributeOverride를 사용하면 된다.
예를 들어 아래의 예제에서 회원에게 주소를 추가한다고 가정한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded Address homeAddress;
@Embedded Address companyAddress;
}
이렇게 했을 경우엔 테이블에 매핑하는 컬럼명이 중복된다. 이때 @AttributeOverride를 사용한다.
@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_steet")),
@AttributeOverride(name="zipcode", column=@Column(name="company_zipcode")),
})
Address companyAddress;
}
생성되는 테이블 생성 SQL은 아래와 같다.
CREATE TABLE MEMBER {
company_city varchar(255),
company_street varchar(255),
company_zipcode varchar(255),
city varchar(255),
street varchar(255),
zipcode varchar(255),
...
}
@AttributeOverrides는 엔티티에 설정해야 한다. 임베디드 타입이 임베디드 타입을 가지고 있더라도 엔티티에 설정해야 한다.
[ ▷ 값 타입과 불변 객체 ]
[ ▷ 값 타입 공유 참조 ]
임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
address.setCity("newCity"); // 회원1의 address 값을 공유해서 사용
member2.setHmeAddress(address);
회원2의 주소만 NewCity로 변경하려고 했지만 회원1의 주소도 NewCity로 변경되버렸다. 회원1과 2가 같은 address 인스턴스를 참조하기 때문이다. 영속성 컨텍스트는 회원1과 2 둘다 city 속성이 변경된 것으로 판단해서 각각 UPDATE SQL을 실행한다.
이렇게 뭔가를 수정했는데 예상치 못한 곳에서 문제가 발생하는 것을 부작용 (side effect)이라고 한다.
[ ▷ 값 타입 복사 ]
값을 공유하여 사용하는 것 대신 값 (인스턴스)를 복사해서 사용한다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
Address newAddress = address.clone();
new Address.setCity("NewCity");
member2.setHomeAddress(newAddress);
이 처럼 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입 (primitive type)이 아니라 객체 타입이다. 따라서 값을 대입하면 항상 복사복을 전달하는 기본 타입과 다르게 항상 참조 값을 전달한다.
Address a = new address("Old");
Address b = a; // 참조 값 전달
b.setCity("New");
복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법은 없다. 따라서 객체의 공유 참조는 피할 수 없다.
따라서 근본적인 해결책으로 객체의 값을 수정하지 못하도록 막아야 한다.
[ ▷ 불변 객체 ]
값 타입은 부작용 없이 쓸 수 있어야 한다. 객체를 불변하게 만들면 부작용을 원천 차단할 수 있다. 따라서 값 타입은 가능하면 불변 객체 (Immutable Object)로 설계해야 한다.
불변 객체의 값은 조회할 수 있지만 수정할 수 없다. 인스턴스 참조 값의 공유는 피할 순 없지만 부작용은 발생하지 않는다. 구현하는 다양한 방법이 있지만 가장 간단한 방법은 생성자로만 값을 설정하고 설정자를 만들지 않는 것이다.
▼ 주소 불변 객
@Embeddable
public class Address {
private String city;
protected Address() {} // JPA에서 기본 생성자는 필수다.
public Address(String city) {this.city = city}
public String getCity() {return city;}
// Setter는 만들지 않는다.
}
Integer, String 등은 자바가 제공하는 대표적인 불변 객체이다. 불변이라는 작은 제약으로 부작용을 방지할 수 있다.
[ ▷ 값 타입의 비교 ]
자바에서 equals()를 재정의하면 hashCode()도 재정의하는 것이 안전하다. 그렇지 않으면 해시를 사용하는 컬렉션 (HashSet, HashMap)이 정상 동작하지 않는다. 자바 IDE에는 대부분 equals, hashCode 메서드를 자동으로 생성해주는 기능이 있다.
[ ▷ 값 타입 컬렉션 ]
값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable을 사용하면 된다.
▼ 값 타입 컬렉션
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@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"))
@Column(name="member_id")
private List<Address> addressHistory = new ArrayList<>();
}
@Embeddable
public class Address {
@Column
private String city;
private String street;
private String zipcode;
...
}
favoriteFodds는 기본값 타입인 String을 컬렉션으로 가진다. 이것을 테이블로 매핑해야 하는데 테이블은 컬럼안에 컬렉션을 포함할 수 없다. 따라서 별도의 테이블을 추가하고 @CollectionTable을 사용해서 추가한 테이블을 매핑해야 한다. 그리고 favoriteFoods 처럼 값으로 사용되는 컬럼이 하나면 @Column을 사용해서 컬럼명을 지정할 수 있다.
테이블 매핑 정보는 @AttributeOverride를 사용해서 재정의할 수 있다. @CollectionTable을 생략하면 기본값을 사용해서 매핑한다. { 엔티티 이름 }_{컬렉션 속성 이름}이 기본값이 된다.
[ ▷ 값 타입 컬렉션 사용 ]
▼ 값 컬렉션 등록
Member member = new Member();
// 임베디드 값 타입
member.setHomeAddress(new Address("통영", "몽돌해수욕장", "660-123"));
// 기본값 타입 컬렉션
member.getFavoriteFoods().add("짬뽕");
member.getFavoriteFoods().add("짜장");
member.getFavoriteFoods().add("탕수육");
// 임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("서울", "강남", "123-123"));
member.getAddressHistory().add(new Address("서울", "강북", "000-000"));
em.persist(member);
JPA는 member 엔티티를 영속화할 때 값 타입들도 함께 저장된다. 실제 실행되는 INSERT SQL은 아래와 같다,
- member: INSERT SQL 1번
- member.homeAddress: 컬렉션이 아닌 임베디드 값 타입으므로 회원 테이블에 포함된다.
- member.favoriteFoods: INSERT SQL 3번
- member.addressHistory: INSERT SQL 2번
따라서 em.persist(member) 한 번 호출로 총 6번의 INSERT SQL을 실행한다.
값 타입은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
값 타입 컬렉션도 조회할 때 페치 전략을 선택할 수 있는데 LAZY가 기본 전략이다.
@ElementCollection(fetch = FetchType.LAZY)
▼ 조회
// SQL: SELECT ID, CITY, STREET, ZIPCODE FROM MEMBER WHERE ID = 1
Member member = em.find(Member.class, 1L); // 1. Member
// 2. member.homeAddress
Address homeAddress = member.getAddress();
// 3. member.favoriteFoods
Set<String> favoriteFoods = member.getFavoriteFoods(); // LAZY
// SQL: SELECT MEMBER ID, FOOD_NAME FROM FAVORITE_FOODS
// WHERE MEMBER_ID = 1
for (String favoriteFood : favoriteFoods) {
System.out.println("favotiteFood = " + favoriteFood);
}
// 4. member.addressHistory
List<Address> addressHistory = member.getAddressHistory(); // LAZY
// SQL: SELECT MEMBER_ID, CITY, STREET, ZIPCODE FROM ADDRESS
// WHERE MEMBER_ID = 1
addressHistory.get(0);
- member: 회원만 조회한다. 이때 임베디드 값 타입인 homeAddress도 함께 조회한다.
- member.homeAddress: 1번에서 회원을 조회할 때 같이 조회한다.
- member.favoriteFoods: LAZY로 설정해서 실제 컬렉션을 사용할 때 SELECT SQL을 1번 호출한다.
- member.addressHistory: LAZY로 설정해서 실제 컬렉션을 사용할 때 SELECT SQL을 1번 호출한다.
▼ 수정
Member member = em.find(Member.class, 1L);
// 1. 임베디드 값 타입 수정
member.setHomeAddress(new Address("새로운도시", "신도시1", "123456");
// 2. 기본값 타입 컬렉션 수정
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove("탕수육");
favoriteFoods.add("치킨");
// 3. 임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울", "기존 주소", "123-123"));
addressHistory.remove(new Address("신도시", "새로운 주소", "123-456"));
- 임베디드 값 타입 수정: homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE 한다. Member 엔티티를 수정하는 것과 같다.
- 기본값 타입 컬렉션 수정: 탕수육을 치킨으로 변경하려면 탕수육을 제거하고 치킨을 추가해야 한다. 자바의 String 타입은 불변 객체이다.
- 임베디드 값 타입 컬렉션 수정: 값 타입은 불변해야 하므로 컬렉션에서 기존 주소를 삭제하고 새로운 주소를 등록했다. 값 타입은 equals, hashCode를 꼭 구현해야 한다.
[ ▷ 값 타입 컬렉션의 제약사항 ]
값 타입 컬렉션에 보관된 값 타입들을 별도의 테이블에 보관되므로 이 테이블에 보관된 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다는 문제가 있다. 이런 문제로 JPA 구현체들은 값 타입 컬렉션에 변경사항이 생기면 값 타입 컬렉션 테이블의 모든 데이터를 삭제하고 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다.
따라서 실무에서는 값 타입 컬렉션이 매핑된 데이터가 많다면 일대다 관계를 고려해야 한다. 추가로 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 따라서 기본 키 제약으로 인해 컬럼에 null을 입력할 수 없고, 같은 값을 중복해서 저장할 수 없다.
해당 문제를 해결하려면 값 타입 컬렉션 대신에 새로운 엔티티를 만들어서 일대다 관계로 설정하고, 추가로 영속성 전이 (cascade) + 고아 객체 제거 (orphan remove) 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.
[ ▷ 엔티티 타입의 특징 ]
- 식별자 (@Id)가 있다: 엔티티 타입은 식별자가 있고 식별자로 구별할 수 있다.
- 생명 주기가 있다: 생성, 영속화, 소멸의 생명 주기가 있다. ( em.persist(entity)로 영속화 / em.remove(entity)로 제거 )
- 공유 참조: 참조 값을 공유할 수 있다. 이것을 공유 참조라 한다. 회원 엔티티가 있다면 다른 엔티티에서 참조할 수 있다.
[ ▷ 값 타입의 특징 ]
- 식별자가 없다.
- 생명 주기를 엔티티에 의존한다: 엔티티를 제거하면 같이 제거된다.
- 공유하지 않는 것이 안전하다: 값을 복사해서 사용해야 한다.
- 오직 하나의 주인만이 관리해야 한다.
- 불변 객체로 만드는 것이 안전하다.
'JPA (Java Persistence API)' 카테고리의 다른 글
10장 객체지향 쿼리 언어 (0) | 2024.10.16 |
---|---|
8장 프록시와 연관관계 관리 (0) | 2024.09.23 |
7장 고급 매핑 (1) | 2024.09.23 |
6장 다양한 연관관계 매핑 (0) | 2024.09.13 |
5장 연관관계 매핑 기초 (0) | 2024.09.11 |