9장. 값 타입

  • JPA의 데이터 타입은 크게 엔티티 타입, 값 타입 두개로 분류 가능
  • 엔티티 타입
    • @Entity 로 정의하는 객체
    • 식별자 있음
      • 식별자로 구별하고 지속 추적 가능
    • 생명 주기가 있음
      • 생성, 영속화, 소멸하는 생명 주기 존재
    • 공유 가능
      • 참조 값 공유할 수 있음 => 공유 참조
  • 값 타입
    • int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입 또는 객체
    • 식별자가 없음
    • 생명 주기를 엔티티에 의존
    • 공유하지 않는 것이 안전
      • 대신 값 복사해서 사용
      • 오직 하나의 주인만이 관리해야 함
      • 불변 객체로 만드는 것이 안전함
    • 다시 3 가지로 나눌 수 있음
      • 기본값 타입: 자바 기본 데이터 타입
        • 자바 기본 타입 (int, double, …)
        • 래퍼 클래스 (ex. Integer)
        • String
      • 임베디드 타입: JPA에서 사용자가 직접 정의한 값 타입
      • 컬렉션 값 타입: 하나 이상의 값 타입 저장시 사용




9.1 기본값 타입

코드
@Entity
public class Member {
  @Id
  @GeneratedValue
  private Long id;

  private String name;

  private int age;

  ...
}
  • 위 예제의 String, int 가 값 타입
  • 식별자 값도 없으며 생명주기는 엔티티에 의존
  • 값 타입은 공유 X




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

  • JPA에서 새로운 값 타입 직접 정의해서 사용하는 것
  • 임베디드 타입도 int, String 처럼 값 타입임
코드
// Embedded type 사용하지 않은 경우
@Entity
public class MemberWithoutEmbeddedType {
  @Id
  @GeneratedValue
  private Long id;

  private String name;

  // 근무 기간
  @Temporal(TemporalType.DATE) 
  Date startDate;
  @Temporal(TemporalType.DATE)
  Date endDate;

  // 집 주소
  private String city;
  private String street;
  private String zipcode;

  ...
}

...

// 근무 기간, 집 주소 Embedded type 만들어서 사용한 경우
@Entity
public class MemberWithEmbeddedType {
    @Id
  @GeneratedValue
  private Long id;

  private String name;

  // 근무 기간
  @Embedded
  Period workPeroid;

  // 집 주소
  @Embedded
  Address homeArrdress;
  
  ...
}

...

// 기간 embedded type
@Embeddable
public class Period {
  @Temporal(TemporalType.DATE)
  Date startDate;
  
  @Temporal(TemporalType.DATE)
  Date startDate;

  ...

  public boolean isWork(Date date) {
    // 값 타입을 위한 메소드 정의 가능
  }

  ...
}

// 주소 embedded type
@Embeddable
public class Address {
  @Column(name="city")  // 매핑할 컬럼 정의 가능
  private String city;

private String street;

private String zipcode;

...
}

  • 임베디드 타입 사용시 다음 2가지 어노테이션 필요 (둘 중 하나는 생략 가능)
    • @Embeddable: 값 타입 정의하는 곳에 표시
    • @Embedded: 값 타입 사용하는 곳에 표시
  • 임베디드 타입은 기본 생성자가 필수



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

  • 임베디드 타입은 엔티티의 값일 뿐이기 때문에 값이 속한 엔티티의 테이블에 매핑됨



9.2.2 임베디드 타입과 연관관계

코드
@Entity
public class Member {
  @Embedded
  Address address;  // 임베디드 타입 포함

  @Embedded
  PhoneNumber phoneNumber;  // 임베디드 타입 포함

  ...
}

...

@Embeddable
public class Zipcode {
  String zip;

  String plusFour;
}

...

@Embeddable
public class PhoneNumber {
  String areaCode;

  String localNumber;

  @ManyToOne
  PhoneServiceProvider provider;  // 엔티티 참조

  ...
}

...

@Entity
public class PhoneServiceProvider {
  @Id
  String name;

  ...
}

  • 임베디드 타입은 값 타입을 포함하거나 엔티티 참조 가능
  • 위 예제의 경우 값 타입 Address가 값 타입 Zipcode 포함, 값 타입 PhoneNumber가 엔티티 타입인 PhoneServiceProvider를 참조



9.2.3 @AttributeOverride: 속성 재정의

  • @AttributeOverride 를 사용하여 임베디드 타입에 정의한 매핑정보 재정의 가능
코드
@Entity
public class Member {
  @Id
  private Long id;
  private String name;

  @Embedded 
  Address homeAddress;

  @Embedded
  @AttributeOverrides({
    @AttributeOverride(name = "city", column = @Column(name = "company_city")),
    @AttributeOverride(name = "street", column = @Column(name = "company_street")),
    @AttributeOverride(name = "zipcode", column = @Column(name = "company_zipcode"))
  })
  Address companyAddress;
}
sql
CREATE table member {
  ...

  city VARCHAR(255),
  street VARCHAR(255),
  zipcode VARCHAR(255),
  company_city VARCHAR(255),
  company_street VARCHAR(255),
  company_zipcode VARCHAR(255),

  ...
}



9.2.4 임베디드 타입과 null

  • 임베디드 타입이 null이면 매핑한 컬럼 값 모두 null 됨




9.3 값 타입과 불변 객체



9.3.1 값 타입 공유 참조

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함
  • 공유 참조로 인한 side effect 발생할 수 있으므로 값을 복사해서 사용해야 함



9.3.2 값 타입 복사

  • 자바에서 기본 타입에 값 대입하면 알아서 값을 복사 전달하지만 객체는 참조값을 전달함
  • 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하므로 대신 인스턴스를 복사하여 사용
  • 복사해서 쓰도록 해도 공유 참조 자체를 막을 수는 없기 때문에 세터 빼서 수정 못하게 막는게 좋음



9.3.3 불변 객체

  • 객체 값 수정 못하게 하면 사이드 이펙트 차단 가능
  • ∴ 값 타입은 될 수 있으면 immutable object 로 설계해야 함
    • ex) 생성자로만 값 설정, 세터 X
  • Integer, String 은 자바가 제공하는 대표적 불변 객체




9.4 값 타입의 비교

  • 자바가 제공하는 객체 비교는 다음 2가지
    • 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
    • 동등성(equivalnce) 비교: 인스턴스의 값을 비교, equals() 사용
  • 값 타입 비교시에는 equals() 를 이용하여 동등성 비교 해야 함
  • 직접 값 타입 정의할 경우 equals() 메소드 재정의해줘야 함
    • equals() 재정의 시 hashCode() 도 재정의 해야함
    • 안하면 컬렉션 정상 동작 안함




9.5 값 타입 컬렉션

  • 값 타입을 하나 이상 저장하려면 컬렉션에 넣고 @ElementCollection, @CollectionTable 어노테이션을 사용
코드
@Entity
public class Member {
  @Id
  @GeneratedValue
  private Long id;

  @Embedded
  private Address homeAddress;

  @ElementCollection
  @CollectionTable(name = "favorite_foods",
    joinColumns = @JoinColumn(name = "member_id"))
  @Column(name = "food_name")
  private Set<String> favoriteFoods = new HashSet<String>();

  @ElementCollection
  @CollectionTable(name = "address",
    joinColumns = @JoinColumn(name = "member_id"))
  private List<Address> addressHistory = new ArrayList<Address>();

  ...
}

@Embedaable
public class Adress {
  @Column
  private String city;

  private String street;

  private String zipcode;

  ...
}

  • favoriteFoods처럼 값으로 사용되는 컬럼 하나면 @Column 을 사용해서 컬럼명 지정 가능
  • @CollectionTable 생략시 기본값으로 매핑됨
    • default: {엔티티이름}_{컬렉션 속성 이름}
    • ex) Member 엔티티의 addressHistory 는 Member_addressHitory 테이블과 매핑



9.5.1 값 타입 컬렉션 사용

코드
Member member = new Member();

// 임베디드 값 타입
member.setHomeAddress(new Address("a", "A", "1"));

// 기본값 타입 컬렉션
member.getFavoriteFoods().add("Q");
member.getFavoriteFoods().add("W");
member.getFavoriteFoods().add("E");

// 임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("b", "B", "2"));
member.getAddressHistory().add(new Address("c", "C", "3"));

em.persist(member);
  • member 엔티티 영속화할 때 member 엔티티의 값 타입들 함께 저장됨
    • member: INSERT 쿼리 1번
    • member.homeAddress: 임베디드 값 타입이라 member 저장 쿼리에 포함
    • member.favoriteFoods: INSERT 쿼리 3번
    • member.addressHistory: INSERT 쿼리 2번
    • 총 INSERT 6번 실행됨
  • 값 타입 컬렉션도 조회할 때 fetch 전략 선택 가능(LAZY가 디폴트임)
    • @ElementColection(fetch = FEtchType.LAZY)
    • member: 회원만 조회함. SELECT 쿼리 1번
    • member.homeAddress: member 조회할 때 같이 조회됨
    • member.favoriteFoods: LAZY로 설정되어 실제 컬렉션 사용시 SELECT 쿼리 1번 호출
    • member.addressHistory: LAZY로 설정되어 실제 컬렉션 사용시 SELECT 쿼리 1번 호출
코드
Member member = em.find(Member.class, 1L);

// 임베디드 값 타입 수정
member.setHomeAddress(new Address("new a", "NEW A", "1-1"));

// 기본값 타입 컬렉션 수정
Set<String> favoriteFoods = member.getFoavriteFoods();
member.getFavoriteFoods().remove("Q");
member.getFavoriteFoods().add("W");

// 임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = member.getAddressHistory();
member.getAddressHistory().remove(new Address("b", "B", "2"));
member.getAddressHistory().add(new Address("new b", "new B", "2-2"));
  • 임베디드 값 타입 수정: homeAddress 임베디드 값 타입은 member 테이블과 매핑되었으므로 member 테이블만 UPDATE 쿼리 수행
  • 기본값 타입 컬렉션 수정: 자바의 String 타입은 수정 불가이므로 제거하고 새로 추가해야 함
  • 임베드다 값 타입 컬렉션 수정: 값 타입은 불변해야 하므로 기존거 삭제하고 새로 등록



9.5.2 값 타입 컬렉션의 제약사항

  • 특정 엔티티 하나에 소속된 값 타입은 변경되도 자신이 속한 텐티티를 DB에서 찾아 값 바꾸면 됨
    • 값 타입 컬렉션에 보관된 값 타입들의 경우에는 별도의 테이블에 보관되므로 변경시 원본 데이터 찾기 어려움
      • ∴ JPA 구현체들은 값 타입 컬렉션에 변경 사항 발생하면 컬렉션이 매핑된 테이블의 연관된 모든 데이터 삭제 후 현재 들어있는 값 새로 DB에 저장함
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼 묶어서 pk 구성해야 함
    • pk 제약조건으로 nullable 하게 사용 못하고 같은 값 중복 저장도 안됨
  • 위 문제들 해결하려면 값 타입 컬렉션 쓰는 대신 새 엔티티 만들어서 1:N 관계로 사용하면 됨
    • 영속성 전이 + 고아 객체 제거 기능 적용하면 값 타입 컬렉션처럼 사용할 수 있음