[자바 ORM 표준 JPA 프로그래밍] 9장. 값 타입
by Jo
9장. 값 타입
- JPA의 데이터 타입은 크게 엔티티 타입, 값 타입 두개로 분류 가능
- 엔티티 타입
@Entity
로 정의하는 객체- 식별자 있음
- 식별자로 구별하고 지속 추적 가능
- 생명 주기가 있음
- 생성, 영속화, 소멸하는 생명 주기 존재
- 공유 가능
- 참조 값 공유할 수 있음 => 공유 참조
- 값 타입
- int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입 또는 객체
- 식별자가 없음
- 생명 주기를 엔티티에 의존
- 공유하지 않는 것이 안전
- 대신 값 복사해서 사용
- 오직 하나의 주인만이 관리해야 함
- 불변 객체로 만드는 것이 안전함
- 다시 3 가지로 나눌 수 있음
- 기본값 타입: 자바 기본 데이터 타입
- 자바 기본 타입 (int, double, …)
- 래퍼 클래스 (ex. Integer)
- String
- 임베디드 타입: JPA에서 사용자가 직접 정의한 값 타입
- 컬렉션 값 타입: 하나 이상의 값 타입 저장시 사용
- 기본값 타입: 자바 기본 데이터 타입
9.1 기본값 타입
코드
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
private int age;
...
}
- 위 예제의 String, int 가 값 타입
- 식별자 값도 없으며 생명주기는 엔티티에 의존
- 값 타입은 공유 X
9.2 임베디드 타입(복합 값 타입)
- JPA에서 새로운 값 타입 직접 정의해서 사용하는 것
- 임베디드 타입도 int, String 처럼 값 타입임
코드
// Embedded type 사용하지 않은 경우
@Entity
public class MemberWithoutEmbeddedType {
@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;
...
}
...
// 근무 기간, 집 주소 Embedded type 만들어서 사용한 경우
@Entity
public class MemberWithEmbeddedType {
@Id
@GeneratedValue
private Long id;
private String name;
// 근무 기간
@Embedded
Period workPeroid;
// 집 주소
@Embedded
Address homeArrdress;
...
}
...
// 기간 embedded type
@Embeddable
public class Period {
@Temporal(TemporalType.DATE)
Date startDate;
@Temporal(TemporalType.DATE)
Date startDate;
...
public boolean isWork(Date date) {
// 값 타입을 위한 메소드 정의 가능
}
...
}
// 주소 embedded type
@Embeddable
public class Address {
@Column(name="city") // 매핑할 컬럼 정의 가능
private String city;
private String street;
private String zipcode;
...
}
- 임베디드 타입 사용시 다음 2가지 어노테이션 필요 (둘 중 하나는 생략 가능)
@Embeddable
: 값 타입 정의하는 곳에 표시@Embedded
: 값 타입 사용하는 곳에 표시
- 임베디드 타입은 기본 생성자가 필수
9.2.1 임베디드 타입과 테이블 매핑
- 임베디드 타입은 엔티티의 값일 뿐이기 때문에 값이 속한 엔티티의 테이블에 매핑됨
9.2.2 임베디드 타입과 연관관계
코드
@Entity
public class Member {
@Embedded
Address address; // 임베디드 타입 포함
@Embedded
PhoneNumber phoneNumber; // 임베디드 타입 포함
...
}
...
@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를 참조
9.2.3 @AttributeOverride: 속성 재정의
@AttributeOverride
를 사용하여 임베디드 타입에 정의한 매핑정보 재정의 가능
코드
@Entity
public class Member {
@Id
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;
}
sql
CREATE table member {
...
city VARCHAR(255),
street VARCHAR(255),
zipcode VARCHAR(255),
company_city VARCHAR(255),
company_street VARCHAR(255),
company_zipcode VARCHAR(255),
...
}
9.2.4 임베디드 타입과 null
- 임베디드 타입이 null이면 매핑한 컬럼 값 모두 null 됨
9.3 값 타입과 불변 객체
9.3.1 값 타입 공유 참조
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함
- 공유 참조로 인한 side effect 발생할 수 있으므로 값을 복사해서 사용해야 함
9.3.2 값 타입 복사
- 자바에서 기본 타입에 값 대입하면 알아서 값을 복사 전달하지만 객체는 참조값을 전달함
- 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하므로 대신 인스턴스를 복사하여 사용
- 복사해서 쓰도록 해도 공유 참조 자체를 막을 수는 없기 때문에 세터 빼서 수정 못하게 막는게 좋음
9.3.3 불변 객체
- 객체 값 수정 못하게 하면 사이드 이펙트 차단 가능
- ∴ 값 타입은 될 수 있으면 immutable object 로 설계해야 함
- ex) 생성자로만 값 설정, 세터 X
- Integer, String 은 자바가 제공하는 대표적 불변 객체
9.4 값 타입의 비교
- 자바가 제공하는 객체 비교는 다음 2가지
- 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
- 동등성(equivalnce) 비교: 인스턴스의 값을 비교,
equals()
사용
- 값 타입 비교시에는
equals()
를 이용하여 동등성 비교 해야 함 - 직접 값 타입 정의할 경우
equals()
메소드 재정의해줘야 함equals()
재정의 시hashCode()
도 재정의 해야함- 안하면 컬렉션 정상 동작 안함
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>();
...
}
@Embedaable
public class Adress {
@Column
private String city;
private String street;
private String zipcode;
...
}
- favoriteFoods처럼 값으로 사용되는 컬럼 하나면
@Column
을 사용해서 컬럼명 지정 가능 @CollectionTable
생략시 기본값으로 매핑됨- default: {엔티티이름}_{컬렉션 속성 이름}
- ex) Member 엔티티의 addressHistory 는 Member_addressHitory 테이블과 매핑
9.5.1 값 타입 컬렉션 사용
코드
Member member = new Member();
// 임베디드 값 타입
member.setHomeAddress(new Address("a", "A", "1"));
// 기본값 타입 컬렉션
member.getFavoriteFoods().add("Q");
member.getFavoriteFoods().add("W");
member.getFavoriteFoods().add("E");
// 임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("b", "B", "2"));
member.getAddressHistory().add(new Address("c", "C", "3"));
em.persist(member);
- member 엔티티 영속화할 때 member 엔티티의 값 타입들 함께 저장됨
- member: INSERT 쿼리 1번
- member.homeAddress: 임베디드 값 타입이라 member 저장 쿼리에 포함
- member.favoriteFoods: INSERT 쿼리 3번
- member.addressHistory: INSERT 쿼리 2번
- 총 INSERT 6번 실행됨
- 값 타입 컬렉션도 조회할 때 fetch 전략 선택 가능(LAZY가 디폴트임)
@ElementColection(fetch = FEtchType.LAZY)
- member: 회원만 조회함. SELECT 쿼리 1번
- member.homeAddress: member 조회할 때 같이 조회됨
- member.favoriteFoods: LAZY로 설정되어 실제 컬렉션 사용시 SELECT 쿼리 1번 호출
- member.addressHistory: LAZY로 설정되어 실제 컬렉션 사용시 SELECT 쿼리 1번 호출
코드
Member member = em.find(Member.class, 1L);
// 임베디드 값 타입 수정
member.setHomeAddress(new Address("new a", "NEW A", "1-1"));
// 기본값 타입 컬렉션 수정
Set<String> favoriteFoods = member.getFoavriteFoods();
member.getFavoriteFoods().remove("Q");
member.getFavoriteFoods().add("W");
// 임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = member.getAddressHistory();
member.getAddressHistory().remove(new Address("b", "B", "2"));
member.getAddressHistory().add(new Address("new b", "new B", "2-2"));
- 임베디드 값 타입 수정: homeAddress 임베디드 값 타입은 member 테이블과 매핑되었으므로 member 테이블만 UPDATE 쿼리 수행
- 기본값 타입 컬렉션 수정: 자바의 String 타입은 수정 불가이므로 제거하고 새로 추가해야 함
- 임베드다 값 타입 컬렉션 수정: 값 타입은 불변해야 하므로 기존거 삭제하고 새로 등록
9.5.2 값 타입 컬렉션의 제약사항
- 특정 엔티티 하나에 소속된 값 타입은 변경되도 자신이 속한 텐티티를 DB에서 찾아 값 바꾸면 됨
- 값 타입 컬렉션에 보관된 값 타입들의 경우에는 별도의 테이블에 보관되므로 변경시 원본 데이터 찾기 어려움
- ∴ JPA 구현체들은 값 타입 컬렉션에 변경 사항 발생하면 컬렉션이 매핑된 테이블의 연관된 모든 데이터 삭제 후 현재 들어있는 값 새로 DB에 저장함
- 값 타입 컬렉션에 보관된 값 타입들의 경우에는 별도의 테이블에 보관되므로 변경시 원본 데이터 찾기 어려움
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼 묶어서 pk 구성해야 함
- pk 제약조건으로 nullable 하게 사용 못하고 같은 값 중복 저장도 안됨
- 위 문제들 해결하려면 값 타입 컬렉션 쓰는 대신 새 엔티티 만들어서 1:N 관계로 사용하면 됨
- 영속성 전이 + 고아 객체 제거 기능 적용하면 값 타입 컬렉션처럼 사용할 수 있음
Subscribe via RSS