값 타입
JPA의 데이터 타입은 크게 엔티티 타입, 값 타입 두개로 분류 가능
- 엔티티 타입
- @Entity 로 정의하는 객체
- 식별자 있음
- 식별자로 구별하고 지속 추적 가능
- 생명 주기가 있음
- 생성, 영속화, 소멸하는 생명 주기 존재
- 공유 가능
- 참조 값 공유할 수 있음 => 공유 참조
- 값 타입
- int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입 또는 객체
- 식별자가 없음
- 생명 주기를 엔티티에 의존
- ex) 회원을 삭제하면 이름, 나이필드로 함께 삭제
- 공유하지 않는 것이 안전
- 대신 값 복사해서 사용
- 오직 하나의 주인만이 관리해야 함
- 불변 객체로 만드는 것이 안전함
- 다시 3 가지로 나눌 수 있음
- 기본값 타입: 자바 기본 데이터 타입
- 자바 기본 타입 (int, double, …)
- 래퍼 클래스 (ex. Integer)
- String
- 임베디드 타입: JPA에서 사용자가 직접 정의한 값 타입
- 컬렉션 값 타입: 하나 이상의 값 타입 저장시 사용
- 기본값 타입: 자바 기본 데이터 타입
임베디드 타입(복합 값 타입)
- JPA에서 새로운 값 타입 직접 정의해서 사용하는 것
- 임베디드 타입도 int, String 처럼 값 타입임
- 임베디드 타입 사용시 다음 2가지 어노테이션 필요 (둘 중 하나는 생략 가능)
- @Embeddable : 값 타입 정의하는 곳에 표시
- @Embedded : 값 타입 사용하는 곳에 표시
// 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;
...
}
- 임베디드 타입은 기본 생성자가 필수
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
@AttributeOverride: 속성 재정의
한 엔티티에서 같은 값 타입을 사용하면?
- @AttributeOverride 를 사용하여 임베디드 타입에 정의한 매핑정보 재정의 가능
@Entity
public class Member {
@Id
private Long id;
private String name;
@Embedded
private 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"))
})
private Address companyAddress;
}
값 타입과 불변 객체
값 타입 공유 참조
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함
- 공유 참조로 인한 side effect 발생할 수 있으므로 값을 복사해서 사용해야 함
불변 객체
- 객체 값 수정 못하게 하면 사이드 이펙트 차단 가능
- 따라서, 값 타입은 될 수 있으면 immutable object 로 설계해야 함
- ex) 생성자로만 값 설정하고 수정자(세터) 를 만들지 않으됨
- Integer, String 은 자바가 제공하는 대표적 불변 객체
값 타입의 비교
- 자바가 제공하는 객체 비교는 다음 2가지이다.
- 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
- 동등성(equivalnce) 비교: 인스턴스의 값을 비교, equals() 사용
- 값 타입 비교시에는 equals() 를 이용하여 동등성 비교 해야 한다.
- 직접 값 타입 정의할 경우 equals() 메소드 재정의해줘야 한다.
- 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<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: {엔티티이름}_{컬렉션 속성 이름}
값 타입 컬렉션 사용
- 값 타입 저장
Member member = new Member();
// 임베디드 값 타입
member.setHomeAddress(new Address("homeCity", "street", "1000"));
// 기본값 타입 컬렉션
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
// 임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("old1", "street1", "1000"));
member.getAddressHistory().add(new Address("old2", "street2", "2000"));
em.persist(member);
- member 엔티티 영속화할 때 member 엔티티의 값 타입들 함께 저장됨
- member: INSERT 쿼리 1번
- member.setHomeAddress: 임베디드 값 타입이라 member 저장 쿼리에 포함
- member.getFavoriteFoods: INSERT 쿼리 3번
- member.getAddressHistory: INSERT 쿼리 2번
- 총 INSERT 6번 실행됨
- 값 타입 조회
em.flush();
em.clear(); // 위에서 타입 저장값 클리어하고 시작
Member findMember = em.find(Member.class, member.getId());
- 값 타입 컬렉션도 조회할 때 fetch 전략 선택 가능( LAZY → default )
- @ElementColection(fetch = FEtchType.LAZY)
- member: 회원만 조회함. SELECT 쿼리 1번
- member.setHomeAddress: member 조회할 때 같이 조회됨
- member.getFavoriteFoods: LAZY로 설정되어 실제 컬렉션 사용시 SELECT 쿼리 1번 호출
- member.getAddressHistory: LAZY로 설정되어 실제 컬렉션 사용시 SELECT 쿼리 1번 호출
- 값 타입 수정
Member member = em.find(Member.class, member.getId());
// 임베디드 값 타입 수정
// homecity -> newcity
Address a = findMember.getHomeAddress();
Member.setHomeAddress(new Address("newcity", a.getStreet(), a.getZipcode());
// 기본값 타입 컬렉션 수정
// 치킨 -> 한식
member.getFavoriteFoods().remove("치킨");
member.getFavoriteFoods().add("한식");
// 임베디드 값 타입 컬렉션 수정
member.getAddressHistory().remove(new Address("old1", "street1", "1000"));
member.getAddressHistory().add(new Address("old3", "street3", "3000"));
- 임베디드 값 타입 수정: HomeAddress 임베디드 값 타입은 member 테이블과 매핑되었으므로 member 테이블만 UPDATE 쿼리 수행
- 기본값 타입 컬렉션 수정: 자바의 String 타입은 수정 불가이므로 제거하고 새로 추가해야 함
- 임베디드 값 타입 컬렉션 수정: 값 타입은 불변해야 하므로 기존거를 삭제하고 새로 등록
값 타입 컬렉션의 제약사항
- 특정 엔티티 하나에 소속된 값 타입은 변경되도 자신이 속한 텐티티를 DB에서 찾아 값 바꾸면 된다.
- 값 타입 컬렉션에 보관된 값 타입들의 경우에는 별도의 테이블에 보관되므로 변경시 원본 데이터 찾기 어렵다.
- 따라서 JPA 구현체들은 값 타입 컬렉션에 변경 사항 발생하면 컬렉션이 매핑된 테이블의 연관된 모든 데이터 삭제 후 현재 들어있는 값 새로 DB에 저장한다.
- 값 타입 컬렉션에 보관된 값 타입들의 경우에는 별도의 테이블에 보관되므로 변경시 원본 데이터 찾기 어렵다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼 묶어서 pk 구성해야 한다.
- pk 제약조건으로 nullable 하게 사용 못하고 같은 값 중복 저장도 안된다.
- 위 문제들 해결하려면 값 타입 컬렉션 쓰는 대신 새 엔티티 만들어서 1:N 관계로 사용하면 된다.
- 영속성 전이 + 고아 객체 제거 기능 적용하면 값 타입 컬렉션처럼 사용할 수 있다.
'Develop > JPA' 카테고리의 다른 글
[JPA/기본편] 객체지향 쿼리 (0) | 2024.03.20 |
---|---|
[JPA/기본편] 프록시 (0) | 2024.03.20 |
[JPA/기본편] 상속 관계 매핑 (0) | 2024.03.20 |
[JPA/기본편] 연관관계 매핑 (0) | 2024.03.20 |
[JPA/기본편] 연관관계 (1) | 2024.03.20 |
댓글