DEV ℧ Developer Diary

[JPA] 값 타입(1)

해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.

값 타입(1)

기본값 타입

JPA의 데이터 타입 분류

  • 엔티티 타입
    • @Entity로 정의하는 객체
    • 데이터가 변해도 식별자로 지속해서 추적 가능
    • 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
  • 값 타입
    • int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경시 추적 불가
    • 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체

값 타입 분류

  • 기본값 타입
    • 자바 기본 타입(int, double)
    • 래퍼 클래스(Integer, Long)
    • String
  • 임베디드 타입(embedded type, 복합 값 타입)
    • 예) x, y 좌표를 넣은 Point class
  • 컬렉션 값 타입(collection value type)

기본값 타입

  • 예): String name, int age
  • 생명주기를 엔티티의 의존
    • 예) 회원을 삭제하면 이름, 나이 필드도 함께 삭제
  • 값 타입은 공유하면 안된다.
    • 예) 회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안됨 사이드 이펙트가 일어날 수 있음.

참고: 자바의 기본 타입은 절대 공유 하지 않는다.

  • int, double 같은 기본 타입(primitive type)은 절대 주소를 공유하지 않으므로 복사를해도 별개의 변수
  • 기본 타입은 항상 값을 복사함
  • Integer같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체(주소값을 복사하므로)이지만 변경X

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

임베디드 타입

  • 새로운 값 타입을 직접 정의할 수 있음
  • JPA는 임베디드 타입(embedded type)이라 함
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
  • int, String과 같은 값 타입

임베디드 타입

  • 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다.

임베디드 타입1

위의 예제는 근무 시작일, 근무 종료일을 근무 관련 객체로, 주소 도시, 주소 번지, 주소 우편번호 관련 필드를 주소 관련 객체로 분리할 수 있다.

각각 필드들을 분리해주면 아래와 같이 설계할 수 있다.

  • 회원 엔티티는 이름, 근무 기간, 집 주소를 가진다

임베디드 타입3

임베디드 타입2

위의 설계를 JPA에서는 임베디드 타입이라는 방식으로 사용 할 수 있다.

임베디드 타입 사용법

  • @Embeddable: 값 타입을 정의하는 곳에 표시
  • @Embedded: 값 타입을 사용하는 곳에 표시
  • 기본 생성자 필수

임베디드 타입의 장점

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

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

임베디드 타입4

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

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

임베디드 타입 예시

간단한 Member Entity를 만들어서 예시를 들어보자.

@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private long id;

    @Column(name = "USERNAME")
    private String username;

    private LocalDateTime startDate;
    private LocalDateTime endDate;

    private String city;
    private String street;
    private String zipcode;

}

위의 Entity를 DB로 만들면 아래와 같다.

임베디드 타입5

이제 위의 Member Entity의 공통되는 필드들을 각각 묶어서 임베디드 타입을 만들어 보자.

먼저 공통되는 필드들 city, street, zipcode는 Address라는 객체로 묶고, startDate, endDate는 Period라는 객체로 묶어서 사용할 것이다.

임베디드 타입으로 사용하기 위해 생성한 객체는 @Embeddable 어노테이션을 추가해야 한다.

@Embeddable
@Getter @Setter
@NoArgsConstructor
public class Address {
    private String city;
    private String street;
    private String zipcode;
}
@Embeddable
@Getter @Setter
@NoArgsConstructor
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;
}

공통으로 빼준 필드를 대신해 임베디드 타입을 넣어준다. 대신 들어간 임베디드 타입에는 @Embedded 어노테이션을 붙여줘야 한다.

@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private long id;

    @Column(name = "USERNAME")
    private String username;

    /* Period 타입으로 치환 */
    @Embedded
    private Period workPeriod;

    /* Address 타입으로 치환 */
    @Embedded
    private Address homeAddress;
}
create table Member (
     MEMBER_ID bigint not null,
     city varchar(255),
     street varchar(255),
     zipcode varchar(255),
     USERNAME varchar(255),
     endDate timestamp,
     startDate timestamp,
     primary key (MEMBER_ID)
)

해당 코드를 실행 시켜도 위와 동일한 테이블을 생성하는 것을 확인 할 수 있다.

테이블을 생성했으니, 데이터를 넣어보도록 하자.

Member member = new Member();
member.setUsername("mho");
member.setHomeAddress(new Address("city", "street", "zipcode"));
member.setWorkPeriod(new Period(LocalDateTime.now(), LocalDateTime.now()));
entityManager.persist(member);

위와 같이 다른 entity 데이터를 넣어주듯이 넣어주면 된다. 아래와 같은 결과를 확인할 수 있다.

임베디드 타입5

임베디드 타입과 연관관계

임베디드 타입으로 묶은 객체를 DB로 설계하면 아래와 같다.

임베디드 타입으로 들어간 객체에 Entity 객체가 들어가는 것도 가능하다.

임베디드 타입5

@AttributeOverride: 속성 재정의

  • 한 엔티티에서 같은 값 타입을 사용하면?

위의 같은 경우는 아래의 경우 찾아 볼수 있다.

@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private long id;

    @Column(name = "USERNAME")
    private String username;

    @Embedded
    private Address homeAddress;

    @Embedded
    private Address companyAddress;
}

해당 방법은 JPA에서 예외를 발생한다.

  • 컬럼 명이 중복됨
  • @AttributeOverrides, @AttributeOverride를 사용해서 컬러 명 속성을 재정의

위의 어노테이션을 사용하면 아래와 같이 변경 할 수있다.

@Entity
@Getter @Setter
public class Member {

  @Id @GeneratedValue
  @Column(name = "MEMBER_ID")
  private long id;

  @Column(name = "USERNAME")
  private String username;

  @Embedded
  private Address homeAddress;

  /* company를 위한 필드로 재정의 */
  @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"))
  })
  private Address companyAddress;
}

create문은 재정의한 컬럼 기준으로 새롭게 테이블을 생성한다.

create table Member (
    MEMBER_ID bigint not null,
    COMPANY_CITY varchar(255),
    COMPANY_STREET varchar(255),
    COMPANY_ZIPCODE varchar(255),
    city varchar(255),
    street varchar(255),
    zipcode varchar(255),
    USERNAME varchar(255),
    endDate timestamp,
    startDate timestamp,
    primary key (MEMBER_ID)
)

임베디드 타입과 null

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