팀 내에서 IDDD(Implementing Domain-Driven Design) 스터디 중 값 객체 Collection을 ORM(hibernate)을 이용하여 구현하는 예제를 확인했습니다. 그러던 중 이게 아주 과거 hibernate xml 구성 기준이어서 현재 JPA(Sprin Data JPA)에서는 어떻게 구현되는지 궁금해 하던 @동묘가 저에게 직접 구현을 보고 싶다고 해서 포스팅 하게 되었습니다.
구현3가지 구현 방법
- Single Column
- Entity
- Join Table
예시 도메인
- 그룹은 애그리게잇 루트(엔터티)입니다.
- 그룹맴버는 값 객체입니다.
- 한 그룹에는 여러 그룹맴버가 있습니다. (Group#groupMembers)
- 그룹과 그룹맴버는 한 애그리게잇으로 묶입니다.
groups.group_members에 그룹맴버들 객체를 직렬화해서 저장합니다. 여기에서는 varchar(4000) 데이터타입으로써 JSON으로 직렬화 하겠습니다.
JavaGroupMember.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>>() { }); } }
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"}] | 이름 |
실질적으로는 Entity처럼 DB 스키마를 구성하나 실제 객체지향세계(ex:Java Application)에서는 값 객체로 보이게 구현
- 이를 위해서 상속을 이용해서 식별자 속성을 은닉시키는 것이 구현의 핵심
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<>(); ... }
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 책에서는 저자가 가장 추천한 방식
group_members을 테이블로 분리하는데 이 테이블은 PK가 없이 groups의 PK를 FK로 가지는 Join Table로 구현
JavaGroup.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<>(); ... }
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 (?, ?, ?);
@ElementCollection을 통해 변경이 발생할 시, 전체를 지우고 다시 입력하는 이슈가 있음. 이를 완화시키기 위해서 @OrderColumn을 추가하는 방법이 있긴하나 완벽하지는 않음
public class Group { ... @ElementCollection @CollectionTable(name = "GROUP_MEMBERS", joinColumns = @JoinColumn(name = "GROUP_ID")) @OrderColumn // !!! private Set<GroupMember> groupMembers = new HashSet<>(); ... }
- Single Column
- Entity
- Join Table
- 소스코드 : https://github.com/redutan/ddd-values
- Implementing Domain-Driven Design. Vaughn Vernon
- 자바 ORM 표준 JPA 프로그래밍. 김영한
- http://kwonnam.pe.kr/wiki/java/jpa/elementcollection
JPA에서 Value Object Collection 3가지 구현 was originally published by MJ at DevOOOOOOOOP on May 29, 2018.