Post

Springboot Jpa - 값 타입

JPA에서는 어떤 값 타입을 다룰까?

Springboot Jpa - 값 타입

기본값 타입

JPA의 데이터 타입 분류

엔티티 타입
@Entity로 정의하는 객체
데이터가 변해도 식별자로 지속해서 추적 가능 회원 엔티티의 키나 나이 값을 변경해도, 식별자(=키값)으로 인식 가능
값 타입
int, Integer, String 등과 같이 단순히 값으로 사용하는 자바 기본 타입이나 객체
식별자가 없고 값만 있으므로, 변경시 추적 불가
기본값 타입(double, Integer, String 등), 임베디드 타입(복합 값 타입), 컬렉션 값 타입 이 있다.

기본값 타입

기본값 타입
double, int와 같은 기본 자료형이나 Integer, Boolean 등의 Wrapper Class, String이 해당된다.
생명 주기를 엔티티에 의존한다. 회원 삭제시, 해당 회원에 나이, 이름, 주소 등의 필드도 같이 삭제된다.
값 타입은 공유하면 안된다. 회원 이름을 변경시, 다른 회원의 이름도 함께 변경되면 안된다.
기본 자료형은 값이 복사되니 안전하고, Wrapper 클래스라서 참조값을 통해 접근하지만 값이 변경되지 않기때문에 안전하다.

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

임베디드 타입
새로운 값 타입을 직접 정의할 수 있다.
주로 기본값 타입을 모아서 만들어서 복합 값 타입이라고 한다.
기본값 타입을 모아서, 추상화 된 타입을 만드는 것이다.

임베디드 사용하기 전 후

임베디드 사용한 후


LocalDateTime startDate(근무 시작일), LocalDateTime endDateTime(근무 종료일)의 데이터를 추상화해 Period workPeriod(근무 기간)을 만들어서 사용하는 것을 말한다.

장점
재사용
높은 응집도
Period.isWork()와 같이 해당 값 타입만 사용하는 의미 있는 메소드를만들 수 있다.
임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존한다.

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

임베디드 사용법
@Embeddable 값 타입을 정의하는 곳에 표시
@Embedded 값 타입을 사용하는 곳에 표시
기본 생성자 필수로 필요하다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
public class Member{
  @Id @GeneratedValue 
  @Column(name="MEMBER_ID")
  private Long id;
  @Column(name="USERNAME")
  private String name;
  
  private LocalDateTime workStartDate;
  private LocalDateTime workEndDate;
  
  private String city;
  private String street;
  private String zipcode;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Entity
public class Member{
  @Id @GeneratedValue 
  @Column(name="MEMBER_ID")
  private Long id;
  @Column(name="USERNAME")
  private String name;
  
  @Embedded
  private Period workPeriod;
  
  @Embedded
  private Address homeAddress;  
}
@Entity
public class Period{
  private LocalDateTime startDate;
  private LocalDateTime endDate;
  
  public boolean isWork() {
    // 오늘 날짜와 startDate, endDate를 비교해
    // 재직중인지에 대해 리턴
  }
}

특징
임베디드 타입은 엔티티의 값일 뿐이다.
임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능하다.
잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.

임베디드 타입과 연관관계

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

PhoneNumber는 임베디드 값 타입이지만, Phone Entity라는 entity를 가지고 있을 수 있다.

만약 Address 타입의 값이 2개여야한다면(회사주소, 집주소), @AttributeOverrides@AttributeOverride를 사용해 컬럼명을 재정의 할 수 있다.

값 타입과 불변 객체

값 타입의 인스턴스를 공유

값 타입의 인스턴스를 복사해서 사용


1
2
3
4
5
int a = 10;
int b = a; // 기본 타입은 값을 복사
b = 4;

//결과 a = 10, b = 4 
1
2
3
4
5
Address a = new Address("서울"); 
Address b = a;// 객체 타입은 참조를 전달
b.setCity("부산");

//결과 a = 부산, b = 부산 

임베디드 타입과 같이 직접 정의한 값 타입은 자바에서 기본 타입이 아닌 객체 타입이다. 객체 타입의 값을 그냥 공유하게 된다면, 참조값이 공유되는 것이라 실제 인스턴스는 1개인 상황으로 한 곳에서만 데이터를 변경하게 되면 여러 곳에서 반영이 된다. 그래서 참조값이 아닌 실제 값을 공유해야하고, 실수를 방지하기 위해 참조값을 공유한 것을 컴파일 단계 알아차릴 수 있도록해야하는데, 문법적으로는 문제가 되지 않아 방지하기 어렵다.

이를 컴파일 단계에서 알기 위해서는 객체 타입인 값은 불변 객체로 만들어 값의 변경이 되지 않도록 막고, 값을 복사해서 사용해야 하는 경우에는 또 다른 새로운 객체를 만들어 사용하도록 한다.

객체 타입의 한계
임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아닌 객체 타입으로, 공유 참조는 피할 수 없다.
불변 객체
생성 시점 이후 절대 값을 변경할 수 없는 객체다.
객체 타입을 수정할 수 없게 만들어 공유 참조의 문제를 해결한다.
Integer, String은 자바가 제공하는 대표적인 불변 객체다.
생성자로만 값을 설정하고, 수정자(Setter)를 만들지 않거나 private로 설정하는 등의 여러가지 방법이 있다.

값 타입의 비교

값 타입을 비교할 때 == 연산자를 통해 비교를 한다면, 참조값의 비교가 되기 때문에 equals()를 사용해서 비교해야한다. 그래서 임베디드와 같은 타입을 정의 할 때에는 hashCode()equals()를 재정의 해야한다.

값 타입 컬렉션

값 타입 컬렉션

값 타입 컬렉션이란, 값 타입을 하나 이상 저장할 때 사용하는 것으로 말 그대로 값 타입을 컬렉션으로 저장한 것을 의미한다. 값 타입 컬렉션을 지정할 때에는 @ElementCollection, @CollectionTable 사용해 지정한다. Member가 있을 때, 좋아하는 음식들 이라는 데이터는 Set<String> favoriteFood, 주소 이력 이라고 한다면 List<Address> addressHistory라고 저장할 것이다. 이러한 것을 값 타입 컬렉션이라고 한다.

값 타입 컬렉션을 가지고 있는 객체

객체에 대한 DB 구조


그런데 DB에서는 한 테이블(=Member)에 이런 컬렉션들을 담을 수 있는 구조가 없고, value만 넣을 수 있다. 그래서 매핑 구조를 위와 같이 변경해 컬렉션에 대한 내용들을 테이블로 따로 분리해야한다. 이때, entity와 달리 이 테이블은 값 타입의 테이블이기 때문에, 모든 값을 다 묶어 PK를 만들어야한다.

실제 코드에 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
public class Member{
  @Id @GeneratedValue 
  @Column(name="MEMBER_ID")
  private Long id;
  @Column(name="USERNAME")
  private String name;

  @ElementCollection
  @CollectionTable(name ="FAVORITE_FOOD", joinColumn = @JoinColumn(name = "MEMBER_ID"))
  @Column(name = "FOOD_NAME")
  private Set<String> favoriteFoods;

  @ElementCollection
  @CollectionTable(name ="ADDRESS", joinColumn = @JoinColumn(name = "MEMBER_ID"))
  private List<Address> addressHistory;  
}
  • 9~12 좋아하는 음식에 대해 값 타입 컬렉션을 지정한다.
  • 11 단순 String의 컬렉션이라, 컬럼명이 지정되지 않았는데 이때 컬럼명을 지정한 것이다.
  • 14~16 주소 이력에 대한 값 타입 컬렉션을 지정한다.

특징과 제약사항, 대안

특징
값 타입 컬렉션은 지연 로딩으로 조회된다.
값 타입 컬렉션의 생명주기는, 소속된 entity에게 의존한다.
엔티티와 다르게 식별자 개념이 없다. -> 모든 컬럼을 묶어서 기본키를 구성해야해서 null입력이나, 중복 저장이 안된다.
값 타입은 불변 객체여야 하기 때문에, 값 타입 컬렉션에 변경 사항이 발생시 entity와 연관된 모든 데이터를 삭제하고 다시 저장한다.
1
2
3
// member.addressHistory에 서울이라는 데이터가 있다고 가정한다.
member.getAddressHistory().remove(new ("서울"));
member.getAddressHistory().add(new ("부산"));

값타입 컬렉션의 내용을 수정하기 위해서는 수정할 값을 아예 값을 삭제하고, 새로운 값을 넣어야한다. 이렇게 수정하면 ADDRESS에 있는 member와 연관 되어있던 데이터를 전부 지우고, 새롭게 추가된 값인 부산을 추가한다.

컬렉션에서는 값을 지우거나 추가할 때, equals()를 기본으로 사용한다.

대안
실무에서는 상황에 따라 값 타입 컬렉션 대신에 1:N 관계를 고려한다.
This post is licensed under CC BY 4.0 by the author.