[자바 ORM 표준 JPA 프로그래밍] 9장 값 타입
JPA의 데이터 타입을 가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있음.
엔티티 타입은 @Entity로 정의하는 객체이고, 값 타입은 int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말함.
엔티티 타입은 식별자를 통해 지속해서 추적할 수 있지만, 값 타입은 식별자가 없고 숫자나 문자같은 속성만 있으므로 추적할 수 없음.
비유하자면 엔티티 타입은 살아있는 생물이고 값 타입은 단순한 수치 정보.
기본값 타입
- 자바 기본 타입(예: int, double)
- 래퍼 클래스(예: Integer)
- String
임베디드 타임(복합 값 타입. JPA에서 사용자가 직접 정의한 값 타입)
컬렉션 값 타입(하나 이상의 값 타입을 저장할 때 사용)
🎈 9.1 기본값 타입
값 타입은 식별자 값도 없고 생명주기도 회원 엔티티에 의존함.
값 타입은 공유하면 안됨. 다른 회원 엔티티의 이름을 변경한다고 해서 나의 이름까지 변경되면 안됨.
🎈 9.2 임베디드 타입(복합 값 타입)
새로운 값 타입을 직접 정의해서 사용하는 것을 JPA에서는 임베디드 타입이라 함.
직접 정의한 임베디드 타입도 int, String처럼 값 타입이라는 것.
회원이 상세한 데이터를 그대로 가지고 있는 것은 객체지향적이지 않으며 응집력을 떨어뜨림.
대신에 근무 기간, 주소 같은 타입이 있다면 코드가 더 명확해진다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded Period workPeriod; //근무 기간
@Embedded Address homeAddress; //집 주소
//...
}
새로 정의한 값 타입들은 재사용할 수 있고 응집도도 아주 높음. 또한 해당 값 타입만 사용하는 의미 있는 메소드도 만들 수 있음.
임베디드 타입을 사용하기 위해서는 다음 2가지 어노테이션이 필요함. 하나는 생략 가능.
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
임베디드 타입은 기본 생성자가 필수이다.
9.2.1 임베디드 타입과 테이블 매핑
임베디드 타입은 엔티티의 값인 뿐이므로 값이 속한 엔티티의 테이블에 매핑함.
임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능함.
잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.
9.2.2 임베디드 타입과 연관관계
임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다.
((임베디드 타입과 연관관계 그림))
9.2.3 @AttributeOverride: 속성 재정의
임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride를 사용하면 됨.
예를 들어 회원에게 주소가 하나 더 필요하면 어떻게 해야 함?
테이블에 매핑하는 컬럼명이 중복되므로 @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")),
@AttributeOverride(name="zipcode", column=@Column(name="COMPANY_ZIPCODE")),
}}
Address companyAddress;
}
9.2.4 임베디드 타입과 null
임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.
member.setAddress(null); //null 입력
em.persist(member);
이러면 주소와 관련된 CITY, STREET, ZIPCODE 컬럼 값은 모두 null이 된다.
🎈 9.3 값 타입과 불변 객체
9.3.1 값 타입 공유 참조
임베디드 타입 같은 값 타임을 여러 엔티티에서 공유하면 위험함.
member.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
address.setCity("NewCity"); //회원1의 address 값을 공유해서 사용
member2.setHomeAddress(address);
회원2의 주소만 "NewCity"로 변경되길 기대했지만 회원1의 주소도 "NewCity"로 변경되어 버림.
회원1과 회원2가 같은 address 인스턴스를 참조하기 때문임.
이렇게 뭔가를 수정했는데 예상치 못한 곳에서 문제가 발생하는 것을 부작용이라고 함. 이런 부작용을 막으려면 값을 복사해서 사용하면 됨.
9.3.2 값 타입 복사
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
//회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();
newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);
이 코드를 실행하면 의도한 대로 회원2의 주소 인스턴스를 복사해서 사용함.
문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아닌 객체 타입이라는 것.
자바는 기본 타입에 값을 대입하면 값을 복사해서 전달함.
int a = 10;
int b = a; //기본 타입은 항상 값을 복사함.
b = 4;
여기서 a, b는 완전히 독립된 값을 가지고 부작용도 없음.
문제는 Address 같은 객체 타입. 자바는 객체에 값을 대입하면 항상 참조 값을 전달함.
Address a = new Address("Old");
Address b = a; //객체 타입은 항상 참조 값을 전달함.
b.setCity("New");
객체의 공유 참조의 근본적인 해결책의 가장 단순한 방법은 객체의 값을 수정하지 못하게 막으면 됨. 예를 들어 Address 객체의 setCity() 같은 수정자 메소드를 모두 제 거하면 부작용의 발생을 막을 수 있음.
9.3.3 불변 객체
값 타입을 부작용 걱정 없이 사용할 수 있어야 함. 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용 원천 차단함.
따라서 값 타입을 불변 객체로 설계해야 함.
불변 객체: 한 번 만들면 절대 변경할 수 없는 객체. 조회할 수 있지만 수정할 수 없음.
불변 객체도 결국은 객체이므로 참조 값 공율ㄹ 피할 수 없음. 하지만 참조 값을 공유해도 인스턴스의 값을 수정할 수 없으므로 부작용이 발생하지 않음.
불변 객체 구현하는 가장 간단한 방법은 생성자로만 값을 설정하고 수정자를 만들지 않는 것.
@Embeddable
public class Address {
private String city;
protected Address() {} //JPA에서 기본 생성자는 필수다.
//생성자로 초기 값을 설정한다
public Address(String city) {this.city = city}
//접근자(Getter)는 노출한다.
public String getCity() {
return city;
}
//수정자(Setter)는 만들지 않는다.
}
🎈 9.4 값 타입의 비교
동일성(identity) 비교: 인스턴스의 참조 값을 비교, ==사용
동등성(equivalence) 비교: 인스턴스의 값을 비교, equals() 사용
값 타입은 비록 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 한다. 따라서 값 타입을 비교할 때는 a.equals(b)를 사용해서 동등성 비교를 해야 한다. 물론 Address의 equals() 메소드를 재정의해야 함.
자바에서 equals()를 재정의하면 hashCode()도 재정의하는 것이 안전함. 그렇지 않으면 해시를 사용하는 컬렉션(HashSet, HashMap)이 정상 동작하지 않음.
🎈9.5 값 타입 컬렉션
값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 어노테이션을 사용하면 됨.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOODS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<String>();
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<Address>();
//...
}
@Embeddable
public class Address {
@Column
private String city;
private String street;
private String zipcode;
//...
}
데베 테이블은 컬럼 안에 컬렉션을 포함할 수 없으므로 별도의 테이블을 추가하고 @CollectionTable을 사용해서 추가한 테이블을 매핑해야 함.
9.5.1 값 타입 컬렉션 사용
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);
em.persist(member) 한 번 호출로 총 6번의 SQL을 실행함.
9.5.2 값 타입 컬렉션의 제약사항
엔티티는 식별자 있으므로 값을 변경해도 데베에 저장된 원본 데이터를 쉽게 찾아서 변경할 수 있음.
값 타입은 단순한 값들의 모음이므로 값을 변경해버리면 데베에 저장된 원본 데이터를 찾기 어려움.
값 타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관되므로, 값이 변경되면 데베에 있는 원본 데이터를 찾기 어렵다는 문제가 있음.
따라서 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면, 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데베에 다시 저장함.
예를 들어 식별자가 100번인 회원이 관리하는 주소 값 타입 컬렉션을 변경하면 회원 100번과 관련된 모든 주소 데이터를 삭제하고 현재 값 타입 컬렉션에 있는 값을 다시 저장함.
따라서 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계를 고려해야 함.
값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 하므로 컬럼에 NULL을 입력할 수 없고 중복해서 저장할 수도 없음.
값 타입 컬렉션 대신에 새로운 엔티티를 만들어서 일대다 관계로 설정하면 됨.