본문 바로가기

Backend/JPA

[JPA] 값 타입(Embeddable, Embedded)

728x90

개요

객체지향적인 어플리케이션을 설계하기 위해선 자율성을 가지는 객체들의 유기적인 협력 관계를 잘 설계해야 한다.

따라서 도메인 로직을 객체지향적으로 설계하기 위해선 엔티티 객체 내부에서도 객체 분리가 이뤄져야 한다.

 

객체지향의 사실과 오해(1)

객체지향의 사실과 오해라는 책을 읽으며 객체지향이라는 개념을 이해하는 데 큰 도움을 얻었다. 한 챕터씩 다시 읽어보며, 그 내용과 내가 알게 된 점 등을 기록하려고 한다. 협력하는 객체들

choi-records.tistory.com

JPA에서 제공하는 값 타입 기능을 이용하면 엔티티 객체 내부에서도 객체 분리를 할 수 있다.

값 타입

엔티티 객체 내부에서 새로운 값 타입을 정의해 사용할 수 있는데, JPA에선 이것을 임베디드 타입이라고 한다.

Member 엔티티의 주소와 근무 기간을 값 타입으로 분리해 보자.

Member.java

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;
    //기간 값 타입
    @Embedded
    private Period period;
    //주소 값 타입
    @Embedded
    private Address address;
    
    // getter
    }
  • 회원 엔티티의 주소와 기간을 값 타입으로 분리했다.

Period.java

@Embeddable
public class Period {

    private LocalDateTime startDate;
    private LocalDateTime endDate;
    
    public boolean isWork(Date date) {
    	// 기간의 책임 메서드	
    }
    // getter, hashCode, equals
}

Address.java

@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;
    
    // getter, hashCode, equals
}
  • 위 Period처럼 기간이 해야 할 책임 작업을 값 타입을 이용해 분리할 수 있다.

위처럼 Member 내 객체 분리를 함으로써 응집력 있게 만들 수 있다.

값 타입과 테이블 매핑

값 타입은 엔티티의 값일 뿐이다. 따라서 아래와 같이 해당 타입이 속한 엔티티 테이블에 값들이 그대로 매핑된다.

만약 JPA를 사용하지 않고 값 타입을 이용하려고 했다면, 엔티티 테이블 하나에 여러 클래스를 매핑해야 했을 것이다. JPA는 이 작업을 매우 간편하게 해 준다.

값 타입과 불변 객체

값 타입은 객체를 값으로 보는 것이다. 물론 객체이므로 여러 기본 타입이나 다른 값 타입의 합으로 이뤄져 있겠지만, 이 합을 하나의 값으로 본다. 따라서 값 타입 객체 공유 참조 관련 문제가 일어나지 않도록 해야 한다.

 

이것이 equals와 hashCode를 재정의해야 하는 이유이다. Address 객체를 불변으로 만들어보자.

@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;

    public Address() {

    }

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

    @Override
    public boolean equals(final Object object) {
        if (this == object) {
            return true;
        }
        if (object == null || getClass() != object.getClass()) {
            return false;
        }
        Address address = (Address) object;
        return Objects.equals(city, address.city) && Objects.equals(street,
            address.street) && Objects.equals(zipcode, address.zipcode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(city, street, zipcode);
    }
}

setter를 만들지 않고, equals와 hashCode 메서드를 재정의함으로써 Address 객체를 값처럼 이용할 수 있는 VO(Value Object)로 만들었다. 이제 값을 수정할 수 없기 때문에 공유 참조와 같은 부작용이 발생하지 않는다.

 

만약 Member의 주소를 수정하고 싶다면, 해당 객체의 값을 바꾸는 것이 아닌 새로운 Address 객체를 생성해야 한다.

값 타입 컬렉션

만약 좋아하는 음식들, 주소 기록 등 여러 값 타입을 가져야 한다면 어떻게 해야 할까?

값 타입을 컬렉션으로 사용할 수 있는 @ElementCollection과 @CollectionTable이 있다.

 

하지만 값 타입은 앞서 말했듯이 객체가 아닌 값이다. 따라서 식별자가 없어 CRUD 작업을 하는 것이 번거롭고, 비효율적인 작업을 거쳐 수행하게 된다. 따라서 값 타입 컬렉션을 이용하기보단 일대다 관계를 고려하는 것이 좋다.

 

[JPA] 영속성 전이와 고아 객체(Cascade, Orphan)

개요이전에 다대일 관계를 다룰 때 다(이하 자식)와 일(이하 부모)이 양방향 연관관계일 때를 생각해 보자. [JPA] 다대일 연관관계 매핑개요어플리케이션 내 객체는 다른 객체와 협력 관계를 통

choi-records.tistory.com

영속성 전이와 고아 객체 제거 기능을 적용하면, 위 기록에서 말했듯 일(부모)과 다(자식) 간의 생명주기를 맞출 수 있다. 또한 별도의 식별자도 가지기 때문에 CRUD 작업을 훨씬 수월하게 수행할 수 있다.

엔티티 타입과 차이점

엔티티 타입과 값 타입의 차이점을 정리하면 아래와 같다.

  • 엔티티와 달리 값 타입은 식별자가 없다. 오직 값으로 사용된다.
  • 생명 주기를 엔티티에 의존한다. (엔티티 - 부모, 값 타입 - 자식)
  • 엔티티와 달리 공유했을 때 공유 참조 문제가 생길 수 있다.
    • 오직 하나의 주인만이 관리하는 것이 안전하다.
    • 불변 객체로 만드는 것이 안전하다.

마무리

값 타입에서 가장 중요한 개념은 VO(Value Object) 일 것 같다. VO에 대해 더 자세하게 공부해 보고 기록을 남겨야 할 것 같다.

잘못된 정보에 대한 지적이나 피드백은 환영입니다. 감사합니다.

참고

 

자바 ORM 표준 JPA 프로그래밍 - 예스24

자바 ORM 표준 JPA는 SQL 작성 없이 객체를 데이터베이스에 직접 저장할 수 있게 도와주고, 객체와 관계형 데이터베이스의 차이도 중간에서 해결해준다. 이 책은 JPA 기초 이론과 핵심 원리, 그리고

www.yes24.com

 

728x90