JPA의 데이터 타입
(1) 엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능하다.
- ex) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
(2) 값 타입
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경 시 추적 불가능하다.
- ex) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
- 값 타입의 종류
- 기본값 타입 (basic value type)
- 자바 기본 타입 (ex. int, double)
- Wrapper Class (ex. Integer)
- String
- 임베디드 타입 (embedded type)
- 컬렉션 값 타입 (collection value type)
- 기본값 타입 (basic value type)
1. 기본값 타입
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private int age;
}
- Member에서 String, int가 값 타입이다.
- Member 엔티티는 id라는 식별자 값도 가지고 생명주기도 있다.
- 값 타입인 name, age 속성은 식별자 값도 없고 생명주기도 회원 엔티티에 의존한다.
- 회원 엔티티 인스턴스를 제거하면 name, age 값도 제거된다.
- 값 타입은 공유하면 안된다.
- 회원 이름 변경 시 다른 회원의 이름도 함께 변경되면 안 된다.
자바에서 int, double 같은 기본 타입(primitive type)은 절대 공유되지 않는다.
기본 타입은 항상 값을 복사한다.
Wrapper 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경할 수 없다.
2. 임베디드 타입(embedded type, 복합 값 타입)
- 새로운 값 타입을 직접 정의할 수 있다.
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.
//임베디드 타입 사용 전
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
//근무 기간
private LocalDateTime startDate;
private LocalDateTime endDate;
//집 주소
private String city;
private String street;
private String zipcode;
}
//임베디드 타입 사용 후
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
//근무기간
@Embedded
private Period workPeriod;
//집 주소
@Embedded
private Address homeAddress;
}
@Embeddable
@Getter @Setter
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
}
@Embeddable
@Getter @Setter
public class Address {
private String city;
private String street;
private String zipcode;
}
- startDate, endDate를 합해서 Period 클래스를 만들었다.
- city, street, zipcode를 합해서 Address 클래스를 만들었다.
- 새로 정의한 값 타입들은 재사용할 수 있고 응집도도 아주 높다.
- @Embeddable : 값 타입을 정의하는 곳에 표시
- @Embedded : 값 타입을 사용하는 곳에 표시
- 임베디드 타입은 기본 생성자가 필수이다.
- 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 타입의 관계를 UML로 표현하면 Composition 관계가 된다.
(1) 임베디드 타입과 테이블 매핑
- 임베디드 타입은 엔티티의 값일 뿐이다.
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
- 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능하다.
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스 수가 더 많다.
(2) 속성 재정의
- 한 엔티티에 같은 값 타입을 사용하게 된다면 컬럼 명이 중복된다.
- @AttributeOverrides, @AttributeOverride를 사용해서 컬럼 명 속성을 재정의 해야 한다.
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name="city", column = @Column(name = "WORK_CITY")),
@AttributeOverride(name="street", column = @Column(name = "WORK_STREET")),
@AttributeOverride(name="zipcode", column = @Column(name = "WORK_ZIPCODE"))
})
private Address workAddress;
}
(3) 임베디드 타입과 null
- 임베디드 타입 null이면 매핑한 컬럼 값은 모두 null이 된다.
member.setAddress(null);
em.persist(member);
- CITY, STREET, ZIPCODE 컬럼 값은 모두 null이 된다.
3. 값 타입과 불변 객체
(1) 값 타입 공유 참조
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
Address address = new Address("city", "street", "100");
Member member = new Member();
member.setHomeAddress(address);
Member member2 = new Member();
member2.setHomeAddress(address);
member.getHomeAddress().setCity("newCity"); //member, member2 모두 newCity로 바뀐다
- 회원1, 회원2 모두 "newCity"로 변경된다.
- 회원1, 회원2 모두 address 인스턴스를 참조하기 때문이다.
- 전형 예상치 못한 곳에서 문제가 발생하는 것을 부작용(Side Effect)이라 한다.
(2) 값 타입 복사
- 실제 인스턴스인 값을 공유하는 것 대신 값(인스턴스)을 복사해서 사용해야 한다.
Address address = new Address("city", "street", "100");
Member member = new Member();
member.setHomeAddress(address);
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member member2 = new Member();
member2.setHomeAddress(copyAddress);
member.getHomeAddress().setCity("newCity");
- 회원1의 address 값을 복사해서 새로운 copyAddress 값을 생성하였다.
- 회원1만 newCity로 변경되는 update 쿼리가 나간다.
- 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
(3) 불변 객체
생성 시점 이후 절대 값을 변경할 수 없는 객체
- 객체 타입을 수정할 수 없게 만들려면 부작용을 원천 차단할 수 있다.
- 값 타입은 불변 객체(immutable object)로 설계해야 한다.
- 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않는다.
- Integer, String은 자바가 제공하는 대표적인 불변 객체
@Embeddable
@Getter //Getter만 열어준다.
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {} //JPA에서 기본 생성자는 필수이다.
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
}
4. 값 타입 컬렉션
- 값 타입을 하나 이상 저장할 때 사용한다.
- @ElementCollection, @CollectionTable 사용한다.
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
- 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@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"))
private List<Address> addressHistory = new ArrayList<>();
}
- 값 타입 컬렉션을 사용하는 멤버 변수에 @ElementCollection을 지정해 주어야 한다.
- @CollectionTable을 사용해서 추가한 테이블을 매핑해야 한다.
(1) 값 타입 컬렉션 저장
Member member = new Member();
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressHistory().add(new Address("old1", "street", "100"));
member.getAddressHistory().add(new Address("old2", "street", "100"));
em.persist(member); //컬렉션 또한 값타입의 하나이다 값타입의 생명주기는 Member에 의존한다.
- 마지막에 member 엔티티만 영속화했다.
- JPA는 이때 member 엔티티의 값 타입도 함께 저장한다.
- member : INSERT SQL 1번
- member.favoriteFoods : INSERT SQL 3번
- member.addressHistory : INSERT SQL 2번
값 타입 컬렉션은 영속성 전이(Cascade)+고아 객체 제거(ORPHAN REMOVE) 기능을 가지고 있다고 볼 수 있다.
(2) 값 타입 컬렉션 조회
//조회
Member findMember = em.find(Member.class, member.getId());
//컬렉션 값타입들은 지연로딩이 기본값이다.
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
System.out.println(address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println(favoriteFood);
}
- 값 타입 컬렉션은 지연 로딩이 기본값이다.
(3) 값 타입 컬렉션 수정
// 기본값 타입 컬렉션 수정
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
//임베디드 값 타입 컬렉션 수정
findMember.getAddressHistory().remove(new AddressEntity("old1", "street", "100"));
//전체 테이블 삭제
findMember.getAddressHistory().add(new AddressEntity("newCity1", "street", "100"));
//insert 두번이 날라간다, old2, newCity 두번 날라간다.
- 기본값 타입 컬렉션 수정
- 자바의 String 타입은 수정할 수 없으므로 "치킨"을 제거하고 "한식"을 추가해야 한다.
- 임베디드 값 타입 컬렉션 수정
- 값 타입은 불변해야 한다. 따라서 컬렉션에서 기존 주소를 삭제하고 새로운 주소를 등록해야 한다.
(4) 값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 값은 변경하면 추적이 어렵다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다.
- null 입력 X, 중복 저장 X
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 단방향 관계를 고려해야 한다.
//일대다 단방향
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
반응형
'JPA > JPA' 카테고리의 다른 글
[JPA] JPQL - 조인(Join) (0) | 2022.01.07 |
---|---|
[JPA] JPQL - 기본 문법 (0) | 2022.01.07 |
[JPA] 영속성 전이(CASCADE)와 고아 객체 제거 (0) | 2022.01.03 |
[JPA] 즉시 로딩과 지연 로딩 (0) | 2022.01.03 |
[JPA] 프록시 (Proxy) (0) | 2022.01.03 |
댓글