No-ah98
Noah
No-ah98
전체 방문자
오늘
어제
  • 분류 전체보기 (40)
    • CS (7)
      • Java (7)
    • shell (1)
    • 개발 실수 (1)
    • 웹 (1)
    • Git (1)
    • 부트캠프 (1)
    • 링크 (0)
    • 오류 (0)
    • 일정 (0)
    • 객체지향 (4)
    • CodingTest (6)
    • TIL (2)
    • JPA (7)
    • JAVA (0)
    • 기타 (0)
    • Spring (5)
    • 알고리즘 (1)
      • 백준 (1)
      • 프로그래머스 (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 2xn타일링
  • 백준
  • 연관관계 편의 메서드
  • 연관관계
  • @ModelAttribute
  • 백엔드 스쿨
  • 캡슐화
  • 문장 뒤집기
  • 멋쟁이사자처럼 백엔드 스쿨
  • 타일링
  • Entity
  • 11726
  • @PathVariable vs @RequestParam 차이
  • DTOvsVO
  • @RequestParam
  • 엔티티 매핑
  • @Controller
  • JPA
  • 멋사 백엔드
  • 프로그래머스
  • 문자 뒤집기
  • @Controller vs @RestController
  • @RestController
  • 계층형 댓글
  • 양방향 연관관계
  • Git 정리
  • 해시
  • 멋쟁이사자처럼
  • Javascript 공백 체크
  • @RequestBody
  • @RequestBody vs @ModelAttribute
  • 연관관계 매핑
  • spring data jpa
  • 객체지향
  • @PathVariable
  • @PathVariable vs @RequestParam
  • 문자열
  • 팰린드롬
  • bash shell 기본 명령어
  • 중복문자제거
  • 중복제거
  • form 공백 체크
  • @RequestBody@ModelAttribute 차이
  • 데이터 셋팅
  • 영속성 관리
  • VO
  • 연관관계 편의 메서드 필요성
  • 대댓글
  • JPA데이터셋팅
  • 깃 정리

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
No-ah98

Noah

값 타입
JPA

값 타입

2022. 4. 12. 12:29

목차

  • 기본값 타입
  • 임베디드 타입(복합 값 타입)
  • 값 타입과 불변 객체
  • 값 타입의 비교
  • 값 타입 컬렉션

JPA의 데이터 타입 분류

  • 엔티티 타입
    • @Entity로 정의하는 객체
    • 데이터가 변해도 식별자로 지속해서 추적 가능 -> 내부 데이터가 변해도 추적이 가능하다
    • 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
  • 값 타입
    • int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경시 추적 불가
    • 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
    • 분류
      • 기본값 타입
        • 자바 기본 타입(int, double)
        • 래퍼 클래스(Integer, Long)
        • String
      • 임베디드 타입(embedded type, 복합 값 타입)
      • 컬렌션 값 타입(collection value type)

기본 값 타입 

  • 예) String name, int age
  • 생명주기를 엔티티에 의존 -> 회원을 삭제하면 이름, 나이 필드도 함께 삭제
  • 값 타입은 공유하면 X -> 회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안됨 ( Side Effect ) 
  • 참고) 자바의 기본타입은 절대 공유 X
    • int, double 같은 기본 타입(primitive type)은 절대 공유 X
    • 기본 타입은 항상 값을 복사함
    • Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경 X

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

  • JPA는 임베디드 타입이라 함
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입 이라고도 함
  • int, String과 같은 값 타입 -> 추적 X, 변경되면 끝 

이해하기

  • 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다
    • 필드 속성 : startDate, endDate, city, street, zipcode
    • 위 속성을 보면 공통적으로 묶을 수 있는 것들이 보인다.
      • (startDate, endDate) -> "Period"
      • (city, street, zipcode) -> "Address"
    •  한번 생각해보자, 만약 내가 다른 사람에게 회원 정보에 대해 얘기를 한다면
    •  필드들을 나열해서 말하는게 자연스러울까 묶어서 "이름, 근무 기간, 주소"값이  있다고 얘기하는게 자연스러울까?
      • 후자처럼 추상화해서 표현하는게 더 자연스러울 것이다.
      • JPA 에서는 이를 반영하여 위처럼 공통으로 묶어서 임베디드 타입으로 구현할 수 있다.

임베디드 타입 사용법

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

임베디드 타입의 장점

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

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

  • 임베디드 타입은 엔티티의 값일 뿐이다 ->임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
  • DB는 데이터 관리에 목적이 있기 때문에 한 통에 다 담기는 것이 맞다.
  • 그러나, 객체는 데이터 뿐만 아니라 메서드(기능, 행위)를 가지고 있기 때문에, 공통 요소를 묶어서 임베디드 타입으로 관리하면 이점이 많다.
    • 의미 있는 메소드
  • 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다.
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음.

임베디드 타입과 연관관계

  • Address는 Zipcode라는 임베디드 타입을 가질 수 있다.
    • -> 임베디드 타입이 임베디드 타입을 가질 수 있음
  • 임베디드 타입인 PhoneNumber는 엔티티인 PhoneEntity를 가질 수 있다.
    • -> 임베디드 타입이 엔티티를 가질 수 있음
      • 가능한 이유) : 엔티티에 대한 외래키만 가지면 되기 때문

@AttributeOverride : 속성 재정의

  • 한 엔티티에서 같은 값 타입을 사용하면?
  • ex) : Member에서 Address가 2개 ( homeAddress, workAddress ) 
@Entity
public class Member {
  ...
  @Embedded
  private Address homeAddress;
  
  @Embedded
  private Address workAddress;
  ...
}
  • 컬럼 명이 중복됨 -> 오류 발생 ( MappingException : Repeated column ~ )
    • @AttributeOverrides, @AttributeOverride를 사용해서 컬럼 명 속성을 재정의 
@Entity
public class Member {
  ...
  @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;
  ...
}

임베디드 타입과 null

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

값 타입과 불변 객체

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

값 타입 공유 참조

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함
  • 부작용(side effect) 발생 

코드로 이해하기 

Address address = new Address("city", "street", "10000");

Member member = new Member();
member.setUsername("park");
member.setHomeAddress(address);
em.persist(member);

Member member2 = new Member();
member2.setUsername("kim");
member2.setHomeAddress(address);
em.persist(member2);

member.getHomeAddress().setCity("new city");

tx.commit();
  • 실행 결과, 분명 member의 Address에서 city값만 변경했는데 update 쿼리가 2개가 나간다
  • DB를 확인해보면 member2의 city값 또한 동일하게 변경됐다
  • 즉, 같은 Address를 공유해서 사용하면 이러한 side effect가 발생하기 때문에 절대 공유하면 안된다.
    • 엔티티 간에 공유하고 싶은 값 타입은 엔티티로 만들어서 해결하자. ( 뒤에 설명 ) 

값 타입 복사

  • 위와 같이 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험한 행위이다.
  • 대신 값(인스턴스)을 복사해서 사용
Address address = new Address("city", "street", "10000");

Member member = new Member();
member.setUsername("park");
member.setHomeAddress(address);
em.persist(member);

//주소 객체 복사
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());

Member member2 = new Member();
member2.setUsername("kim");
member2.setHomeAddress(copyAddress);
em.persist(member2);

member.getHomeAddress().setCity("new city");

객체 타입의 한계

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
  • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
  • 자바 기본 타입에 값을 대입하면 값을 복사한다.
  • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
    • -> 개발자가 실수로 직접 대입하는 것을 막을 방법이 없다.
  • 객체의 공유 참조는 피할 수 없다.

불변 객체( immutable object )

  • 객체 타입을 수정할 수 없게 만들어 부작용을 원천 차단
  • 값 타입은 불변 객체(immutable object)로 설계해야함
  • 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨
    • 안만들거나, set을 private으로 제한
  • 참고 : Integer, String은 자바가 제공하는 대표적인 불변 객체 
불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.
  • 불변인데도 불구하고, 객체의 필드 값을 변경하고 싶은 경우
    • 객체를 통째로 교체하자.
Address address = new Address("old city", "street", "10000");

Member member = new Member();
member.setUsername("park");
member.setHomeAddress(address);
em.persist(member);

Address newAddress = new Address("new City", address.getStreet(), address.getZipcode());
member.setHomeAddress(newAddress);

tx.commit();

값 타입의 비교

  • 값 타입 : 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 함
int a = 10;
int b = 10;
System.out.println("a == b : " + (a == b )); // true 반환
  • 그런데 객체 타입은 값이 같아도 주소가 다르기 때문에 비교 시 false를 반환한다.
Address a = new Address("서울");
Address b = new Address("서울");

System.out.println(" a == b : " + ( a == b )); // a,b 주소 값이 다르기 때문에 false 반환
  • 동일성(identity) 비교
    • 인스턴스의 참조 값을 비교, == 사용
  • 동등성(equivalence) 비교 
    • 인스턴스의 값을 비교, equals() 사용
  • 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 함.
  • 값 타입의 equals() 메소드를 적절하게 재정의(주로 모든 필드 사용) + hash코드도 포함해서 정의 

값 타입 컬렉션

  • 값 타입을 하나 이상 저장할 때 사용
  • 연관관계에서 엔티티를 컬렉션으로 가지고 있는것이 아니라,
  • 좋아하는 음식, 주소변경 히스토리를 저장하는 값 타입을 컬렉션에 저장하고 싶을 때 사용 
public class Member {
    @Id
    private Long id;
    
    private Set<String> favoriteFoods;
    private List<Address> addressHistory;
}
  • @ElementCollection, @CollectionaTable(테이블명 지정) 사용 
  • 이러한 관계를 데이터베이스에서는 컬렉션을 같은 테이블에 저정할 수 없다.
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요함
  • 일대다 관계로 풀어서 별도의 테이블을 만들어서 조인이 가능하도록 해야한다.

코드로 이해하기 (Member)

  • 값 타입 컬렉션 추가 
  • addressHistory는 임베디드 타입이므로 컬럼명 명시를 안해줘도 됨
  • favorite은 예외적으로 컬럼명을 지정해준다.
@Entity
@Getter
@Setter
public class Member extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    ...

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

    ...
}
======== favorite 테이블 생성 로그, FK 설정(member id)======== 

Hibernate: 
    
    create table favorite_food (
       member_id bigint not null,
        food_name varchar(255)
    )
        
Hibernate: 
    
    alter table favorite_food 
       add constraint FKetdr9m6ysgxkvs2f7vf15sac4 
       foreign key (member_id) 
       references Member
======== address 테이블 생성 로그, FK 설정(member id)======== 
Hibernate: 
    
    create table address (
       member_id bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255)
    )

Hibernate: 
    
    alter table address 
       add constraint FK6ncq4527mw0seu2b7wr19mqpg 
       foreign key (member_id) 
       references Member

컬렉션 값 타입 저장

  • 실제 값 타입 컬렉션을 저장할 때 코드를 보면서 이해해보자.
    • 이 코드에서 주목해야 할 점은 컬렉션에 값만 넣고 em.persist는 member만 해줬다는 점이다.
Member member = new Member();
member.setUsername("park");
member.setHomeAddress(new Address("city", "street", "10000"));

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

member.getAddressHistory().add(new Address("city1", "street1", "10001"));
member.getAddressHistory().add(new Address("city2", "street2", "10002"));
member.getAddressHistory().add(new Address("city3", "street3", "10003"));

em.persist(member);

tx.commit();
  • 코드를 실행 해보면 컬렉션을 저장하는 테이블에 insert 쿼리가 6개가 나간다.
    • "응?? 난 분명 em.persit 함수는 member만 사용했는데?? 심지어 cascade 설정도 안해줬는데 ??"
      • 여기서 알 수있는 점은 컬렉션 값 타입이 엔티티의 생명 주기를 따라 간다는 것이다
      • 값 타입이기 때문에 Member에 소속된 값 타입은 전부 Member의 생명 주기를 따라간다.
      • 즉, 테이블만 따로 만들어서 관리하기 때문에 별도로 persist를 할 필요 없이 엔티티만 저장하면 되는 것이다.
        • 일대다 관계에서 Cascade.ALL과 orphanRemoval == true랑 유사하다.
      • 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다
ibernate: 
    /* insert hello.jpa.Member
        */ insert 
        into
            Member
            (id, createdBy, createdDate, lastModifiedBy, lastModifiedDate, age, description, city, street, zipcode, locker_id, roleType, name, endDate, startDate) 
        values
            (null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hello.jpa.Member.addressHistory */ 
        insert 
        into
            address
            (member_id, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hello.jpa.Member.addressHistory */ 
        insert 
        into
            address
            (member_id, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hello.jpa.Member.addressHistory */ 
        insert 
        into
            address
            (member_id, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hello.jpa.Member.favoriteFoods */ 
        insert 
        into
            favorite_food
            (member_id, food_name) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row hello.jpa.Member.favoriteFoods */ 
        insert 
        into
            favorite_food
            (member_id, food_name) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row hello.jpa.Member.favoriteFoods */ 
        insert 
        into
            favorite_food
            (member_id, food_name) 
        values
            (?, ?)

컬렉션 값 타입 조회

  • 영속성 컨텍스트를 비워놓고(em.flush(), em.clear) Member를 조회해보자
  • Member에 소속된 Embaded 타입인 city, street, zipcode는 검색이 되지만 
  • 컬렉션 값 타입들은 조회되지 않는다.
  • "컬렉션 값 타입은 지연 로딩 전략을 사용한다"는 것을 알 수 있다.
Member member = new Member();
member.setUsername("park");
member.setHomeAddress(new Address("city", "street", "10000"));

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

member.getAddressHistory().add(new Address("city1", "street1", "10001"));
member.getAddressHistory().add(new Address("city2", "street2", "10002"));
member.getAddressHistory().add(new Address("city3", "street3", "10003"));

em.persist(member);

em.flush();
em.clear();

System.out.println("================== START ==================");
em.find(Member.class, member.getId());
================== START ==================
Hibernate: 
    select
        member0_.id as id1_7_0_,
        member0_.createdBy as createdB2_7_0_,
        member0_.createdDate as createdD3_7_0_,
        member0_.lastModifiedBy as lastModi4_7_0_,
        member0_.lastModifiedDate as lastModi5_7_0_,
        member0_.age as age6_7_0_,
        member0_.description as descript7_7_0_,
        member0_.city as city8_7_0_,
        member0_.street as street9_7_0_,
        member0_.zipcode as zipcode10_7_0_,
        member0_.locker_id as locker_15_7_0_,
        member0_.roleType as roleTyp11_7_0_,
        member0_.team_id as team_id16_7_0_,
        member0_.name as name12_7_0_,
        member0_.endDate as endDate13_7_0_,
        member0_.startDate as startDa14_7_0_,
        locker1_.id as id1_6_1_,
        locker1_.name as name2_6_1_ 
    from
        Member member0_ 
    left outer join
        Locker locker1_ 
            on member0_.locker_id=locker1_.id 
    where
        member0_.id=?
  • 컬렉션 값 타입들을 터치해서 실제로 지연 로딩 전략을 사용하는지 알아보자.
    • 컬렉션 값 타입의 필드들을 터치하자마자 지연 로딩 전략을 사용한다.
System.out.println("================== START ==================");
em.find(Member.class, member.getId());
Member findMember = em.find(Member.class, member.getId());

List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
    System.out.println("address = " + address.getCity());
}

Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
    System.out.println("favoriteFood = " + favoriteFood);
}

tx.commit();
================== START ==================
Hibernate: 
    select
        member0_.id as id1_7_0_,
        member0_.createdBy as createdB2_7_0_,
        member0_.createdDate as createdD3_7_0_,
        member0_.lastModifiedBy as lastModi4_7_0_,
        member0_.lastModifiedDate as lastModi5_7_0_,
        member0_.age as age6_7_0_,
        member0_.description as descript7_7_0_,
        member0_.city as city8_7_0_,
        member0_.street as street9_7_0_,
        member0_.zipcode as zipcode10_7_0_,
        member0_.locker_id as locker_15_7_0_,
        member0_.roleType as roleTyp11_7_0_,
        member0_.team_id as team_id16_7_0_,
        member0_.name as name12_7_0_,
        member0_.endDate as endDate13_7_0_,
        member0_.startDate as startDa14_7_0_,
        locker1_.id as id1_6_1_,
        locker1_.name as name2_6_1_ 
    from
        Member member0_ 
    left outer join
        Locker locker1_ 
            on member0_.locker_id=locker1_.id 
    where
        member0_.id=?
Hibernate: 
    select
        addresshis0_.member_id as member_i1_0_0_,
        addresshis0_.city as city2_0_0_,
        addresshis0_.street as street3_0_0_,
        addresshis0_.zipcode as zipcode4_0_0_ 
    from
        address addresshis0_ 
    where
        addresshis0_.member_id=?
address = city1
address = city2
address = city3
Hibernate: 
    select
        favoritefo0_.member_id as member_i1_4_0_,
        favoritefo0_.food_name as food_nam2_4_0_ 
    from
        favorite_food favoritefo0_ 
    where
        favoritefo0_.member_id=?
favoriteFood = 족발
favoriteFood = 치킨
favoriteFood = 피자

컬렉션 값 타입 수정

  • 한번 더 강조하자면 "값 타입은 절대 불변해야 한다." 
  • 따라서, 수정해야 한다면 객체를 통째로 바꿔야한다.
System.out.println("================== START ==================");
Member findMember = em.find(Member.class, member.getId());

findMember.setHomeAddress(new Address("newCity", "newStreet", "20001"));

tx.commit();
  • 컬렉션 값 타입 수정1 - String 
    • Set<String> 타입인 좋아하는 음식을 수정하려면, String 자체가 불변 객체 이므로,
    • 삭제하고, 다시 리스트에 넣어준다.
System.out.println("================== START ==================");
Member findMember = em.find(Member.class, member.getId());

findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("피자");

tx.commit();

 

  • 발생한 쿼리를 확인 해보자
    • 멤버를 SELECT 하고,
    • 지연로딩으로 favoriteFood를 SELECT 하고,
    • favoriteFood에서 치킨을 DELETE하고
    • favoriteFood에 한식을 INSERT한다.
    • 값 타입의 변경을 캐치해서 JPA에서 알아서 삭제, 추가 쿼리를 실행한다.
    • 영속성 전이가 되는 것 처럼 동작한다. 값 타입 컬렉션들은 소속 엔티티에 의존 관계를 다 맡긴다.
================== START ==================
Hibernate: 
    select
        member0_.id as id1_7_0_,
        member0_.createdBy as createdB2_7_0_,
        member0_.createdDate as createdD3_7_0_,
        member0_.lastModifiedBy as lastModi4_7_0_,
        member0_.lastModifiedDate as lastModi5_7_0_,
        member0_.age as age6_7_0_,
        member0_.description as descript7_7_0_,
        member0_.city as city8_7_0_,
        member0_.street as street9_7_0_,
        member0_.zipcode as zipcode10_7_0_,
        member0_.locker_id as locker_15_7_0_,
        member0_.roleType as roleTyp11_7_0_,
        member0_.team_id as team_id16_7_0_,
        member0_.name as name12_7_0_,
        member0_.endDate as endDate13_7_0_,
        member0_.startDate as startDa14_7_0_,
        locker1_.id as id1_6_1_,
        locker1_.name as name2_6_1_ 
    from
        Member member0_ 
    left outer join
        Locker locker1_ 
            on member0_.locker_id=locker1_.id 
    where
        member0_.id=?
        
Hibernate: 
    select
        favoritefo0_.member_id as member_i1_4_0_,
        favoritefo0_.food_name as food_nam2_4_0_ 
    from
        favorite_food favoritefo0_ 
    where
        favoritefo0_.member_id=?

Hibernate: 
    /* delete collection row hello.jpa.Member.favoriteFoods */ delete 
        from
            favorite_food 
        where
            member_id=? 
            and food_name=?
            
Hibernate: 
    /* insert collection
        row hello.jpa.Member.favoriteFoods */ 
        insert 
        into
            favorite_food
            (member_id, food_name) 
        values
            (?, ?)
  • 컬렉션 값 타입 수정2 - Address 객체
    • List<Address> 타입의 AddressHistory의 값을 바꿔보자.
      • 이때, equals, hashCode가 재정의 되어 있어야 한다.
System.out.println("================== START ==================");
Member findMember = em.find(Member.class, member.getId());

findMember.getAddressHistory().remove(new Address("city1", "street1", "10001"));
findMember.getAddressHistory().add(new Address("new city1", "street1", "10001"));


tx.commit();
  • 발생한 쿼리를 살펴보자
    • Member SELECT,
    • 지연 로딩으로 AddressHistory SELECT하고
    • Address(AddressHistory) 테이블중 Member 소속인 row들 전체 DELETE.
      • 응?? 난 분명 city1만 삭제한다고 했는데??
    • 그리고, Address 3개를 새로 INSERT 한다.
    • 이 부분에 대해서 아래 제약사항에서 자세히 알아보자.
================== START ==================
Hibernate: 
    select
        member0_.id as id1_7_0_,
        member0_.createdBy as createdB2_7_0_,
        member0_.createdDate as createdD3_7_0_,
        member0_.lastModifiedBy as lastModi4_7_0_,
        member0_.lastModifiedDate as lastModi5_7_0_,
        member0_.age as age6_7_0_,
        member0_.description as descript7_7_0_,
        member0_.city as city8_7_0_,
        member0_.street as street9_7_0_,
        member0_.zipcode as zipcode10_7_0_,
        member0_.locker_id as locker_15_7_0_,
        member0_.roleType as roleTyp11_7_0_,
        member0_.team_id as team_id16_7_0_,
        member0_.name as name12_7_0_,
        member0_.endDate as endDate13_7_0_,
        member0_.startDate as startDa14_7_0_,
        locker1_.id as id1_6_1_,
        locker1_.name as name2_6_1_ 
    from
        Member member0_ 
    left outer join
        Locker locker1_ 
            on member0_.locker_id=locker1_.id 
    where
        member0_.id=?
Hibernate: 
    select
        addresshis0_.member_id as member_i1_0_0_,
        addresshis0_.city as city2_0_0_,
        addresshis0_.street as street3_0_0_,
        addresshis0_.zipcode as zipcode4_0_0_ 
    from
        address addresshis0_ 
    where
        addresshis0_.member_id=?
        
Hibernate: 
    /* delete collection hello.jpa.Member.addressHistory */ 
    delete 
        from
            address 
        where
            member_id=?
Hibernate: 
    /* insert collection
        row hello.jpa.Member.addressHistory */ insert 
        into
            address
            (member_id, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hello.jpa.Member.addressHistory */ insert 
        into
            address
            (member_id, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hello.jpa.Member.addressHistory */ insert 
        into
            address
            (member_id, city, street, zipcode) 
        values
            (?, ?, ?, ?)

값 타입 컬렉션의 제약사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
    • 값 타입 컬렉션 생성하는 테이블을 확인 해보면 ID(PK)가 따로 존재 하지 않는다.
    • 즉, 뭘 수정해야 할지를 알 방법이 없다.
Hibernate: 
    
    create table address (
       member_id bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255)
    )
  • 그래서 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. 
    • 물론 어노테이션을 통한 해결 방법이 있지만, 복잡하게 사용해야 한다
    • 실무에서 쓰면 안된다는 거다..
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다.
    • null 입력 X, 중복 저장 X

값 타입 컬렉션 제안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려하는 것이 낫다.
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용하자.
  • 영속성 전이 + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용하자

코드로 이해하기

  • Member 클래스에서 AddressHistory 엔티티로 대체
    • @OneToMany와 @JoinColumn 으로 일대다 단방향 매핑을 한다.
      • 값 타입을 엔티티로 승격하지 않는 한, 일대다 값 타입 컬렉션은 양방향 매핑을 못한다.
    • AddressEntity에서 내부적으로 Address 값 타입을 포함한다.
@Entity
@Getter
@Setter
public class Member extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    ...

    @ElementCollection
    @CollectionTable(
        name = "favorite_food",
        joinColumns = @JoinColumn(name = "member_id"))
    @Column(name = "food_name")
    private Set<String> favoriteFoods = new HashSet<>();

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "member_id")
    private List<AddressEntity> addressHistory = new ArrayList<>();
}
@Entity
@Getter
@Setter
@Table(name = "ADDRESS")
public class AddressEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Address address;

    public AddressEntity() {
    }

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

코드로 이해하기(Main)

  • AddressEntity를 넣는다.
  • @OneToMany와 @JoinColumn 으로 일대다 단방향 매핑을 했으니까,
  • 키를 관리하는 Member 테이블의 반대인 AddressEntity(Address 테이블)에 Update 쿼리가 나가는 것은 이미 학습했다. 참조
  • 이제는 ADDRESS 테이블에 ID라는 개념이 생겼다. 그리고 Member ID를 FK로 가지고 있다.
  • 자체적인 아이디가 있다는 것은 엔티티라는 얘기고, 이제는 특정 row를 찾아서 내부의 값을 수정할 수 있다.
  • 실무에서는 이런식으로 값 타입 컬렉션은 엔티티로 승격 시켜서 많이 사용한다.
Member member = new Member();
member.setUsername("park");
member.setHomeAddress(new Address("city", "street", "10000"));

member.getAddressHistory().add(new AddressEntity("city1", "street1", "10001"));
member.getAddressHistory().add(new AddressEntity("city2", "street2", "10002"));
member.getAddressHistory().add(new AddressEntity("city3", "street3", "10003"));

em.persist(member);

em.flush();
em.clear();
Hibernate: 
    /* insert hello.jpa.Member
        */ insert 
        into
            Member
            (id, createdBy, createdDate, lastModifiedBy, lastModifiedDate, age, description, city, street, zipcode, locker_id, roleType, name, endDate, startDate) 
        values
            (null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert hello.jpa.AddressEntity
        */ insert 
        into
            ADDRESS
            (id, city, street, zipcode) 
        values
            (null, ?, ?, ?)
Hibernate: 
    /* insert hello.jpa.AddressEntity
        */ insert 
        into
            ADDRESS
            (id, city, street, zipcode) 
        values
            (null, ?, ?, ?)
Hibernate: 
    /* insert hello.jpa.AddressEntity
        */ insert 
        into
            ADDRESS
            (id, city, street, zipcode) 
        values
            (null, ?, ?, ?)
Hibernate: 
    /* create one-to-many row hello.jpa.Member.addressHistory */ update
        ADDRESS 
    set
        member_id=? 
    where
        id=?
Hibernate: 
    /* create one-to-many row hello.jpa.Member.addressHistory */ update
        ADDRESS 
    set
        member_id=? 
    where
        id=?
Hibernate: 
    /* create one-to-many row hello.jpa.Member.addressHistory */ update
        ADDRESS 
    set
        member_id=? 
    where
        id=?

정리

  • 엔티티 타입의 특징
    • 식별자 O
    • 생명 주기 관리
    • 공유
  • 값 타입의 특징
    • 식별자 X
    • 생명 주기를 엔티티에 의존
    • 공유하지 않는 것이 안전(복사해서 사용)
    • 불변 객체로 만드는 것이 안전
  • 값 타입은 정말 값 타입이라 판단될 때만 사용
  • 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안됨
  • 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티로 승격화해서 사용하자.

Reference

자바 ORM 표준 JPA 프로그래밍

'JPA' 카테고리의 다른 글

프록시  (0) 2022.04.07
고급 매핑  (0) 2022.04.05
다양한 연관관계 매핑  (0) 2022.03.31
연관관계 매핑 기초  (0) 2022.03.29
엔티티 매핑  (0) 2022.03.24
    'JPA' 카테고리의 다른 글
    • 프록시
    • 고급 매핑
    • 다양한 연관관계 매핑
    • 연관관계 매핑 기초
    No-ah98
    No-ah98

    티스토리툴바