JPA

[JPA] 임베디드 타입

행복하개! 2020. 10. 23. 01:58

 

 

 

자바에는 기본값 타입이라는 것이 존재하는데,

 

먼저 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