🍃이 글은 inflearn에서 김영한의 스프링 부트와 JPA 실무 완전 정복 로드맵을 학습하고 작성한 것입니다.🍃
1. 엔티티 타입
@Entity로 정의하는 객체
데이터가 변해도 식별자로 지속해서 추적 가능
예)회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
2. 값 타입
int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
식별자가 없고 값만 있으므로 변경 시 추적 불가
예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
1. 기본값 타입
2. 임베디드 타입
3. 컬렉션 값 타입
특징
생명 주기를 엔티티에 의존한다.
회원 엔티티를 삭제하면 String name, int age 등의 값 타입이 전부 삭제된다.
값 타입은 절대로 공유하면 안 된다.
내 회원 이름을 변경할 때 다른 회원의 이름도 함께 변경되는 사이드 이펙트 문제가 일어나면 안 된다.
멤버 엔티티를 위 그림보다 아래 그림처럼 근무 기간, 집 주소로 좀 더 추상화 해서 설계하면 더 좋다.
이렇게 컬럼들을 묶어낼 수 있는 것이 임베디드 타입이다.
근무 기간과 집 주소 클래스를 생성해 준다.
@Embeddable
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Address {
// 기본 생성자 꼭 필요하다.
private String city;
private String street;
@Column(name = "ZIPCODE") // 가능
private String zipcode;
}
@Embeddable
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Period {
// 기본 생성자 꼭 필요하다.
private LocalDateTime startDate;
private LocalDateTime endDate;
pubilc boolean isWork() {
//지금 근무 기간인지 체크하는 의미 있는 메소드 추가 가능
}
}
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
//기간
@Embedded
private Period period;
//주소
@Embedded
private Address address;
}
재사용이 가능하다.
관련된 필드와 의미 있는 메소드로 이루어져서 응집도가 높다.
임베디드 타입을 포함한 모든 값 타입은, 이 값 타입을 소유한 엔티티에 생명주기를 의존한다.
임베디드 타입은 엔티티의 값일 뿐이다.
임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다.
임베디드 타입의 값이 null이면 매핑한 컬럼값은 모두 null이다.
// 위에서 만든 임베디드 타입 Address 객체
Address address = new Address("city", "street", "1000");
Member member1 = new Member();
member1.setAddress(address);
Member member2 = new Member();
member2.setAddress(address);
member1.getAddress().setCity("newCity");
이렇게 돼버리면 member2의 도시 주소도 "newCity"로 바뀌는 사이드 이펙트 문제가 발생한다.
그래서 임베디드 타입을 불변 객체로 만들어서 변경 자체를 불가능하게 해야 한다.
불변 객체는 생성 시점 이후 절대 값을 변경할 수 없는 객체이다.
값 타입은 불변 객체(immutable object)로 설계해야 한다.
생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 된다.
Integer, String이 자바가 제공하는 대표적인 불변 객체이다.
위에서 만든 임베디드 타입 Address, Period 클래스
- 기본 생성자와 모든 필드 값을 파라미터로 받는 생성자를 만들고 setter를 만들지 않는다.
- 따라서 값을 변경하는 것이 불가능해지기 때문에 생성할 때 항상 new로 새로 만들어야 한다.
- 사이드 이펙트 문제가 발생하지 않는다.
동일성 비교: 인스턴스의 참조 값을 비교한다. (== 사용)
동등성 비교: 인스턴스의 값을 비교한다. (equals() 사용)
값 타입은 new로 생성하기 때문에 a.equals(b)를 사용해서 동등성 비교를 해야 한다.
그러기 위해선 값 타입의 equals() 메소드를 오버라이딩 해야 true가 된다.
참고로 String, Integer 같은 래퍼 클래스는 equals() 메소드가 이미 오버라이딩 돼 있다.
intellj가 제공하는 자동 완성 기능을 사용하자.
- generate(command + n)에서 equals() and hashCode()를 적용한다.
- equals()를 구현할 때 hashCode()도 그것에 맞게 구현 돼야 한다.
값 타입을 컬렉션에 담아서 사용한다.
하지만 값 타입 컬렉션 대신에 일대다 관계를 고려하자.
일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용하면 된다.
영속성 전이 + 고아 객체 제거를 사용해서 생명주기가 엔티티에 의해 관리되는 값 타입 컬렉션처럼 사용하자.
@Entity
public class Member {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "ADDRESS_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
}
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
@Embedded
private Address address; // 값 타입
}
Member member = new Member();
member.getAddressHistory().add(new AddressEntity("city1", "street1", "1000"));
member.getAddressHistory().add(new AddressEntity("city2", "street2", "2000"));