Post

[강의] 자바 ORM 표준 JPA 프로그래밍 - 기본편 09 값 타입

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

김영한님의 인프런 강의(자바 ORM 표준 JPA 프로그래밍 - 기본편) 을 수강하면서 강의 내용을 일부 발췌해 요약한 글.

섹션 9 값 타입

JPA의 데이터 타입 분류는 다음과 같다,

  • 엔티티 타입
    • @Entity로 정의하는 객체
    • 데이터가 변해도 식별자로 지속해서 추적 가능
  • 값 타입
    • int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경시 추적 불가
    • 값 타입은 세가지로 분류된다.
      • 기본값 타입 : 자바 기본타입(int, double), 래퍼 클래서(Integer, Long), String
      • 임베디드 타입(embedded type, 복합 값 타입)
      • 컬렉션 값 타입(collection value type)

값 타입

기본값 타입

자바 기본타입(int, double), 래퍼 클래서(Integer, Long), String으로 생명주기를 엔티티에 의존한다. 예를 들어 엔티티인 회원을 삭제하면 기본값 타입인 이름, 나이 필드도 함께 삭제된다. 또한 값 타입은 부수효과가 일어나면 안되므로 절대 공유되면 안된다. 기본 타입은 항상 값을 복사하지만 Integer같은 래퍼 클래스나 String같은 특수 클래스는 참조 값을 복사하므로 공유 가능한 객체이다. 따라서 변경 자체를 불가능하게 만들어 Side Effect를 없앤다.

임베디드 타입

새로운 값 타입을 직접 정의할 수 있으며, 주로 기본 값 타입을 모아서 만드므로 복합 값 타입이라고도 한다. 예를 들어 회원의 주소 도시, 주소 번지, 주소 우편번호를 Address 타입으로 묶을 수 있다.

JPA에서는 @Embeddable(값 타입을 정의하는 곳에 표시), @Embedded(값 타입을 사용하는 곳에 표시)를 사용한다.

임베디드 타입은 재사용이 가능하며, 높은 응집도를 가진다. 해당 값 타입만 사용하는 의미 있는 메서드를 만들 수 있다는 장점이 있다. 임베디드 타입 역시 값 타입을 소유한 엔티티에 생명주기를 의존한다.

임베디드 타입을 사용하기 전후로 매핑하는 테이블은 같다 는 점을 주의하자. 임베디드 타입은 엔티티 값일 뿐이다. 임베디드 타입을 사용하면 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능해지며 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.

한 엔티티에서 같은 타입을 사용하는 경우에는 @AttributOverrides/@AttributOverride 를 사용해서 컬럼명 속성을 재정의할 수 있다. 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null이 된다.

값 타입과 불변 객체

값 타입은 복잡한 객체를 조금이라도 단순화하고자 만든 개념이므로 항상 단순하고 안전하게 다룰 수 있어야 한다. 그러나 임베디드 타입같은 경우, 값 타입을 여러 엔티티에서 공유하면 부작용이 발생할 가능성이 있고 매우 위험할 수 있다. 따라서 값 타입의 실제 인스턴스인 값을 공유하는 것이 아니라 값(인스턴스)를 복사해서 사용해야 하는데, 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입(primitive type)이 아닌 객체 타입이므로 참조 값을 직접 대입하는 것을 막을 방법이 없다.

해결 방법은 객체 타입을 수정할 수 없게 하는 것이다. 값타입은 생성 시점 이후 절대 값을 변경할 수 없는 불변 객체로 설계해야 하는데, 가장 쉬운 예시로는 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않도록 제약해 Side Effect를 막을 수 있다.

값 타입의 비교

값 타입은 인스턴스가 다르더라도 안에 값이 같으면 같은 것으로 본다. 예를 들어 int a = 10, int b = 10 이면 a == b 가 true 이다. 그러나 Address 객체의 값이 같은 두 인스턴스 a, b를 만들어 == 비교를 하면 참조값이 다르므로 false 이다.

인스턴스의 참조 값을 비교(==)하는 동일성(identity) 비교와 인스턴스의 값을 비교(equals())하는 동등성(equivalence) 비교 를 잘 구분하여야 한다.

값 타입의 경우 a.equals(b)를 사용해 동등성 비교를 해야 하며 equals() 메서드를 적절하게 재정의해야 한다.

값 타입 컬렉션

값 타입을 컬렉션에 담아 사용하는 것을 값 타입 컬렉션이라고 한다. 값 타입을 하나 이상 젖아할 때 사용하며 @ElementCollection, @CollectionTable 어노테이션을 사용해 매한다. 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없으므로 컬렉션을 저장하기 위한 별도의 테이블이 필요하다. 값 타입 컬렉션 또한 값 타입이므로 라이프 사이클이 객체에 의존한다. 즉, 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 할 수 있다. 또한 값 타입 컬렉션도 지연 로딩 전략을 사용한다. @ElementCollection의 fecth 기본값이 LAZY이다. 값 타입 수정을 원하는 경우 인스턴스 자체를 완전히 교체 해야 한다!

앞서 언급했듯이 값 타입은 엔티티와 다르게 식별자 개념이 없으므로 값을 변경하면 추적이 어렵다. 값 타입 컬렉션에 변경이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장하므로 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. (null 입력 X, 중복 저장 X.)

실무에서는 상황에 따라 값 타인 컬렉션 대신 일대다 관계를 고려하는 것이 좋다. 값 타입을 일대다 관계를 매핑하기 위한 엔티티로 래핑하여 영속성 전이와 고아 객체 제거를 사용해 값 타입 컬렉션처럼 사용한다. 추적할 필요도 없고 값이 바뀌어도 update할 필요가 없는 경우처럼 매우 단순한 경우만 값 타입 컬렉션을 사용하고 웬만하면 엔티티를 사용하는 것이 좋다. 즉, 값 타입은 정말 값 타입이라고 판단될 경우에만 사용한다!

값 타입 매핑 실전 예제

@Embeddable, @Embedded 사용해 Adress 클래스 만들기 setter는 priavate으로 막아주고 equals, HashCode 구현해 프록시를 고려 값 타입을 생성하면 다음의 예시와 같이 의미 있는 비즈니스 메소드를 만들 수 있다. 뿐만 아니라 각 컬럼의 길이나 제약, Validation 등의 규칙을 공통으로 관리하기 용이하다.

1
2
3
public String fullAddress() {
return getCity() + getStreet() + getZipcode();
}

실제 운영에서 모델링하는 경우에도 값 타입은 별도로 빼 주고 «Value Type» 으로 적어주면 깔끔하다.

This post is licensed under CC BY 4.0 by the author.