동기
팀 내에서 IDDD(Implementing Domain-Driven Design) 스터디 중 값 객체 Collection을 ORM(hibernate)을 이용하여 구현하는 예제 를 확인했습니다.
그러던 중 이게 아주 과거 hibernate xml 구성 기준이어서 현재 JPA(Sprin Data JPA)에서는 어떻게 구현되는지 궁금 해 하던 @동묘 가 저에게 직접 구현을 보고 싶다고 해서 포스팅 하게 되었습니다.
구현
3가지 구현 방법
Single Column
Entity
Join Table
예시 도메인
그룹은 애그리게잇 루트(엔터티)입니다.
그룹맴버는 값 객체입니다.
한 그룹에는 여러 그룹맴버가 있습니다. (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 group1Members = new HashSet >();
...
}
GroupMembersConverter.java
@Converter
public class GroupMembersConverter implements AttributeConverter , String > {
private ObjectMapper om = new ObjectMapper ();
@Override
public String convertToDatabaseColumn ( Set attribute ) {
return om . writeValueAsString ( attribute );
}
@Override
public Set convertToEntityAttribute ( String dbData ) {
return om . readValue ( dbData , new TypeReference >() { });
}
}
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 (
description , group_members , name , group_id
) values (
? , ? , ? , ?
);
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 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 ( description , name , group_id ) 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 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 ( description , name , group_id ) 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 groupMembers = new HashSet >();
...
}
Summary
Single Column
Entity
Join Table
Reference
소스코드 : 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.
source : http://redutan.github.io/2018/05/29/ddd-values-on-jpa
---------------------------------------------------------------------------
Visit this link to stop these emails: http://zpr.io/nXidW