본문 바로가기

개발공부/DATABASE

[JPA] 임베디드 타입이란?

💡 코드가 보이지 않으시다면 드래그 혹은 오른쪽 아래 🌜 아이콘을 눌러 테마 색을 변경해주세요.

 

 

안녕하세요!

키크니 개발자 입니다. 🦒

 

임베디드 타입을 다시 복습하고자 정리했습니다.

 

임베디드 타입(Embedded Type)이란?

  • 복합 값 타입을 의미합니다.
  • 새로운 값 타입을 직접 정의할 수 있습니다.
  • JPA는 임베디드 타입(embedded type)이라고 불려집니다.
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 하며, int, String과 같은 값 타입을 의미합니다.

💡 JPA의 데이터 타입 분류

엔티티 타입

  • @Entity로 정의하는 객체입니다.
  • 데이터가 변해도 식별자로 지속해서 추적 가능합니다.
  • 예)회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능

값 타입

  • int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 의미합니다.
  • 식별자가 없고 값만 있으므로 변경시 추적 불가합니다.
  • 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
  • 값 타입 분류
    • 기본 값 타입 
      • 자바 기본 타입(int, double)
      • 래퍼 클래스(Integer, Long)
      • String
    • 임베디드 타입
    • 컬렉션 값 타입

임베디드 타입 사용법

  • @Embeddable : 값 타입을 정의하는 곳에 표시 (임베디드 타입을 사용하기 위해 생성한 Class에 표시) 
  • @Embedded : 값 타입을 사용하는 곳에 표시 (임베디드 타입을 사용하는 Entity에 표시)

임베디드 타입의 장점

  • 재사용이 가능하다. (Period, Address 정보는 Member 외에도 다른 곳에서 사용할 수 있습니다.)
  • 높은 응집도를 가집니다.
  • Period.isWork() 처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있습니다. (현재 근무기간에 속하는지에 대한 의미있는 메소드를 뽑을 수 있으며 객체지향적으로 접근할 수 있습니다.)
  • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티 티에 생명주기를 의존합니다. (엔티티가 생성될 때 임베디드 타입에도 값이 들어오고, 엔티티 사용이 끝났을 떄 값 타입도 함께 사용이 끝납니다.)

임베디드 타입 예시

아래 Member 엔티티는 처음에 7개의 필드를 갖고 있었는데 임베디드 타입을 사용하여 4개의 필드로 줄인 것을 확인할 수 있습니다.

workPeriod (startDate, endDate)

homeAddress (city, street, zipcode)

임베디드 타입 특징

  • 임베디드 타입은 엔티티의 값일 뿐입니다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블에는 변화가 없습니다.
  • 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능합니다. 
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많습니다. (Address, Period 등 class로 뺴놓으면 활용할 수 있는 것이 많습니다. 이러한 value 타입들이 많아져 클래스 수가 더 많다고 볼 수 있습니다.)
  • 도메인의 언어를 공통으로 맞출 수 있다는 장점이 있습니다. (예 : Address를 사용할 때에는 city, street, zipcode 필드를 고정으로 사용합니다.)
  • 임베디드 클래스 안에 entity를 넣어서 사용할 수 있습니다.
@Entity
public class Member extends BaseEntity {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @Embedded  // (1)
    private Period workPeriod;

    @Embedded  // (1)
    private Address homeAddress;
}

(1) @Embedded : 임베디드 타입을 사용하는 entity에 사용합니다.

@Getter
@Setter
@NoArgsConstructor  // (1)
@AllArgsConstructor
@Embeddable  // (2)
public class Address {
    private String city;
    private String street;
    private String zipcode;
}


@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Embeddable  // (2)
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    public boolean isWork() {
        // 현재 일하고 있는지에 대한 로직
    }
}

(1) 임베디드 타입을 사용할 때에는 기본 생성자가 필수입니다.

(2) @Embeddable : 임베디드 타입인 class에 표시해줍니다. 

Member member = new Member();
member.setUsername("hello");
member.setHomeAddress(new Address("city", "street", "zipcode"));
member.setWorkPeriod(new Period());

em.persist();

member를 persist하면 임베디드 타입이 없는 엔티티 생성 쿼리문과 같은 쿼리문을 확인할 수 있습니다.

 

만약 하나의 엔티티에 같은 임베디드 타입을 사용하고 싶을 때에는?

@AttributeOverride를 사용합니다.

 

@AttributeOverride 적용 전

@Entity
public class Member extends BaseEntity {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @Embedded
    private Period workPeriod;
    
    @Embedded
    private Address homeAddress;

    @Embedded
    private Address workAddress;
}

homeAddress와 workAddress의 컬럼명이 중복되므로 Repeated column in mapping for entity error가 발생합니다.

 

@AttributeOverride 적용 후

@Entity
public class Member extends BaseEntity {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;

    @Embedded
    @AttrubyteOverrides({	// (1)
        @AttributeOverride(name="city",
                        column=@column(name="WORK_CITY")),
        @AttributeOverride(name="street",
                        column=@column(name="WORK_STREET")),
        @AttributeOverride(name="zipcode",
                        column=@column(name="WORK_ZIPCODe"))
    })
    private Address workAddress;
}

(1) 많이 사용하지는 않지만 위의 코드처럼 컬럼 명을 재정의 해주면 됩니다.

 

임베디드 타입에 Null을 넣게 되면?

member.setAddress(null); // null 입력
em.persist(member);

임베디드 타입(Address)안에 필드들(city, zipcode, street)도 모두 null값이 됩니다.

 

임베디드 타입 사용시 주의해야할 점

임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 사이드 이펙트로 인해 부작용이 발생할 수 있습니다.

Address address = new Address("city", "street", "zipcode");

Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member);

Member member2 = new Member();
member.setUsername("member2");
member.setHomeAddress(address);
em.persist(member2);

member.getHomeAddress().setCity("newCity");  // (1)

(1) member의 address city를 newCity로 변경 바꾸려고 한 것이지만,

member2의 address city도 newCity로  같이 변경됩니다.

 

이를 해결하려면 어떻게 해야하는가?

Address의 Setter를 구현하지 않거나, private으로 구현합니다.

  • 객체 타입을 수정할 수 없게 만들어 부작용을 원천에 차단합니다.
  • 값 타입은 불변 객체로 설계해야 합니다.
  • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됩니다.
  • 참고 : Integer, String은 자바가 제공하는 대표적인 불변 객체를 의미합니다.

불변 객체란? 생성 시점 이후 절대 값을 변경할 수 없는 객체를 의미합니다.

 

⭐️  참고한 곳  


자바 ORM 표준 JPA 프로그래밍 - 기본편

https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

 

 

배워야 할 것이 더 많은 주니어 개발자입니다. 🐣
내용 전달보다는 정리를 목적으로 포스팅을 하고 있습니다.
잘못 된 내용이나 부족한 부분은 댓글로 주시면 감사드리겠습니다. 
반응형