JPA에서 값객체 생성(@Embeddable, @Embedded)

hongo·2023년 8월 13일
1
post-custom-banner

JPA에서 값객체 사용

현재 프로젝트를 하면서 Expense라는 테이블이 필요해졌다.

DB는 MySql을 사용하고 있다.

ColumnNameDataType
IdBIGINT
CurrencyVARCHAR(50)
AmountDECIMAL(38,3)
Category_idBIGINT

해당 테이블을 java코드로 작성하면 다음과 같다.

@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
public class Expense extends BaseEntity {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String currency;

    private Amount amount;

    @ManyToOne(fetch = LAZY, cascade = CascadeType.PERSIST)
    @JoinColumn(name = "category_id", nullable = false)
    private Category category;

	...
}

Expense의 비용은 값객체인 Amount 클래스를 만들어서 표현했다.

@Getter
public class Amount extends Number {

    @Column(name = "amount", precision = 38, scale = 3)
    private BigDecimal value;
    
    ...
        
}

application.ymljpa.hibernate.ddl-auto: create로 하고 실행시켜보자. Expense테이블을 생성하는 쿼리는 다음과 같다.

create table expense (
        category_id bigint not null,
        id bigint not null auto_increment,
        currency varchar(255) not null,
        amount varbinary(255),
        primary key (id)
    ) engine=InnoDB

amountvarbinary라는 타입으로 할당된 것을 볼 수 있다. varbinary는 바이너리 데이터를 저장할 때 사용되는 타입이라고 한다.

BigDecimal 타입을 Amountvalue에 할당하고 Expense객체를 save하면 varbinary의 최댓값을 초과했다는 예외가 발생한다.

Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'amount' at row 1

특별한 처리없이 값객체를 사용하면 varbinary타입으로 할당되어, 예측하지 않은 결과가 일어날 수 있다는 걸 배웠다. 그럼 값객체는 어떻게 사용해야 할까?

@Embeddable & @Embedded

@Embeddable

@Embeddable
@Getter
@NoArgsConstructor(access = PROTECTED)
public class Amount extends Number {

    private BigDecimal value;
    
    ...
        
}

값객체에 @Embeddable 어노테이션을 붙여주면 해결된다. @Embeddable은 해당 엔티티가 다른 엔티티에게 내장될 것임을 나타낸다고 한다. 밸덩 참고

값객체외에도 객체간의 상속 관계를 표현할 때 자주 사용되는 어노테이션이다. (부모클래스를 테이블화하고 싶지 않은 경우 @Embeddable을 적용)

@Embedded

내장 객체를 필드로 둘 때는 @Embedded를 사용한다.

@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
@SQLDelete(sql = "UPDATE expense SET status = 'DELETED' WHERE id = ?")
@Where(clause = "status = 'USABLE'")
public class Expense extends BaseEntity {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String currency;

    @Embedded 
    private Amount amount;
}

Amount@Embeddable을 붙인 상태에서 어플리케이션을 실행하면 다음과 같은 DDL이 생성된다.

create table expense (
        value decimal(38,2),
        category_id bigint not null,
        created_at datetime(6) not null,
        id bigint not null auto_increment,
        modified_at datetime(6) not null,
        currency varchar(255) not null,
        status enum ('DELETED','USABLE') not null,
        primary key (id)
    ) engine=InnoDB;

두둥! 타입은 varbinary에서 decimal(38,2)로 잘 바뀐 것을 볼 수 있지만 컬럼명이 value인 것을 알 수 있다. 컬럼명을 amount로 바꾸기 위해 value필드에 @Column을 사용해서 이름을 설정해준다.

@Embeddable
@Getter
@NoArgsConstructor(access = PROTECTED)
public class Amount extends Number {

    public static final Amount ZERO = new Amount(0);

    @Column(name = "amount", precision = 38, scale = 3)
    private BigDecimal value;
    ...
        
}

이제 어플리케이션을 실행시키면 원하는 DDL문이 실행되는 것을 볼 수 있다.

    create table expense (
        amount decimal(38,3),
        category_id bigint not null,
        created_at datetime(6) not null,
        id bigint not null auto_increment,
        modified_at datetime(6) not null,
        currency varchar(255) not null,
        status enum ('DELETED','USABLE') not null,
        primary key (id)
    ) engine=InnoDB;

부록 - @Embeddable, @Embedded 둘 중 하나만 있어도 잘 되는데?

@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
@SQLDelete(sql = "UPDATE expense SET status = 'DELETED' WHERE id = ?")
@Where(clause = "status = 'USABLE'")
public class Expense extends BaseEntity {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String currency;

    // @Embedded 
    private Amount amount;
}

@Embeddable@Embedded 중 하나만 붙여도 내장객체로 잘 동작한다. hibernate를 사용하면 둘 중 하나의 어노테이션만 사용해도, 동작한다고 한다. 스택오버플로우 참고

그래도 명시적으로 내장객체임을 암시하기 위해 두 어노테이션을 사용하는 게 좋을 것 같다.

post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 8월 17일

헐 홍고 교수님 잘 지내시나여 이 블로그를 이제서야 보다닛

답글 달기