DB/자바 ORM 표준 JPA 프로그래밍

[9장] 값 타입

꾸준함. 2025. 3. 4. 21:11

기본값 타입

  • 아래 예제의 Member에서 String, int가 값 타입
  • Member 엔티티는 id라는 식별자 값도 가지고 생명주기도 있지만
  • 값 타입인 name, age 속성은 식별자 값도 없고 생명주기도 회원 엔티티에 의존
    • 이에 따라 회원 엔티티 인스턴스를 제거하면 name, age 값도 제거됨

 

 

임베디드 타입 (복합 값 타입)

  • 임베디드 타입은 새로운 값 타입을 직접 정의해서 사용하는 것
  • 중요한 것은 직접 정의한 임베디드 타입 또한 int, String처럼 값 타입이라는 것

 

임베디드 타입 적용 전

  • 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 그리고 주소 우편 번호를 멤버 변수로 가짐

 

 

임베디드 타입 적용 후

  • 앞선 설명은 단순히 정보를 풀어둔 것뿐
  • 근무 시작일과 우편번호는 서로 아무 관련이 없으므로 다음처럼 회원 엔티티를 설명하는 것이 더 명확함
    • 회원 엔티티는 이름, 근무 기간, 집 주소를 가짐

 

  • 회원이 상세한 데이터를 그대로 가지고 있는 것은 객체지향적이지 않으며 응집력만 떨어뜨리므로 근무 기간과 주소 같은 타입이 있다면 코드가 더 명확해질 것

 

 

https://velog.io/@conatuseus/JPA-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%ED%83%80%EC%9E%85embedded-type-8ak3ygq8wo

 

부연 설명

  • startDate와 endDate를 합해서 Period 클래스를 생성했고
  • city, street, 그리고 zipCode를 합해서 Address 클래스를 생성
  • 회원 엔티티가 더욱 의미 있고 응집력 있게 변한 것을 알 수 있음
  • 임베디드 타입을 사용하려면 다음 두 가지 어노테이션이 필요함
    • @Embeddable: 값 타입을 정의하는 곳에 표시
    • @Embedded: 값 타입을 사용하는 곳에 표시

 

  • 임베디드 타입은 기본 생성자가 필수
  • 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하기 때문에 엔티티와 임베디드 타입의 관계를 UML로 표현하면 composition 관계

 

1. 임베디드 타입과 테이블 매핑

  • 임베디드 타입은 엔티티의 값일 뿐이기 때문에 값이 속한 엔티티의 테이블에 매핑됨
    • 앞선 예제에서 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같음

 

  • 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하면 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음
    • 테이블 칼럼과 객체 필드를 매핑하는 반복 작업은 JPA에 맡기고 더 세밀한 객체지향 모델을 설계하는 데 집중하는 것을 권장

 

https://s-y-130.tistory.com/283

 

2. 임베디드 타입과 연관관계

  • 임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있음
    • 엔티티는 공유될 수 있으므로 참조한다고 표현
    • 값 타입은 특정 주인에 소속되고 논리적인 개념상 공유되지 않으므로 포함한다고 표현

 

 

https://s-y-130.tistory.com/283

 

부연 설명

  • 값 타입인 Address가 값 타입인 Zipcode를 포함
  • 값 타입인 PhoneNumber가 엔티티 타입인 PhoneServiceProvider를 참조

 

3. @AttributeOverride: 속성 재정의

  • 임베디드 타입에 정의한 매핑 정보를 재정의하려면 엔티티에 @AttributeOverride를 사용하면 됨
  • 단, @AttributeOverride를 사용하면 어노테이션을 너무 많이 사용해서 엔티티 코드가 지저분해질 수 있음

 

 

값 타입과 불변 객체

  • 값 타입은 복잡한 객체 세상을 조금이라도 단순화하기 위해 만든 개념이기 때문에 값 타입은 단순하고 안전하게 다룰 수 있어야 함

 

1. 값 타입 공유 참조

  • 임베디드 타입 같은 값 타입을 여러 엔티티에 공유하면 위험함

 

 

https://s-y-130.tistory.com/283

 

 

부연 설명

  • 회원2에 새로운 주소를 할당하기 위해 회원1의 주소를 그대로 참조해서 사용했음
  • 회원1과 회원2가 같은 address 인스턴스를 참조하기 때문에 회원2의 주소만 "NewCity"로 변경되길 기대했지만 회원1의 주소도 "NewCity"로 변경됐음
    • 영속성 컨텍스트는 회원1과 회원2 둘 다 city 속성이 변경된 것으로 판단해서 회원1과 회원2 각각 UPDATE SQL을 실행함

 

  • 위와 같은 부작용을 막으려면 값을 복사해서 사용해야 함

 

2. 값 타입 복사

  • 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험한 행위이기 때문에 인스턴스를 복사해서 사용해야 함

 

 

https://myeongju00.tistory.com/80

 

부연 설명

  • 회원2에 새로운 주소를 할당하기 위해 clone() 메서드를 호출했으며 해당 메서드는 자신을 복사해서 반환하도록 구현됨
  • 회원1의 주소 인스턴스를 복사해서 사용했으므로 코드를 실행하면 의도한 대로 회원2의 주소만 "NewCity"로 변경됨

 

앞선 예제처럼 항상 값을 복사해서 사용할 경우 공유 참조로 인해 발생하는 부작용을 피할 수 있습니다.

하지만 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입 (Primitive Type)이 아니라 객체 타입이라는 것입니다.

  • 자바는 기본 타입에 값을 대입하면 값을 복사해서 전달
  • 반면, 객체에 값을 대입하면 항상 참조 값을 전달
  • 객체를 대입할 때마다 인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있지만 문제는 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다는 것
  • 정리하면 객체의 공유 참조는 피할 수 없으므로 객체의 값을 수정하지 못하도록 조치를 취해야 함

 

3. 불변 객체

  • 객체를 불변하게 생성하면 값을 수정할 수 없으므로 앞서 언급한 부작용을 원천 차단할 수 있음
  • 따라서 값 타입은 될 수 있으면 불변 객체 (Immutable Object)로 설계해야 함
    • 불변 객체는 한 번 생성되면 절대 변경할 수 없는 객체
    • 불변 객체의 값은 조회 가능하지만 수정할 수 없음
    • 불변 객체는 공유되더라도 인스턴스의 값을 수정할 수 없으므로 부작용이 발생하지 않음

 

불변 객체를 구현하는 다양한 방법이 있지만 가장 간단한 방법은 생성자로만 값을 설정하고 Setter와 같은 수정자를 구현하지 않으면 됩니다.

 

 

값 타입의 비교

  • 자바가 제공하는 객체 비교는 두 가지
    • 동일성 (Identity) 비교: 인스턴스의 참조 값을 비교, == 사용
    • 동등성 (Equivalence) 비교: 인스턴스의 값을 비교, equals() 사용

 

  • 값 타입은 비록 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 함
    • 이에 따라 값 타입을 비교할 때는 a.equals(b)를 사용해서 동등성 비교를 진행
    • 따라서 equals() 재정의가 필수
    • 값 타입의 equals() 메서드를 재정의할 때는 보통 모든 필드의 값을 비교하도록 구현
    • 해시를 사용하는 컬렉션이 정상 동작하도록 hashCode() 또한 재정의하는 것을 권장

 

값 타입의 컬렉션

값 타입을 하나 이상 저장하기 위해서는 컬렉션에 보관하고 @ElementCollection과 @CollectionTable 어노테이션을 사용하면 됩니다.

 

https://myeongju00.tistory.com/80

 

https://myeongju00.tistory.com/80

 

부연 설명

  • favoriteFoods는 기본값 타입인 String을 컬렉션으로 가지는데 관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수 없는 제약 조건 존재
    • 이에 따라 별도의 테이블을 추가하고 @CollectionTable을 사용해서 추가한 테이블을 매핑해야 함
    • favoriteFoods처럼 값으로 사용되는 컬럼이 하나면 @Column을 사용해서 컬럼명을 지정할 수 있음

 

  • addressHistory는 임베디드 타입인 Address를 컬렉션을 가지며 이것도 마찬가지로 별도의 테이블을 사용해야 함
    • 테이블 매핑정보는 @AttributeOverride를 사용해서 재정의 가능

 

1. 값 타입 컬렉션 사용

 

1.1 값 타입 컬렉션 저장

 

 

부연 설명

  • 코드 마지막에 member 엔티티만 영속화했는데 JPA는 이때 member 엔티티의 값 타입도 함께 저장함
  • 실제 데이터베이스에 실행되는 INSERT SQL은 다음과 같음
    • member: INSERT SQL 1번
    • member.homeAddress: 컬렉션이 아닌 임베디드 값 타입이므로 회원 테이블을 저장하는 SQL에 포함됨
    • member.favoriteFoods: INSERT SQL 3번
    • member.addressHistory: INSERT SQL 2번

 

1.2 값 타입 컬렉션 조회

값 타입 컬렉션도 조회할 때 fetch 전략을 선택할 수 있는데 default 값은 FetchType.LAZY입니다.

 

 

부연 설명

지연 로딩으로 모두 설정하고 위 예제를 실행할 때 호출하는 SELECT SQL은 다음과 같습니다.

  • member: 회원만 조회하며 이때 임베디드 값 타입인 homeAddress도 함께 조회, SELECT SQL을 1번 호출
  • member.homeAddress: 앞서 회원을 조회할 때 같이 조회
  • member.favoriteFoods: LAZY로 설정해서 실제 컬렉션을 사용할 때 SELECT SQL을 1번 호출
  • member.addressHistory: LAZY로 설정해서 실제 컬렉션을 사용할 때 SELECT SQL을 1번 호출

 

1.3 값 타입 컬렉션 수정

 

 

부연 설명

위 예제에서 값 타입 컬렉션을 수정하면 다음과 같이 동작합니다.

  • 임베디드 값 타입 수정: homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE, 사실상 Member 엔티티를 수정하는 것과 같음
  • 기본값 타입 컬렉션 수정: 자바의 String 타입은 Immutable 하기 때문에 탕수육을 치킨으로 변경하려면 탕수육을 제거하고 치킨을 추가해야 함
  • 임베디드 값 타입 컬렉션 수정: 값 타입은 불변해야 하므로 컬렉션에서 기존 주소를 삭제하고 새로운 주소를 등록

 

2. 값 타입 컬렉션의 제약사항

  • 엔티티는 식별자가 있으므로 엔티티의 값을 변경해도 식별자로 데이터베이스에 저장된 원본 데이터를 쉽게 찾아서 변경 가능
  • 반면, 값 타입은 식별자라는 개념이 없고 단순한 값들의 모음이므로 값을 변경해 버리면 데이터베이스에 저장된 원본 데이터를 조회하기 어려움
    • 값 타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관되는데 여기에 보관된 값 타입의 값이 변경될 경우 데이터베이스에 있는 원본 데이터를 찾기 어렵다는 문제가 있음
    • 이런 문제로 인해 JPA 구현체들은 값 타입 컬렉션에 변경 사항 발생 시 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장함
    • 이에 따라 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 너무 많아질 경우 값 타입 컬렉션 대신 일대다 관계를 고려해야 함

 

  • 추가로 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 하므로 데이터베이스 기본 키 제약 조건으로 인해 컬럼에 null을 입력할 수 없고, 같은 값을 중복해서 저장할 수 없는 제약도 있음

 

위 제약사항으로 인한 문제 해결을 위해, 앞서 정의한 Address 임베디드 타입을 새로운 엔티티로 전환하여 일대다 관계를 설정했습니다.

여기에 추가로 영속성 전이 (Cascade) 및 고아 객체 제거 (Orphan Removal) 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있습니다.


 

참고

자바 ORM 표준 JPA 프로그래밍 - 김영한 저

 

반응형