본문 바로가기
JPA/JPA

[JPA] 값 타입

by 걸어가는 신사 2022. 1. 4.

JPA의 데이터 타입

(1) 엔티티 타입

  • @Entity로 정의하는 객체
  • 데이터가 변해도 식별자로 지속해서 추적 가능하다.
  • ex) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능

(2) 값 타입

  • int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
  • 식별자가 없고 값만 있으므로 변경 시 추적 불가능하다.
  • ex) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
  • 값 타입의 종류
    • 기본값 타입 (basic value type)
      • 자바 기본 타입 (ex. int, double)
      • Wrapper Class (ex. Integer)
      • String
    • 임베디드 타입 (embedded type)
    • 컬렉션 값 타입 (collection value type)

 

1. 기본값 타입

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;
    private int age;
}
  • Member에서 String, int가 값 타입이다.
  • Member 엔티티는 id라는 식별자 값도 가지고 생명주기도 있다.
  • 값 타입인 name, age 속성은 식별자 값도 없고 생명주기도 회원 엔티티에 의존한다.
    • 회원 엔티티 인스턴스를 제거하면 name, age 값도 제거된다.
  • 값 타입은 공유하면 안된다.
    • 회원 이름 변경 시 다른 회원의 이름도 함께 변경되면 안 된다. 
자바에서 int, double 같은 기본 타입(primitive type)은 절대 공유되지 않는다.
기본 타입은 항상 값을 복사한다.
Wrapper 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경할 수 없다.

 

2. 임베디드 타입(embedded type, 복합 값 타입)

  • 새로운 값 타입을 직접 정의할 수 있다.
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.

임베디드 타입 사용 전 후

//임베디드 타입 사용 전
@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    //근무 기간
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    //집 주소
    private String city;
    private String street;
    private String zipcode;
}
//임베디드 타입 사용 후
@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    //근무기간
    @Embedded
    private Period workPeriod;

    //집 주소
    @Embedded
    private Address homeAddress;
}
@Embeddable
@Getter @Setter
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;
}
@Embeddable
@Getter @Setter
public class Address {
    private String city;
    private String street;
    private String zipcode;
}

 

  • startDate, endDate를 합해서 Period 클래스를 만들었다.
  • city, street, zipcode를 합해서 Address 클래스를 만들었다.
  • 새로 정의한 값 타입들은 재사용할 수 있고 응집도도 아주 높다.

 

 

  • @Embeddable :  값 타입을 정의하는 곳에 표시
  • @Embedded :  값 타입을 사용하는 곳에 표시
  • 임베디드 타입은 기본 생성자가 필수이다.
  • 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 타입의 관계를 UML로 표현하면 Composition 관계가 된다.

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

회원-테이블 매핑

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

(2) 속성 재정의

  • 한 엔티티에 같은 값 타입을 사용하게 된다면 컬럼 명이 중복된다.
  • @AttributeOverrides, @AttributeOverride를 사용해서 컬럼 명 속성을 재정의 해야 한다.
@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded
    private Address homeAddress;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name="city", column = @Column(name = "WORK_CITY")),
            @AttributeOverride(name="street", column = @Column(name = "WORK_STREET")),
            @AttributeOverride(name="zipcode", column = @Column(name = "WORK_ZIPCODE"))
    })
    private Address workAddress;
}

(3) 임베디드 타입과 null

  • 임베디드 타입 null이면 매핑한 컬럼 값은 모두 null이 된다.
member.setAddress(null);
em.persist(member);
  • CITY, STREET, ZIPCODE 컬럼 값은 모두 null이 된다.

 

3. 값 타입과 불변 객체

(1) 값 타입 공유 참조

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

값 타입 공유 참조

Address address = new Address("city", "street", "100");
Member member = new Member();
member.setHomeAddress(address);

Member member2 = new Member();
member2.setHomeAddress(address);

member.getHomeAddress().setCity("newCity"); //member, member2 모두 newCity로 바뀐다
  • 회원1, 회원2 모두 "newCity"로 변경된다.
  • 회원1, 회원2 모두 address 인스턴스를 참조하기 때문이다.
  • 전형 예상치 못한 곳에서 문제가 발생하는 것을 부작용(Side Effect)이라 한다.

(2) 값 타입 복사

  • 실제 인스턴스인 값을 공유하는 것 대신 값(인스턴스)을 복사해서 사용해야 한다.

값 타입 복사

Address address = new Address("city", "street", "100");
Member member = new Member();
member.setHomeAddress(address);

Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member member2 = new Member();
member2.setHomeAddress(copyAddress);

member.getHomeAddress().setCity("newCity");
  • 회원1의 address 값을 복사해서 새로운 copyAddress 값을 생성하였다.
  • 회원1만 newCity로 변경되는 update 쿼리가 나간다.
  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.

(3) 불변 객체

생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 객체 타입을 수정할 수 없게 만들려면 부작용을 원천 차단할 수 있다.
  • 값 타입은 불변 객체(immutable object)로 설계해야 한다.
  • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않는다.
  • Integer, String은 자바가 제공하는 대표적인 불변 객체
@Embeddable
@Getter //Getter만 열어준다.
public class Address {
    private String city;
    private String street;
    private String zipcode;

    public Address() {} //JPA에서 기본 생성자는 필수이다.

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}

 

4. 값 타입 컬렉션

  • 값 타입을 하나 이상 저장할 때 사용한다.
  • @ElementCollection, @CollectionTable 사용한다.
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

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

    @ElementCollection
    @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<>();
}
  • 값 타입 컬렉션을 사용하는 멤버 변수에 @ElementCollection을 지정해 주어야 한다.
  • @CollectionTable을 사용해서 추가한 테이블을 매핑해야 한다.

(1) 값 타입 컬렉션 저장

Member member = new Member();

member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");

member.getAddressHistory().add(new Address("old1", "street", "100"));
member.getAddressHistory().add(new Address("old2", "street", "100"));

em.persist(member); //컬렉션 또한 값타입의 하나이다 값타입의 생명주기는 Member에 의존한다.
  • 마지막에 member 엔티티만 영속화했다.
  • JPA는 이때 member 엔티티의 값 타입도 함께 저장한다.
    • member : INSERT SQL 1번
    • member.favoriteFoods : INSERT SQL 3번
    • member.addressHistory : INSERT SQL 2번
값 타입 컬렉션은 영속성 전이(Cascade)+고아 객체 제거(ORPHAN REMOVE) 기능을 가지고 있다고 볼 수 있다.

(2) 값 타입 컬렉션 조회

//조회
Member findMember = em.find(Member.class, member.getId());
//컬렉션 값타입들은 지연로딩이 기본값이다.

List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
System.out.println(address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println(favoriteFood);
}
  • 값 타입 컬렉션은 지연 로딩이 기본값이다.

(3) 값 타입 컬렉션 수정

// 기본값 타입 컬렉션 수정
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");

//임베디드 값 타입 컬렉션 수정
findMember.getAddressHistory().remove(new AddressEntity("old1", "street", "100")); 
//전체 테이블 삭제
findMember.getAddressHistory().add(new AddressEntity("newCity1", "street", "100"));
//insert 두번이 날라간다, old2, newCity 두번 날라간다.
  • 기본값 타입 컬렉션 수정
    • 자바의 String 타입은 수정할 수 없으므로 "치킨"을 제거하고 "한식"을 추가해야 한다.
  • 임베디드 값 타입 컬렉션 수정
    • 값 타입은 불변해야 한다. 따라서 컬렉션에서 기존 주소를 삭제하고 새로운 주소를 등록해야 한다.

(4) 값 타입 컬렉션의 제약사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다.
    • null 입력 X, 중복 저장 X
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
  • 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 단방향 관계를 고려해야 한다.
//일대다 단방향
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();

 

반응형

'JPA > JPA' 카테고리의 다른 글

[JPA] JPQL - 조인(Join)  (0) 2022.01.07
[JPA] JPQL - 기본 문법  (0) 2022.01.07
[JPA] 영속성 전이(CASCADE)와 고아 객체 제거  (0) 2022.01.03
[JPA] 즉시 로딩과 지연 로딩  (0) 2022.01.03
[JPA] 프록시 (Proxy)  (0) 2022.01.03

댓글