기록

[자바 ORM 표준 JPA 프로그래밍] JPA 값 타입 - 기본 값 타입, 임베디드 타입, 값 타입 컬렉션 (9장) 본문

교육/책

[자바 ORM 표준 JPA 프로그래밍] JPA 값 타입 - 기본 값 타입, 임베디드 타입, 값 타입 컬렉션 (9장)

youngyin 2024. 12. 1. 18:00

시작하면서

자바 ORM 표준 JPA 프로그래밍을 공부하면서 9장에서 다루는 "값 타입"에 대해 정리해보았습니다. 이번 글에서는 값 타입의 종류와 그 특징을 설명하며, 실제 개발에 어떤 점들을 고려해야 하는지 소개합니다.

값 타입이란?

JPA에서 값 타입은 int, Integer, String처럼 단순한 값을 나타내는 자바 타입이나 객체를 말합니다. JPA에서는 이러한 값 타입을 크게 세 가지로 나눌 수 있습니다.

  1. 기본 값 타입: 자바에서 제공하는 기본 데이터 타입 (int, double, Integer, String 등)
  2. 임베디드 타입 (복합 값 타입): 사용자가 직접 정의한 여러 속성을 하나로 묶은 값 타입
  3. 컬렉션 값 타입: 값 타입을 여러 개 저장하기 위해 컬렉션에 보관하는 방식입니다.

각 값 타입은 JPA에서 특정한 역할을 하고 제약이 있으므로, 이들을 잘 이해하고 적절하게 사용하는 것이 중요합니다.

값 타입 선택 가이드

  • 기본 값 타입: 단순한 값을 저장할 때 사용합니다. 기본적으로 변경 가능하므로 복사해서 사용하는 것이 안전합니다.
  • 임베디드 타입: 여러 필드를 묶어서 하나의 값 객체로 관리하고 싶을 때 사용합니다. 코드의 재사용성과 가독성을 높이는데 유용합니다.
  • 컬렉션 값 타입: 값 타입을 하나 이상 관리해야 할 때 사용합니다. 식별자가 없고 변경 추적이 어렵기 때문에 주의해서 사용해야 합니다.

1. 기본 값 타입

기본 값 타입은 자바의 기본 타입과 유사하게 사용됩니다. 예를 들어, int, double 같은 기본 타입은 절대 공유되지 않고 값 자체를 복사해서 사용합니다. 래퍼 클래스인 IntegerString도 마찬가지입니다. 이러한 타입들은 객체이지만, 자바 언어 차원에서 기본 타입처럼 사용할 수 있어 JPA에서도 기본 값 타입으로 정의됩니다.

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

2.1 임베디드 타입이란?

임베디드 타입은 새로운 값 타입을 직접 정의해서 사용할 수 있는 기능을 의미합니다. 예를 들어, 회원 엔티티가 이름, 근무 시작일, 근무 종료일, 주소(도시, 번지, 우편번호) 같은 여러 필드를 가진다면, 이러한 속성들을 별도로 관리하기 위해 임베디드 타입으로 분리할 수 있습니다.

임베디드 타입을 정의하려면 @Embeddable 어노테이션을 사용하거나, 엔티티 내에 @Embedded로 선언하면 됩니다. 이렇게 하면 코드의 재사용성과 가독성을 높일 수 있습니다.

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

예를 들어, 회원 엔티티가 근무기간집 주소를 임베디드 타입으로 가지고 있다고 가정해 봅시다.

@Embeddable
public class Period {
    private LocalDate startDate;
    private LocalDate endDate;
    // getters and setters
}

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
    // getters and setters
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;
    // getters and setters
}

 

위 코드에서 PeriodAddress는 임베디드 타입으로 정의되어 Member 엔티티에 포함됩니다. 이를 통해 관련된 값들을 그룹화하여 깔끔하게 관리할 수 있습니다. 예를 들어, 위 구조를 사용하여 Member 테이블에 데이터가 저장될 때는 다음과 같은 INSERT 쿼리가 실행됩니다:

INSERT INTO Member (id, name, startDate, endDate, city, street, zipcode) VALUES (1, 'John Doe', '2023-01-01', '2023-12-31', 'Seoul', 'Gangnam-daero', '12345');

 

이렇게 PeriodAddress의 필드들이 Member 테이블의 컬럼으로 매핑되어 저장됩니다.

2.3 @AttributeOverride로 속성 재정의하기

임베디드 타입의 속성을 재정의해야 할 경우 @AttributeOverride 어노테이션을 사용할 수 있습니다. 예를 들어, 회원 엔티티에 추가적인 주소 정보가 필요하다면 다음과 같이 매핑 정보를 재정의할 수 있습니다.

@Embedded
@AttributeOverrides({
    @AttributeOverride(name = "city", column = @Column(name = "work_city")),
    @AttributeOverride(name = "street", column = @Column(name = "work_street")),
    @AttributeOverride(name = "zipcode", column = @Column(name = "work_zipcode"))
})
private Address workAddress;

 

이렇게 하면 같은 Address 임베디드 타입을 재사용하면서도 다른 컬럼 매핑을 지정할 수 있습니다.

3. 값 타입과 불변 객체

3.1 값 타입의 공유 참조 문제

값 타입은 여러 엔티티에서 공유될 경우 위험할 수 있습니다. 예를 들어, 회원1과 회원2가 같은 주소 객체를 참조하고 있다면, 한쪽에서 주소를 변경할 경우 다른 쪽에도 영향을 미치게 됩니다. 이는 값 타입의 의도와 맞지 않으며, 예기치 못한 버그를 초래할 수 있습니다.

Member member1 = new Member();
member1.setHomeAddress(new Address("OldCity"));
System.out.println("회원1의 주소: " + member1.getHomeAddress().getCity()); // 출력: OldCity

Address address = member1.getHomeAddress();
address.setCity("NewCity");

Member member2 = new Member();
member2.setHomeAddress(address);
System.out.println("회원2의 주소: " + member2.getHomeAddress().getCity()); // 출력: NewCity
System.out.println("회원1의 주소: " + member1.getHomeAddress().getCity()); // 출력: NewCity (원치 않는 변경 발생)

 

위 코드에서 회원2에 새로운 주소를 설정하려고 했지만, 회원1의 주소도 함께 변경되는 문제가 발생합니다. 이러한 문제를 해결하기 위해 값 타입은 불변 객체로 설계하는 것이 좋습니다.

3.2 불변 객체로 만들기

값 타입을 불변하게 만들면 공유 참조로 인한 부작용을 방지할 수 있습니다. 불변 객체로 만들기 위해서는 객체 생성 이후에는 값을 변경할 수 없도록 설계합니다. 예를 들어, 모든 필드를 final로 선언하고, setter 메서드를 제공하지 않으면 됩니다.

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

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    // getters only, no setters
}

4. 값 타입 컬렉션

값 타입을 하나 이상 저장해야 한다면 컬렉션에 보관할 수 있습니다. JPA에서는 이를 위해 @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<>();
}

 

위 예시에서 favoriteFoods는 회원이 좋아하는 음식을 저장하는 값 타입 컬렉션입니다. 값 타입 컬렉션은 기본적으로 LAZY 로딩 전략을 사용하며, 이를 통해 여러 값 타입을 손쉽게 관리할 수 있습니다.

4.1 값 타입 컬렉션의 제약사항

값 타입 컬렉션은 식별자가 없기 때문에 영속성 컨텍스트에서 관리하기가 어렵습니다. 따라서 값 타입 컬렉션은 상태를 변경하기보다는 통째로 삭제하고 다시 추가하는 방식으로 관리하는 것이 일반적입니다. 만약 식별자가 필요하거나 값의 변경을 자주 추적해야 한다면, 값 타입이 아닌 엔티티로 설계하는 것이 더 적합합니다.

마무리

JPA에서 값 타입을 사용하는 것은 객체지향적인 설계를 데이터베이스와 연결하는 중요한 방법입니다. 기본 값 타입, 임베디드 타입, 값 타입 컬렉션을 적절히 활용하면 보다 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있습니다. 하지만 값 타입의 특성을 잘 이해하고, 특히 공유 참조 문제와 불변 객체 설계에 유의해야 합니다.

Comments