기본값 타입
- JPA의 데이터 타입 분류
- 엔티티 타입: @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능
- 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
- 값 타입: int, Integer, String과 같이 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경 시 추적 불가
- 예) 숫자 100을 200으로 변경하면 완전 대체
- 엔티티 타입: @Entity로 정의하는 객체
- 값 타입 분류
- 기본값 타입
- 임베디드 값 타입
- 컬렉션 값 타입
- 기본값 타입
- 예) String name, int age
- 생명주기가 엔티티에 의존
- 회원 엔티티 삭제 시 이름, 나이 필드 함께 삭제
- 값 타입은 공유하면 안됨
- 예) 회원 이름 변경 시 다른 회원의 이름도 변경되면 안됨
- 참고: int, double 같은 기본 타입(primitive type)은 절대 공유되지 않음 -> 항상 값 복사
- Integer같은 래퍼 클래스나 String 같은 특수 클래스는 공유 가능한 객체이지만 변경 방법이 없음
int a = 10;
int b = a; // 10이란 값만 복사
b = 20;
// a = 10, b = 20
Integer a = new Integer(10);
Integer b = a // 참조값 복사
a.changeVal(20); // 변경 불가 (이런 방법 존재 X)
// a = b = 10
임베디드 타입(복합값 타입)
- 새로운 값 타입을 직접 정의할 수 있음
- JPA는 임베디드 타입이라고 함 (주로 기본값 타입을 모아 만들기 대문에 복합값 타입이라고도 함)
- int, String처럼 값 타입임
- @Embeddable: 값 타입 정의하는 곳에 표시 (클래스 위)
- @Embedded: 값 타입 사용하는 곳에 표시 (필드 위)
- 회원은 이름, 근무기간, 집주소를 가진다 (근무기간, 집주소에 맞게 클래스 생성)
- 장점:
- 재사용성
- 응집성
- 해당 값 타입만의 의미있는 메소드 사용 가능
- 해당 값 타입을 소유한 엔티티의 생명주기에 의존
- 매핑하는 DB는 변화없음
// 임베디드 타입 사용 전
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
//근무 기간
private LocalDateTime startDate;
private LocalDateTime endDate;
//집주소
private String city;
private String street;
private String zipcode;
}
- 기본 생성자 필수!
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
//근무 기간
@Embedded // 생략가능하지만 명시적으로 알려주기 위해
private Period workPeriod;
//집주소
@Embedded
private Address homeAddress;
}
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
public LocalDateTime getStartDate() {
return startDate;
}
public void setStartDate(LocalDateTime startDate) {
this.startDate = startDate;
}
public LocalDateTime getEndDate() {
return endDate;
}
public void setEndDate(LocalDateTime endDate) {
this.endDate = endDate;
}
public Period() {
}
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getZipcode() {
return zipcode;
}
public void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
public Address() {
}
}
- 객체와 테이블을 아주 세밀하게 매핑 가능
- 매핑한 테이블의 수보다 클래스의 수가 더 많은 것이 좋은 설계
- 임베디드 타입이 다시 임베디드 타입이나 엔티티를 필드로 가질 수 있다
// 같은 값 타입 사용 시
//집주소
@Embedded
private Address homeAddress;
//근무 주소
@Embedded
@AttributeOverride() // 내부 설정 통해 재정의
private Address workAddress;
- 임베디드 타입의 값이 null이면 그 클래스의 필드 값들 모두 null
값 타입과 불변 객체
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 한번에 다같이 값이 변경될 수 있음 (side effect)
- 예) 회원1이 city 값을 NewCity로 변경하면 회원2의 city 값도 함께 변경됨
- 따라서 값(인스턴스)를 복사하여 사용
- 이렇게 항상 값을 복사하여 사용하면 위와 같은 부작용을 피할 수 있다
- 그러나 임베디드 타입처럼 직접 정의한 값 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없음
- 기본 타입은 직접 대입해도 알아서 값만 복사함
- 불변 객체: 생성 이후 값이 변하지 않는 객체
- 객체 타입을 수정할 수 없게하여 부작용 원천 차단
- 값 타입은 불변 객체로 설계해야함
- 생성자로만 값을 설정하고 setter를 만들지 않는 것
- setter를 아예 안 만들거나, private으로 설정
- Integer, String은 자바의 대표적인 불변 객체
- 불변 객체의 값을 바꾸고 싶다면?
Member member = new Member();
member.setUsername("a");
Address address = new Address("1", "2", "3");
member.setHomeAddress(address);
em.persist(member);
Address newAddress = new Address("A", "2", "3"); // 일부만 바꾸고 싶어도 새로 만들어 갈아끼움
member.setHomeAddress(newAddress);
값 타입의 비교
- 값 타입: 인스턴스가 달라도 그 값 자체가 같다면 같은 것으로 봐야함
- 예) int a = 10, int b = 10 일 때 a == b가 true 인 것 처럼
- 동일성 비교(==): 참조 값 비교
- 동등성 비교(equals()): 값 자체 비교
- 따라서 값 타입은 equals() 사용하여 비교하여야 함
- equals()도 기본 설정이 == 비교이기 때문에 재정의 해주어야 함 (주로 모든 필드에 대해)
- 해당 불변 객체에 인텔리제이 'Generate > equals() and hashcode()' 사용하기
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
값 타입 컬렉션
- 값 타입을 하나 이상 저장할 때 사용
- 해당 컬렉션 필드 위에 @ElementCollection, @CollectionTable 사용
- DB에는 한 테이블에 컬렉션을 넣을 수 있는 방법이 없기 때문에 컬렉션 저장을 위한 별도의 테이블 생성하는 개념
- 별도 생성된 테이블의 컬럼은 모두 PK -> 따로 식별자를 생성한다면 엔티티와 같아진다
- 기존에 배운 기본값 타입, 복합값 타입과 같이 생애주기가 주인을 따르기 때문에 persist 한번에 모두 저장
- 기본적으로 지연 로딩 전략 사용
- 참고: cascade, orphanRemoval 기능을 필수로 가진다고 볼 수 있다
// 저장
Member member = new Member();
member.setHomeAddress(new Address("1", "2", "3"));
member.getFavoriteFoods().add("chicken");
member.getFavoriteFoods().add("pizza");
member.getFavoriteFoods().add("coke");
member.getAddressHistory().add(new Address("a", "b", "c"));
em.persist(member);
em.flush();
em.clear();
// 조회
Member findMember = em.find(Member.class, member.getId());
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
System.out.println("address = " + address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String food : favoriteFoods) {
System.out.println("food = " + food);
}
// 복합값 타입 수정
//findMember.getHomeAddress().setCity("new");
Address address = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("new", address.getStreet(), address.getZipcode())); // 아예 새로 세팅
// 값 타입 컬렉션 수정
findMember.getFavoriteFoods().remove("chicken"); // delete 문 한번
findMember.getFavoriteFoods().add("new"); // insert 문 한번
// Address 클래스에 equals()가 제대로 재정의 되어있어야 함
findMember.getAddressHistory().remove(new Address("old1", "b", "c")); // delete 한번
findMember.getAddressHistory().add(new Address("new1", "b", "c")); // insert 두번
- 제약 사항:
- 값 타입 컬렉션에 변경이 생기면 주인 엔티티와 연관된 모든 데이터를 삭제한 후 존재해야할 값들을 모두 다시 저장
- 예) old1, old2 저장된 상태에서 old1 -> new1 수정되면 먼저 다 지운 후, old2, new1 다시 저장
- 값 변경 시 추적이 어렵다
- 식별자 개념이 없다(모든 컬럼을 묶어 기본키로 구성) -> null 입력 불가능, 중복 저장 불가능
- 따라서 실무에서는 상황에 따라 일대다 관계를 대안으로 고려
- 값 타입 컬렉션에 변경이 생기면 주인 엔티티와 연관된 모든 데이터를 삭제한 후 존재해야할 값들을 모두 다시 저장
- 정말 단순한 상황이 아닌 이상 값 타입 컬렉션을 엔티티로 승격하여 관리
// Member class
//@ElementCollection
//@CollectionTable(name = "ADDRESS", joinColumns =
// @JoinColumn(name = "MEMBER_ID")
//)
//private List<Address> addressHistory = new ArrayList<>();
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
// AddressEntity class
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
@Embedded
private Address address;
- 위와 같이 엔티티로 만들어 일대다 연관관계를 맺어주면 훨씬 유연하게 관리 가능하다
- 특히 지속해서 값을 추적, 변경해야 하는 경우
- getter 사용하여 equals() and hashCode() 생성 권장
- 사용하지 않으면 필드에 직접 접근하는데, 프록시일 때는 작동하지 않음
출처: 김영한님 JPA 프로그래밍 - 기본편
'course > inflearn' 카테고리의 다른 글
[JPA 프로그래밍 기본편] JPQL - 기본 문법 (0) | 2022.11.03 |
---|---|
[JPA 프로그래밍 기본편] 객체지향 쿼리 언어 소개 (0) | 2022.09.19 |
[JPA 프로그래밍 기본편] 프록시와 연관관계 관리 (0) | 2022.09.09 |
[JPA 프로그래밍 기본편] 고급 매핑 (0) | 2022.09.06 |
[JPA 프로그래밍 기본편] 다양한 연관관계 매핑 (0) | 2022.09.03 |