본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
JPA

값 타입 - 값 타입 컬렉션(1)

by 방구석 대학생 2020. 10. 22.

"인프런 - 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 작성한 글 입니다."

www.inflearn.com/course/ORM-JPA-Basic#

 

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

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

www.inflearn.com

 

임베디드 타입과 같은 값 타입을 컬렉션에 담아서 사용할 수 있을까?

값 타입 컬렉션은 값 타입을 하나 이상 저장할 때 사용할수 있다.

그런데 값 타입을 컬렉션에 담아서 사용하는 경우 DB 테이블로 구현할 때 문제가 발생한다.

- 단순하게 값 타입이 하나일 때는 필드 속성으로 활용하여 테이블에 넣으면 되는데, 관계형 데이터베이스는 기본적으로 컬렉션을 담을 수 있는 구조가 없다.(데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.)

 

 

* 컬렉션은 결국 일대다 개념이다

: 그러므로 해당 컬렉션에 대해 별도의 테이블을 생성해야 한다.

(컬렉션 들은 일대다 개념이기 때문에 DB 에서 한 테이블에 넣을 수 있는 방법이 없다, 그렇기 때문에 일대다 로 풀어서 별도의 테이블로 만들어 내야 한다.)

- 임베디드 타입의 경우 해당 타입에 속하는 필드 속성들을 묶어서 하나의 기본 키로 만들어야 한다.

왜? 값 타입 테이블에서 식별자 id 값을 가져와서 기본 키로 사용하게 되면, 해당 테이블은 값 타입의 테이블이 아니라 Entity 가 되어버리기 때문이다.

- 값 타입 컬렉션의 테이블을 생성하기 위해 @ElementCollection, @CollectionTable 어노테이션을 사용할 수 있다.

 

- Member 도메인 클래스에서 임베디드 타입의 컬렉션을 별도의 테이블로 만들어보자.

- Member.java

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

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

 

@ElementCollection 어노테이션

- 값 타입 컬렉션임을 지정하는 어노테이션이다.

 

@CollectionTable 어노테이션

- 해당 컬렉션의 테이블을 생성하는 어노테이션이다.

- name 속성 : 테이블의 이름을 지정해 줄 수 있다.

- joinColumns 속성 : @JoinColumn 어노테이션을 통해 외래 키 값을 지정해 줄 수 있다.

 

위의 코드를 보면 favoriteFood 객체의 컬렉션 타입인 Set 컬렉션의 경우, 임베디드 타입을 Address 타입으로 가지는 List 컬렉션과 달리 컬렉션의 데이터 필드가 String 하나 밖에 없는 상태이기 때문에 @Column 어노테이션의 name 속성을 통해서 컬럼 명을 String 이 아닌 다른 이름으로 지정해 줄 수 있다.

(Address 타입의 경우 임베디드 타입 내부에 속해있는 값 타입들의 변수 명을 그대로 사용하면 된다 : city, street, zipcode)

 

애플리케이션을 실행해보면 아래와 같은 hibernate SQL 을 출력하는 것을 확인 할 수 있다.

- hibernate SQL

Hibernate: 
    
    create table Member (
       MEMBER_ID bigint not null,
        city varchar(255), // homeAddress field
        street varchar(255), // homeAddress field
        ZIPCODE varchar(255), // homeAddress field
        USERNAME varchar(255),
        endDate timestamp,
        startDate timestamp,
        TEAM_ID bigint,
        primary key (MEMBER_ID)
    )
Hibernate: 
    
    create table FAVORITE_FOOD ( //String 타입 Set 컬렉션 테이블 생성
       MEMBER_ID bigint not null,
        FOOD_NAME varchar(255)
    )
Hibernate: 
    
    create table ADDRESS ( // 임베디드 타입인 Address 타입 List 컬렉션 테이블 생성
       MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        ZIPCODE varchar(255)
    )

 

이번엔 아래의 코드를 작성하여 값 타입 컬렉션에 데이터를 삽입 한 후 애플리케이션을 실행해보자.

- JpaMain.java

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

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

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

em.persist(member);

 

 

애플리케이션을 실행해보면 아래와 같은 hibernate SQL 을 출력하는 것을 확인할 수 있다.

- hibernate SQL

Hibernate: // homeAddress 데이터 삽입
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (city, street, ZIPCODE, USERNAME, endDate, startDate, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?, ?)
Hibernate: // addressHisotory 데이터 삽입
    /* insert collection // 컬렉션에 데이터를 삽입한다.
        row hellojpa.Member.addressHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, ZIPCODE) 
        values
            (?, ?, ?, ?)
Hibernate: // addressHisotory 데이터 삽입 
    /* insert collection
        row hellojpa.Member.addressHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, ZIPCODE) 
        values
            (?, ?, ?, ?)
Hibernate: // favoriteFood 데이터 삽입 
    /* insert collection
        row hellojpa.Member.favoriteFood */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)
Hibernate: // favoriteFood 데이터 삽입 
    /* insert collection
        row hellojpa.Member.favoriteFood */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)
Hibernate: // favoriteFood 데이터 삽입 
    /* insert collection
        row hellojpa.Member.favoriteFood */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)

 

여기서 특이한 점은 값 타입 컬렉션을 따로 persist 하지 않고 member 만 persist 하니까 값 타입 컬렉션들이 자동으로 같이 persist 되었다.

즉, 다른 테이블인데도 불구하고 라이프 사이클이 같이 돌아갔다.(member 를 저장할 때 같이 들어간 것)

왜? 해당 Entity 에 속하는 값 타입이기 때문이다.

 

값 타입 컬렉션도 본인 스스로의 라이프 사이클이 없다.(모든 라이프 사이클이 member 에 소속 된다.)

즉, member 의 값을 바꾸거나 하면 같이 바뀐다는 뜻이다.

값 타입들은 별도로 update 하거나 persist 할 필요가 없다. 그냥 member 에서 값을 바꾸면 자동으로 update 된다.

일대다 연관관계 에서 영속성 전이 기능인 cascade 옵션을 ALL 로 넣고 고아 객체 옵션인 orphanRemoval 을 true 로 지정해 놓은 것과 비슷하다고 볼 수 있다.

(갑 타입 컬렉션은 영속성 전이, 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.)

 

여기서 find 메소드를 통해 member 객체를 조회할 경우 hibernate SQL 이 어떻게 출력될까?

- JpaMain.java

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

- hibernate SQL

===============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=?

 

위의 hibernate SQL 을 보면 member 테이블에만 select 쿼리를 전달한다는 것을 알 수 있다.

(이때, homeAddress 와 같이 컬렉션 테이블이 아닌 일반적인 임베디드 타입은 함께 select 된다.)

그 말은 즉, 컬렉션 테이블들은 모두 지연 로딩(LAZY) 이라는 뜻이다.

 

이제 아래의 코드를 추가로 작성한 후 애플리케이션을 실행해보자.

- JpaMain.java

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

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

- hibernate SQL

===============START================
Hibernate: // findMember 객체 데이터 select
    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=?
Hibernate: // 컬렉션 타입을 직접 호출하여야 해당 컬렉션 테이블에 대한 select 쿼리가 전달된다.(지연 로딩 - LAZY)
	// AddressHistory 컬렉션 데이터 select
    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 = old1
address = old2
Hibernate: // FavoriteFood 컬렉션 데이터 select
    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 = 피자

 

위의 hibernate SQL 을 보면, 출력문에서의 getCity() 메소드와 컬렉션 객체 변수 favoriteFoods 처럼 컬렉션 타입을 직접 사용하는 코드가 실행되면 지연 로딩(LAZY) 기능을 통해 그제서야 해당하는 컬렉션에 대해 select 쿼리가 DB 에 전달되는 것을 확인할 수 있다.

 

 

 

다음 글에서는 컬렉션 에서늬 값 타입 데이터를 수정해보자.