Hibernate/JPA(EntityManager)

[JPA] 임베디드 내장 타입 @Embadded 와 불변 객체

유혁스쿨 2023. 6. 29. 19:12
728x90
반응형

회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다고 가정해본다.

이름 근무 시작일 근무 종료일 주소 도시 주소 번지 주소 우편번호

 

하지만, 근무 시작일과 근무 종료일 그리고 도시, 번지, 우편번호는 종목이 비슷하다는 생각이 들게된다.

엔터티를 설명하게 될때 회원 엔티티는 이름, 근무기간, 주소 를 가진다 라고 추상화 하여 간략하게 설명할 수 있다.

 

이것들을 공통적으로 묶어낼 수 있는것을 임베디드 타입이라고 정의할 수 있다.

 

@Entity(name = "USERS")
@Setter
@Getter
@ToString
public class User {
    @Id @GeneratedValue
    @Column(name = "USER_ID")
    private Long id;
    private String username;
    
    private LocalDateTime startDate;
    private LocalDateTime endDate;
    
    private String city;
    private String street;
    private String zipcode;
}

위의 User 엔터티는 엔티티에 필드를 모두 나열하여 구현한 것이다.

이것은 아래와 같이 임베디드 내장타입으로 분리해서 관리할 수 있게 된다.

@Entity(name = "USERS")
@Setter
@Getter
@ToString
public class User {
    @Id @GeneratedValue
    @Column(name = "USER_ID")
    private Long id;
    private String username;
    
    @Embadded
    private Period period;

    @Embadded
    private Address address;
}

[Period] 내장 타입 선언

@Embeddable
@Getter
public class Period {

    private LocalDateTime startDate;
    private LocalDateTime endDate;

    protected Period() { //protected 이유 : JPA스펙상 만들어놓은거구나! 함부로 new로 생성하면 안되겠구나!
    }

    public Period(LocalDateTime startDate, LocalDateTime endDate) {
        this.startDate = startDate;
        this.endDate = endDate;
    }
}

[Address] 내장 타입 선언

@Embeddable
@Getter
public class Address {
    private String city;
    private String street;
    private String zipcode;

    protected Address() { //protected 이유 : JPA스펙상 만들어놓은거구나! 함부로 new로 생성하면 안되겠구나!
    }

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}

내장 타입을 선언하는 User에는 Period와 Address 클래스타입 필드를 선언함과 동시에 @Embadded 어노티이션을 지정해 주고,

내장 타입을 구현하는 Period나 Address 클래스에는 @Embaddable 어노테이션을 지정해주기만 하면 된다.

이렇게 되면 JPA에서 엔티티를 읽어들여 테이블로 생성해 줄때 내장타입에 선언된 필드를 컬럼으로 자동으로 추가해준다.

 

임베디드 타입의 장점

재사용이 가능하고, 높은 응집도를 가질 수있으며, 값 타입만 사용하는 의미있는 메소드를 만들 수 있다.

또한 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존하게 된다.

@Embeddable
@Getter
public class Period {

    private LocalDateTime startDate;
    private LocalDateTime endDate;

    public boolean isWork(LocalDateTime currentDate) { // 시작,종료 기간 범위를 넘었는지 여부
    	if(this.startDate < currentDate && this.endDate > currentDate) {
        	return true;
        } else {
        	return false;
        }
    }

    protected Period() { //protected 이유 : JPA스펙상 만들어놓은거구나! 함부로 new로 생성하면 안되겠구나!
    }

    public Period(LocalDateTime startDate, LocalDateTime endDate) {
        this.startDate = startDate;
        this.endDate = endDate;
    }
}

사실 DataBase 테이블은 데이터를 잘 관리해야 하기 때문에 필요한 모든 컬럼을 가지고 있어야 한다.

객체는 데이터 뿐만 아니라 메서드와 같은 기능, 행위까지 들고있기 때문에 함께 묶어두었을 때 이점이 많다.

 

객체와 테이블을 아주 세밀하게 매핑하는 것이 가능해진다.

잘 설계된 대부분의 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많은경우가 많다. ( = Value타입이 많아진다.)

 

임베디드 타입과 연관관계

임베디드 타입을 구현한 클래스의 필드에 Entity 클래스 객체를 필드로 선언하는것도 가능하다.

 

@Embeddable
@Getter
public class Address {

	/* ... 중략 ... */
    @OneToOne
    @JoinColumn(name = "locker_id")
    private Locker lokcer; //Locker 엔터티
    
	/* ... 중략 ... */
    
}

위와같이 엔터티 클래스를 필드로 선언하고 JPA를 실행하면 임베디드 타입에 선언된 엔터티를 읽어들여 Address 내장타입을 선언한 User 엔터티와 Address 내장타입에 선언한 Locker 엔티티의 연관관계를 지정할 수 있게 된다.

 

@AttributeOverride - 속성 재정의

한 엔터티에서 같은 값 타입을 사용하면 컬럼명이 중복되어 오류가 발생한다.

@AttributeOverride 혹은 @AttributeOverrides 어노테이션을 사용해서 컬럼 명 속성을 재정의 할 수 있다.

    @Embadded
    private Address homeAddress;

    @Embadded
    @AttributeOverrides(
            {@AttributeOverride(name = "CITY", column = @Column("WORK_CITY")),
             @AttributeOverride(name = "STREET", column = @Column("WORK_STREET")})
    private Address workAddress;

위와 같은 문법으로 지정하면 CITY 컬럼을 WORK_CITY로 변경할 수 있게 된다. (STREET도 적용됨)

@Entity(name = "USERS")
@Setter
@Getter
@ToString
public class User {
    @Id @GeneratedValue
    @Column(name = "USER_ID")
    private Long id;
    private String username;
    
    @Embadded
    private Address homeAddress;

    @Embadded
    @AttributeOverrides(
            {@AttributeOverride(name = "CITY", column = @Column("WORK_CITY")),
             @AttributeOverride(name = "STREET", column = @Column("WORK_STREET")})
    private Address workAddress;
}

 

값 타입과 불변 객체

임베디드와 같은 값 타입 여러 엔티티에서 공유하는 것은 위험하다.

 

@SpringBootTest
@Transactional
public class JpaTest {
    @PersistenceContext
    EntityManager em;
    
    @Test
    public void xToOneTest() {
        Address address = new Address("city", "street", "12345")

        User userA = new User();
        userA.setUsername("userA");
        userA.setAddress(address);
        
        User userB = new User();
        userB.setUsername("userB");
        userB.setAddress(address);
        
        userB.getAddress().setCity("newCity");
    }
    
}

위와 같이 코드를 실행하게 되면 DataBase에서는 "city"값을 둘고있는  User가 각각 userA, userB로 존재하게 되고,

마지막에 setCity코드가 실행되면 두 회원의 city 값이 모두 newCity로 변경된다.

이것을 일부로 의도해서 같이 쓰고싶어 설계했다고 말 할 수도 있다.

그런 경우에는 엔티티를 공유해서 사용해야 한다.

아래와 같이 값을 복사해서 사용해야 한다.

Address address = new Address("city", "street", "12345")
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode())
@SpringBootTest
@Transactional
public class JpaTest {
    @PersistenceContext
    EntityManager em;
    
    @Test
    public void xToOneTest() {
        Address address = new Address("city", "street", "12345")

        User userA = new User();
        user.setUsername("userA");
        user.setAddress(address);
        
        Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode())
        
        User userB = new User();
        userB.setUsername("userB");
        userB.setAddress(copyAddress);
        
        userB.getAddress().setCity("newCity");
    }
    
}

위와같이 코드를 하게 되면 마지막 setCity() 코드가 실행되더라도 userA의 Address에는 영향을 주지 않게 된다.

 

객체 타입의 한계 

설계 당시 객체 타입을 수정할 수 없게 만든다.

값 타입을 불변 객체(immutable Object)로 설계 해야한다.

불변 객체란 생성 시점 이후 절대 값을 변경 할 수 없는 객체를 말한다.

생성자로만 값을 설정하고 Setter를 만들지 않거나 Private로 만들면 된다.

(Integer, String이 자바가 제공하는 대표적인 불변 객체.)

 

만약 위처럼 City를 변경한 객체를 주입하고자 한다면 변경하고자 하는 필드를 제외하고 나머지 필드는 복사해서 주입한다.

Address address = new Address("city", "street", "12345")
Address newAddress = new Address("newCity", address.getStreet(), address.getZipcode())

값이라는게 150을 160으로 바꾸는것과 같이 Address를 통으로 바꾸는게 이론적으로는 맞다고 생각한다는 김영한 님의 의견이 있다.

728x90
반응형