본문 바로가기
[IT]/JPA

[JPA] JPA 임베디드 타입 (값 타입 과 불변객체)

by dop 2021. 3. 25.

JPA의 데이터 타입

JPA의 데이터 타입은 크게 두 가지 엔티티 타입값 타입으로 나뉜다. 엔티티 타입은 연관관계 매핑 시, @Entity로 선언한 객체를 필드 값으로 넣은 것을 떠올리면 되고, 값 타입은 int, Integer, String처럼 그 자체가 단순한 값을 가지고 있는 타입들이 속한다.

 

- 엔티티 타입

  - @Entity로 정의하는 객체

  - 데이터가 변해도 식별자로 추적 가능

  - 예) Member 엔티티 이름, 나이 등을 변경 시 id값으로 식별할 수 있음.

- 값 타입

  - 단순히 자바 기본 타입이나 객체

  - 식별자가 없고 단순한 값이므로 추적이 불가능함.

  - 예) 물품 개수를 100개에서 200개로 변경하면 완전히 다른 값으로 대체된다.

 

엔티티 타입은 @Entity로 선언한 객체 자체를 타입으로 가지게 되어 종류라는 개념이 없지만 값 타입은 조금 다르다.

 

값 타입의 종류

- 기본 값 타입

 자바의 기본 타입(int, double), Wrapper Class(Integer, Long), String 이 기본 값 타입에 해당한다.

- 임베디드 타입(복합 값 타입, Embedded)

예를 들어, 주소라는 데이터를 각각의 String값으로 city, street, zipcode로 구분해 Member클래스에 3개의 값 타입으로 넣는다면, 필요한 것은 주소 데이터 하나인데 변수가 3개나 늘어나게 된다. 클래스는 변수가 많아질수록 해석하기 어렵고 복잡하게 느껴진다. 

이렇게 city, street, zipcode처럼 모여서 하나의 값(주소)으로 만든 타입을 JPA에서는 '임베디드(Embedded) 타입'이라고 부른다. 임베디드 타입을 사용하면 알아보기도 쉽고, 응집력과 재사용성이 높게 디자인할 수 있다. (유지보수 측면에서도 탁월하다.)

- 컬렉션 값 타입

 자바의 Collection 값을 타입으로 쓰는 경우. (List, Set 등)

 

임베디드 타입

임베디드 타입 값이 null이라면 임베디드 타입에 들어있는 모든 값은 null이 된다. 그리고 임베디드 타입은 기본 생성자가 필수다! 

임베디드 타입을 사용할 때는 아래와 같이 크게 4가지 어노테이션을 사용한다. 

  • @Embeddable : 값 타입을 정의하는 곳에 표시 (임베디드 클래스)
  • @Embedded : 값 타입을 사용하는 곳에 표시 (필드 값)
  • @AttributeOverrides, @AttributeOverride : 표시될 칼럼명 재정의 할 때 사용

다음은 기본적인 임베디드 타입을 적용한 코드이다.

import javax.persistence.*;

@Embeddable //클래스에 사용
@Getter // Setter가 없는 이유는 곧 알게 된다.
public class Address {
    private String city;
    private String street;
    private String zipcode;

    protected Address(){//기본생성자 필수

    }
    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}
import javax.persistence.*;
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private long id;

    private String name;
    
    @Embedded // 필드에 사용
    private Address address;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}

 

이렇게 Address클래스와 Member클래스에 어노테이션을 달아주면, Address를 임베디드 타입으로 사용할 수 있다. 임베디드 타입의 생명주기는 소유한 엔티티의 생명주기를 의존하며, 이렇게 구성한 Address클래스에 주소 관련된 메소드를 작성하는 방식으로 응집도를 높일 수 있어, 책임의 분리가 가능하다.

 

그렇다면 하나의 엔티티에서 동일한 임베디드 변수를 여러 개 사용한다면 매핑은 어떻게 될까? 예를 들어 Member클래스가 HomeAddress와 WorkAddress가 Address타입으로 존재한다고 하면, DB에서는 무슨 기준으로 Home과 WorkAddress의 city 값을 구분하는가? 현재로써는 방법이 없어 보인다...

 

이런 모호한 상황을 방지하기 위해 AttributeOverride(s)어노테이션을 사용한다. 다음 코드를 보자.

import javax.persistence.*;
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private long id;

    private String name;
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "home_city")),
            @AttributeOverride(name = "street", column = @Column(name = "home_street")),
            @AttributeOverride(name = "zipcode", column = @Column(name = "home_zipcode"))
    })
    private Address homeAddress;

    @Embedded
    @AttributeOverrides({
            @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;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}

(임베디드 타입이 변수 하나로 이뤄져 있다면 @AttributeOverride 한 줄만 작성하면 된다.)

 

Address 클래스는 그대로이다. 조금 복잡해 보이지만 각 값의 별칭을 지어준 코드에 불과하다. 처음 본다고 겁먹지 말고 조금만 들여다보면 이해할 수 있을 것이다. 다음은 위의 코드가 실행되었을 때 생성되는 SQL문이다. 

 

이로써 각 city, street, zipcode를 DB에서도 구분할 수 있게 되었다.

 

값 타입과 불변 객체

코드를 유심히 보았다면 Address 클래스에 Setter어노테이션이 빠져있는 걸 보았을 것이다. 사실 주석도 달아놨다.

임베디드 타입은 Setter 어노테이션을 쓰지 않는 걸 권장한다고 한다. 그 이유는 무엇일까?

 

다시 값 타입을 생각해보자 기본 값 타입 int, double, String과 같은 값은 변경되면 그 자체 값이 변한다. 누가 그 값을 복사해 갔는데, 내 값을 변경한다고 복사해간 값이 변하지 않는다. (Call By Value) 

하지만 객체는 좀 다르다. 객체는 참조 값을 복사(Call By Reference) 하기 때문에, 어느 한쪽이 변경되면, 참조 주소가 변경되어 복사된 모든 객체에 영향을 미친다. 

 

어떻게 보면 별로 상관없을 수도 있다. 한 번 만든 객체를 재사용하지 않는다면 말이다. 그런데 만약에 변경하면 어떤 일이 일어나는가? 앞서 말했듯이 값 타입은 추적이 불가능하다. 뭐 어찌어찌 로그를 찾던지, DB서버를 롤백하던지, 방법은 있겠지만, 굳이 이런 상황이 생길 가능성을 안고 개발할 필요가 있을까?

 

코드에 근본적인 문제가 있다면, 그 문제가 발생하지 않도록 아예 가능성을 차단하는 게 좋다. 안일함은 항상 문제를 발생시키고 항상 예외는 생기기 마련이다. 임베디드 객체의 값을 변경할 수 없도록 set메소드를 잠가놓으면 불변한 객체로 사용할 수 있게 된다. (앞서 Setter어노테이션을 주석처리해놓은 이유이다.)

 

그렇다면 주소변경을 어떻게 적용하는가? set메소드를 잠가놓았을 뿐, 우리에겐 생성자라는 도구가 남아있다. 처음 주소 값을 넣을 때 Address객체를 만들 듯, Address newAdress = new Address(args...); 이렇게 새 주소가 담긴 객체를 생성하고 Member.setAddress(newAddress); member 객체에 지정해주면 된다. 

 

(Member의 Setter는 열려있다. 임베디드 객체 Address의 Setter만 잠갔다)

ps. Address클래스 보려고 스크롤 올릴 필요 없다!

import javax.persistence.*;

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

    protected Address(){

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

 

값 타입의 비교

Java에서는 크게 동일성 비교와 동등성 비교 두 가지가 있다. (instance of...)

동일성(identity) 비교 : 인스턴스의 참조 값을 비교, == 사용

동등성(equivalence) 비교 : 인스턴스의 값을 비교, equals(obj) 사용

 

값 타입은 인스턴스가 달라도 그 안의 이 같다면 같은 것으로 봐야 한다.

int a = 10; int b = 10;  a == b? True. 

임베디드 타입도 값 타입이기 때문에 이를 만족해야 한다. 객체를 따로생성하고 == 으로 비교한다면, 어김없이 False를 반환한다. 따라서 임베디드 타입이라면 equals() 메소드를 적절하게 재정의하여 같은 값을 갖는지 확인해야 한다. IDE의 Equals()와 Hashcode메소드 자동생성 기능을 활용하는게 좋다. (타이핑 하다가 본인도 모르게 String을 ==으로 비교할지도 모른다.)

 

import javax.persistence.Embeddable;

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

    protected Address() {

    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Address)) return false;
        Address address = (Address) o;
        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);
    }
}

 

값 타입 컬렉션

(추가예정입니다.)

 

728x90