JPA 값 타입 - 값 타입 컬렉션

2022. 7. 14. 23:38·공부/JPA
반응형

출처 : 자바 ORM 표준 JPA 프로그래밍 인프런 강의

 

JPA 게시글은 대부분 인프런의 김영한님의 강의인 '자바 ORM 표준 JPA 프로그래밍' 기반으로 내용을 정리했습니다.

 

값 타입 컬렉션

  • 값 타입을 컬렉션에 담아서 쓰는 걸 값 타입 컬렉션이라 한다.

값 타입 컬렉션에서 문제는 DB 테이블과 매핑할 때가 문제이다. DB 테이블에는 컬렉션을 담을 수 있는 방법이 없다. 컬렉션은 개념적으로 1대다이기 때문에 별도의 테이블로 뽑아줘야 한다.

예제

Member.class

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

 

Member 클래스에 값 타입 컬렉션 실습을 위해 Set과 List를 이용한 객체를 만들었다. favoriteFoods의 경우엔 @Column이 예외적으로 들어갔는데, 이는 Address의 경우엔 임베디드 타입 실습으로 인해 클래스가 따로 정의되어 있고, 필드의 이름이 다 정해져 있지만 favoriteFoods의 경우엔 제네릭에 String이 들어가 있으므로 필드의 이름을 정해주기 위해 @Column으로 필드의 이름을 지정해주었다.

그리고 값 타입 컬렉션을 사용하기 위한 애노테이션 @ElementCollection과 @CollectionTable을 달아주었다. @CollectionTable의 경우 컬렉션 테이블 이름과 그 테이블과 Member와의 외래키를 지정해준다. joinColumns 속성으로 Member의 컬럼 하나를 지정하면 그게 외래키가 된다.

위의 코드를 다 작성하고 나면 ADDRESS, FAVORITE_FOOD, MEMBER 테이블이 생성된다.

Hibernate: 
    
    create table FAVORITE_FOOD (
       MEMBER_ID bigint not null,
        FOOD_NAME varchar(255)
    )
    
    
Hibernate: 
    
    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)
    )
    
    
Hibernate: 
    
    create table ADDRESS (
       MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255)
    )

 

값 타입 컬렉션

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

 

저장

Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity","street", "123"));

member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("라떼");

member.getAddressHistory().add(new Address("old_1","street", "123"));
member.getAddressHistory().add(new Address("old_2","street", "123"));


em.persist(member);

tx.commit();

위의 예제 코드에서 em.persist()로 인해 몇 개의 INSERT SQL가 날라갈까?

member 1개, favoritFood 3개, Address 2개로 총 6개의 INSERT 쿼리가 날라간다. 

컬렉션 값 타입은 따로 persist를 해주지 않았는데도 불구하고 member가 persist 되자 그에 속한 값 타입의 테이블에게 까지 INSERT SQL가 날라간다. 즉, member와 생명 주기를 같이 하고 있는 것이다. 왜냐하면 컬렉션 값 타입은 본인만의 생명 주기가 없다. 그래서 Member에게 소속된 것이다. 쉽게 말하면 그냥 Member에 있는 username과 같이 Member에게 생명 주기를 맡겨 버린다. 

컬렉션 값 타입의 생명 주기를 보면 CASCADE + 고아 객체 제거 기능을 설정한 객체와 똑같이 움직인다.

 

 

조회

위 예제에 이어서 코드를 작성해보자.

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


System.out.println("============= START ===============");
Member findMember = em.find(Member.class, member.getId());

영속성 컨텍스트를 비워주기 위해 em.flush(), em.clear()를 적어주고 em.find()로 Member를 찾아와 보면 다음과 같은 로그를 확인할 수 있다.

============= START ===============
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_6_0_,
        member0_.city as city2_6_0_,
        member0_.street as street3_6_0_,
        member0_.zipcode as zipcode4_6_0_,
        member0_.USERNAME as USERNAME5_6_0_,
        member0_.endDate as endDate6_6_0_,
        member0_.startDate as startDat7_6_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?

위 로그에서 알 수 있는 건 테이블 Address, favorite_food를 가져오지 않고 오직 Member만 가져왔다는 것이다. 이것으로 유추할 수 있는 점은 컬렉션 타입을 사용하면 기본 동작이 지연 로딩으로 동작한다는 것이다. 참고로 city, street, zipcode 데이터는 컬렉션 값 타입 외에도 임베디드 값 타입이 있어 Member 테이블에서 같이 조회가 되는 것이다.

그래서 컬렉션 값 타입 데이터를 가져와야 할 때는 favorite_food 테이블이나 Address 테이블에서 SELECT 하는 쿼리가 발생하면 가져온다. 이는 Address 뿐만 아니라 favorite_food 테이블에도 동일하게 적용된다.

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

 

 

수정

일반적으로 컬렉션을 수정할 때는 아래와 같이 수정하면 된다.

Address oldAdd = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", oldAdd.getStreet(),oldAdd.getZipcode() ));

객체의 경우엔 객체 안에 있는 값 하나를 변경하는 방법으로 변경하는 것이 아니라, 아예 새로운 객체를 만들어서 넣어줘야 한다. 

 

JPA 값 타입 - 값 타입과 불변 객체

JPA 게시글은 대부분 인프런의 김영한님의 강의인 '자바 ORM 표준 JPA 프로그래밍' 기반으로 내용을 정리했습니다. 값 타입과 불변 객체 값 타입 공유 참조 임베디드 타입 같은 값 타입을 여러 엔티

devhan.tistory.com

 

컬렉션을 String을 수정할 때는 별 다른 방법 없이 일반적으로 컬렉션을 사용하는 방법으로 수정해주면 된다.

// 치킨 -> 한식
// String이기 때문에 그냥 기존에 있던 치킨을 지우고 한식을 추가해주는 방법 밖에는 없다.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");

 

address의 경우에는 불변 타입인 String이 아니라 제네릭이 Address로 제한되어있다. 이 경우에는 전 글에의 '불변 객체'에 대해 잘 이해했으면 위와 비슷한 방법으로 진행되어야 함을 알 수 있을 것이다.

// old_1 -> newCity1로 변경
findMember.getAddressHistory().remove(new Address("old_1","street", "123"));
findMember.getAddressHistory().add(new Address("newCity1","street", "123"));

다만 String과는 다르게 새로운 객체를 만들어서 remove()시켰는데 이는 remove() 자체가 기본적으로 equals()로 동작하기 때문이다. 즉, 기존에 있는 요소를 찾기 위해 equals() 동작해 객체의 올바른 비교를 위해 Address.class 파일에 Override 된 hasCode가 꼭 존재해야 한다.

// Address.class

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Address address = (Address) o;
    return Objects.equals(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode);
}

@Override
public int hashCode() {
    return Objects.hash(city, street, zipcode);
}

 

실행시켜보면 아래와 같은 쿼리를 볼 수 있다.

Hibernate: 
    /* delete collection hellojpa.Member.addressHistory */ delete 
        from
            ADDRESS 
        where
            MEMBER_ID=?
            
            
Hibernate: 
    /* insert collection
        row hellojpa.Member.addressHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hellojpa.Member.addressHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode) 
        values
            (?, ?, ?, ?)

? 뭔가 싶다. 조회하고 주소 값을 변경했을 뿐인데 갑자기 현재의 MEMBER_ID로 저장된 ADDRESS 데이터를 모두 날려버린다. 그러고 나서 두 번의 INSERT 쿼리가 전송된다. INSERT 쿼리의 주인공들은 old_1의 데이터는 지워졌고, old_2가 남았다. 그리고 방금 새로 수정한 newCity1의 쿼리가 전송된 것이다. 그냥 아예 MBMER_ID와 연관된 주소 데이터를 다 지워버리고 남아있는 데이터들을 다시 넣는 것이다.

뭔가 결론적으로 의도대로 되긴 됐지만 찝-찝하다.

 

값 타입 컬렉션의 제약사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. 이게 기본 동작이다.
    이 위험을 해결하는 방법이 @OrderColumn()을 사용하여 컬렉션 값 타입으로 생성된 테이블에 식별자와 기본키를 제대로 넣어주는 방법을 사용하면 INSERT 쿼리가 아닌 UPDATE 쿼리가 전송되긴 하는데 이 방법을 사용해도 원하는 대로 동작하지 않을 때가 있어 이렇게 사용할 거면 그냥 사용하지 않는 것이 좋다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다.
    null 입력 X, 중복 저장 X

 

값 타입 컬렉션 대안

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

AddressEntity.class

@Entity
@Table(name = "ADDRESS")
public class AddressEntity {

    @Id @GeneratedValue
    private Long id;

    private Address address;

    public AddressEntity() {
    }

    public AddressEntity(Address address) {
        this.address = address;
    }

    public AddressEntity(String city, String street, String zipcode) {
        this.address = new Address(city, street, zipcode);
    }
    
    //...getter/setter 생략
}

 

Member.class 수정

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

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

 

코드를 수정하고 실행하면 아까 위에서 봤던 테이블과 달리 ID라는 컬럼이 생긴다.

반응형
저작자표시 비영리 변경금지 (새창열림)
'공부/JPA' 카테고리의 다른 글
  • JPA 객체지향 쿼리 언어(JPQL) - 기본 문법과 쿼리 API
  • JPA 객체지향 쿼리 언어 - JPQL, QueryDSL 등 소개
  • JPA 값 타입 - 값 타입의 비교
  • JPA 값 타입 - 값 타입과 불변 객체
데부한
데부한
어차피 할 거면 긍정적으로 하고 싶은 개발자
    반응형
  • 데부한
    동동이개발바닥
    데부한
  • 전체
    오늘
    어제
    • 분류 전체보기 (307)
      • 방통대 컴퓨터과학과 (27)
        • 잡담 (9)
        • 3학년1학기 (17)
      • 프로젝트 및 컨퍼런스 회고 (1)
        • 프로젝트 (4)
        • 한이음 프로젝트 (0)
        • 회고 (3)
      • 공부 (165)
        • Spring (37)
        • JPA (71)
        • 인프런 워밍업 클럽_BE (10)
        • Java (6)
        • React.js (27)
        • 넥사크로 (11)
        • 기타 (3)
      • 알고리즘 (85)
        • 알고리즘 유형 (10)
        • 알고리즘 풀이 (57)
        • SQL 풀이 (18)
      • 에러 해결 (13)
      • 잡담 (7)
        • 국비교육 (2)
        • 구매후기 (5)
        • 진짜 잡담 (0)
  • 블로그 메뉴

    • Github
    • Linkedin
    • 홈
    • 방명록
    • 글쓰기
    • 관리
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    토비의스프링부트
    JPA
    SQL
    Spring
    프로그래머스
    oracle
    자바스크립트
    스프링부트
    운영체제
    넥사크로
    Java
    RESTful
    코딩테스트
    인프런
    프론트엔드
    개발자
    방통대
    springboot
    알고리즘
    토이프로젝트
    SpringBoot를 이용한 RESTful Web Service 개발
    egov
    IT
    QueryDSL
    에러해결
    기출문제
    전자정부프레임워크
    백준
    MSA
    react
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
데부한
JPA 값 타입 - 값 타입 컬렉션
상단으로

티스토리툴바