ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 값 타입
    IT/JPA 2020. 12. 29. 22:33

    1. 값 타입

      1) 값 타입의 분류

       - 기본 값 타입 : 자바 기본타입(int, double..), 래퍼클래스(Integer), String

       - 임베디드 타입 : 복합 값 타입(사용자가 직접 정의한 값 타입)

       - 컬렉션 값 타입 : 한개 이상의 값 타입(List..)

     

      2) 기본 값 타입

       - 기본 값 타입은 생명주기가 회원 엔티티에 의존한다.

       - 값 타입은 공유해선 안된다.

       - 자바에서 int, double 같은 기본 값 타입은 절대 공유되지 않는다(a=b 코드에서 값만 복사됨)

     

      3) 임베디드 값 타입

    @Entity
    public class Member{
    	@Id @GeneratedValue
        private Long id;
        private String name;
        
        @Embeded Period workPeriod;
        @Embeded Period homeAddress;
        ....
    }
    
    @Embeddable
    public class Period{
    	@Temporal(TemporalType.DATE) java.util.Date startDate;
        @Temporal(TemporalType.DATE) java.util.Date endDate;
        ....
    }
    
    @Embeddable
    public class Address{
    	@Column(name="city")
        private String city;
        private String street;
        private String zipcode;
        ...
    }

       - Period 와 Address 값 타입을 만들어 사용하였다.

       - @Embeddable : 값 타입을 정의하는 곳에 표시

       - @Embedded : 값 타입을 사용하는 곳에 표시

       - 임베디드 타입은 기본 생성자가 필수다.

     

      4) 임베디드 타입과 테이블 매핑

       - 임베디드 타입은 엔티티의 값일 뿐이기 때문에 속한 엔티티의 테이블에 매핑한다.

       - 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다.

       - 잘 설계한 ORM은 매핑한 테이블의 수보다 클래스의 수가 더 많다.

     

      5) 임베디드 타입과 연관관계

       - 임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다.

    @Entity
    public class Member {
    	@Embedded Address address;			//임베디드 타입 포함
        @Embedded PhoneNumber phoneNumber;	//임베디드 타입 포함
        ...
    }
    
    @Embeddeable
    public class Address {
    	@Embedded Zipcode zipcode;		//임베디드 타입 포함
        ....
    }
    
    @Embeddable
    public class Zipcode {
    	String zip;
        String plusFour;
    }
    
    @Embeddable
    public class PhoneNumber{
    	@ManyToOne PhoneServiceProvider provider;	//엔티티 참조
        ...
    }
    
    @Entity
    public class PhoneServiceProvider{
    	@Id String name;
        ...
    }

       - 값 타입인 Address가 값 타입인 Zipcode를 포함하고, 값 타입인 PhoneNumber가 엔티티 타입인 PhoneServiceProvider를 참조한다.

     

      6) @AttributeOverride: 속성의 재정의

       - 임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride를 사용하면 된다.

    @Entity
    public class Member {
    	....
        
        @Embedded 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"))
        })
        Address companyAddress;
    }

       - @AttributeOverride를 사용하면 어노테이션을 너무 많이 사용해서 코드가 지저분해진다.

       - @AttributeOverrides는 엔티티에 설정해야 한다. 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 설정해야 한다.

     

      7) 임베디드 타입과 null

    member.setAddress(null); //null 입력
    em.persist(member);

      - 임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.

     

    2. 값 타입과 불변객체

      1) 값 타입 공유 참조

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

    member1.setHomeAddress(new Address("OldCity"));
    Address address = member1.getHomeAddress();
    
    address.setCity("NewCity");	//회원1의 address 값을 공유해서 사용
    member2.setHomeAddress(address);

       - 회원2에 새로운 주소를 할당하려고 회원1의 주소를 그대로 참조해서 사용했다.

       - 회원1과 2 모두 "NewCity"로 변경된다. (같은 address 인스턴스 참조)

       - 영속성 컨텍스트는 회원1과 회원2 둘다 city속성이 변경된 것으로 판단해서 회원1, 회원2 각각 UPDATE를 실행한다.

       - 이러한 부작용(side effect)를 막으려면 값을 복사해서 사용해야 한다.

     

      2) 값 타입 복사

    member1.setHomeAddress(new Address("OldCity"));
    Address address = member1.getHomeAddress();
    
    //회원 1의 address 값을 복사해서 새로운 newAddress 값을 생성
    Address new Address = address.clone();
    
    new Address.setCity("NewCity");
    member2.setHomeAddress(newAddress);

       - 회원2에 새로운 주소를 할당하기 위해 clone() 메소드를 만들었는데, 이 메소드는 자신을 복사해서 반환한다.

       - 회원1의 주소 인스턴스를 복사해서 사용하므로, 회원2의 주소만 "NewCity"로 변경한다.

       - 임베디드와 같이 직접 정의한 값은 자바의 객체 타입이다.

       - 기본타입은 값을 대입하면 복사해서 전달한다.

       - 자바의 객체에 값을 대입하면 항상 참조값을 전달한다.

    Address a = new Address("Old");
    Address b = a.clone();	//항상 복사해서 넘겨야 한다.
    //Address b = a;	//이렇게 참조만 하면 side effect 가 발생한다.
    b.setCity("New");

     

      3) 불변 객체

       - 값 타입은 부작용 걱정 없이 사용할 수 있어야 한다.

       - 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다.

       - 값 타입은 될 수 있으면 불변 객체로 설계해야 한다.

       - 불변 객체의 값은 조회할 수 있지만 수정할 수 없다.

       - 가장 간단한 방법으로는 생성자로만 값을 설정하고, 수정자를 만들지 않으면 된다.

    @Embeddable
    public class Address{
    	private String city;
        protected Address() {}
        //생성자로 초기값을 설정
        public Address(String city){this.city = city}
        //접근자는 노출한다
        public String getCity(){
        	return city;
        }
        //수정자는 만들지 않는다.
    }
    
    
    //사용
    Address address = member1.getAddress();
    Address newAddress = new Address(address.getCity());
    member2.setHomeAddress(newAddress);

     

    3. 값 타입의 비교

      1) 값 타입의 비교

    int a = 10;
    int b = 10;
    
    Address a = new Address("서울시","종로구","1번지");
    Address b = new Address("서울시","종로구","1번지");

       - int a의 숫자 10과 int b의 숫자 10은 같다고 표현한다.

       - Address a와 Address b 는 같다고 표현한다.

       - 동일성 : 인스턴스의 참조 값을 비교, == 사용

       - 동등성 : 인스턴스의 값을 비교, equals() 사용

       - Address 값 타입을 a==b 로 동일성 비교하면 거짓이다.

       - 값 타입을 비교할 때는 equals() 메소드를 재정의 해야한다. (보통 모든 필드값 비교하게 구현)

       - equals()를 재정의하면 hashCode()도 재정의 하는것이 안전하다(HashMap, HashSet 등의 컬렉션 사용에 영향)

     

    4. 값 타입 컬렉션

      1) 값 타입 컬렉션 사용

       - 값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 어노테이션을 사용하면 된다.

    @Entity
    public class Member{
    	@Id @GeneratedValue
        private Long id;
        
        @Embedded
        private Address homeAddress;
        
        @ElementCollection
        @CollectionTable(name="FAVORITE_FOODS",
        	joinColumns = @JoinColumn(name="MEMBER_ID"))
        @Column(name="FOOD_NAME")
        private Set<String> favoriteFoods = new HashSet<String>();
        
        @ElementCollection
        @CollectionTable(name = "ADDRESS", joinColumns
        	= @JoinColumn(name="MEMBER_ID))
        private List<Address> addressHistory = new ArrayList<Address>();
        // ...
    }
    
    @Embeddable
    public class Address {
    	@Column
        private String city;
        private String street;
        private String zipcode;
        ...
    }

       - 값 타입 컬렉션을 사용하는 favoriteFoods, addressHistory에 @ElementCollection을 지정했다.

       - favoriteFoods는 String 컬렉션을 테이블로 매핑해야 하는데 관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수 없다. 따라서 별도의 테이블을 추가하고 @CollectionTable을 사용해서 추가한 테이블을 매핑한다.

       - favoriteFoods 처럼 값으로 사용되는 컬럼이 하나면 @Column을 사용하여 컬럼명을 지정할 수 있다.

       - addressHistory는 Address를 컬렉션으로 가지며 벌도의 테이블을 사용해야 한다. 테이블 매핑정보는 @AttributeOverride를 사용해서 재정의할 수 있다.

       - @CollectionTable을 생략하면 기본값을 사용해서 매핑하는데, (엔티티이름)_(컬렉션속성이름) 이다.

    //값 타입 컬렉션의 사용
    
    Member member = new Member();
    
    //임베디드 값 타입
    member.setHomeAddress(new Address("통영", "몽돌해수욕장","660-123"));
    
    //기본값 타입 컬렉션
    member.getFavoriteFoods().add("짬뽕");
    member.getFavoriteFoods().add("짜장");
    member.getFavoriteFoods().add("탕수육");
    
    //임베디드 값 타입 컬렉션
    member.getAddressHistory().add(new Address("서울","강남","123-123"));
    member.getAddressHistory().add(new Address("서울","강북","000-000"));
    
    em.persist(member);

       - 등록 코드를 보면 member 엔티티만 영속화 했다. 이때 member 엔티티의 값 타입도 함께 저장된다.

       - 실제 실행되는 INSERT SQL은 다음과 같다

       - member : INSERT SQL 1번

       - member.homeAddress : 컬렉션이 아닌 임베디드 값 타입이므로 회원테이블을 저장하는 SQL에 포함된다.

       - member.favoriteFoods : INSERT SQL 3번

       - member.addressHistory : INSERT SQL 2번

       - 따라서 em.persist(member)는 총 6번의 INSERT SQL을 실행한다.

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

       - 값 타입 컬렉션도 조회할 때 fetch 전략을 선택할 수 있는데 LAZY가 기본이다.

       - 1. member 회원만 조회한다. 임베디드 값 타입인 homeAddress도 함께 좋회한다.

       - 2. member.homeAddress : 1번에서 이미 조회되었다.

       - 3. member.favoriteFoods : LAZY로 설정해서 실제 컬렉션을 사용할때 호출한다.

       - 4. member.addressHistory : LAZY로 설정해서 실제 컬렉션을 사용할때 호출한다.

       - 1. 임베디드 값 타입수정 : MEMBER테이블과 매핑했으므로 MEMBER테이블만 UPDATE한다.

       - 2. 기본값 타입 컬렉션 수정 : 탕수육을 치킨으로 변경하려면 탕수육을 제거하고 치킨을 추가해야 한다. 자바의 String 타입은 수정할 수 없다(primitive type)

       - 3. 임베디드 값 타입 컬렉션 수정 : 값 타입은 불변해야 한다. 따라서 컬렉션에서 기존 주소를 삭제하고 새로운 주소를 등록했다. 참고로 값 타입은 equals, hashcode를 꼭 구현해야 한다.

     

      2) 값 타입 컬렉션의 제약사항

       - 값 타입은 식별자라는 개념이 없고 단순 값들의 모음이므로 값을 변경하면 데이터베이스에 원본을 찾기 어렵다.

       - 특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 데이터베이스에서 찾고 값을 변경하면 된다.

       - 값 타입 컬렉션은 별도의 테이블에 보관되는데, 데이터베이스의 원본 데이터를 찾기 업렵다.

       - 값 타입 컬렉션에 변경사항이 발생하면 매핑테이블에 연관된 모든 데이터를 삭제하고 현재 값 타입 컬렉션에 있는 모든 값을 데이터베이스에 다시 저장한다. (100만개이면 100만번 다시저장한다)

       - 실무에서는 값 타입 컬렉션에 매핑된 테이블에 데이터가 많다면 대신 일대다 관계를 고려해야 한다.

       - 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성하므로 컬럼에 null 입력이 불가능하고, 같은 값을 중복 저장도 불가능하다.

       - 결국 값 타입 컬렉션을 사용하는 대신에 일대다 관계로 설정하고 영속성전이+고아객체제거를 적용하면 값 타입 컬렉션처럼 사용할 수 있다.

       - 값 타입 컬렉션을 변경했을때 JPA구현체는 테이블의 기본키를 식별해서 변경된 내용반 반영하려고 하지만, 컬렉션이나 여러 조건에 따라 기본 키를 식별할 수도 있고 하지 못할수도 있다. 따라서 모두 삭제하고 저장하는 최악의 시나리오를 항상 고려야하야 한다.

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

    고급 주제와 성능 최적화  (0) 2021.02.28
    웹 애플리케이션과 영속성 관리  (0) 2021.01.24
    고급 매핑  (0) 2020.12.19
    연관관계 매핑  (0) 2020.12.08
    JPA 의 영속성 관리  (0) 2020.11.29
Designed by Tistory.