JAVA/JPA 학습기록

[JPA] 값 타입 컬렉션, 임베디드 타입

kth990303 2021. 5. 26. 23:51
반응형

만약 객체가 다른 객체의 테이블을 참조할 필요는 없는데,

해당 객체가 여러 가지 값을 가지고 있어야 하는 경우는 어떻게 할까?

 

예를 들자면, kth990303 학생과 aru0504 학생이 있다고 하자.

kth990303과 aru0504는 같이 점심을 먹기로 했다.

그러기 위해선 아래 두 가지를 알아야 한다.

  1. kth990303과 aru0504는 각각 언제, 어디서 점심을 먹을지
  2. kth990303과 aru0504는 점심으로 무엇을 먹기를 선호하는지

1번을 해결하기 위해서는 각각 일하는 장소와 시간대를 알아야할 것이다.

그러기 위해선 두 객체(Member 객체)에 이 정보들이 있어야할 것이다. 여기서 임베디드 타입이 사용된다.

 

2번을 해결하기 위해서는 둘이 의견을 통일하여 점심 메뉴를 골라야 한다.

그러나 둘 다 선택장애가 있어 점심메뉴를 쉽게 고르기 어려웠던 그들은,

서로 좋아하는 메뉴들을 모두 말해보고 결정해보기로 했다.

 

kth990303은 치킨, 피자, 햄버거를 좋아하고, aru0504는 피자, 쌀국수, 보쌈을 좋아한다.

이런 경우는 객체 필드의 자료형이 List이거나 Set이어야 할 것이다.

그렇지 않으면 kth990303은 치킨만을, aru0504는 피자만을 말해버릴 수도 있기 때문이다.

이번 시간엔 List, Set을 필드로 가질 때 jpa에서 어떻게 일어나는지 요약해볼 것이다.


인프런 김영한님의 강좌를 듣고

요약한 포스팅입니다.

수정할 점은 댓글로 피드백 부탁드립니다 :)



임베디드 타입 (@Embeddable, @Embedded)

복합값 타입이라고도 불리는 임베디드 타입은, 새로운 값 타입을 직접 지정하기 위해 생겨난 타입이다. 엔티티 단위가 아닌 값 타입이다. 여기서 값 타입이라 함은, 객체 단위가 아닌 말 그대로 name, age와 같은 값타입이라는 것이다. 따라서 임베디트 타입의 생명주기는 객체의 생명주기에 소속된다.

 

아까 예시를 기억하는가?

kth990303과 aru0504가 같이 점심을 먹게 해주기 위해 우리가 시뮬레이션 프로그램을 만들어줄 것이다. (솔직히 이 프로그램이 없어도 이 둘이 먹는데엔 지장이 없을 것 같지만...ㅎㅎ)

 

kth990303과 aru0504는 Member 객체의 인스턴스이다.

Member 객체는 멤버 번호, 이름 정도만 있으면 될 듯하다.

그리고 여기에 우리는 추가로 

  1. kth990303과 aru0504는 각각 언제, 어디서 점심을 먹을지

위 정보를 기록해주기 위해 아래처럼 Member 클래스를 만들 것이다.

 

Member1.class

@Entity
public class Member1 {
    @Id @GeneratedValue
    @Column(name="MEMBER_ID")
    private Long id;

    @Column(name="USERNAME")
    private String username;

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address workAddress;
}

??? 일하는 시간이랑 장소를 적어준 건 좋은데, LocalDateTime, String 자료형이 아닌 이상한 자료형이 있다!

객체지향 언어 배우면서 클래스 만들면서 우리만의 객체를 만들어준 것을 기억하는가?

마찬가지로 우리가 따로 자료형을 만들어준 것이다!

다만, 여기서 Period, Address는 클래스 타입이 아닌 값타입이라는 것!

workPeriod, workAddress는 Member 클래스에 내장된 타입이므로 @Embedded를 붙여준다.

 

여기서 우리가 왜 따로 Period형, Address형 임베디드 타입을 만들어줬느냐?

(임베디드 타입에 대해 쓰는 포스팅이니까 만들었지)

가 아니고, 중복을 막기 위해, 그리고 가독성을 위해 만들었다고 보면 된다.

 

Period.class

@Embeddable
public class Period {

    private String startDate;
    private String endDate;

    public Period() {
    }

    public Period(String startDate, String endDate) {
        this.startDate = startDate;
        this.endDate = endDate;
    }
}

Address.class

@Embeddable
public class Address {
    private String city;
    private String street;

    public Address() {

    }

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

이렇게 새로운 값타입을 지정해주기 위해 임베디드 타입을 생성해주었다.

임베디드 타입 클래스엔 반드시 @Embeddable 어노테이션을 붙여줘야 하며, 반드시 기본생성자가 존재해야 한다!

두 개 이상의 생성자가 있을 땐, 디폴트값으로 기본생성자가 생기지 않으므로 위와 같이 만들어줘야 한다. (아니면 lombok을 이용하자. 편함)

 

메인은 아래와 같이 생성하고 실행해보자.

	try{
            Member1 memberA = new Member1();
            memberA.setUsername("kth990303");
            memberA.setWorkPeriod(new Period("09:00", "12:30"));
            memberA.setWorkAddress(new Address("Seoul", "streetA"));

            Member1 memberB = new Member1();
            memberB.setUsername("aru0504");
            memberB.setWorkPeriod(new Period("10:00", "12:00"));
            memberB.setWorkAddress(new Address("Seoul", "streetA"));

            em.persist(memberA);
            em.persist(memberB);

            tx.commit();
        } catch(Exception e){
            e.printStackTrace();
            tx.rollback();
        } finally{
            em.close();
        }

참고로 Address, Period 테이블은 생성되지 않는다.

Member 테이블에 원하는대로 값이 잘 들어감을 조회할 수 있다.

이로써 kth990303과 aru0504는 "Seoul, Street A번가"에서 두 사람 모두 일이 끝나는 12시 반쯤 점심을 먹으면 될 것 같다!


값 타입 컬렉션(List, Set) @ElementCollection, @CollectionTable

그럼 이제 아래 문제까지 해결해보도록 하자.

아까 예시에서

  1. kth990303과 aru0504는 각각 언제, 어디서 점심을 먹을지
  2. kth990303과 aru0504는 점심으로 무엇을 먹기를 선호하는지

를 기억하는가? 1번은 해결됐으므로 2번 정보를 입력해보도록 하자.

 

kth990303은 치킨, 피자, 햄버거를, aru0504는 피자, 쌀국수, 보쌈을 좋아한다고 했다.

이 정보들을 입력해주기 위해 우리는 Member 객체 필드에 아래 코드를 추가할 것이다.

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

Set으로 favoriteFoods 목록들을 담아주는 코드이다.

물론 위와 같이 진행하지 않고 일대다 연관관계 매핑을 이용하여 엔티티를 만들어주어도 된다.

일대다 연관관계 매핑은 이전에 배웠으므로, 우리는 위와 같이 만들어보도록 하자.

어? 그런데 저 위에 붙어있는 괴상한 어노테이션들은 뭘까?

 

이렇게 정보가 여러가지인 경우는 객체 필드엔 Set, List로 담을 수 있지만, db에는 정규화 규칙때문에 담을 수가 없다.

따라서 JPA에서 제공하는 @CollectionTable 어노테이션을 이용해 별도의 테이블을 만들어주고 관리하게 해주기 위해 붙인 어노테이션이다. joinColumn으로 연관관계 주인이 Member 클래스임을 알 수 있으며, 테이블명은 FAVORITE_FOOD 임을 알 수 있다.

@ElementCollection 어노테이션으로 값 타입 컬렉션을 필드로 가진다는 사실 또한 명시해주어야 한다.

 

이제 메인에 아래 코드를 넣고 실행해보자.

memberA.getFavoriteFoods().add("치킨");
memberA.getFavoriteFoods().add("피자");
memberA.getFavoriteFoods().add("햄버거");

memberB.getFavoriteFoods().add("피자");
memberB.getFavoriteFoods().add("쌀국수");
memberB.getFavoriteFoods().add("보쌈");

em.persist(memberA);
em.persist(memberB);
            
em.flush();
em.clear();
Member1 kth990303 = em.find(Member1.class, memberA.getId());
Member1 aru0504 = em.find(Member1.class, memberB.getId());
System.out.println("================ what's favorite food? =======================");
Set<String> favoriteFoods = kth990303.getFavoriteFoods();
Set<String> favoriteFoods1 = aru0504.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
    System.out.println("kth990303's favoriteFood = " + favoriteFood);
}
for (String favoriteFood : favoriteFoods1) {
    System.out.println("aru0504's favoriteFood = " + favoriteFood);
}

테이블 생성되고 결과 잘 됨을 확인

연관관계 매핑도 Member랑 잘 됐고, 결과 또한 잘 나옴을 확인할 수 있다.

둘 다 좋아하는 피자를 먹으면 되겠다!

 

특이사항으로 아래 결과 또한 살펴보자.

================ what's favorite food? =======================
Hibernate: 
    select
        favoritefo0_.MEMBER_ID as member_i1_3_0_,
        favoritefo0_.FOOD_NAME as food_nam2_3_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.MEMBER_ID=?
kth990303's favoriteFood = 치킨
kth990303's favoriteFood = 햄버거
kth990303's favoriteFood = 피자
Hibernate: 
    select
        favoritefo0_.MEMBER_ID as member_i1_3_0_,
        favoritefo0_.FOOD_NAME as food_nam2_3_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.MEMBER_ID=?
aru0504's favoriteFood = 보쌈
aru0504's favoriteFood = 쌀국수
aru0504's favoriteFood = 피자

잘 보면

 

System.out.println("================ what's favorite food? =======================");

 

이 코드가 실행된 이후에 FAVORITE_FOOD 테이블에서 쿼리를 날려 결과를 조회함을 확인할 수 있다.

이를 통해 값 타입 컬렉션에 기본적으로 지연로딩 전략이 사용됨을 알 수 있다!

따라서 값 타입 컬렉션은 영속성 전에 고아 객체 제거 기능, 즉 cascade + orphanRemoval=true 기능을 필수로 가지고 있다고 보면 된다.


사실 아직 내용이 다 끝난 것이 아니다.

값 타입 컬렉션 정보들을 수정할 때, 그리고 값 타입과 엔티티 타입의 특징의 차이점으로 인한 주의점들 또한 명심해야 한다. 이 부분은 다음 포스팅에 작성할 예정이다. 귀차니즘이 없다면 아마 작성할 것이다.  귀찮아서 아래에 추가한다...

 

+) 수정은 반드시 새로운 값을 만들어 집어넣어야 한다. ex. new Address 이런식으로 생성자로 생성한 후에 대입하면 된다. 값타입 특성상 절대 setter로 수정하는 일이 없어야 하기 때문이다.

 

오랜만에 JPA 하니까 알차고 좋은 듯 하다.

반응형