본문 바로가기

SpringFramework/JPA

JPA - 값타입

엔티티 타입(Entity Type)과 값 타입(Value Type)의 특징

- 엔티티 타입(Entity Type)

    - 식별자(@Id)가 있다. 그리고 식별자로 구별할 수 있다

    - 생명 주기가 있다. (생성 - 영속 - 소멸)

    - 공유할 수 있다. 다른 엔티티에서 얼마든지 해당 엔티티를 참조할 수 있다.

 

- 값 타입(Value Type)

    - 임베디드 타입

    - 식별자가 없다.

    - 생명주기를 엔티티에 의존한다. 즉 의존하는 엔티티가 제거되면 같이 제거된다.

    - 공유하지 않는 것이 안전하다. 오직 하나의 주인만이 관리해야하며, 불변객체로 만드는 것이 안전하다.

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

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @Embedded Period workPeriod; // 근무기간

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

@Embeddable
public class Period {
    @Temporal(TemporalType.DATE) Date startDate;

    @Temporal(TemporalType.DATE) Date endDate;
}

@Embeddable
public class Address {
    @Column(name="city")
    private String city;

    private String street;
    
    private String zipcode;
}

- 임베디드 타입(=컴포넌트)은 기본 생성자가 필수 (보통 class를 만들면 선언하지 않아도 기본생성자가 컴파일하는 시점에 자동으로 만들어짐)

- 새로 정의한 값 타입들은 재사용할 수도 있고, 응집도도 아주 높다.

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

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

- @AttributeOverride: 매핑정보를 재정의 (임베디드 타입을 여러번 사용할 때 사용. 보통 같은 임베디드를 여러번 사용하는 일은 많지 않다.)

값 타입과 불변 객체

값 타입은 단순하고 안전하게 다루어야 한다.

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity");
member2.setHomeAddress(address);

공유 참조의 문제점이 발생하는 예제코드이다. 회원1의 주소를 참조해서 사용하여 회원2에 도시 정보를 "NewCity"로 변경하려고했으나, 같은 참조객체를 바라보고 있기때문에, 회원1도 "NewCity"로 변경이 되고 말았다.(자바의 기본타입이 아닌 객체 타입이기 때문에 발생하는 문제) 따라서 이 문제를 해결하기 위해서는 값 타입을 복사해서 사용해야 한다.

Address newAddress = address.clone(); // 객체 복사 (새로운 참조 주소값이 나옴)

 

(!) 자바의 기본 타입은 대입을 했을 경우에는 값을 복사하여 넘기고, 위와같이 객체를 대입했을 때는 참조를 넘긴다. 값타입을 다룰때는 이러한 특징을 유의해야한다.

 

- 불변객체

: 한 번 만들면 절대 변경할 수 없는 객체를 불변 객체라 한다. 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 차단할 수 있다. 따라서 값타입은 조회는 가능하지만, 수정은 할 수 없는 불변 객체로 설계하는것이 좋다.

값 타입 컬렉션

- 값 타입을 컬렉션에 담아서 쓴다. (연관관계 매핑에서 엔티티를 컬렉션으로 사용하는 것이 아니라 값 타입을 컬렉션에 쓰는 것)

- 값 타입 컬렉션은 값 타입을 하나 이상 저장할 때 사용한다.

- 컬렉션은 1:N의 개념이기 때문에 DB는 컬렉션을 하나의 테이블에 담을 수 없다. 따라서 별도의 테이블이 필요하다.

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

@Embeddable
public class Address {
    @Column
    private String city;
    private String street;
    private String zipcode;
}

public static main(String args[]) {
    Member member = new Member();
    member.setUsername("member1");
    member.setHomeAddress(new Address("homeCity", "street", "10000"));

    member.getFavoriteFoods().add("치킨");
    member.getFavoriteFoods().add("족발");
    member.getFavoriteFoods().add("피자");

    member.getAddressHistory().add(new Address("old1", "street1", "10001"));
    member.getAddressHistory().add(new Address("old2", "street2", "10002"));

    em.persist(member);
}

=> 값 타입 컬렉션으로 FAVORITE_FOOD와 ADDRESS 테이블이 생성되는 모습이다.

 

- 값 타입 컬렉션 조회시에는 지연 로딩을 기본적으로 사용

@ElementCollection(fetch = FetchType.LAZY)

 

값 타입 수정

Member member = em.find(Member.class, 1L);

// 1. 임베디드 값 타입 수정
// @Embeddable 되어있는 값 타입을 수정했지만 사실상 Member 테이블만 update한다.
member.setHomeAddress(new Address("새로운도시", "신도시1","123456");

// 2. 기본값 타입 컬렉션 수정
// String 타입은 수정 불가능, 탕수육을 삭제하고 치킨을 추가해주어야 함
Set<String> favoriteFoods = member.getFavoriteFoods();
member.getFavoriteFoods().remove("탕수육");
member.getFavoriteFoods().add("치킨");

// 3. 임베디드 값 타입 컬렉션 수정
// 값 타입은 불변해야한다. 따라서 기존 주소를 제거하고 새로 주소를 추가한다.
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울","기존주소", "123-123"));
addressHistory.add(new Address("새로운도시","새로운주소","000-000"));

 

값 타임 컬렉션의 제약사항

- 값타입은 엔티티와 다르게 식별자 개념이 없다.

- 값은 변경이 된다면, 추적이 어렵다.

-값타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관된다. 

따라서 여기에 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본데이터를 찾기 어렵다는 문제가 있다.

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

 

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

=> 값타입 컬렉션을 매핑하는 테이블은 모든 칼럼을 묶어서 기본키를 구성해야함 (NOT NULL, 중복저장 비허용)

 

값 타입 컬렉션 대안

- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려

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

- 영속성 전이(Cascade) + 고아객체(orphan)제거를 사용해서 값 타입 컬렉션처럼 사용하자.

@Entity
public class AddressEntity {
	@Id
    @GeneratedValue
    private Long id;
    
    @Embedded 
    Address address;
}

......

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<AddressEntity>();

참고자료

- 서적 - 자바 ORM 표준 JPA 프로그래밍 제 9장 - 김영한 지음

- https://lovecode.tistory.com/52

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

JPA - Criteria  (0) 2021.11.19
JPA - 객체지향 쿼리 언어, JPQL  (0) 2021.11.17
JPA - 연관관계 고급매핑  (0) 2021.11.15
JPA - 연관관계  (0) 2021.11.14
JPA - 엔티티 매핑  (0) 2021.11.12