프레임워크(Framework)/JPA

[JPA] 값 타입 컬렉션 - 값 타입 (4)

잇트루 2022. 12. 11. 22:47
반응형
본 내용은 온라인 강의 사이트 인프런의 김영한 님의 강의 내용이 포함되어 있습니다.
'자바 ORM 표준 JPA 프로그래밍 - 기본편'
 

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

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

www.inflearn.com

 

Intro

JPA의 데이터 타입을 분류하면 엔티티 타입과 값 타입으로 구분할 수 있다.

 

엔티티 타입

  • @Entity로 정의하는 객체
  • 데이터가 변해도 식별자를 통해 지속해서 추적할 수 있다.
  • 예) 회원 엔티티의 키나 나이 등의 값을 변경해도 식별자로 인식 가능

 

값 타입

  • int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
  • 식별자가 없고 숫자나 문자 같은 속성만 있어 변경 시 추적이 불가능하다.
  • 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체

 

값 타입은 다음 3가지로 나눌 수 있다.

  • 기본값 타입 : 자바 기본 타입, 래퍼 클래스, String
    • 자바에서 제공하는 기본 데이터 타입

 

  • 임베디드 타입(Embedded type, 복합 값 타입) : 좌표, 포지션 등
    • JPA에서 사용자가 직접 정의한 값 타입

 

  • 컬렉션 값 타입(Collection value type) : 기본값 타입이나 임베디드 타입을 저장할 수 있는 것
    • 하나 이상의 값 타입을 저장할 때 사용

 

값 타입 컬렉션

값 타입을 하나 이상 저장하려면 컬렉션에 보관하고, @ElementCollection, @CollectionTable 어노테이션을 사용하여 값 타입 컬렉션을 구현할 수 있다.

  • 데이터베이스는 컬렉션과 같은 데이터를 테이블에 저장할 수 없다.
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.

값 타입 컬렉션 사용

위 이미지와 같이 데이터베이스 테이블을 만들기 위해 값 타입 컬렉션을 정의한 코드이다.

// Member 클래스
@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @Embedded
    private Address homeAddress;

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

    ...
}

// Address 클래스
@Embeddable
public class Address {

    @Column(name = "city")
    private String city;
    private String street;
    private String zipcode;

    ...
}
  • favoriteFoods와 addressHistory에 @ElementCollection 어노테이션을 지정한다.
  • favoriteFoods는 String 타입의 Set 컬렉션
    • 관계형 데이터베이스는 컬렉션을 포함할 수 없음
    • @CollectionTable 어노테이션을 통해 추가할 테이블을 매핑해야 한다.
    • joinColumns 속성을 통해 MEMBER_ID와 조인할 수 있도록 한다.
    • 값으로 사용되는 컬럼이 하나면 @Column을 통해 컬럼명을 지정할 수 있다.

 

  • addressHistoy는 Address 타입의 List 컬렉션
    • @CollectionTable 어노테이션을 통해 별도의 테이블을 매핑해야 한다.
    • joinColumns 속성을 통해 MEMBER_ID와 조인할 수 있도록 한다.

 

값 타입 컬렉션 저장

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

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

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

em.persist(member);
  • em.persist(member) 하나만 실행해도, FAVORITE_FOOD와 ADDRESS 테이블에 값이 저장된다.
  • 값 타입 컬렉션들은 다른 테이블임에도 불구하고 생명주기가 Member 엔티티에 속하게 된다.
  • em.persist(member) 한 번 호출에 7번의 INSERT SQL이 실행된다.
    • member를 저장하기 위한 INSERT SQL 1번
    • homeAddress는 컬렉션이 아닌 임베디드 값 타입이므로 member를 저장하기 위한 SQL에 포함
    • favoriteFoods를 저장하기 위한 INSERT SQL 4번
    • addressHistory를 저장하기 위한 INSERT SQL 2번

 

값 타입 컬렉션 조회

값 타입 컬렉션은 조회할 때 페치 전략을 선택할 수 있다.(기본 값 LAZY)

System.out.println("===============START===============");
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);
}

 

출력

===============START===============
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_7_0_,
        member0_.city as city2_7_0_,
        member0_.street as street3_7_0_,
        member0_.zipcode as zipcode4_7_0_,
        member0_.USERNAME as username5_7_0_
    from
        Member member0_ 
    where
        member0_.MEMBER_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 = old1
address = old2
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 = 치킨
favoriteFood = 피자
  • 값 타입 컬렉션의 조회는 기본이 지연 로딩(LAZY) 전략이다.
  • em.find(Member.class, member.getId()); 실행 시 값 타입 컬렉션을 바로 조회하지 않는다.
    • addressHistory 조회 시 해당 값 타입 컬렉션 테이블 조회 SQL 발생
    • favoriteFoods 조회 시 해당 값 타입 컬렉션 테이블 조회 SQL 발생

 

값 타입 컬렉션 수정

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

Member findMember = em.find(Member.class, member.getId());

// 임베디드 값 타입 수정
Address old = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", old.getStreet(), old.getZipcode()));

// 기본 값 타입 컬렉션 수정
Set<String> favoriteFoods = findMember.getFavoriteFoods();
favoriteFoods.remove("치킨");
favoriteFoods.add("김치볶음밥");

// 임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = findMember.getAddressHistory();
addressHistory.remove(new Address("old1", "street", "10000"));
addressHistory.add(new Address("newCity1", "street", "10000"));

임베디드 값 타입 수정

  • homAddress는 임베디드 값 타입으로 MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE 한다.
  • Member 엔티티 수정하는 것과 동일하다.

 

기본 값 타입 컬렉션 수정

  • 기본 값 타입 컬렉션을 수정하기 위해서는 수정할 값을 제거하고, 새로운 값을 추가해야 한다.
  • 치킨에서 김치볶음밥으로 수정하기 위해 치킨을 제거하고 김치볶음밥을 추가했다.

 

임베디드 값 타입 컬렉션 수정

  • 값 타입은 불변해야 하기 때문에 기존 주소를 삭제하고 새로운 주소를 등록한다.
  • 임베디드 값 타입 컬렉션을 수정하기 위해서는 equals() 메서드 재정의와 hashCode() 메서드를 구현해야 한다.

 

값 타입 컬렉션의 제약사항

값 타입은 엔티티와 다르게 식별자 개념이 없다. 따라서 값을 변경하게 되면 데이터베이스에 저장된 원본 데이터를 찾기 어렵다.

 

값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.

  • 위 코드에서는 마치 수정할 값만 제거하고 추가한 것처럼 보이나 실제 SQL 실행은 다르게 나타난다.
  • 값 타입 컬렉션 테이블에 존재하는 모든 데이터를 제거한 뒤, 값 타입 컬렉션에 남아있는 현재 값을 다시 저장하는 구조

 

값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다.

  • null 입력 X, 중복 저장 X

 

값 타입 컬렉션 대안

값 타입 컬렉션의 변경 사항에 대한 제약사항 때문에 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.

  • 일대다 관계를 위한 엔티티를 만들고, 해당 엔티티에서 값 타입을 사용
  • 영속성 전이(Cascade) + 고아 객체 제거를 사용하여 값 타입 컬렉션처럼 사용

 

Address 엔티티 생성

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

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

    private Address address;

    ...
}

 

일대다 매핑

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @Embedded
    private Address homeAddress;

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

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

    ...
}
  • 값 타입 컬렉션은 수정할 필요가 없는 정말 단순한 경우에만 사용하는 것이 좋다.
반응형