[JPA] Chapter 9. 값 타입

joyful·2021년 8월 14일
0

JPA

목록 보기
13/18
post-thumbnail

들어가기 앞서

이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.




9.1 기본값 타입

  • 자바가 제공하는 기본 데이터 타입
  • 공유하면 안 됨

💻 예시 - 기본값 타입

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    
    //값 타입
    private String name;
    private int age;
}
필드식별자생명주기
idOO
namex엔티티에 의존
agex엔티티에 의존

→ 엔티티 인스턴스 제거 시 name, age도 같이 제거 됨

💡 참고

래퍼 클래스나 String 타입은 객체지만 자바언어에서 기본 타입처럼 사용할 수 있게 지원하므로 기본값 타입으로 분류



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

  • 사용자가 직접 정의한 값
  • 기본값 타입과 같은 값 타입
  • 기본 생성자 필수

💻 기본 회원 엔티티

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    
    //근무 기간
    @Temporal(TemporalType.DATE)
    java.util.Date startDate;
    @Temporal(TemporalType.DATE)
    java.util.Date endDate;
    
    //집 주소 표현
    private String city;
    private String street;
    private String zipcode;
    
    ...
}
  • 객체지향적이지 않으며 응집력이 떨어짐

💻 임베디드 타입으로 변경

  • 컴포지션 관계 UML

    📕 컴포지션(Composition)

    다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 메소드를 호출하는 기법

/*
 ** 회원 엔티티
 */
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    
    //근무 기간
    @Embedded Period workPeriod;  //근무 기간
    @Embedded Address homAddress;  //집 주소
    
    ...
}
/*
 ** 임베디드 타입 - 기간
 */
@Embeddable
public class Period {

    @Temporal(TemporalType.DATE)
    java.util.Date startDate;
    java.util.Date endDate;
    //..
    
    public boolean isWork(Date date) {
        //.. 값 타입을 위한 메소드 정의
    }
}
/*
 ** 임베디드 타입 - 주소
 */
@Embeddable
public class Address {

    @Column(name="city")  //매핑할 컬럼 정의 가능
    private String city;
    private String street;
    private String zipcode;
    //..
}
  • 재사용이 가능하며 응집도도 매우 높음
  • 어노테이션
    • @Embeddable : 값 타입을 정의하는 곳에 표시
    • @Embedded : 값 타입을 사용하는 곳에 표시

💡 참고

하이버네이트는 임베디드 타입을 컴포넌트(component)라고 함

*컴포넌트 → 하나의 컴포넌트는 엔티티 참조가 아닌 value 타입으로서 영속화 되는 하나의 포함된 객체를 의미


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

  • 임베디드 타입 = 엔티티의 값
    → 값이 속한 엔티티의 테이블에 매핑
  • 객체와 테이블을 아주 세밀하게(fine-grained) 매핑 가능

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

  • 임베디드 타입은 값 타입을 포함하거나 엔티티 참조 가능

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

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

@Entity
public class Member {

    @Embedded
    Address address;  //임베디드 타입 포함
    @Embedded
    PhoneNumber phoneNumber;  //임베디드 타입 포함
    //...
}

@Embeddable
public class Address {
    String street;
    String city;
    String state;
    @Embedded
    Zipcode zipcode;  //임베디드 타입 포함
}

@Embeddable
public class PhoneNumber {
    String areaCode;
    String localNumber;
    @ManyToOne
    phoneServiceProvider provider;  //엔티티 참조
}

@Entity
public class PhonServiceProvider {
    @Id
    String name;
    ...
}

9.2.3 @AttributeOverride: 속성 재정의

  • 임베디드 타입에 정의한 매핑 정보를 재정의
    ex) 회원에게 주소가 하나 더 필요한 경우

💻 같은 임베디드 타입을 가지고 있는 회원

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    
    // 테이블에 매핑하는 컬럼명이 중복됨
    @Embedded
    Address homeAddress;
    @Embedded
    Address companyAddress;
}

💻 임베디드 타입 재정의

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;
    
    @Embedded
    Address homeAddress;
    
    @Embedded
    @AttributeOverride({
        @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;
}

✅ 주의

  • 어노테이션을 너무 많이 사용하므로 엔티티 코드가 지저분해짐
  • 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 설정해야 함

9.2.4 임베디드 타입과 null

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


9.3 값 타입과 불변 객체

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

9.3.1 값 타입 공유 참조

  • 임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유시 위험 존재

📊 값 타입 공유 참조

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity");  //회원1의 address 값을 공유해서 사용
member2.setHomeAddress(address);
  • 같은 인스턴스 참조
    • 영속성 컨텍스트가 회원1과 회원2 둘다 city 속성이 변경된 것으로 판단
      → 회원1, 회원2 각각 UPDATE SQL 실행

💡 부작용(side effect)

  • 무언가를 수정했는데 전혀 예상치 못한 곳에서 문제가 발생하는 것
  • 해결방법 → 값을 복사해서 사용

9.3.2 값 타입 복사

📊 값 타입 복사

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

//회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();

newAddress.setCity("NewCity");
member2.setHomeaddress(newAddress);
  • 영속성 컨텍스트가 회원2의 주소만 변경된 것으로 판단
    → 회원2에 대해서만 UPDATE SQL 실행

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 방지할 수 있음

✅ 주의

  • 직접 정의한 값 타입 = 객체 타입

    • 자바는 객체에 값을 대입하면 항상 참조값을 전달
    • 값을 대입하게 될 경우 같은 인스턴스를 공유 참조하게 됨
      → 공유 참조로 인한 부작용 발생
  • 문제점

    • 인스턴스를 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 존재하지 않음
      → 자바는 대입하려는 것이 값 타입인지 아닌지 신경쓰지 않기 때문
  • 해결 방법

    • 객체의 값을 수정하지 못하게 막기 ex) 수정자 메소드 모두 제거
      → 공유 참조를 해도 값을 변경하지 못하므로 부작용의 발생 방지

9.3.3 불변 객체

한 번 만들면 절대 변경할 수 없는 객체

  • 값 타입은 될 수 있으면 불변 객체(immutable Object)로 설계해야 함
    • 객체를 불변하게 만들면 값을 수정할 수 없음
      → 부작용 원천 차단 가능
  • 불변 객체의 값 → 조회 O, 수정 X
    • 인스턴스의 참조 값 공유를 피할 순 없음
    • 인스턴스의 값을 수정할 수 없으므로 부작용이 발생하지 않음
  • 방법 : 생성자로만 값을 설정하고 수정자를 만들지 않음

💻 불변 객체 예시

/*
 ** 주소 불변 객체
 */
@Embbeddable
public class Address {

    private String city;
    
    protected Address() {}  //JPA에서 기본 생성자는 필수
    
    //생성자로 초기 값 설정
    public Address(String city) {
        this.city = city;
    }
    
    //접근자(Getter) 노출
    public string getCity() {
        return city;
    }
    
    //수정자 생성 x
}
/*
 ** 불변 객체 사용
 */
Address address = member1.getHomeAddress();
//회원1의 주소값을 조회해서 새로운 주소값을 생성
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);
  • 값 수정시 새로운 객체 생성해서 사용


9.4 값 타입의 비교

  • 자바가 제공하는 객체 비교

    비교대상사용
    동일성
    (Identity)
    인스턴스의 참조 값==
    동등성
    (Equivalence)
    인스턴스의 값equals()
  • 값 타입 → a.equals(b)를 사용한 동등성 비교

  • 값 타입의 equals() 메소드 재정의 필요
    → 모든 필드의 값을 비교하도록 구현

💡 참고

  • 자바에서 equals()를 재정의할 때 hashCode()도 같이 재정의하는 것이 안전함
    → 해시를 사용하는 컬렉션(HashSet, HashMap)의 정상 작동을 위함
  • 자바 IDE에는 대부분 equals, hashCode 메소드를 자동으로 생성해주는 기능 존재


9.5 값 타입 컬렉션

  • 값 타입을 하나 이상 저장할 경우 컬렉션에 보관

  • 단순한 자료를 저장하거나 추적이 필요 없을 경우 사용

  • @ElementCollection

    • 컬렉션 객체임을 JPA에게 알려주는 어노테이션
    • 연관된 부모 Entity 하나에만 연관되어 관리
      (부모 Entity와 독립적으로 사용x)
    • 항상 부모와 함께 저장 및 삭제 → cascade 옵션 제공 x
    • 부모 Entity Id와 추가 컬럼(basic 또는 embedded 타입)으로 구성
    • 기본적으로 식별자 개념이 존재하지 않음
      → 컬렉션 값 변경 시, 전체 삭제 후 새로 추가
  • @CollectionTable

    • 기본 또는 포함 가능한 유형의 컬렉션 매핑에 사용되는 테이블 지정
    • 컬렉션 값 필드 또는 속성에 적용
    • RDB에는 컬렉션과 같은 형태의 데이터를 컬럼에 저장할 수 없음
      → 별도의 테이블을 생성하여 컬렉션을 관리해야 함
    • 기본값 : {엔티티이름}_{컬렉션 속성 이름}
      ex) Member 엔티티의 addressHistoryMember_addressHistory
    • 테이블 매핑정보는 @AttributeOverride를 사용하여 재정의 가능

💻 값 타입 컬렉션

@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>();
    //...
}

@Embeddable
public class Address {

    @Column
    private String city;
    private String street;
    private String zipcode;
    //...
}

9.5.1 값 타입 컬렉션 사용

✅ 예시

💻 값 타입 컬렉션 등록

Member member = new Member();

//임베디드 값 타입
member.setHomeAddress(new Address("통영", "몽돌해수욕장", "660-123");

//기본값 타입 컬렉션
member.getFavoriteFoods().add("짬뽕");
member.getFavoriteFoods().add("짜장");
member.getFavoriteFoods().add("탕수육");

//임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("서울", "강남", "123-123");
member.getAddressHistory().add(new Address("서울", "강북", "000-000");

em.persist(member);

💻 실행된 SQL

INSERT INTO MEMBER (ID, CITY, STREET, ZIPCODE) VALUES (1, '통영', '몽돌해수욕장', '660-123')
INSERT INTO FAVORITE_FOODS (MEMBER_ID, FOOD_NAME) VALUES (1, "짬뽕")
INSERT INTO FAVORITE_FOODS (MEMBER_ID, FOOD_NAME) VALUES (1, "짜장")
INSERT INTO FAVORITE_FOODS (MEMBER_ID, FOOD_NAME) VALUES (1, "탕수육")
INSERT INTO ADDRESS (MEMBER_ID, CITY, STREET, ZIPCODE) VALUES (1, '서울', '강남, '123-123')
INSERT INTO ADDRESS (MEMBER_ID, CITY, STREET, ZIPCODE) VALUES (1, '서울', '강북', '000-000')
  • 실제 데이터베이스에 실행되는 INSERT SQL
    • member : 1번
    • member.homeAddress
      : 임베디드 값 타입 → 회원테이블 저장 SQL에 포함
    • member.favoriteFoods : 3번
    • member.addressHistory : 2번

💡 참고

값 타입 컬렉션은 영속성 전이(cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능을 필수로 가진다고 볼 수 있음


✅ 페치 전략

  • 값 타입 컬렉션 조회 시 페치 전략 선택 가능
  • LAZY가 기본
    @ElementCollection(fetch = FetchType.LAZY)

💻 조회

//SQL: SELECT ID, CITY, STREET, ZIPCODE FROM MEMBER WHERE ID = 1
Member member = em.find(Member.class, 1L); //1. member

//2. member.homeAddress
Address homeAddress = member.getHomeAddress(); //LAZY

//3. member.favoriteFoods
Set<String> favoriteFoods = member.getFavoriteFoods(); //LAZY

//SQL: SELECT MEMBER_ID, FOOD_NAME FROM FAVORITE_FOODS WHERE MEMBER_ID = 1
for (String favoriteFood : favoriteFoods) {
    System.out.println("favoriteFood = " + favoriteFood);
}

//4. member.addressHistory
List<Address> addressHistory = member.getAddressHistory(); //LAZY

//SQL: SELECT MEMBER_ID, CITY, STREET, ZIPCODE FROM ADDRESS WHERE MEMBER_ID = 1
addressHistory.get(0);
  • 실행 시 데이터베이스에 호출하는 SELECT SQL
    • member : 1번
      • 회원만 조회
      • 임베디드 값 타입인 homeAddress도 함께 조회
    • member.favoriteFoods : LAZY로 설정, 실제 컬렉션 사용시 1번 호출
    • member.addressHistory : 상동

💻 수정

Member member = em.find(Member.class, 1L);

//임베디드 값 타입 수정
member.setHomeAddress(new Address("새로운도시", "신도시1", "123456");

//기본값 타입 컬렉션 수정
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove("탕수육");
favoriteFoods.add("치킨");

//임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울", "기존 주소", "123-123"));
addressHistory.add(new Address("새로운도시", "새로운 주소", "123-456");
  • 임베디드 값 타입 수정

    • MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE
    • Member 엔티티를 수정하는 것과 동일
  • 기본값 타입 컬렉션 수정

    • 기준 값 제거 후 변경하려는 값 추가
    • 자바의 String 타입은 수정 불가능
  • 임베디드 값 타입 컬렉션 수정

    • 값 타입은 불변해야 함
    • 컬렉션에서 기존 주소를 삭제 후 새로운 주소 등록
    • equals, hashcode 구현 필수

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

  • 문제점

    값 타입은 식별자라는 개념이 존재하지 않으며 단순한 값들의 모음
    → 값 변경시 데이터베이스에 저장된 원본 데이터 조회 어려움

    • 특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 데이터베이스에서 찾고 값을 변경하면 됨
    • 값 타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관
      → 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 조회하기 어려움
  • 해결 방안

    1. 값 타입 컬렉션에 변경 사항 발생
    2. 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터 삭제
    3. 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어 기본 키를 구성
    → 데이터베이스 기본 키 제약 조건으로 인해 제약 존재

    • 컬럼에 null 입력 불가능
    • 같은 값 중복 저장 불가능
  • 실무) 값 타입 컬렉션이 매핑된 테이블에 데이터가 많을 경우

    • 값 타입 컬렉션 대신에 새로운 엔티티를 만들어 일대다 관계로 설정
    • 영속성 전이(Cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능 적용
      → 값 타입 컬렉션처럼 사용 가능

💻 예시

@Entity
public class AddressEntity {

    @Id
    @GeneratedValue
    private Long id;
    
    @Embedded
    Address address;
    ...
}

@Entity
public class Member {

    ...
    
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList
    
}

💡 참고

값 타입 컬렉션 변경할 때 JPA 구현체들이 테이블의 기본 키를 식별해서 변경된 내용만 반영하려고 하나, 사용하는 컬렉션이나 여러 조건에 따라 기본 키 식별 여부가 달라지므로 모두 삭제하고 다시 저장하는 최악의 시나리오를 고려하며 사용해야 한다.



9.6 정리

✅ 엔티티 타입과 값 타입의 특징

구분엔티티 타입값 타입
식별자존재(@Id)
- 식별자로 구분 가능
X
생명 주기존재(생성, 영속화, 소멸)
- 영속화 : em.persist(entity)
- 소멸 : em.remove(entity)
엔티티에 의존
- 의존하는 엔티티 제거시 같이 제거

공유참조 값 공유 가능(공유 참조)
ex) 회원 엔티티 존재
→ 다른 엔티티에서 자유롭게 참조 가능
공유하지 않는 것이 안전
- 값을 복사해서 사용해야 함
- 오직 하나의 주인만이 관리해야 함
- 불변 객체로 만드는 것이 안전

✅ 주의

  • 값 타입은 정말 값 타입이라 판단될 때만 사용해야 함
  • 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안 됨
  • 엔티티 타입 사용 기준
    • 식별자 필요
    • 지속적인 값 추적, 구분, 변경 필요



📖 참고

profile
기쁘게 코딩하고 싶은 백엔드 개발자

0개의 댓글