자바에는 기본값 타입이라는 것이 존재하는데,
먼저 int, double 같은 것과, Integer Long 과 같은 Wrapper 클래스, String 같은 것이 존재한다.
String name;
int age;
와 같은 것이 그 예이다.
참고로 int, double같은 기본 타입은 절대 공유할 수 없다. C언어야 메모리 접근이 가능하기 때문에 가능하지만, 자바는 그렇지 않다.
int a = 10;
int b = a;
b = 20;
b = a 과정에서는 값이 복사되는 것이지 공유되는 것이 아니다.
또한 Wrapper 클래스나 String 같은 특수한 클래스는 공유 가능한 객체긴 하지만 변경할 수가 없다.
이 녀석들은 기본 값 타입이기 때문에 생명주기 자체는 엔티티가 제거 되면 당연히 같이 제거가 된다.
근데 임베디드 타입은 뭐냐..
어떤 새로운 값 타입을 직접 정의하는 것을 임베디드 타입이라고 한다. JPA는 Embedded Type이라고 칭하고 주로 기본 값 타입(int, String)을 모아서 복합 값 타입으로 사용한다.
예를 들어 회원 엔티티가 이름, 근무 시작일, 근무 종료일을 가진다고 가정해보자.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY) // EAGER은 즉시 가져오는 것
@JoinColumn
private Team team;
private LocalDateTime startDate;
private LocalDateTime endDate;
}
아래의 startDate와 endDate가 근무 시작일과 종료일을 나타낸다고 가정해보자.
근데 예를 들어 이를 가지고 계산해야하는 값이 있다거나, 다른 곳에도 똑같이 사용해야한다고 가정한다면
private LocalDateTime startDate;
private LocalDateTime endDate;
이것만 따로 뺄 수 있으면 좋겠다.
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
public Period() {
}
public Period(LocalDateTime startDate, LocalDateTime endDate) {
this.startDate = startDate;
this.endDate = 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;
}
}
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY) // EAGER은 즉시 가져오는 것
@JoinColumn
private Team team;
@Embedded
private Period workPeriod;
}
startDate, endDate를 Period라는 클래스로 따로 빼서 @Embeddable이라는 어노테이션을 붙이면 임베디드 타입으로 사용이 가능해진다.
사용할 때는 위의 코드와 같이 @Embedded를 붙여 사용하면 된다.
이렇게 되면 장점이 딱딱하게 근무 시작일, 근무 종료일 이게 아니라 근무 기간! 이런식으로 유연하게 표현이 가능해진다.
이 임베디드 타입의 장점은 재사용성을 높여주고 높은 응집도를 구성하여 객체지향적으로 설계가 가능해지고 임베디드 클래스 안에 별도의 메서드를 정의하여 사용할 수 있다. 또한 생명주기 또한 기본 값 타입과 마찬가지로(사실 기본 값 타입이라) 생명주기가 엔티티에 의존한다.
중요한 점은 임베디드를 적용하기 전과 후는 테이블 자체는 같다는 점이다. 임베디드 타입은 단지 엔티티의 값일 뿐인데, 조금도 잘 설계하고자 할 때 사용하는 것이다.
추가 정보 :
1. 만약 똑같은 임베디드 타입을 같은 엔티티에 적용하고 싶을때는 아래와 같이Override를 해야한다.
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "startDate",column = @Column(name = "blabla")),
@AttributeOverride(name = "endDate", column = @Column(name = "blablabla")))
})
private Address address;
2. 임베디드 타입 값을 null로 하면 매핑한 컬럼 또한 모두 null이 된다.
주의점
임베디드 타입을 여러 엔티티에서 공유하면 위험하다. 사이드 이펙트가 발생한다.
Address address = new Address("city", "street", "10000");
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(address);
em.persist(member2);
member1.getHomeAddress().setCity("new City");
이렇게 하면 무슨 문제점이 발생할까? DB에 조회해 보면 member1, 2 모두 new city로 바뀐다. 그 이유는 레퍼런스를 공유 하기 때문이다. 이런 레퍼런스 공유는 컴파일 단계에서 막을 방법이 없다.
이런 코드를 사용하고 싶으면 인스턴스 값을 복사해서 사용해야한다.
예방 방법 :
1. 생성자로만 값을 셋팅하게 한다.
2. Setter를 제거한다.
그러면 값을 어떻게 셋팅하나요? -> new로 다시 생성해서 삽입해야한다.
ex)
Period newPeriod = new Period(period.getStartDate ... );
member.setPeriod(newPeriod);
이런식으로 new로 하되, Getter를 사용해서 복사한다.
값 타입 비교
값 타입을 비교할 때는 레퍼런스를 비교하기 때문에 equals를 이용해서 비교한다. 그리고 그냥 equals를 하는 것이 아니고 재정의한 equals를 써야한다.
예시 :
@Embeddable
public class Address {
@Column(length = 10)
private String city;
@Column(length = 20)
private String street;
@Column(length = 5)
private String zipcode;
public String fullAddress() {
return getCity() + ", " + getStreet() + ", " + getZipcode();
}
public String getCity() {
return city;
}
public String getStreet() {
return street;
}
public String getZipcode() {
return zipcode;
}
private void setCity(String city) {
this.city = city;
}
private void setStreet(String street) {
this.street = street;
}
private void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
@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(getCity(), address.getCity()) &&
Objects.equals(getStreet(), address.getStreet()) &&
Objects.equals(getZipcode(), address.getZipcode()); // 게터로 해야 프록시 문제 없음 무조건 게터, 메서드를 통해서 해야함
}
@Override
public int hashCode() {
return Objects.hash(getCity(), getStreet(), getZipcode());
}
}
여기서 속성을 각각 비교해서 같은 지를 확인해야한다. 또한 그냥 this.city 이런식으로 필드를 직접 비교하면 안되고 Getter를 통해서 비교해야 하는데, 그 이유는 프록시 객체에서 equals를 호출하는데, 만약 아직 영속성 컨텍스트에 저장되어 있지 않은 상태라면 메서드를 호출함으로써 DB에서 진짜 엔티티를 조회해야하는데 그냥 필드로 되어있으면 프록시가 초기화가 안되어 값이 불러와지지 못할 수 있기 때문이다. Getter를 통해서 사용하자. 그리고 가능하면 인텔리제이에서 제공하는 equals 정의 기능을 사용하는게 낫다.
'JPA' 카테고리의 다른 글
[JPA] JPQL 기본 (0) | 2020.10.27 |
---|---|
[JPA] 값 타입 Collections (2) | 2020.10.24 |
[JPA] orphanRemoval (0) | 2020.10.22 |
[JPA] CASCADE (0) | 2020.10.22 |
[JPA] 지연로딩 (0) | 2020.10.22 |