JPA에서 Value Object Collection 3가지 구현

동기

팀 내에서 IDDD(Implementing Domain-Driven Design) 스터디 중 값 객체 Collection을 ORM(hibernate)을 이용하여 구현하는 예제를 확인했습니다. 그러던 중 이게 아주 과거 hibernate xml 구성 기준이어서 현재 JPA(Sprin Data JPA)에서는 어떻게 구현되는지 궁금해 하던 @동묘가 저에게 직접 구현을 보고 싶다고 해서 포스팅 하게 되었습니다.

구현

3가지 구현 방법

  1. Single Column
  2. Entity
  3. Join Table

예시 도메인

Group과 GroupMember

  • 그룹은 애그리게잇 루트(엔터티)입니다.
  • 그룹맴버는 값 객체입니다.
  • 한 그룹에는 여러 그룹맴버가 있습니다. (Group#groupMembers)
  • 그룹과 그룹맴버는 한 애그리게잇으로 묶입니다.

Many Values Serialized into a Single Column

groups.group_members에 그룹맴버들 객체를 직렬화해서 저장합니다. 여기에서는 varchar(4000) 데이터타입으로써 JSON으로 직렬화 하겠습니다.

Java

GroupMember.java

@Embeddable
@Value
public class GroupMember {
    private String name;
    @Enumerated(EnumType.STRING)
    private GroupMemberType type;

    // For JPA
    GroupMember() {
        this.name = null;
        this.type = null;
    }

    // For Jackson
    @JsonCreator
    public GroupMember(@JsonProperty("name") String name, 
                       @JsonProperty("type") GroupMemberType type) {
        this.name = name;
        this.type = type;
    }
}

Group.java

@Entity
@Table(name = "GROUPS")
@Getter
@EqualsAndHashCode
@ToString
@NoArgsConstructor(access = AccessLevel.PACKAGE)
public class Group {
    @Id
    @GeneratedValue
    private Long groupId;
    private String description;
    private String name;

    /**
     * ORM과 한 열로 직렬화되는 여러 값 : ORM and Many Values Serialized into a Single Column
     */
    @Convert(converter = GroupMembersConverter.class)
    @Column(name = "GROUP_MEMBERS", length = 4000)
    private Set<GroupMember> group1Members = new HashSet<>();
    ...
}

GroupMembersConverter.java

@Converter
public class GroupMembersConverter implements AttributeConverter<Set<GroupMember>, String> {
    private ObjectMapper om = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(Set<GroupMember> attribute) {
        return om.writeValueAsString(attribute);
    }

    @Override
    public Set<GroupMember> convertToEntityAttribute(String dbData) {
        return om.readValue(dbData, new TypeReference<Set<GroupMember>>() { });
    }
}

SQL

Schema

create table groups (
    group_id bigint not null, 
    description varchar(255), 
    group_members varchar(4000), /* !!! */
    name varchar(255), 
    primary key (group_id)
);

Insert

insert into groups (
    group_id, description, group_members, name
) values (
    ?, ?, ?, ?
);

저장 후 테이블 조회 예시

GROUP_ID DESCRIPTION GROUP_MEMBERS NAME
1 설명 [{"name":"하위그룹","type":"GROUP"},{"name":"회원1","type":"MEMBER"}] 이름

Many Values Backed by a Database Entity

실질적으로는 Entity처럼 DB 스키마를 구성하나 실제 객체지향세계(ex:Java Application)에서는 값 객체로 보이게 구현

  • 이를 위해서 상속을 이용해서 식별자 속성을 은닉시키는 것이 구현의 핵심

Java

IdentifiedValueObject.java

@MappedSuperclass
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class IdentifiedValueObject {
    @Id
    @GeneratedValue
    @Getter(AccessLevel.PACKAGE)
    @Setter(AccessLevel.PACKAGE)
    private Long id;    // 패키지 접근제어를 통해서 식별자 은닉. 단, hibernate는 접근 가능
}

GroupMember.java

@Entity
@Table(name = "GROUP_MEMBERS")
@Value
@EqualsAndHashCode(callSuper = false)
public class GroupMember extends IdentifiedValueObject {
    private String name;
    @Enumerated(EnumType.STRING)
    private GroupMemberType type;

    // For JPA
    GroupMember() {
        this.name = null;
        this.type = null;
    }
}

Group.java

public class Group {
    ...
    /**
     * ORM과 데이터베이스 엔터티로 지원되는 여러 값 : ORM and Many Values Backed by a Database Entity
     */
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) // Aggregate Root 를 위한 일관성 설정
    @JoinColumn(name = "GROUP_ID")
    private Set<GroupMember> groupMembers = new HashSet<>();
    ...
}

SQL

Schema

create table groups (
    group_id bigint not null, 
    description varchar(255), 
    name varchar(255), 
    primary key (group_id)
);
create table group_members (
    id bigint not null,     // PK!!
    name varchar(255), 
    type varchar(255), 
    group_id bigint, 
    primary key (id)
)
alter table group_members 
    add constraint fk_groups_group_members
    foreign key (group_id) references groups;

Insert

insert into groups (group_id, description, name) values (?, ?, ?)
insert into group_members (name, type, id) values (?, ?, ?)
insert into group_members (name, type, id) values (?, ?, ?)
update group_members set group_id=? where id=?  /* Update ?! */
update group_members set group_id=? where id=?
  • 일반적으로 가장 무난하며, IDDD 책에서는 저자가 가장 추천한 방식

Many Values Backed by a Join Table

group_members을 테이블로 분리하는데 이 테이블은 PK가 없이 groups의 PK를 FK로 가지는 Join Table로 구현

Java

Group.java

public class Group {
    @Id
    @GeneratedValue
    private Long groupId;
    private String description;
    private String name;
    /**
     * ORM과 조인 테이블로 지원되는 여러 값 : ORM and Many Values Backed by a Join Table
     */
    @ElementCollection
    @CollectionTable(name = "GROUP_MEMBERS", joinColumns = @JoinColumn(name = "GROUP_ID"))
    private Set<GroupMember> groupMembers = new HashSet<>();
    ...
}

SQL

Schema

create table groups (
    group_id bigint not null, 
    description varchar(255), 
    name varchar(255), 
    primary key (group_id)
);
create table group_members (    /* No PK */
    group_id bigint not null, 
    name varchar(255), 
    type varchar(255)
);
alter table group_members 
    add constraint fk_groups_group_members 
    foreign key (group_id) references groups;

Insert

insert into groups (group_id, description, name) values (?, ?, ?);
insert into group_members (group_id, name, type) values (?, ?, ?);
insert into group_members (group_id, name, type) values (?, ?, ?);

이슈

값 객체 Collection에 새로운 값이 추가되거나 기존 값이 변경될 시 All Delete and Re-insert

@ElementCollection을 통해 변경이 발생할 시, 전체를 지우고 다시 입력하는 이슈가 있음. 이를 완화시키기 위해서 @OrderColumn을 추가하는 방법이 있긴하나 완벽하지는 않음

public class Group {
    ...
    @ElementCollection
    @CollectionTable(name = "GROUP_MEMBERS", joinColumns = @JoinColumn(name = "GROUP_ID"))
    @OrderColumn    // !!!
    private Set<GroupMember> groupMembers = new HashSet<>();
    ...
}

Summary

Total DB schema

  1. Single Column
  2. Entity
  3. Join Table

Reference

MJ

MJ
Backend 개발자 사람입니다. 어플리케이션의 복잡성을 다루는 DDD에 관심이 많습니다. 어제보다 더 나은 개발자가 되려고 항상 노력합니다.

spring boot 2.4.x 에서 openfeign + hystrix 통합하기

spring-boot 2.4.x spring-cloud 2020.x 의존성 상황에서 feign.hystrix.enabled=true가 안됨`feign.circuitbreaker.enabled=true` 로 바꿔보지만 openfeign과 hystr...… Continue reading

IDDD 14장. 애플리케이션

Published on June 19, 2018