09. 값 타입

zwundzwzig·2023년 10월 27일
0

JPA 데이터 타입을 크게 두 가지로 분류하면 @Entity로 정의하는 엔티티 타입과 Integer, int, String 처럼 단순 자바 기본 타입으로 정의하는 값 타입으로 나눌 수 있다.

식별자를 통해 추적하는 엔티티 타입과 달리, 값 타입은 속성으로만 구성돼 있다는 특징이 있다. 값 타입은 크게 3가지로 나뉜다.

Basic Value Type

앞서 말한, 자바 기본 타입을 사용한 기본값 타입이다.

자바 기본 타입 이외에도 래퍼 클래스도 이에 포함된다.

속한 엔티티와 생명주기를 같이 한다.

Embedded Type

복합값 타입이라 불리며, 하나의 멤버 변수에 복수의 값을 가진 객체를 넣는 것이다.

@Entity
@Table(name = "communities")
@Getter
public class Community extends BaseTimeEntity {
	@Embedded
    private RecruitPeriod recruitPeriod;
}

@Embeddable
@NoArgsConstructor
@AllArgsConstructor
public class RecruitPeriod {
    @Column
    private LocalDateTime beginRecruit;
    @Column
    private LocalDateTime finishRecruit;
}

위와 같이 사용할 수 있다. 이때 RecruitPeriod와 같은 임베디드타입의 클래스는 기본생성자가 필수로 있어야 한다.

물론, 이렇게 임베디드 타입의 컬럼이 나뉜다고 해서 또 다른 테이블이 생기는 게 아니다. 이 복합값 타입을 통해 테이블마다 중복되는 컬럼을 한 번에 모을 수 있다.

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

@AttributeOverride

임베디드 타입에 정의한 매핑 정보를 재정의할 때 엔티티에 설정하면 된다.

public class Member {
	@Id @GeneratedValue
    	@Column(name = "MEMBER_ID")
    	private Long id;

    	@Embedded
    	private Address homeAddress;

    	@Embedded
        @AttributeOverrides({
        	@AttributeOverride(
            	name="city", 
                column=@Column(name="COMPANY_CITY")
            ),
            @AttributeOverride(
            	name="street", 
                column=@Column(name="COMPANY_STREET")
            )
        })
    	private Address companyAddress;
 }

값 타입과 불변 객체

값 타입은 복잡한 객체를 조금이라도 단순화하려고 만든 개념이다.

하지만, 임베디드 타입의 값 타입을 여러 엔티티에서 공유하면 아래처럼 다른 엔티티가 변경한 임베디드 타입의 값 타입이 예기치 못하게 영향을 줄 수도 있다.

Member member1 = new Member();
member1.setName("member1");
member1.setHomeAddress(address);
em.persist(member1);

Member member2 = new Member();
member2.setName("member2");
member2.setHomeAddress(address);
em.persist(member2);

// member2의 City를 newCity로
member2.getHomeAddress().setCity("newCity");

따라서, 인스턴스를 복사하는 방식으로 임베디드 타입의 값을 다뤄야 한다.

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

Member member1 = new Member();
member1.setName("member1");
member1.setHomeAddress(address);
em.persist(member1);

Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());

Member member2 = new Member();
member2.setName("member2");
member2.setHomeAddress(copyAddress);
em.persist(member2);

// member2의 City를 newCity로
member2.getHomeAddress().setCity("newCity");

하지만 이는 근본적으로 객체의 공유 참조를 막는 해결책이 되지 못한다.

불변 객체

무분별한 setter를 없애 값타입을 불변 객체로 만드는 것을 저자는 추천한다.

Collection Value Type

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

// Member 클래스
@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Embedded
    private Address homeAddress;

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

    ...
}

// Address 클래스
@Embeddable
public class Address {

    @Column(name = "city")
    private String city;
    private String street;
    private String zipcode;

    ...
}

RDBMS는 컬렉션을 저장할 수 없기 때문에 컬렉션 값 타입을 사용하면 된다.

Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));

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

member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));

em.persist(member);

멤버만 저장해도 다른 테이블에도 INSERT 문이 호출돼 저장된다.

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

수정

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

Member findMember = em.find(Member.class, member.getId());

// 임베디드 값 타입 수정
Address old = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", old.getStreet(), old.getZipcode()));

// 기본 값 타입 컬렉션 수정
Set<String> favoriteFoods = findMember.getFavoriteFoods();
favoriteFoods.remove("치킨");
favoriteFoods.add("김치볶음밥");

// 임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = findMember.getAddressHistory();
addressHistory.remove(new Address("old1", "street", "10000"));
addressHistory.add(new Address("newCity1", "street", "10000"));

임베디드 값 타입 수정

homAddress는 임베디드 값 타입으로 MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE 한다.
Member 엔티티 수정하는 것과 동일하다.

기본 값 타입 컬렉션 수정

기본 값 타입 컬렉션을 수정하기 위해서는 수정할 값을 제거하고, 새로운 값을 추가해야 한다.
치킨에서 김치볶음밥으로 수정하기 위해 치킨을 제거하고 김치볶음밥을 추가했다.

임베디드 값 타입 컬렉션 수정

값 타입은 불변해야 하기 때문에 기존 주소를 삭제하고 새로운 주소를 등록한다.
임베디드 값 타입 컬렉션을 수정하기 위해서는 equals() 메서드 재정의와 hashCode() 메서드를 구현해야 한다.

값 타입 컬렉션의 제약사항

값 타입은 엔티티와 다르게 식별자 개념이 없다. 따라서 값을 변경하게 되면 데이터베이스에 저장된 원본 데이터를 찾기 어렵다.

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

그렇기 때문에 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 복합 키를 구성해야 한다.

일대다

상황에 따라 값 타입 컬렉션 대신 고려해야 한다.

일대다 관계를 위한 엔티티를 만들고, 해당 엔티티에서 값 타입을 사용한다. 중간 테이블과 같은 개념이다.

profile
개발이란?

0개의 댓글