[JPA] 9. 값 타입

DAUN JO·2021년 11월 10일
0

JPA

목록 보기
8/11
post-thumbnail

자바 ORM 표준 JPA 프로그래밍 공부 기록


  • 기본 값 타입
    - 자바 기본 타입
    • 래퍼 클래스
    • String
  • 임베디드 타입
  • 컬렉션 값 타입


📍 기본 값 타입

예) String name, int age

  • 생명주기를 엔티티에 의존한다.
    따라서 엔티티를 제거하면 name, age도 제거된다.

  • 값 타입은 공유하면 안 된다.
    기본 타입은 항상 값을 복사함.
    Integer같은 래퍼 클래스나 String 같은 특수한 클래스에 경우 객체지만 자바 에서 기본 타입처럼 사용할 수 있게 지원. 이들은 객체지만 변경 X



📍 임베디드 타입

새로운 값 타입을 직접 정의해서 사용하는 것
=> 직접 정의한 임베디드 타입도 값 타입이다

  • @Embedded : 값 타입을 사용하는 곳
  • @Embeddable : 값 타입을 정의하는 곳
    기본 생성자 필수

💭 임베디드 타입의 장점

  • 재사용
  • 높은 응집도
  • 해당 값 타입만 사용하는 의미있는 메소드 만들 수 있음
  • 값 타입을 소유한 엔티티에 생명주기를 의존함


@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String userName;

    @Embedded
    private Period workPriod;

    @Embedded
    private Address homeAddress;
	
    ...
    
    

@Embeddable
public class Address {
    //집 주소 표현
    private String city;
    private String street;
    private String zipcode;
    
    ...
}

@Embeddable
public class Period {
    //근무기간
    private LocalDate startDate;
    private LocalDate endDate;
    
    ...

💭 @AttributeOverride : 속성 재정의

임베디드 타입에 정의한 매핑 정보를 재정의하려면 @AttributeOverride를 사용하면 된다. 예를 들어 Member에게 주소가 2개 필요하다면

@Entity
public class Member {

    ...

    @Embedded
    private Address homeAddress;
    @Embedded
    private Address companyAddress;
    ...

위 상황에서 문제는 테이블에 매핑하는 컬럼명이 중복되는 것이다. (각 Address 안의 컬럼 명이 동일)

이 때는 @AttributeOverride을 사용해서 매핑 정보를 재정의한다.

@Entity
public class Member {

    ...

    @Embedded
    private Address homeAddress;
    @Embedded
    @AttributeOverrides({
    	@AttributeOverride(name="city", column=@Column(name="COMPANY_CITY")),
        @AttributeOverride(name="street", column=@Column(name="COMPANY_STREET")),
        @AttributeOverride(name="zipcode", column=@Column(name="COMPANY_ZIPCODE"))
    })
    private Address companyAddress;
    ...



📍 값 타입과 불변 객체

💭 값 타입 공유 참조

임베디드 타입은 여러 엔티티에서 공유하면 위험하다

            Address address = new Address("TEST","TEST","TEST");

            Member m1 = new Member();
            m1.setUserName("TEST1");
            m1.setHomeAddress(address);
            em.persist(m1);

            Member m2 = new Member();
            m2.setUserName("TEST2");
            m2.setHomeAddress(address);
            em.persist(m2);

            m1.getHomeAddress().setCity("NewCity");

member1의 주소만 "NewCity"로 변경되길 기대했지만 멤버2의 주소도 같이 변경되었다. 둘이 같은 Address를 참조하기 때문이다. 이러한 공유 참조로 인해 발생하는 버그는 찾기 어렵다. 이런 부작용을 막기 위해서는 값을 복사해서 사용하면 된다.


💭 값 타입 복사

            Address address = new Address("TEST","TEST","TEST");

            Member m1 = new Member();
            m1.setUserName("TEST1");
            m1.setHomeAddress(address);
            em.persist(m1);

            Address newAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());

            Member m2 = new Member();
            m2.setUserName("TEST2");
            m2.setHomeAddress(newAddress);
            em.persist(m2);


            m1.getHomeAddress().setCity("NewCity");

기존 address를 복사하여 newAddress를 새로 만들어서 위 과정을 반복하면 의도한대로 동작한다.


객체 대입 시 마다 인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있다. 그러나 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 수 없다. 자바는 그저 기본타입이면 값을 복사해서 넘기고 객체면 참조를 넘긴다.

객체의 공유 참조는 피할수가 없다.
=> setter 사용을 하지 말아야 한다.

💭 불변 객체

  • 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없는 객체

객체를 불변하게 만들면 부작용을 차단할 수 있다.
값 타입은 가능하면 불변 객체로 설계한다.
=> 생성자만 값을 설정하고 setter 생성 X



📍 값 타입의 비교

• 동일성 비교: 인스턴스의 참조 값을 비교, ==
• 동등성 비교: 인스턴스의 값을 비교, equals()

값 타입은 인스턴스가 달라도 그 안이 같으면 같은 것으로 봐야한다. 따라서 값 타입을 비교할 때는 equals()를 사용해서 동등 비교를 해야한다.


@Embeddable
public class Address {
    //집 주소 표현
    private String city;
    private String street;
    private String zipcode;
    
    ...
    
    
    @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);
    }
}



📍 값 타입 컬렉션

값 타입을 하나 이상 저장하려면 컬렉션에 보관하면 된다

  • @ElementCollection, @CollectionTable 어노테이션을 사용
  • 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다
  • 값 타입 컬렉션은 조회시 LAZY가 기본이다.
Member m = new Member();
m.setUserName("TEST");
m.setHomeAddress(new Address("TEST","TE","ST"));

m.getFavoriteFoods().add("Chicken");
m.getFavoriteFoods().add("Pizza");

m.getAddressHistory().add(new Address("TEST2","TE2","ST2"));
m.getAddressHistory().add(new Address("TEST3","TE3","ST3"));

em.persist(m);


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

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

출력

=======================================
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_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?

지연 로딩이 적용된 것을 확인할 수 있다.


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

List<Address> addressHistory = findMember.getAddressHistory();

for(Address address : addressHistory){
	System.out.println(address.getCity());
}

System.out.println("=======================================");

Set<String> favoriteFoods = findMember.getFavoriteFoods();
for(String food : favoriteFoods){
	System.out.println(food);
}

출력 확인

=======================================
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_ 
    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=?
TEST2
TEST3
=======================================
Hibernate: 
    select
        favoritefo0_.MEMBER_ID as MEMBER_I1_4_0_,
        favoritefo0_.FOOD_NAME as FOOD_NAM2_4_0_ 
    from
        FAVORITE_FOODS favoritefo0_ 
    where
        favoritefo0_.MEMBER_ID=?
Pizza
Chicken

위와 같이 실제 컬렉션을 사용할 때 SELECT SQL을 한번 씩 호출하는 것을 볼 수 있다.



💭 값 타입 수정

값 타입은 엔티티와 다르게 식별자라는 개념이 없으므로 변경하면 추적이 안된다.
따라서 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.


===================== homeAddress


//값타입은 완전히 교체
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
      
      
      
Hibernate: 
    /* update
        com.jpa.db.Member */ update
            Member 
        set
            city=?,
            street=?,
            zipcode=?,
            userName=? 
        where
            MEMBER_ID=?
            
            
            
            
===================== favoriteFoods                
            
            
//치킨=>한식
Set<String> favFoods = findMember.getFavoriteFoods();
findMember.getFavoriteFoods().remove("Chicken");
findMember.getFavoriteFoods().add("Korean Food");
         


Hibernate: 
    /* delete collection row com.jpa.db.Member.favoriteFoods */ delete 
        from
            FAVORITE_FOODS 
        where
            MEMBER_ID=? 
            and FOOD_NAME=?
Hibernate: 
    /* insert collection
        row com.jpa.db.Member.favoriteFoods */ insert 
        into
            FAVORITE_FOODS
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)
            
            
            
===================== addressHistory            
            
            
findMember.getAddressHistory().remove(new Address("TEST2","TE2","ST2"));
//equals를 제대로 구현해야 지워진다.
findMember.getAddressHistory().add(new Address("newCity2","TE2","ST2"));



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

// 2번 인서트 ??

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


❓❓ 근데 왜 Set<String> 의 경우에는 1번 delete하고 1번 insert가 될까? Set이라서?
List<String>으로 변경하면 1번 delete하고 2번 insert가 일어남.
그리고 List<Address>Set<Address>로 변경할 경우에는 또 1번 delete했다가 2번 insert가 일어난다.. 아무리 찾아봐도 모르겠다


아무튼 값 타입 컬렉션에 데이터가 많다면 성능에 문제가 생기므로 실무에서는 사용하면 ... 안 된다.
값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.



📍 엔티티 타입 vs 값 타입

💭 엔티티 타입

  • 식별자가 있다
  • 생명주기가 있다
  • em.persist로 영속화 한다
  • em.remove로 제거한다
  • 공유할 수 있다 (공유 참조)

💭 엔티티 타입

  • 식별자가 없다
  • 생명주기를 엔티티에 의존한다
  • 공유하지 않는 것이 안전하다 (불변 객체화 안전)
profile
🍕

0개의 댓글