2024.10.30 업데이트
임베디드 값 타입은 꽤나 사용성을 높여준다. 임베디드 값 타입은 무엇이냐면 특정 엔티티에 필요한 필드를 클래스타입으로 받는 경우이다.
아래 예를 보자.
좌측 테이블은 기본타입으로만 설정된 테이블이다. 물론 이게 잘못된 건 아니다. 근데 기본 타입이 아니고 클래스로 설계된 타입을 사용할 때 얻는 이점이 매우 많기 때문에 임베디드 값 타입을 사용하는 것을 고려해볼 수 있다. 그래서 위와 같이 만들면 아래 사진 처럼 된다.
객체 입장에서는, Period, Address 라는 클래스가 Member 엔티티에 들어있게 되지만, DB에서는 이전과 다를 것 없이 그냥 각 필드들이 컬럼으로 만들어진다. 그럼 어차피 DB 테이블에선 이전과 다를 것 없이 동일하게 필드로 표시되는데 임베디드 값 타입을 사용시 어떤 이점이 있을까?
- 재사용성
- 높은 응집도
- 응집도가 높다는 것은, 의미 있는 메서드를 만들어 사용할 수 있고 그에 따라 객체 지향형 설계가 가능해짐 예를 들어, Period.isWork()와 같은 메서드를 만들어서 해당 객체에서만 사용되는 메서드를 Period 클래스에서 구현 가능
- 임베디드 값 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함
그럼 임베디드 값 타입을 사용하는 방법을 알아보자. 우선 임베디드 값 타입을 사용하는 방법은 다음과 같다.
- @Embeddable → 값 타입을 정의하는 곳에 표시
- @Embedded → 값 타입을 사용하는 곳에 표시
- 기본 생성자 필수 (값 타입을 정의하는 클래스에서)
EmbedMember
package cwchoiit.embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.time.LocalDateTime;
@Entity
public class EmbedMember {
@Id
@GeneratedValue
private Long id;
private String username;
private LocalDateTime startDate;
private LocalDateTime endDate;
private String city;
private String street;
private String zipCode;
}
- 이렇게 있는 엔티티에서 하고 싶은 건 (startDate, endDate) 이 두 필드를 하나의 클래스로 관리하고, (city, street, zipCode) 이 세 필드를 하나의 클래스로 관리하고 싶은 것이다.
우선, 그래서 Period 클래스를 하나 만들어보자.
Period
package cwchoiit.embedded;
import javax.persistence.Embeddable;
import java.time.LocalDateTime;
@Embeddable
public class Period {
private final LocalDateTime startDate;
private final LocalDateTime endDate;
public Period() {
this.startDate = LocalDateTime.now();
this.endDate = LocalDateTime.now();
}
public Period(LocalDateTime startDate, LocalDateTime endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
public boolean isWork() {
return !endDate.isAfter(LocalDateTime.now());
}
public LocalDateTime getStartDate() {
return startDate;
}
public LocalDateTime getEndDate() {
return endDate;
}
}
Address
package cwchoiit.embedded;
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private final String city;
private final String street;
private final String zipCode;
public Address() {
this.city = null;
this.street = null;
this.zipCode = null;
}
public Address(String city, String street, String zipCode) {
this.city = city;
this.street = street;
this.zipCode = zipCode;
}
public String getCity() {
return city;
}
public String getStreet() {
return street;
}
public String getZipCode() {
return zipCode;
}
}
- 이렇게 두 개의 클래스가 있고 이 두개의 클래스에 어노테이션으로 @Embeddable을 사용해준다. 그니까 직관적으로 이 클래스는 다른 클래스에 임베디드가 될 수 있다는 의미다.
- 그리고 이렇게 따로 클래스로 뽑아버리니 좋은점은 각 필드간 응집도가 높아져서 이 클래스에서 유용하게 사용할 수 있는 메서드를 만들수도 있고 만든 메서드는 재활용 할수도 있어진다는 점이다. (예: Period.isWork())
- 그리고 위에서 말한 기본 생성자 필수라는 게 이 @Embeddable 애노테이션이 붙은 클래스들에게 적용되는 말이다.
- 그리고! 가장 중요한 것은, 이 두 임베디드 값 타입은 모두 불변 객체라는 것이다. 왜 불변객체로 만드는 걸까? 이후에 설명하겠다.
EmbedMember
package cwchoiit.embedded;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class EmbedMember {
@Id
@GeneratedValue
private Long id;
private String username;
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Period getWorkPeriod() {
return workPeriod;
}
public void setWorkPeriod(Period workPeriod) {
this.workPeriod = workPeriod;
}
public Address getHomeAddress() {
return homeAddress;
}
public void setHomeAddress(Address homeAddress) {
this.homeAddress = homeAddress;
}
}
- 위처럼 Period, Address 클래스를 타입으로 설정한 필드에 어노테이션으로 @Embedded를 붙여준다. 즉 이것도 마찬가지로 이 필드는 임베디드 되었다는 의미다.
- 이렇게 멤버 클래스를 만들면 DB에 생성되는 테이블은 임베디드 값 타입을 사용하나 사용하지 않나 완전히 똑같다. 즉, 테이블에는 그냥 아래 모습 그대로 들어간다.
그럼 왜 사용할까? 다시 말하지만 객체 지향적으로 코드를 작성하기에 훨씬 유리하기 때문이다. 사용성도 높아지고. 각 클래스가 가지고 있는 메서드를 사용하는 이점이 생각보다 엄청 크다.
그래서 이렇게 임베디드 값 타입을 사용한 엔티티를 사용하는 방법은 뭐 별거 없다.
Main
package cwchoiit;
import cwchoiit.embedded.Address;
import cwchoiit.embedded.EmbedMember;
import cwchoiit.embedded.Period;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.time.LocalDateTime;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("hellojpa");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
try {
EmbedMember embedMember = new EmbedMember();
embedMember.setUsername("embed");
embedMember.setHomeAddress(new Address("city", "street", "zipCode"));
embedMember.setWorkPeriod(new Period(LocalDateTime.now(), LocalDateTime.now()));
entityManager.persist(embedMember);
entityManager.getTransaction().commit();
} catch (Exception e) {
entityManager.getTransaction().rollback();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
}
- 이전과 동일하게 사용하면 된다.
임베디드 값 타입과 연관관계
임베디드 값 타입으로 만든 클래스는 내부에 임베디드 값 타입으로 만든 클래스도 당연히 가질 수 있다. 위 그림에서 Address가 Zipcode를 가지고 있는 모습을 볼 수 있다. 근데, 재밌는게, 임베디드 값 타입으로 만든 클래스가 엔티티도 가질 수 있다. 위 그림에서 PhoneNumber가 PhoneEntity를 가지고 있음을 볼 수 있다.
그러니까 아래 코드처럼 말이다.
package cwchoiit.embedded;
import cwchoiit.fetch.Team;
import javax.persistence.Embeddable;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import java.time.LocalDateTime;
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
- 이 Period라는 임베디드 값 타입 클래스는 아래와 같이 Team 이라는 엔티티도 가질 수 있다. (물론, 그렇게 자주 사용되는 방식은 아니다)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
@AttributeOverrides와 @AttributeOverride
또 한가지는 같은 임베디드 값 타입의 클래스를 여러개 사용하고 싶을수가 있다. 예를 들면 아래 코드를 보자.
EmbedMember
package cwchoiit.embedded;
import javax.persistence.*;
@Entity
public class EmbedMember {
@Id
@GeneratedValue
private Long id;
private String username;
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
@Embedded
private Address officeAddress;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Period getWorkPeriod() {
return workPeriod;
}
public void setWorkPeriod(Period workPeriod) {
this.workPeriod = workPeriod;
}
public Address getHomeAddress() {
return homeAddress;
}
public void setHomeAddress(Address homeAddress) {
this.homeAddress = homeAddress;
}
public Address getOfficeAddress() {
return officeAddress;
}
public void setOfficeAddress(Address officeAddress) {
this.officeAddress = officeAddress;
}
}
- 이 코드에서 이 부분을 자세히 보자.
@Embedded
private Address homeAddress;
@Embedded
private Address officeAddress;
- 충분히 의미 있을 만한 필드들이다. 그런데 둘 다 같은 임베디드 값 타입이다. 이럴때 이렇게만 두고 실행하면 에러가 난다. 그래서 아래처럼 바꿔줘야 한다.
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name="city", column=@Column(name = "OFFICE_CITY")),
@AttributeOverride(name="street", column=@Column(name = "OFFICE_STREET")),
@AttributeOverride(name="zipCode", column=@Column(name = "OFFICE_ZIPCODE"))
})
private Address officeAddress;
- @AttributeOverrides와 @AttributeOverride를 사용해서, 이 애노테이션이 달린 임베디드 값 타입의 클래스의 어떤 필드를 어떤 컬럼명으로 매핑할건지를 지정해줘야 동일한 컬럼명을 사용하지 않게 되어 에러가 발생하지 않는다.
임베디드 값 타입과 같은 값 타입은 왜 불변객체여야 할까?
굉장히 중요한 부분이다. 만약, 예를 들어 회원1과 회원2 모두 같은 Address 참조값을 가지고 있다고 가정해보자. 개발자가 그렇게 만든것이다. 최초의 의도는 두 회원이 같은 주소를 가지기 때문에 Address 객체를 하나 만들어서 두 회원에 모두 세팅한 것이다.
여기까지는 그럴수 있다. 그리고 그걸 막을 방법도 없다. 근데 만약, 두 회원 중 하나를 선택해서 주소를 변경해 버리면 어떻게 될까? 다음과 같은 일이 일어난다.
나의 의도는 회원1의 주소만 바꾸려는 의도였는데 회원2의 주소까지 변경되어 버린다. 이거는 막을 방법도 없고 이런 버그는 거의 잡는게 불가능하다. 코드로 이 내용을 구현해보자.
우선 불변객체가 아니어야 하므로 Address를 불변객체에서 그렇지 않도록 변경해보자.
Address - 불변객체가 아님
package cwchoiit.embedded;
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private String city;
private String street;
private String zipCode;
public Address() {
}
public Address(String city, String street, String zipCode) {
this.city = city;
this.street = street;
this.zipCode = 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;
}
}
Main
package cwchoiit;
import cwchoiit.embedded.Address;
import cwchoiit.embedded.EmbedMember;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("hellojpa");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
try {
Address address = new Address("city", "street", "zipCode");
EmbedMember embedMember = new EmbedMember();
embedMember.setUsername("embed");
embedMember.setHomeAddress(address);
EmbedMember embedMember2 = new EmbedMember();
embedMember2.setUsername("embed2");
embedMember2.setHomeAddress(address);
entityManager.persist(embedMember);
entityManager.persist(embedMember2);
entityManager.getTransaction().commit();
} catch (Exception e) {
entityManager.getTransaction().rollback();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
}
- 지금 코드를 보면, 하나의 Address 객체를 만들고 두개의 EmbedMember의 HomeAddress에 적용시켰다. 이러면 두 회원은 같은 값 타입을 참조하고 있게 된다.
- 그리고, 이렇게 코드를 작성하면 컴파일러 단계에서 이를 방지할 수 있는 방법이 있나? 없다. 자바는 그냥 HomeAddress를 세팅하기 위한 세터에 적절한 타입의 Address를 받았다고 생각하기 때문에 이 시점에 컴파일러는 아무런 문제를 인지하지 못한다. 이게 핵심이다. 그리고 실제로 지금까지는 아무런 문제도 없다.
실행 결과
두 회원이 모두 같은 주소를 가지고 있다. 여기까지는 아무런 문제가 없다. 그런데 내가 여기서 둘 중 하나의 주소를 변경하는 시점부터 문제가 발생한다. 저 코드에서 딱 한줄을 추가해보자.
package cwchoiit;
import cwchoiit.embedded.Address;
import cwchoiit.embedded.EmbedMember;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("hellojpa");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
try {
Address address = new Address("city", "street", "zipCode");
EmbedMember embedMember = new EmbedMember();
embedMember.setUsername("embed");
embedMember.setHomeAddress(address);
EmbedMember embedMember2 = new EmbedMember();
embedMember2.setUsername("embed2");
embedMember2.setHomeAddress(address);
entityManager.persist(embedMember);
entityManager.persist(embedMember2);
embedMember.getHomeAddress().setCity("newCity");
entityManager.getTransaction().commit();
} catch (Exception e) {
entityManager.getTransaction().rollback();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
}
- 바로 이 부분이다.
embedMember.getHomeAddress().setCity("newCity");
- 난 딱 하나의 회원의 주소를 변경했을 뿐인데, 실행을 한다면 이런 결과를 얻게 된다.
- 보면 알겠지만 업데이트 쿼리가 두번 나간다. 그리고 실제 결과도 이렇다. 같이 변경됐다.
이런 부작용이 발생할 수가 있다. 그러니까, 애시당초에 부작용을 발생시키지 않으려면 값의 변경을 할 수 없게 막아야 한다. 어떻게? 불변객체로 만들어서! 그래서 불변객체로 만들어야 한다는 것이다. 그래서 이렇게 강제하는 것이다. "한번 세팅된 값을 바꾸고 싶어? 그러면 새로 객체 만들어!"
그래서 불변 객체로 만든 Address는 이렇게 만들 수 있다.
package cwchoiit.embedded;
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private final String city;
private final String street;
private final String zipCode;
public Address() {
this.city = null;
this.street = null;
this.zipCode = null;
}
public Address(String city, String street, String zipCode) {
this.city = city;
this.street = street;
this.zipCode = zipCode;
}
public String getCity() {
return city;
}
public String getStreet() {
return street;
}
public String getZipCode() {
return zipCode;
}
}
- 아까 위에서 본 그대로이다. 세터를 제거하고 필드를 전부 `final`로 변경했다. 그리고 오로지 생성자로만 값을 세팅할 수 있게 했다.
- 여기서 좀 더 친절한 코드가 되고 싶으면 새로운 불변객체를 만들어내는 with(...), from(...) 등의 메서드를 만들어서 변경하고자 하는 값을 받고 그 값을 세팅한 새로운 객체를 뱉어내는 메서드들도 만들 수 있겠다.
'JPA(Java Persistence API)' 카테고리의 다른 글
[JPA] Part 15. JPQL (0) | 2023.10.28 |
---|---|
[JPA] Part 14. 값 타입 컬렉션 (0) | 2023.10.25 |
[JPA] Part 12. CASCADE, 고아 객체 (2) | 2023.10.23 |
[JPA] Part 11. 지연로딩과 즉시로딩 (2) | 2023.10.23 |
[JPA] Part 10. 프록시 (0) | 2023.10.22 |