ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA > 값 타입
    BackEnd/JPA 2021. 4. 25. 20:25

     

    JPA 타입을 크게 분류하면 엔티티 타입과 값 타입으로 분류할 수 있다.

    엔티티 타입은 식별자를 통해 지속해서 추적할 수 있지만,

    값 타입은 식별자가 없고 숫자나 문자같은 속성만 있으므로 추적할 수 없다.

    예를 들어 회원 엔티티는 그 회원의 키나 나이 값을 변경해도 같은 회원으로 볼 수 있지만, 값 타입은 추적할 수 없다.

     

    JPA 는 기본값 타입, 임베디드 타입, 컬렉션을 값 타입을 가질 수 있다.

    모든 값 타입은 값 타입을 소유한 엔티티에 생명 주기를 의존한다.


    1. 기본값타입

    자바 기본 타입 (Primitive) , Wrapper 클래스 , String 클래스는 JPA 의 기본값 타입이다.

    Primitive Type 은 복사를 하기 때문에 값이 같이 변경되지 않고, 

    Wrapper 클래스나 String 클래스는 Reference 가 공유 되기 때문에 공유가 가능하지만 변경이 불가하게 되어있다. 

    그렇기 때문에 수정 등으로 인한 Side Effect 가 없다는 장점이 있다.

     

    2. 임베디드 타입  (복합 값 타입)

    새로운 값 타입을 직접 정의 할 수 있는데, 주로 기본 값 타입을 모아서 임베디드 타입이라고 한다.

    예를 들어 회원 엔티티에 주소나 근무기간에 대한 정보들은 String, Integer 같은 값으로 정의하는데, 이들을 새로운 클래스에 따로 모아두고 사용하는 것이다. 

    또, 근무기간이라는 개념에 근무 시작일, 근무 종료일, 주소에는 도시,도로명, 우편번호 등 상세 정보를 정의한다.

    근무기간,  주소 이렇게 추상화할 수 있는 것을 임베디드 타입이라고 한다.

     

    회원은 Entity 로 값 타입을 사용하기 위해 @Embedded 를 쓸 수 있다. 

    근무기간, 주소 는 값 타입을 정의하기 위해 @Embeddable 를 쓸 수 있다. 

    양 쪽에 각각 설정해도 되고, 한 쪽에만 설정에도 된다.

    기본 생성자는 필수이다.

    @Entity
    public class Member{
    
        @Embedded
        private Period period;
    
        @Embedded
        private Address address;
    
    }
    @Embeddable
    public class Period {
    
        private LocalDateTime startData;
        private LocalDateTime endData;
    }
     create table Member (
           MEMBER_ID bigint not null,
            createdBy varchar(255),
            createdDate timestamp,
            lastModifiedBy varchar(255),
            lastModifiedDate timestamp,
            city varchar(255),
            street varchar(255),
            zipcode varchar(255),
            endData timestamp,
            startData timestamp,
            USERNAME varchar(255),
            TEAM_ID bigint,
            primary key (MEMBER_ID)
        )

     

    이렇게 하면 공통으로 사용할 수 있는 언어들을 많이 만들 수 있어, 재사용성이 높다고 할 수 있다.

    그리고 값 타입 클래스 내에서 해당 객체와 관련된 유효성 검사 등을 넣을 수 있고 높은 응집도를 가진다.

     

    이런 방식의 큰 장점은 객체와 테이블을 세밀하게 매핑할 수 있다는 점이다.

    잘 설계한 ORM 애플리케이션은 매핑한 테이블 수보다 클래스의 수가 더 많다.

    ORM 을 사용하지 않고 개발하면 테이블 컬럼과 객체 필드를 대부분 1:1 로 매핑한다. JPA 를 사용하면 더 세밀한 객체지향 모델을 설계하는데 집중할 수 있다.

    Member member = new Member();
    member.setUsername("aa");
    member.setAddress(new Address("aa","aa","11"));
    
    em.persist(member);

    임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 타입의 관계를 UML 로 표현하면 컴포지션 관계가 된다. 

    https://brunch.co.kr/@oemilk/132

     

    임베디드 타입 내에서도 다른 Entity 를 가지는 것이 가능하다.

    만약 Address 타입을 이용해 집 주소와 직장 주소를 따로 저장하고 싶다면, AttributesOverrides 속성을 이용할 수 있다.

        @Embedded
        private Address homeaddress;
    
        @Embedded
        @AttributeOverrides({
                @AttributeOverride(name="city", column = @Column(name="WORK_CITY")),
                @AttributeOverride(name="street", column = @Column(name="WORK_STREET")),
                @AttributeOverride(name="zipcode", column = @Column(name="WORK_ZIPCODE")),
        })
        private Address workaddress;
    
    
         create table Member (
           MEMBER_ID bigint not null,
            createdBy varchar(255),
            createdDate timestamp,
            lastModifiedBy varchar(255),
            lastModifiedDate timestamp,
            city varchar(255),
            street varchar(255),
            zipcode varchar(255),
            endData timestamp,
            startData timestamp,
            USERNAME varchar(255),
            WORK_CITY varchar(255),
            WORK_STREET varchar(255),
            WORK_ZIPCODE varchar(255),
            TEAM_ID bigint,
            primary key (MEMBER_ID)
        )

     

    하지만 객체를 공유하는 것은 reference 를 공유해 사용하는 여러 곳에서 값을 함부로 수정하면 찾기 힘든 오류를 발생시키고, 값 타입 객체도 마찬가지이다. 이런 이유로 값 타입 객체도 set 메서드를 제공하지 않고 생성자만 제공하는 방식을 이용해 불변으로 설계하는 것이 바람직하다.

    Address address= new Address("city","street","2");
    
    Address copyAddress= new Address(address.getCity(),address.getStreet(),address.getZipCode());

     

    값 타입의 비교

    인스턴스 값을 비교할 때 "==" 으로 비교하는 것을 동일성(identity ) 비교라고 하고 , 

    equals 메서드로 비교하는 것을 동등성(equivalence) 비교라고 한다.

     

    address 와 copyAddress 를 equals 로 비교하면 equals 의 기본이 == 비교이기 때문에 false 가 나온다.

    하지만 동일한 주소지일 경우에 동일성비교에서 TRUE 값을 받고자 하기 때문에 아래와 같이 equals 를 재정의해준다.

     

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

    * equals 재정의 시 "use getters during code generation" 이라는 옵션은 equal 비교에서 필드에 바로 접근하지 않고 getter 를 통해 접근하도록 하는 것이다. 필드에 직접 접근하는 것은 proxy 를 이용할 때 제대로 값을 가져올 수 없기 때문에 getter 를 통해 가져오도록 하자.

    3. 컬렉션 값 타입

    이전 주소지의 기록, 좋아하는 여러 가지 음식들 등 컬렉션으로 받아야 하는 요소들을 별도의 테이블에 저장할 수도 있다. 

    관계형 데이터베이스에는 컬럼에 컬렉션을 포함할 수 없기 때문에 별도의 테이블로 만든다. 

    컬렉션 변수 위에 @ElementCollection 과 @CollectionTable 을 선언하고, Member_Id 를 FK 로 설정한다. 

    favoriteFoods 의 경우 음식이름 String 1 개의 컬럼만 필요하기 때문에 @Column 으로 컬럼명 지정이 가능하다.

    * CollectionTable 을 생략하면 기본값으로 매핑한다. {엔티티이름}_{컬렉션 속성명} (예) Member_addressHistory

    @Entity
    public class Member { 
    
        @Id
        private Long id;
        
        @ElementCollection
        @CollectionTable(name="FAVORITE_FOODS",
                joinColumns = @JoinColumn(name="MEMBER_ID")) //MEMBER_ID 외래키
        @Column(name = "FOOD_NAME")
        private Set<String> favoriteFoods = new HashSet<>();
    
        @ElementCollection
        @CollectionTable(name="ADDRESS_HISTORY",
                joinColumns = @JoinColumn(name="MEMBER_ID"))
        private List<Address> addressHistory = new ArrayList<>();
     //...   
    }

    이 때 주의할 것은 자신의 ID 를 PK 로 가진 객체는 하나의 개별 Entity 가 되는 것이기 때문에,

    컬렉션 테이블은 Entity 를 이루는 여러개의 값이 모여 하나의 PK 로 묶어야한다. 

    예를 들어 Member 엔티티에 주소지를 List 로 갖고 있으면 Member의 ID 와 컬렉션 데이터가 함께 묶여 PK 가 되는 것이다. 

    Member member = new Member();
    member.setUsername("aa");
    member.setHomeaddress(new Address("homeCity","aa","11"));
    
    member.getFavoriteFoods().add("야채");
    member.getFavoriteFoods().add("파스타");
    member.getFavoriteFoods().add("피자");
    
    member.getAddressHistory().add(new Address("old1","street1","22"));
    member.getAddressHistory().add(new Address("old2","street1","33"));
    
    em.persist(member);

    위 코드에서 member insert sql 1 개, favoriteFood 는 3개의 insert sql , address history 는 2개의 insert sql 이 나간다.

    Hibernate: 
        /* insert hellojpa.Member
            */ insert 
            into
                Member
                (createdBy, createdDate, lastModifiedBy, lastModifiedDate, city, street, zipcode, endData, startData, USERNAME, MEMBER_ID) 
            values
                (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    Hibernate: 
        /* insert collection
            row hellojpa.Member.addressHistory */ insert 
            into
                ADDRESS_HISTORY
                (MEMBER_ID, city, street, zipcode) 
            values
                (?, ?, ?, ?)
    Hibernate: 
        /* insert collection
            row hellojpa.Member.addressHistory */ insert 
            into
                ADDRESS_HISTORY
                (MEMBER_ID, city, street, zipcode) 
            values
                (?, ?, ?, ?)
    Hibernate: 
        /* insert collection
            row hellojpa.Member.favoriteFoods */ insert 
            into
                FAVORITE_FOODS
                (MEMBER_ID, FOOD_NAME) 
            values
                (?, ?)
    Hibernate: 
        /* insert collection
            row hellojpa.Member.favoriteFoods */ insert 
            into
                FAVORITE_FOODS
                (MEMBER_ID, FOOD_NAME) 
            values
                (?, ?)
    Hibernate: 
        /* insert collection
            row hellojpa.Member.favoriteFoods */ insert 
            into
                FAVORITE_FOODS
                (MEMBER_ID, FOOD_NAME) 
            values
                (?, ?)

    값 타입 컬렉션은 지연로딩 전략을 가진다. Address_History 나 Favorite_Food 를 사용할 때 select 쿼리를 실행한다.

       Member findMem = em.find(Member.class, member.getId());
       findMem.getFavoriteFoods().remove("야채");
       findMem.getFavoriteFoods().add("연어");
    
    //equals 를 구현해야 함!
       findMem.getAddressHistory().remove(new Address("old1","street1","22")); 
       findMem.getAddressHistory().add(new Address("new1","street1","22"));
    

     

    값 타입 컬렉션은 member 에 따라  컬렉션들의 생명주기가 따라가지 때문에 cascade + 고아 객체 제거 기능을 가진다고 할 수 있다.

     

    값타입 컬렉션도 조회할 때 페치전략을 선택할 수 있는데, 기본은 Lazy 이다.

     

    값 타입은 값이 변경되면 원본 데이터를 찾기 어렵다는 문제가 있다. 그래서 컬렉션에 변경사항이 발생하면, 변경된 데이터뿐만 아니라 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 컬렉션에 있는 현재 값을 새로 insert 한다.

    모든 컬럼을 묶어 기본키를 구성하고 있어 식별자(ID)가 따로 없고, 값의 변경을 추적하기 어렵기 때문이다.

     

    또, 모든 컬럼을 묶어서 기본키를 구성하는데 기본키 제약조건이 null 을 입력할 수 없고, 중복 값을 저장할 수 없다는 제약도 있다.

     

    실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면, 컬렉션 대신 일대다 관계를 고려하는 것이 나을 수 있다.

    일대다 관계를 위한 엔티티를 만들고 여기에 값 타입을 사용하고, 

    영속성 전이(cascade) + 고아 객체 제거를 사용해 값 타입 컬렉션 처럼 사용할 수 있다.

     

    컬렉션은 view 단에서 select box 선택이 여러개일때 추적할 필요, update 할 필요가 없는 경우 사용할 수 있다.

    만약 식별자가 필요하고, 값을 추적해야하고, 변경해야한다면 값 타입이 아닌 엔티티로 만들어야한다.

     

    //컬렉션 대신 일대다
    @Entity
    public class Member { 
    
        @Id
        private Long id;
        
        @OneToMany(cascade=CascadeType.ALL , orphanRemoval=true)
        @JoinColumn(name="MEMBER_ID")
        private List<AddressEntity> addressHistory = new ArrayList<AddressEntity>();
     //...   
    }
    
    @Entity
    public class AddressEntity {
        @Id
        private Long id;
        
        @Embedded Address address;
        //...
    }

    www.inflearn.com/course/ORM-JPA-Basic/dashboard

     

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

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

    www.inflearn.com

     

    반응형

    'BackEnd > JPA' 카테고리의 다른 글

    JPA 경로표현식  (0) 2021.04.27
    JPA > JPQL, 프로젝션  (0) 2021.04.27
    JPA > 영속성 전이와 고아객체  (0) 2021.04.25
    JPA > 연관관계 매핑  (0) 2021.04.18
    JPA 기본  (0) 2021.04.04
Designed by Tistory.