Spring Framework/[인프런] Java ORM 표준 프로그래밍 - JPA

Chapter 09. 값 타입

계란💕 2022. 9. 7. 17:25

9.1 기본값 타입 

 

JPA의 데이터 타입 분류

  • 엔티티 타입(entity type)
    • 엔티티 타입이란 @Entity로 정의하는 클래스 객체를 말한다.
    • 데이터가 변해도 식별자로 지속해서 추적 가능하다
    • ex) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능하다.
  • 값 타입
    • int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경 시 추적이 불가능하다
    • ex) 숫자 100 => 200 바꾸면 완전히 다른 값으로 대체
      1. 기본값 타입: 자바 기본 타입(int, double) / 래퍼 클래스 / String
        • 생명 주기를 엔티티에 의존한다.
        • ex) 회원 삭제하면 이름, 나이도 함께 삭제
        • 값 타입은 공유하면 안 된다. ex) 회원 이름 변경 시 다른 회원의 이름도 함께 변경되면 안된다. (부수 효과)
          • cf) 자바의 기본 타입은 절대 공유되지 않는다.
          •  기본 타입은 항상 값을 복사한다.
          • Integer같은 래퍼 클래스나 String같은 특수한 클래스는 공유 가능한 객체이지만 변경할 수 없다.
      2. 임베디드 타입(embedded type, 복합 값 타입): 새로운 클래스를 만들어서 사용하는 경우
      3. 컬렉션 값 타입 (collection value type): List 처럼 뭔가 담을 수 있는 형태

 

 

 

9.2 임베디드 타입 

 

임베디드 타입(Embedded type, 내장 타입, 복합 값 타입)

  • 새로운 값 타입을 직접 정의 가능하다
  • JPA는 임베디드 타입이다.
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.
  • 추적이 안 되고 변경하면 끝이다.

 

 

임베디드 타입 예시

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

 

 

임베디드 타입 사용법

  • @Embeddable: 값 타입을 정의하는 곳에 표시한다.
  • @Embedded: 값 타입을 사용하는 곳에 표시한다.
  • @Embeddable, @Embedded 둘 중 하나만 넣어도 되지만 둘 다 넣는 걸 권장한다.
  • 기본 생성자가 필수
  • 임베디드 타입의 장점
    • 재사용성 
    • 높은 응집도
    • Period.isWork() 처럼 해당 값 타입만 사용하는 의미있는 매소드를 만들 수 있다.
    • 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티에 생명 주기를 의존한다.

 

 

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

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

 

 

임베디드 타입과 연관 관계 

 

 

임베디드 타입과 null

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

 

 

  Ex) 임베디드 타입

  • Member 클래스
    • 여기에서 기간 / 주소 끼리 속성을 묶으려면?
    • 아래처럼 따로 빼버린다.
    • @Embeddable을 붙여서 값 타입임을 보여준다.
java
열기

 

java
열기
java
열기

  Note) 실행 결과 - embed 한 객체들이 모두 Member클래스의 필드로  들어간다.

 

 

  Ex) 같은 엔티티가 중복되면?

java
열기

 

  Note) 실행 결과 - MappingException

  • Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: hellojpa.Member column: city (should be mapped with insert="false" update="false")
    • 이를 해결하기 위해 @AttributeOverride(s)를 사용한다.

 

  • 따라서, 멤버 클래스에 다음과 같이 애너테이션 @AttributeOverrides을 추가한다.
    • 그러면 상단의 homeAddress의 속성 city / street / zipcode는 이름 그대로 들어가고 workAddress는 아래에서 정한대로 column name이 들어간다.
    • @AttributeOverride의 name 안에는 Address에서 정의한 필드명이 그대로 들어가야한다.
java
열기

  Note) 실행 결과

  •  create table member을 보면 새로 오버라이드 한 것과 기존에 쓴 것 모두 나온다.

 

 

  Ex) 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null 이다.

java
열기

 

 

 

9.3 값 타입과 불변 객체

 

값 타입 공유 참조

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
  •  부작용(side effect) 발생 가능성 때문이다.

 

 

  Ex) 값 타입 공유 참조 - 부작용

java
열기

  Note) 실행 결과 - 업데이트 쿼리 두 번 실행된다.

 

 

 

값 타입 복사

  • 위의 예제에서 봤듯이 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다.
  • 따라서, 값(인스턴스)을 복사해서 사용한다.

 

 

객체 타입의 한계

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
  • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
  • 자바 기본 타입에 값을 대입하면 값을 복사한다.
  • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
  • 객체의 공유 참조는 피할 수 없다.
    • ex) 위 사진에서 a, b는 Address라는 같은 인스턴스를 가리키므로 둘다 "New"로 바뀐다.

 

 

불변 객체(immutable object)

  • 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단 가능
  • 값 타입은 불변 객체(immutable object, 생성 시점 이후로 절대 값을 변경할 수 없는 객체)로 설계해야한다.
  • 생성자로만 값을 설정하고 수정자(setter)를 만들지 않으면 된다. 또는 private으로 만든다.
  • ex) Ingeger, String은 불변 객체

 

 

  Ex) 그럼에도 불구하고 바꾸려면?

  • 아래처럼 newAddress를 새로 넣어야 한다.
java
열기

 

 

9.4 값 타입의 비교

 

값 타입의 비교

  • 값 타입: 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다.
  • 동일성 비교(identity) 비교: 인스턴스의 참조값을 비교한다. '=='
  • 동등성 비교(equivalence) 비교: 인스턴스의 을 비교, equals() 
  • 값 타입을 equals()을 사용해서 동등성 비교를 해야한다.
  • 값 타입의 equals()를 적절하게 재정의한다. (주로 모든 필드사용) 
    • equals() 를 구현하면 그에 맞게 hashcode()도 꼭 재정의한다.

 

 

  Ex) Address에 다음과 같이 두 메서드를 override한다.

java
열기

  Note) 실행 결과

 

 

 

9.5 값 타입 컬렉션 

 

값 타입 컬렉션

  • 값 타입을 컬렉션에 담아 쓰는 걸 말한다. 하나 이상 저장할 때 사용한다.
  • @ElementCollection, @CollectionTable 사용한다.
    • @ElementCollection
    • @CollectionTable 
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
    • 컬렉션은 일대다라서 데이터베이스에는 하나의 테이블에 넣을 수 있는 방법이 없다. (그래서 별도 테이블 필요)
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요한다.
  • 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다.
  • 데이터베이스는 보통 value만 넣을 수 있다. 요즘에야 Json 지원하기도 한다.
    • 컬렉션은 "일대다" 개념이다. 
    • member의 주소 이력을 저장할 때, 별도의 테이블을 뽑아서 저장 가능하다.
    • 테이블 FAVORITE_FOOD 의 기본 키: MEMBER_ID + FOOD_NAME
    • 테이블 ADDRESS의 기본 키: MEMBER_ID + CITY + STREET + ZIPCODE 모두 묶어서 PK를 쓴다.
    • ADDRESS, FAVORITE_FOOD 에 Long id를 기본 키로 넣으면? 
      • 값 타입이 아닌 엔티티가 되버린다. 

 

 

  Ex) 값 타입 저장

  • 멤버 
    • 둘다 MEMBER_ID로 조인한다.
java
열기

 

  Note) 실행 결과

  • MEMBER에 주소 3가지 정보가 들어간다.
  • address 테이블에도 멤버ID(외 래키)와 세가지 주소가 들어간다.

 

 

 

 

  Ex) 값 타입 저장

java
열기

  Note) 실행 결과

  • 멤버가 먼저 INSERT
  • 주소는 3개 food는 2개 INSERT 된다.
  • 값 타입을 따로 persist()하지 않았는데 member를 persist()하니까 값 타입 컬렉션도 함께 저장된다.
  • 값 타입은 라이프 사이클이 따로 없다. 따라서 생명 주기가 member에 소속된다.
java
열기

 

 

 

  Ex) 값 타입 조회 - LAZY (디폴트)

java
열기

  Note) 실행 결과

  • 멤버 테이블만 가져오고 끝난다. => 컬렉션들은  "지연 로딩"
  • Address는 멤버에 소속된 값 타입이라서 같이 들어온다.

 

 

  • 멤버 외에 다른 테이블도 가져오도록 하려면? 
    • addressHistory 추가
java
열기

  Note) 실행 결과

  • 이전 예제에 컬렉션 addressHistory 리스트를 추가했다.
  • history 테이블 가져와서 주소 정보 찍는다.
  • food 테이블 가져와서 food 정보를 찍는다.
  • @ElementCollection의 fetch type이 LAZY로 기본 설정되어 있기 때문이다.
java
열기

 

 

 

  Ex) 값 타입 수정의 잘못된 예시

  • 값 타입은 불변이어야한다.
java
열기

 

  Note) 실행 결과

  • 그런데 값 타입은 부작용(side effect)이 생길 수 있어서 이런 식으로 수정하면 절대 안된다.
java
열기

 

 

 

  Ex) 값 타입 수정의 올바른 예시

  • Address 자체를 통으로 갈아 끼워야한다.
java
열기

  Note) 실행 결과

  • 값 타입은 추적도 안되니까 완전히 교체를 해줘야한다.
java
열기

 

 

 

  Ex) 값 타입 컬렉션 업데이트

java
열기

  Note) 실행 결과

  • 컬렉션의 값만 변경해도 실제 데이터베이스 쿼리가 날아가고 JPA가 데이터를 바꿔준다.
  • MEMBER_ID, FOOD_NAME 찾아서 DELETE 한 다음에 데이터를 새로 집어 넣는다.
java
열기

 

 

 

값 타입 컬렉션의 제약 사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 그래서 값을 변경하면 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블을 모든 컬럼을 묶어서 기본 키를 구성해야한다 => null 입력 X, 중복 저장
  • ex) addressHistory를 지우는 경우, 멤버 아이디와 관련한  정보를 DB에서 모두 지운다.
    • 그 다음 컬렉션에 최종으로 남은 것들에 대해서만 INSERT 실행한다.

 

 

  Ex) 주소 바꾸기 - 값 타입 컬렉션의 제약 사항

  • 컬렉션은 대상을 찾을 때,  기본적으로 equals()를 쓰기 때문에 괄호 안에 완전히 같은 값을 넣어준다.
    • 전에 오버라이드한 equals(), hashcode() 를 사용
java
열기

 

  Note) 실행 결과

  • 순서는 JPA가 알아서 고른다.
  • addressHistory를 DELETE 하는데 ADDRESS 테이블을 통으로 지운다. 멤버 아이디 기준으로
    • 그러고나서 address  관련 INSERT가 두 번 날아간다.
    • old1을 remove() 하고나면 old2, newCity1은 남아 있을 것이다. 이 두 개를 테이블을 완전히 갈아끼운 것이다.
    • 왜 INSERT 가 두 번 날아갈까?
java
열기

 

 

 

 

  cf) addressHistory 에 다음 애너테이션 OrderColumn을 넣는 것도 위험하다.

@OrderColumn(name ="address_history_order")
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name ="MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();

 

 

 

값 타입 컬렉션 대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려한다.
  • 일댕다 관계를 위한 엔티티를 만들고 여기에서 값 타입을 사용한다.
  • 영속성 전이(cascade) + 고아 객체를 사용해서 값 타입 컬렉션 처럼 사용한다.
    • ex) AddressEntity

 

 

  Ex) 대안 - 값 타입을 엔티티로 승급

 

  • Address는 값 타입, AddressEntity는 엔티티
java
열기

 

  • 다음과 같이 멤버의 addressHistory를 바꾼다.
java
열기

 

java
열기

 

  Note) 실행 결과

  • 일대다 단방향 매핑은 update 쿼리가 나간다. (다른 테이블에 FK 가 있기 때문)
  • 값 타입으로 쓸 때보다 훨씬 유용하다.
java
열기
  • 데이터베이스를 보면 ADDRESS에 ID라는 개념이 생겼다.
  • 자체적인 ID가 있다는 것은  값 타입이 아니라 엔티티라는 뜻이다. 수정 가능 
    • => 값 타입을 엔티티로 승급

 

 

엔티티와 값 타입 비교

  • 엔티티 타입의 특징: 식별자 있음 / 생명 주기 관리 / 공유
  • 값 타입의 특징: 식별자 없음 / 생명 주기를 엔티티에 의존 / 공유하지 않는게 안전 (또는 복사해서 사용) / 불변 객체로 만드는 게 안전하다.

 

 

 

9.6 실전 예제 6 - 값 타입 매핑

값 타입 매핑

 

  cf) 

  • equals(), hashcode()를 재정의하는 경우에 필드에 직접 접근하면 프록시일 때 문제가 생긴다.
  • getter()로 접근해야 프록시인 경우에 문제가 안 생긴다.

 

 

  Ex)

  • jpa-shop
  • Member
java
열기

 

  • Address
java
열기

 

  • Delivery
java
열기

 

  Note) 실행 결과

  • 주소 관련된 필드 3개가 공통으로 들어간다.
  • Address 클래스에 fullAddress() 와 같은 메서드를 추가하거나 @Column(length= "제한 길이")를 넣어서 쓸 수도 있다.
java
열기

 

 

 

출처 - https://www.inflearn.com/course/ORM-JPA-Basic

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com