ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 값 타입 - 2
    JPA/JPA 기본 2023. 7. 11. 18:06

    값 타입의 비교

     

    기본 타입은 내용이 같으면 비교했을 때 true가 나오지만, 객체 타입은 "서울시"라는 값이 같더라도 비교를 하면 false가 나오게 된다. 래퍼런스(참조 값)을 비교하는데, 인스턴스가 다르기 때문에 false가 나오는 것이다.

     

     

    • 동일성 비교 : 인스턴스의 참조 값을 비교, == 사용
    • 동등성 비교 : 인스턴스의 값을 비교, equals() 사용
    • 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 함
    • 값 타입은 equals() 메서드를 적절하게 재정의

     

    그냥 equlas를 사용해서 a.equals(b)를 하면 false가 나온다. equals는 default 값이 == 비교이기 때문이고, 오버라이딩을 통해 재정의를 한 다음 사용해줘야 한다.


     

    값 타입 컬렉션

     

    값 타입 컬렉션이란 값 타입을 컬렉션에 담아서 사용하는 것을 의미한다.

    FAVORITE_FOOD와 ADDRESS를 볼 때, Id 값을 PK로 설정하는 것이 아니라 컬렉션 전체를 묶어서 PK로 설정해야 한다.

    ADDRESS를 예로 들면 MEMBER_ID, CITY, STREET, ZIPCODE를 하나로 묶은 것이 PK가 되는 것이다.

     

     

    • 값 타입을 하나 이상 저장할 때 사용
    • @ElementCollection, @CollectionTable 사용
    • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
    • 컬렉션을 저장하기 위한 별도의 테이블이 필요함

     

     

    값 타입 컬렉션 예시 코드

    @Entity
    @Getter @Setter
    public class Member {
        @Id
        @GeneratedValue
        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<>();
    }

    @ElementCollection과 @CollectionTable을 이용해서 값 타입 컬렉션을 만들어줬다. 이후에 Member_Id와 매핑을 해주면서 member 엔티티와 연관관계를 만들어 주었다.

     

     

    값 타입 컬렉션 사용

     

     

    값 타임 저장

    Member member = new Member();
    member.setName("member1");
    member.setHomeAddress(new Address("homeCity", "street", "10000"));
    
    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)만 했을 뿐인데 getFavoriteFoods와 getAddressHistory의 쿼리까지 같이 나간다. 값 타입 컬렉션은 영속성 전이(Cascade) 기능을 필수로 가진다는 것을 알 수 있다.

     

     

     

    값 타입 조회

    Member findMember = em.find(Member.class, member.getId());
    // embedded 타입인 homeAddress는 같이 조회쿼리가 나감
    // 컬렉션들은 지연로딩이기 때문에 조회쿼리가 나가지 않음
    
    List<Address> addressHistory = findMember.getAddressHistory();
    for(Address address : addressHistory) {
       System.out.println("address = " + address.getCity());
    }

    em.find를 하면 id, name embedded 타입인 homeAdress의 쿼리만 나간다. 컬렉션들은 기본적으로 지연로딩이기 때문에 조회쿼리가 나가지 않는다. 아래처럼 루프를 돌려서 값을 불러올 때에 쿼리가 나가게 된다.

     

     

     

    값 타입 수정

    Adress 클래스에 있는 equals(), hashCode() 메서드

    @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.getStreet()) &&
                Objects.equals(zipcode, address.zipcode);
    }
    
    
    @Override
    public int hashCode() {
        return Objects.hash(city, street, zipcode);
    }

     

     

    // 치킨 -> 한식 수정
    findMember.getFavoriteFoods().remove("치킨");
    findMember.getFavoriteFoods().add("한식");
    // 컬렉션의 값만 변경해도 쿼리가 나가면서 데이터가 변한다 -> 마치 영속성 전이처럼
    
    findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
    // equals, hashCode가 없으면 안됨
    findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));

     

     

    값 타입 컬렉션의 제약사항

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

     

    db에 쿼리를 날릴 때 ADDRESS에 있는 member_id와 관련된 address를 다 지운 다음에 필요한 데이터를 다시 넣어준다.

    에를 들어 기존에 old1, old2의 address가 있는데, old1을 newCity1로 바꾼다면 맨 처음에 모든 데이터를 다 지운 다음에 old2와 newCity1의 데이터를 다시 넣어주는 방식이다.

     

    너무 비효율적인 방식이기 때문에 이렇게 하면 안되고 다른 방식을 사용해야한다.

     

     

    AddressEntity를 따로 만들어서 사용

    @Entity
    @Getter @Setter
    @Table(name = "ADDRESS")
    public class AddressEntity {
    
        @Id @GeneratedValue
        private Long id;
    
        private Address address;
    
        public AddressEntity() {
    
        }
    
        public AddressEntity(Address address) {
            this.address = address;
        }
    
        public AddressEntity(String city, String street, String zipcode) {
            this.address = new Address(city, street, zipcode);
        }
    }

     

    Member 클래스에서도 위와 같이 설정

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList<>();
    
    /*
    @ElementCollection
    @CollectionTable(name = "ADDRESS", joinColumns =
        @JoinColumn(name = "MEMBER_ID")
    )
    private List<Address> addressHistory = new ArrayList<>();
     */

     

     

    값 타입 컬렉션을 사용하는 경우는 추적하거나 값이 변할 필요가 없는 단순한 선택? 같은 경우에만 사용한다.

    이외에는 전부 엔티티를 사용한다고 한다.

     

    실무에서는 값 타입 컬렉션 대신에 일대다 관계를 고려하는 것이 좋다.

     

     

     

    정리

    엔티티 타입의 특징

    • 식별자 O
    • 생명 주기 관리
    • 공유 가능

     

    값 타입의 특징

    • 식별자 X
    • 생명 주기를 엔티티에 의존
    • 공유하지 않는 것이 안전(복사해서 사용)
    • 불변객체로 만들어야 함

     

     

    값 타입은 정말 값 타입이라 판단될 때만 사용해야하며, 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안된다.

     

    식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 값 타입이 아닌 엔티티로 사용해야 한다.

    'JPA > JPA 기본' 카테고리의 다른 글

    값 타입  (0) 2023.07.11
    영속성 전이(CASCADE)  (0) 2023.07.11
    즉시로딩과 지연로딩  (0) 2023.07.11
    프록시  (0) 2023.07.11
    상속관계 매핑  (0) 2023.07.11
Designed by Tistory.