ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JPA] 데이터 타입 분류 (Embedded)
    JPA 2020. 3. 29. 00:24
    반응형

    JPA 데이터 타입 분류


    1. 엔티티 타입

    • @Entity로 정의하는 클래스 객체를 의미
    • 엔티티 타입은 데이터(속성) 값이 변해도 식별자(@Id)를 통해 추적이 가능함

     

    2. 값 타입

    • 자바 기본 타입 또는 객체(int, Integer, String..)
    • 값 타입은 식별자가 존재하지 않기 때문에 추적이 불가능
    • 값 타입 분류
      • 기본값 타입
        • 자바 기본 타입(primitive type)
        • 래퍼 클래스(wrapper)
        • String
        • 특징
          • 생명주기를 엔티티에 의존함(엔티티가 삭제되면 필드도 함께 삭제)
          • 값 타입은 공유되서는 안됨(특정 엔티티 속성의 일부가 변경되는 것이 다른 엔티티에 영향을 미치면 안 됨)
          • 자바의 기본 타입(primitive type)은 항상 값을 복사하는 개념이기 때문에 공유되는 특성이 없다.
      • 임베디드 타입(Embedded Type, 복합 값 타입)
        • ex) X. Y 좌표를 묶어서 사용 → Position을 생성
      • 컬렉션 값 타입(Collection value Type)
        • 자바 컬렉션에 기본값 또는 임베디드 타입을 넣는 경우

     

     

    임베디드 타입(Embeded Type)


    @Embeddable, @Embedded 

    새로운 값 타입을 직접 정의할 수 있고 주로 기본 값 타입을 모아서 만들기 때문에 복합 값 타입이라고도 불린다.

    엔티티가 아닌 값 타입을 유의하자. 그냥 임베디드 타입은 엔티티 타입의 값일 뿐이다.

     

    간단한 예제를 통해 알아보자.

    @Entity
    public class Member {
    
    	@Id @GeneratedValue
    	private Long id;
    	private String name;
        
    	// 기간 관련
    	private LocalDateTime createdDate; 
    	private LocalDateTime updatedDate;	
        
    	// 주소관련
    	private String city;
    	private String zipcode;
    	private String street;
    }

    위 Member 엔티티를 필드를 살펴보면 기간주소 관련된 필드가 존재하는 것을 볼 수 있다.

    공통사항 특징을 가지고 두 가지의 클래스를 추가 생성해보자

    @Entity
    public class Member {
    
    	@Id @GeneratedValue
    	private Long id;
    	private String name;
        
    	// 기간 관련
    	@Embedded
    	private Period period;
    	
    	// 주소
    	@Embedded
    	private Address address;
    }
    @Embeddable
    public class Period {
    	private LocalDateTime createdDate; 
    	private LocalDateTime updatedDate;
    	
          // 기본 생성자 필수
          public Period() {
    
          }
    
    }
    @Embeddable
    public class Address {
    	private String city;
    	private String zipcode;
    	private String street;
        
    	// 기본 생성자 필수
          public Address() {
    
          }
    }

    기간을 의미하는 필드를 모아둔 Period 클래스와 주소를 의미하는 필드를 모아둔 Address 클래스를 생성했다.

    @Embeddable 애노테이션은 값을 정의하는 클래스에 적용시키고 @Embedded 애노테이션은 값을 사용하는 곳에 엔티티 클래스에 적용시켜 준다. 기본 생성자를 필수적으로 넣어주어야 한다.

     

    임베디드 타입의 특징은 클래스 재사용이 가능하고 높은 응집성을 가지고 있어 객체지향적인 설계에 어울린다는 점이다. 또한 임베디드 타입은 일종의 값 타입일 뿐이므로 엔티티와 생명주기를 함께한다.

     

    참고로 엔티티 안에서 엠베디드 타입을 사용해서 클래스를 나누는 것은 데이터베이스 테이블의 칼럼과 전~혀 관계가 없다. 즉, 임베디드 타입을 사용하기 전과 후의 테이블 차이는 없다!

     

    따라서 잘 설계된 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다 :)

     

    @AttributeOverride

    하나의 엔티티 속에서 아래 코드와 같은 값 타입을 사용한다면 어떤 상황이 발생할까?

    @Entity
    public class Member10 {
    
        @Id @GeneratedValue
        private Long id;
    
        private String name;
    
        // 기간
        @Embedded
        private Period period;
    
        // 집 주소
        @Embedded
        private Address homeAddress;
    
        // 회사 주소
        @Embedded
        private Address workAddress;
    }

    집 주소와 회사 주소는 같은 임베디드 타입의 Address 클래스를 사용하기 때문에 같은 필드명을 가지게 되어 데이터베이스에 중복 칼럼이 매핑되어 에러가 발생한다. 

     

    임베디드 타입의 중복을 피하기 위해서 @AttributeOverride 애노테이션을 통해 데이터베이스 칼럼명을 정해줄 수 있다.

    @Embedded
    @AttributeOverrides({
      @AttributeOverride(
        name="city",
        column=@Column(name="home_city")),
      @AttributeOverride(
        name="street", 
        column=@Column(name= "home_street")),
      @AttributeOverride(
        name="zipcode", 
        column=@Column(name= "home_zipcode"))
    })
    private Address homeAddress;

     

    값 타입과 불변 객체


    임베디드 같은 값 타입은 여러 엔티티에서 공유하면 side-effect가 발생할 수 있다.

    // 임베디드 타입
    Address address = new Address("서울", "192-31", "오목로");
    			
    Member member1 = new Member();
    member1.setName("devyu");
    member1.setHomeAddress(address);
    
    // member1 저장 
    em.persist(member);
    
    Member member2 = new Member();
    member2.setName("puregyu");
    member2.setHomeAddress(address);
    
    // member2 저장
    em.persist(member2);
    
    // member1의 임베디드 타입의 주소클래스의 거리명을
    // 오목로 -> 신정로 수정
    member1.getHomeAddress().setStreet("신정로");

    거리명이 member1과 member2 둘다 변경됨

    데이터베이스를 보면 member1만 거리명을 변경했지만 member2까지 거리명이 변경되는 부작용을 확인할 수 있다. 임베디드 타입은 값 타입이지만 결국 객체이기 때문에 주소 값(참조값)이 공유되기 때문이다.

     

    값 타입의 실제 인스턴스 주소를 공유하는 것은 매우 매우 위험하기 때문에 인스턴스를 복사해서 사용해야 한다.

    // 임베디드 타입
    Address address1 = new Address("서울", "192-31", "오목로");
    			
    Member member1 = new Member();
    member1.setName("devyu");
    member1.setHomeAddress(address1);
    
    // member1 저장 
    em.persist(member);
    
    // 입베디드 타입 새롭게 생성
    Address address2 = new Address(address1.getCity(), address1.getZipcode(), address1.getStreet());
    
    Member member2 = new Member();
    member2.setName("puregyu");
    // member2에는 새롭게 생성한 address2를 넣어줌
    member2.setHomeAddress(address2);
    
    // member2 저장
    em.persist(member2);
    
    // member1의 임베디드 타입의 주소클래스의 거리명을
    // 오목로 -> 신정로 수정
    member1.getHomeAddress().setStreet("신정로");

     

    값 타입에 대한 에러는 컴파일러가 잡아낼 수 없기 때문에 개발자가 실수할 가능성이 매우 높고 Side-Effect는 잡아내기 힘들기 때문에 고생할 수 있다. 그렇기에 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단할 수 있다. 값 타입을 불변 객체(Immutable Object)로 설계하여 생성시점 이후 절대 값을 직접 변경할 수 없게 만들면 된다. 무조건 생성자를 통해 객체를 생성하고 Setter 메서드를 지워주자.

     

    값 타입 켈렉션


    엔티티 타입이 아닌 값 타입을 Set, List와 같은 컬렉션에 담아서 사용하는 것이다.

    @Entity
    public class Member {
    
    	@Id @GeneratedValue
    	private Long id;
    	
    	private String name;
    
    	@Embedded
    	private Period period;
    	
    	@Embedded
    	private Address Address;
    	
    	// 값 타입 컬렉션
    	@ElementCollection
    	@CollectionTable(
                name = "FAVORITY_FOOD",
                joinColumns = @JoinColumn(name="MEMBER_ID"))
    	private Set<String> favorityFoods = new HashSet<String>();
    	
    	// 값 타입 컬렉션(임베디드)
    	@ElementCollection
    	@CollectionTable(
                name = "ADDRESS",
                joinColumns = @JoinColumn(name="MEMBER_ID"))
    	private List<Address> addressHistory = new ArrayList<Address>();
    }

    값 타입 컬렉션을 사용하기 위해서는 @ElementCollection, @CollectionTable 애노테이션을 사용해서 테이블과 매핑해주어야 한다.

    @ElementCollection 값 타입 컬렉션임을 의미하고 @CollectionTable 는 새롭게 생성되는 테이블에 대한 정보를 입력하는 애노테이션이다. name 속성을 사용해서 테이블 명을 지정해 주었고 joinColums 속성은 엔티티의 id값을 외래 키로 설정하여 join을 하면 된다.

     

    데이터베이스는 컬렉션을 같은 테이블에 저장할 수는 없다. 컬렉션 저장을 위해 별도의 테이블이 필요하다. 컬렉션 자체가 일대다 개념이기 때문에 하나의 테이블로 저장할 수 없는 것이다.

     

    그럼 실제 사용을 해보자.

    // === 값 타입 컬렉션 사용===
    Member member = new Member();
    
    member.setName("민짱");
    // 임베디드 타입
    member.setHomeAddress(new Address("서울", "192-31", "마포로"));
    
    // 컬렉션
    member.getFavorityFoods().add("피자");
    member.getFavorityFoods().add("사과");
    member.getFavorityFoods().add("우유");
    
    // 컬렉션
    member.getAddressHistory().add(new Address("대구", "192-31", "마포로"));
    member.getAddressHistory().add(new Address("부산", "192-31", "마포로"));
    member.getAddressHistory().add(new Address("우산", "192-31", "마포로"));
    
    em.persist(member);

     

    값 타입 컬렉션은 다른 테이블이지만 모든 생명주기가 엔티티에 소속되기 때문에 em.persist(member) 명령 만으로도 다른 테이블에 INSERT를 한 것이다. (마치 1:N 엔티티 매핑에서 Cascade.ALL 설정과 유사) 쉽게 생각하자면 값 타입 컬렉션은 member의 name필드와 별 다를 바 없다.

     

    참고로 값 컬렉션은 지연 로딩으로 세팅되어있으므로 실제 데이터를 사용할 때 쿼리가 발생한다.

     

    값 타입 컬렉션의 제약사항

    1. 값 타입은 엔티티와 다르게 식별자가 존재하지 않는다.
    2. 값 타입 컬렉션에 변경사항에 발생하면 컬렉션에 있는 데이터를 전부 삭제하고 새롭게 변경사항을 저장한다.

     

     

     

    반응형

    댓글

Designed by Tistory.