DevKim

[JPA] 값 타입 - 기본값 타입, 임베디드 타입, 컬렉션 타입 본문

JPA

[JPA] 값 타입 - 기본값 타입, 임베디드 타입, 컬렉션 타입

on_doing 2021. 6. 23. 15:02
728x90

JPA의 데이터 타입은 엔티티 타입/ 값 타입으로 나뉜다.

 

[ 엔티티 타입 ]

- @Entity로 정의하는 객체이며, 데이터가 변해도 식별자로 지속해서 추적가능하다.


[ 값 타입 ]

- int,String 처럼 단순히 값으로 사용하는 자바 기본 타입/객체

- 식별자가 없고 값만 있으므로 변경시 추적 불가하다.

 

값 타입은 기본값 타입, 임베디드 타입, 컬렉션 값 타입으로 나뉜다.

 

<01> 기본 값 타입

- 자바 기본 타입(int,double) , 래퍼 클래스(Integer,Long), String 이있다.

- 생명주기를 엔티티에 의존하며, 값 타입은 공유하면 안되고 공유되지도 않는다. 

- 기본 타입은 공유가 안되고 항상 값을 복사한다. (a=b 이면 a에 b의 값이 복사됨 -> 두개는 다른 것)

- 래퍼 클래스는 공유 가능한 객체 ( 복사가 아니라, 주소값이 넘어가기 때문) 이지만 어차피 변경이 불가능하다.


<02> 임베디드 타입 (복합 값 타입)

- 기본 값 타입을 모아서 만들어진 타입이다.

만약 Member 엔티티가 이만큼의 값을 가진다고할때,

너무 번잡하므로 Date두개를 묶고 주소에 관한건 묶을 수 없을까?

** 변경 전 **

@Entity
public class Member{
	...

    //기간
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    //주소
    private String city;
    private String street;
    private String zipcode;

}

묶어서 이렇게 나타낼 수 있다.

 ** 변경 후 **

값 타입을 정의하는 곳에 @Embeddable 

이때, 기본 생성자는 필수다.

@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
@Embeddable
public class Period {

    //기간 Period
    private LocalDateTime startDate;
    private LocalDateTime endDate;

}

 

 값 타입을 사용하는 곳엔 @Embedded

@Entity
public class Member{
	...

    //기간
    @Embedded
    private Period workPeriod;

    //주소
    @Embedded
    private Address homeAddress;

}

이렇게 임베디드 타입을 사용하게 되면, 다음과 같은 장점이 있다.

 

1. 재사용이 가능하며, 응집도가 높다.

2. Period.isWork( ) 처럼 해당 기간에 일했는지 안했는지를 알 수 있는,

해당 값 타입만 사용하는 의미있는 메소드를 만들 수 있다.

Member member = new Member();
member.setUsername("테스트");
member.setHomeAddress(new Address("city","street","zipcode"));
member.setWorkPeriod(new Period());

 

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

만약 member2의 city만 바꾸고 싶어서 아래와 같이 작성한다면 모든 멤버의 city가 new city로 변경되는 것을 확인할 수 있다.


[ 임베디드 타입과 테이블 매핑 ]

 

임베디드 타입은 엔티티의 값일 뿐이다. 따라서, DB 입장에선 달라질게 없다. 매핑만 잘 해주면 된다.


[ 값 타입과 불변 객체 ]

" 값 타입은 단순하고 안전하게 다룰 수 있어야한다. "

 

만약 입베디드 같은 값 타입을 여러 엔티티에서 공유하면 매우매우 위험한 side effect가 발생한다.

이렇게 Newcity로 값을 바꾸면 모든 회원의 city값이 Newcity로 바뀌는..

진짜 잡을 수 없는 버그가 탄생한다 ㅎㅎ

 

기본 타입은 값을 대입하면 값을 복사하지만,

임베디드 타입같은 객체 타입은 참조를 전달하므로, 값이 모두 바뀐다.

Address address = new Address("city","street","zipcode");
            
Member member = new Member();
member.setUsername("테스트1");
member.setHomeAddress(address);

Member member2 = new Member();
member2.setUsername("테스트2");
member2.setHomeAddress(address);

member2.getHomeAddress().setCity("new city");

객체 타입을 수정할 수 없게 무조건 불변 객체로 설계해야한다.

Setter를 아예 만들지 않거나, private으로 생성해야한다.

 

실제로 값을 바꾸고 싶다면 어떻게 해야할까?

Setter모두 지운 후.. member의 city값을 변경 하고 싶다면?

번거롭지만 new address 생성해서 통째로 갈아끼우는 것이 맞다고한다.

Address address = new Address("city","street","zipcode");

Member member = new Member();
member.setUsername("테스트");
member.setHomeAddress(address);
em.persist(member);

/*
값을 변경하고 싶다면,, 통으로 바꿔서 갈아 끼워야함
*/
Address newAddress = new Address("newCity",address.getStreet(), address.getZipcode());
member.setHomeAddress(newAddress);

[ 값 타입의 비교 ]

기본 값 타입은 a=10, b=10 일때 a==b가 True이다.

하지만 Address a = new Address("하이") , Address b = new Address("하이") 일때, a == b는 False이다.

 

Java의 ==는 인스턴스의 참조 값을 비교하므로,

equals 를 사용해서 값을 비교해주어야한다.

Address address1 = new Address("city","street","zipcode");
Address address2 = new Address("city","street","zipcode");

System.out.println(address1==address2) //False
System.out.println(address1.equals(address2)) //True 
// -> override 생성 전에는 False (equals도 기본값이 == 임)
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);
    }
}

[ 값 타입 컬렉션 ]

@Getter
@Setter
@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

   ...

    //주소
    @Embedded
    private Address homeAddress;

    /*
     * 여러개 저장하고 싶으므로 값타입을 컬렉션으로 저장
     */

    @ElementCollection
    @CollectionTable(name = "FACORITE_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<>();

}

이렇게 캆 타입 컬렉션을 생성했을 때,

변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고

값 타입 컬렉션에 있는 현재 모든 값을 다시 저장한다.

-> insert 문이 여러번 나가게 되고, 효율적이지 않다.

 

따라서,

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

 

** 일대다 관계를 위한 엔티티를 만들고,

영속성 전이(Cascade )

+

고아 객체 제거

를 사용해서 값 타입 컬렉션 처럼 사용하는 것이다. **


[ 정리 ]

값 타입은 정말 간단한 상황에서 사용해야한다.

식별자가 필요하고 지속적으로 값을 추적,변경해야한다면 그것은 값 타입보단 엔티티로 생성하는 것이 맞다.

728x90
Comments