RE-Heat 개발자 일지

[JPA] [9] 값 타입 (하편) 본문

백엔드/JPA

[JPA] [9] 값 타입 (하편)

RE-Heat 2023. 8. 13. 23:57

https://www.inflearn.com/course/ORM-JPA-Basic

 

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

초급자를 위해 준비한 [웹 개발, 백엔드] 강의입니다. JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자

www.inflearn.com

인프런 김영한 님의 강의를 듣고 작성한 글입니다.

 

[4] 값 타입의 비교

값 타입은 인스턴스가 달라도 값이 같으면 같은 것으로 봐야 한다.
//값 타입 비교
int a = 10;
int b = 10;
System.out.println("a == b: " + (a == b)); // true

//객체 타입 비교
Address addr1 = new Address(“서울시”) 
Address addr2 = new Address(“서울시”)
System.out.println("addr1 == addr2: " + (addr2 == addr2)); // false
  • 값 타입은 인스턴스가 달라도 값이 같으면 같은 것으로 본다.
  • 반면 객체 타입은 값이 같아도 주소값이 달라 == 비교하면 false로 반환한다.

■ 동일성 vs 동등성

  • 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용 [primitive 타입]
  • 동등성(equivalence) 비교: 인스턴스의 값을 비교, equals() 사용 [임베디드 타입]
  • 값 타입은 a.equals(b)를 사용해 동등성 비교를 해야 한다.
  • 값 타입의 equals() 메소드를 적절하게 재정의해야 한다. 추가로 hashcode()도 지정해야 한다.
    • 롬복을 쓰면 @EqualsAndHashCode 애노테이션을 붙여주면 된다.
    • 아니면 인텔리J에서 자동으로 만들어주는 equals()를 쓰면 된다.

참고] hashCode() : 두 객체가 같은 객체인지 동일성을 비교하는 연산자

 

결론 : 직접 정의한 값 타입은 Equals()를 만들고 동등성 비교를 해야 한다.

 

[5] 값 타입 컬렉션

값 타입을 하나 이상 저장할 때 사용한다. 

• 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.

• 컬렉션은 개념적으로 일대다이기 때문에 저장하기 위한 별도의 테이블이 필요함

▶ 예제

MemberEmb

@Entity
@Getter
@Setter
@NoArgsConstructor
public class MemberEmb {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @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<>();
}
  • @ElementCollection으로 값 타입 컬렉션이라는 것을 명시한다.
  • @ColletionTable로 컬렉션 테이블 이름을 지정하고, @JoinColumn으로 Member와의 외래키를 지정한다.
  • FavoriteFoods에만 @Columb이 들어간 이유는?
    • Address엔 필드 이름이 정해져 있지만, favoriteFoods는 제너릭에 String이 들어가 있어 필드의 이름을 지정하기 위해 @Column을 썼다.

 

▶ 값 타입 컬렉션

① 저장

JpaMain - 저장

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

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

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

em.persist(member);
            
tx.commit();

 

  • em.persist()로 Insert SQL이 Member 1개, FavoriteFood 3개, Address 2개 총 6개 날아간다.
  • 값 타입 컬렉션 persist 해주지 않아도 member가 persist 되면 알아서 SQL이 날아간다.
    • 값 타입이라 라이프 사이클이 없기 때문이다. 
    • 이는 일대다 연관관계에서 Cascade=ALL + orphanRemoval=true로 설정한 것과 유사하다.
    • 따라서 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.

② 조회

  • 우선 Member만 가져왔다. 이는 컬렉션 값 타입이 지연 로딩 전략을 취한다는 것을 의미한다.

  • 실제로 address.getCity()나 favoriteFoods를 호출해야 그때 맞춰 select 쿼리를 날리는 것을 확인할 수 있다.

③ 수정

1. 기본 개념

//homeCity -> newCity
//틀린 방식
//findMember.getHomeAddress().setCity("newCity");

Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
  • 일반적으로 컬렉션을 수정할 땐 위 방식을 쓴다.
  • 객체 안 값을 변경 X, 새로운 객체를 만들어서 통째로 갈아 끼워야 한다.

2. Set<String> 수정

//치킨 => 한식
//String은 통째로 갈아끼워야 함.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
  • 불변객체인 String을 수정할 땐 '치킨'을 지우고 '한식'을 추가해 주는 방법 밖에 없다.
  • String이 값 타입이므로 업데이트가 불가능(setter)하다. 통째로 바꿔야 한다.

 

//주소 바꾸기
//remove()안에 equals로 확인하기 때문에 equals hashcode가 중요하다!
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));

remove()를 할 때 객체가 맞는지 equals(), hashcode()로 확인하므로 이에 대한 재정의가 중요하다.

 

 

■ 값 타입 컬렉션의 제약사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
    • Address는 id(식별자)가 없으므로 값이 중간에 변경되면 어떤 게 변경되었는지 알기 힘들다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다: null 입력 X, 중복 저장 X
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.

  • 대안 : @OrderColumn()을 사용
    • @OrderColumn으로 값 타입 컬렉션으로 생성된 테이블에 식별자·기본키를 제대로 넣어주면 insert가 아닌 update 쿼리가 날아간다. 하지만, 이런 방법도 원하는 대로 동작하지 않을 때가 있어 위험하다.
  • 결론 : 값 타입 컬렉션은 정말 단순할 때(추적 X, 업데이트 X)만 사용하고 가급적 사용하지 않는 게 낫다.
    • 예시) 셀렉트 박스에서 [치킨, 피자] 등 여러 메뉴 선택할 때

 

■ 값 타입 컬렉션의 대안

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

 

AddressEntity

@Entity
@Getter
@Setter
@Table(name = "ADDRESS")
@NoArgsConstructor
@AllArgsConstructor
public class AddressEntity {
    @Id
    @GeneratedValue
    private Long id;

    private Address address;

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

 

MemberEmb

//    @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<>();
  • 값 타입 컬렉션 대신 일대다 매핑을 하고 CascadeType.ALL + orphanRemoval = true로 읽기만 가능하게 만듦

DB 확인

  • Address에 자체적인 Id가 생긴 걸 알 수 있다.

 

■ 정리

  • 엔티티 타입의 특징
    • 식별자 O
    • 생명 주기 관리
    • 공유
  • 값 타입의 특징
    • 식별자 X
    • 생명 주기를 엔티티에 의존
    • 공유하지 않는 것이 안전(복사해서 사용)
    • 불변 객체로 만드는 것이 안전

 

[6] 실전 예제 6 - 값 타입 매핑

 

  • city, street, zipcode를 묶어 값 타입으로
  • Member와 Delivery에 있던 city, street, zipcode를 Address로 대체

  • @Column(length=10)로 글자 수를 제한하거나 fullAddress() 같은 메소드를 만들어 사용하면 값 타입의 장점을 극대화할 수 있다.
  • equals메소드를 오버라이드 할 땐 getter로 필드에 접근해야 한다. 이렇게 하지 않으면 프록시에 접근할 땐 값을 얻어올 수 없기 때문이다.