JPA - @Embedded,@Embeddable 임베디드타입

2021. 4. 17. 01:39 Spring Data/Spring Data JPA

JPA - @Embedded,@Embeddable 임베디드타입

 

지금까지는 엔티티에 연관관계를 제외하고는 모두 자바의 기본타입에 해당하는 값만 매핑하였다. 하지만 예를 들어서 주소라는 값을 하나의 엔티티에 매핑하고 싶은데, 도시명,구,동 이렇게 세가지의 기본타입(String)의 값을 매핑해야한다면 과연 3개를 쭉 나열하는 것이 객체지향적인 것인지 3개를 하나의 객체로 묶어서 하나의 객체로 값을 매핑하는 것이 객체지향적인 것인지 고민을 하자만 바로 후자일 것이다. 주소라는 하나의 객체를 만들고 그 안에 도시명,구,동 필드를 넣고 회원이라는 엔티티에는 주소라는 하나의 객체를 레퍼런스함으로써 조금더 객체지향적으로 엔티티를 매핑하는 방법인 것이다. 

 

import lombok.Getter;
import lombok.Setter;
import org.javers.core.metamodel.annotation.Entity;
 
@Entity
@Table(name = "CUSTOMER")
@Getter
@Setter
public class Customer {
    @Id
    @Column(name = "CUSTOMER_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "CUSTOMER_SEQ_GENERATOR")
    @TableGenerator(
            name="CUSTOMER_SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME", //MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="CUSTOMER_SEQ", //SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
 
    @Column(name = "CUSTOME_NAME")
    private String name;
 
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "CREATED_DATE")
    private Date created = new Date();
 
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "UPDATED_DATE")
    private Date updated = new Date();
 
    @Embedded
    private Address address;
 
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name="city", column = @Column(name = "COMPANY_CITY"))
            ,@AttributeOverride(name="gu", column = @Column(name = "COMPANY_GU"))
            ,@AttributeOverride(name="dong", column = @Column(name = "COMPANY_DONG"))
    })
    private Address companyAddress;
 
    @Embedded
    private PhoneNumber phoneNumber;
}

 

import lombok.Getter;
import lombok.Setter;
import org.javers.core.metamodel.annotation.Entity;
 
@Embeddable
@Getter
public class Address {
    @Column(name = "CITY")
    private String city;
 
    @Column(name = "GU")
    private String gu;
 
    @Column(name = "DONG")
    private String dong;
 
    public Address() {
    }
 
    public Address(String city, String gu, String dong) {
        this.city = city;
        this.gu = gu;
        this.dong = dong;
    }
}
 
@Embeddable
@Getter
public class PhoneNumber {
    @Column(name = "AREACODE")
    private String areaCode;
 
    @Column(name = "LOCALNUMBER")
    private String localNumber;
 
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "PHONE_INFO_ID")
    private PhoneNumberInfo phoneNumberInfo;
 
    public PhoneNumber() {
    }
 
    public PhoneNumber(String areaCode, String localNumber, PhoneNumberInfo phoneNumberInfo) {
        super();
        this.areaCode = areaCode;
        this.localNumber = localNumber;
        this.phoneNumberInfo = phoneNumberInfo;
    }
}
 
@Entity
@Table(name = "PHONE_NUMBER_INFO")
@Getter
@Setter
public class PhoneNumberInfo {
    @Id
    @Column(name = "PHONE_ID")
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "PHONE_SEQ_GENERATOR")
    @TableGenerator(
            name = "PHONE_SEQ_GENERATOR",
            table = "MY_SEQUENCE",
            pkColumnName = "SEQ_NAME", //MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue = "PHONE_SEQ", //SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize = 1
    )
    private Long id;
 
    @Column(name = "PHONE_ORNER_NAME")
    private String name;
 
    @Column(name = "ORNER_AGE")
    private int age;
 
    /*@OneToOne(mappedBy = "info")
    private PhoneNumber number;*/
}
public class EmbeddedTest {
 
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //엔티티매니저 팩토리 생성
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
 
        //엔티티매니저 생성
        EntityManager em = emf.createEntityManager();
 
        //트랜잭션 획득
        EntityTransaction tx = em.getTransaction();
 
        try {
            tx.begin();
 
            Customer c = new Customer();
            c.setName("윤여성");
 
            Address a = new Address("서울특별시","강북구","미아동");
            Address ca = new Address("서울특별시","강북구","미아동");
 
            PhoneNumberInfo info = new PhoneNumberInfo();
 
            info.setName("윤여성고객님");
            info.setAge(28);
 
            PhoneNumber phoneNumber = new PhoneNumber("02","969-8156",info);
 
            c.setPhoneNumber(phoneNumber);
            c.setAddress(a);
            c.setCompanyAddress(ca);
 
            em.persist(c);
 
            tx.commit();
        }catch (Exception e) {
            // TODO: handle exception
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
}

 

위의 소스를 간단히 설명하자면 임베디드 타입의 값을 사용할 엔티티에는 @Embedded라는 어노테이션으로 해당 임베디드타입의 객체를 필드로 둔다. 그리고 임베디드 타입을 정의하는 클래스는 @Embeddable이라는 어노테이션으로 클래스를 정의해준다. 크게 복잡하지 않아 자세한 설명은 하지 않고 몇가지만 설명하겠다. 우선 위 소비자는 2개의 주소 인스턴스 필드를 갖는다. 하나는 집주소, 하나는 회사주소 그런데 주소클래스의 필드명이 같다면 데이터베이스에서 이것이 회사주소인지 집주소인지 알수 없기 때문에 소비자클래스에 @AttributeOverrides로 하나의 주소객체의 칼럼명을 변경하였다.

그리고 핸드폰이라는 임베디드타입의 객체는 핸드폰정보라는 객체와 연관관계를 갖는다. 이렇게 임베디드타입은 단순히 매핑 뿐아니라 연관관계도 맺어줄수 있다는 점이다. 이렇게 임베디드타입을 사용하면 프로그래밍 관점에서 응집력도 훨씬 높아지기에 더욱 객체지향적인 프로그래밍 설계가 되는 것이다. 또한 임베디드타입은 어느 엔티티에서나 재사용이 가능하기 때문에 공통적인 매핑칼럼은 이렇게 임베디드타입으로 정의 해놓고 가져다 사용할 수 있다.

 

마지막 하나더 설명할 것이 있다면, 사실 엔티티의 값은 절대 공유되서는 안되는 값이다. 만약 1번 소비자의 주소 객체를 2번소비자의 주소객체로 레퍼런스를 전달해 공유한다면 2번 소비자의 주소객체에 수정이 일어난다면 1번 소비자의 주소객체 또한 변경이 일어날것이다.(UPDATE SQL) 그렇기 때문에 이러한 객체의 공유를 사전 차단하기 위해 @Embeddable클래스를 불변으로 만드는 것이다. 데이터 set은 생성자로만 해주고 setter자체를 생성하지 않는 것이다.(@Embeddable 클래스는 반드시 기본생성자가 필수다.) 즉, setter메소드는 만들지 않고 데이터 set용 생성자를 만들어주면 된다.(완전히 불변이라고 할 수 없지만 다른 2번소비자가 1번소비자의 주소객체의 주소만 변경해서 같은 인스턴스를 공유하는 것 정도를 사전차단하는 것이다.)

출처: https://coding-start.tistory.com/84?category=781616 [코딩스타트]