자바 ORM 표준 JPA 프로그래밍 - 기본편 : 값 타입 매핑

jkky98·2024년 10월 4일
0

Spring

목록 보기
52/77

값 타입

JPA 관점에서 클래스는 @Entity의 유무에 따라 구분된다.

@Entity가 달린 엔티티 타입은 식별자(@Id)로 하여금 영속성 컨텍스트의 관리 대상이 된다. 엔티티가 변경되더라도 식별자로 인식이 가능해진다.

@Entity가 달리지 않은 값 타입은 int, Integer와 같이 Premitive 타입이나 래퍼 클래스, 임베디드 타입, 컬렉션 값 타입을 일컫는다.

기본값 타입

int와 같은 Premitive 타입이나 String과 같은 래퍼 클래스에 대해서 기본값 타입이라고 하며 생명주기를 엔티티에 의존한다.

기본값 타입 사용시 고민해야할 지점은 공유에 대한 문제이다.

Premitive 타입의 경우 항상 값을 복사하기에 공유 문제가 나타나지 않지만 클래스 기반의 래퍼 클래스의 경우 이러한 위험이 존재하지만 기본적으로 불변으로 설계되었기 때문에 안전하다.

Integer original = 5;
Integer incremented = original + 1; // 새로운 Integer 객체가 생성됨

임베디드 타입

@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;

    @OneToOne(mappedBy = "member", fetch = FetchType.LAZY)
    private Locker locker;

    private String city
    private String street
    private String zipcode

Member 엔티티에 city, street, zipcode와 같은 필드들이 존재한다 했을 때 이들은 모두 주소에 관련된 것들이니 Address라는 클래스에 넣어 사용할 수 있지 않을까라는 생각을 해볼 수 있다.

위의 구조를 다음과 같이 바꾸어 자바 객체의 참조 특성을 적용할 수 있다.

@Embeddable
public class Address {

    public String city;
    public String street;
    public String zipcode;
@Entity
public class Member extends BaseEntity {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;

    @OneToOne(mappedBy = "member", fetch = FetchType.LAZY)
    private Locker locker;

    @Embedded
    private Address address;

이전의 코드인 Member에 city, street, zipcode를 단순하게 넣는 경우와 DB 테이블의 형태는 동일하다. 하지만 아래의 경우로 설계한다면 우리는 Address라는 클래스로 city, street, zipcode를 한번 감싸 사용이 가능하다.

JPA는 이러한 임베디드 타입에 대한 매핑을 @Embedded, @Embeddable(임베디드 클래스에 기본생성자 필수)로 하여금 가능하게 해준다.

이렇게 객체로 묶어 사용할 수 있게 된다면 재사용성이 높아지고 해당 값 타입만의 메서드를 만들 수 있다. 그리고 여전히 임베디드 타입 객체는 해당 엔티티의 생명주기에 의존하게 된다.

  • 실제로 잘 설계된 ORM 어플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.

@AttributeOverride

한 엔티티에서 같은 임베디드 타입을 쓰는 두개의 Embedded 필드가 존재할 경우 기본적으로는 매핑이 되지 않고 예외가 발생한다.

Address HomeAddress와 Address ParentAddress를 같은 엔티티에 그냥 두고 @Embbeded만 걸어줄 경우 회원 테이블에는 city, street, zipcode가 중복되어 존재해야하기 때문에 당연히 문제가 된다. 이러한 문제를 해결하기 위해 @AttributeOverride 애노테이션을 사용하여 column명의 중복을 피해줄 수 있다.

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

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

위의 엔티티처럼 임베디드 타입의 필드와 컬럼명을 개발자가 직접 매핑해주어 이러한 중복문제를 해결할 수 있다.

동일한 타입의 문제뿐만 아니라 HomeAddress, WorkAddress로 클래스를 나누어 각각 적용해줄 수도 있다. 물론 이때도 서로의 필드명이 같을 경우 column명 중복문제가 발생하므로 @AttributeOverrides 설정이 필요할 수 있다.

값 타입 컬렉션(뷁)

먼저 설명하자면 값 타입 컬렉션을 엔티티 필드로 사용하지 않도록 하는 것을 권장한다.

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ElementCollection
    @CollectionTable(name = "user_phone_numbers", joinColumns = @JoinColumn(name = "user_id"))
    private List<PhoneNumber> phoneNumbers = new ArrayList<>();

값 타입 컬렉션이란 말 그대로 값 타입을 컬렉션에 넣어 사용하는 것으로 엔티티 필드에 이를 적용할 수도 있다.

이전에 우리가 엔티티 필드에 컬렉션을 사용한 경우는 연관관계 매핑에서 가능했지만 @ElementCollection를 활용하면 연관관계가 아닌 상황에서 값 타입 컬렉션을 사용 가능하다.

그런데 DB는 사실 하나의 테이블 레코드에 여러 데이터를 넣을 수 없다. 3차원 테이블이 아니기 때문이다. 3차원이상의 테이블로 구현하기위해 관계형 데이터베이스라는 것이 존재하는 것이고 결국 외래키등으로 연결한 새로운 테이블이 필요하다는 것이다.

그렇다. 결국 값 타입 컬렉션을 엔티티 필드에 적용해도 결국 새로운 테이블이 생성된다. 그런데 이러한 값 타입 컬렉션은 엔티티가 아니기 때문에 식별자 관리가 불가능하고 테이블에도 Id가 존재하지 않는다.

어차피 테이블도 생성되고 외래키 걸어서 사용해야 한다면 값 타입 컬렉션을 필드에 적용할 것이 아니라 새로운 엔티티를 설계하는 것이 옳을 것이다. 그래서 사용하지 않는다.

대안

실무에서는 상황에 따라 값 타입 컬렉션 대신 1:N 관계로의 설계를 고려한다. 주인 엔티티쪽에 연관관계의 주인을 주는 방법이다.

일반적으로 엔티티-엔티티 연관관계는 N:1로 풀어내는 것이 권장되는 방식이었다. 외래키가 존재하는 쪽에 연관관계의 주인을 주는 것 말이다.

그러므로 꼭 값 타입이라 판단될 때만 이렇게 1:N으로 풀어주도록 한다. 식별자가 필요하고 지속적으로 값을 추적해야하고 변경해야한다면 그것은 값 타입이 아닌 엔티티이므로 다시 생각해보아야 한다.

임베디드 타입 설계시 불변성 꼭 적용

이전에 Primitive(기본형)은 공유 문제에 있어 안전하며 래퍼 클래스또한 불변이라 공유 문제에 안전하다고 말했다. 그러므로 직접 설계한 임베디드 타입 또한 불변성을 적용해주어야 한다. 생성시점 이후의 수정을 막는다던지 수정시에 return을 새로운 객체 생성을 통해 수정을 구현한다던지 해야한다.

즉 이러한 값 타입들의 불변을 중요시 하는 이유는 이들이 엔티티 타입의 필드로 들어가기 위해 공유문제에 대한 일관성을 확보하는 것이라고 생각하면 된다.

평소에 엔티티 필드에 사용하던 Primitive나 래퍼 클래스는 위의 내용을 기반으로 안전하니 직접 설계한 클래스를 반영할 때 불변적 특징을 우선 디폴트로 깔고 가야한다는 것이다.(JPA가 불변을 강제하지는 않는다. 하지만 기본적으로는 불변 필드를 구성한다.)

profile
자바집사의 거북이 수련법

0개의 댓글