프로젝트에서 헥사고날 아키텍처를 중요하며 가장 중요한 것은 결합도를 떨어뜨리는 일이다.
도메인 계층과 외부 계층 혹은 외부 계층 간에 있어서, 각 계층은 다른 계층에 데이터를 전달할 때 DTO를 이용하여 데이터를 교환한다.
즉, 도메인 계층에서 사용하는 도메인 모델을 그대로 영속성 계층에 전달하는 것이 아니라, 두 계층 사이에 DTO를 두고 해당 DTO로 변환하여 전달해야 한다.
그러기 위해 각 계층에서 사용하는 데이터를 DTO로 변환하는 로직(mapper)이 필요했다.
내가 직접 코드를 짤 수도 있지만, 매퍼 클래스를 자동으로 생성해주는 라이브러리가 있다고 해서 그걸 적용하기로 했다.
공식 사이트에 따르면 다음과 같다.
MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach.
MapStruct는 설정 기반 접근 방식을 기반으로 하는 Java bean 타입 간의 매핑 구현을 단순화하는 코드 생성기이다.
@Getter
@Setter
@Builder
를 이용해 생성되므로, Lombok 보다 먼저 의존성이 선언된 경우 실행할 수 없다.build.gradle
에 의존성을 추가한다.
dependencies {
...
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
...
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
...
}
나는 빌더 패턴을 사용하여 매퍼 구현체가 생성되었으면 하므로, 도메인 계층에서 사용하는 Member 객체에 어노테이션을 추가했다.
@Getter
추가Setter
가 아닌 Builder
패턴을 사용하도록 @Builder
추가@Builder
@Getter
public class Member {
private Long id;
private String userKey;
private MemberStatus status;
...
}
@Builder
와 @Getter
추가@Entity
@Builder
@AllArgsConstructor
@Getter
@Table(name = "user")
public class MemberEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String userKey;
@Convert(converter = MemberStatusAttributeConverter.class)
private MemberStatus status;
...
}
매핑을 위한 인터페이스는 내가 생성해 줘야 한다.
@Mapper(componentModel = "spring")
public interface MemberPersistenceMapper {
MemberPersistenceMapper INSTANCE = Mappers.getMapper(MemberPersistenceMapper.class);
MemberEntity toMemberEntity(Member member);
Member toMember(MemberEntity memberEntity);
}
@Mapper
어노테이션을 붙이면 MapStruct가 자동으로 해당 인터페이스 매퍼의 구현체를 생성해준다.@Mapper(componentModel = "spring")
로 지정해주어야 한다.INSTANCE
를 선언해야 한다.to변환할타입
으로 선언한다.@Mapping
을 이용하여 설정할 수 있다.예를 들어 Member의 status
가 MemberEntity에선 statusCode
라면
@Mapping(source = "status", target = "statusCode")
MemberEntity toMemberEntity(Member member);
인터페이스에서 선언했던 INSTANCE
변수를 이용해 메소드를 호출한다.
MemberMapper.INSTANCE.toMember(this);
처음엔 Adapter에서 호출해서 사용했었는데, 그러면 매퍼가 수정되면 Adapter 들도 수정되어야 한다는 피드백을 들었다. (SRP를 위반하는 경우겠지?)
따라서, 정적 팩토리 패턴을 이용해 각 객체 안에 매퍼를 호출하는 코드(생성 코드)를 숨겨두었다.
public static 변환후타입 from(변환전타입 객체)
메서드를 만들고, 그 안에서 매퍼를 통해 객체를 생성 후 반환한다.Member toMember()
@Entity
@Builder
@AllArgsConstructor
@Getter
@Table(name = "user")
public class MemberEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String userKey;
@Convert(converter = MemberStatusAttributeConverter.class)
private MemberStatus status;
...
public static MemberEntity from(Member member) {
return MemberPersistenceMapper.INSTANCE.toMemberEntity(member);
}
public Member toMember() {
return MemberPersistenceMapper.INSTANCE.toMember(this);
}
}
컴파일 및 빌드를 했더니 main/generated/경로
하위에 구현체가 생겨났다.
파일을 열어보면 아래와 같이 자동으로 코드를 만들어줬음을 확인할 수 있다.
package com.flab.funding.infrastructure.adapters.output.persistence.mapper;
import com.flab.funding.domain.model.Member;
import com.flab.funding.infrastructure.adapters.output.persistence.entity.MemberEntity;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2024-02-01T14:59:00+0900",
comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.9 (Oracle Corporation)"
)
@Component
public class MemberPersistenceMapperImpl implements MemberPersistenceMapper {
@Override
public MemberEntity toMemberEntity(Member member) {
if ( member == null ) {
return null;
}
MemberEntity.MemberEntityBuilder memberEntity = MemberEntity.builder();
memberEntity.id( member.getId() );
memberEntity.userKey( member.getUserKey() );
memberEntity.status( member.getStatus() );
memberEntity.linkType( member.getLinkType() );
memberEntity.email( member.getEmail() );
memberEntity.userName( member.getUserName() );
memberEntity.nickName( member.getNickName() );
memberEntity.phoneNum( member.getPhoneNum() );
memberEntity.gender( member.getGender() );
memberEntity.birthday( member.getBirthday() );
memberEntity.password( member.getPassword() );
memberEntity.lastLoginAt( member.getLastLoginAt() );
memberEntity.createdAt( member.getCreatedAt() );
memberEntity.updatedAt( member.getUpdatedAt() );
return memberEntity.build();
}
@Override
public Member toMember(MemberEntity memberEntity) {
if ( memberEntity == null ) {
return null;
}
Member.MemberBuilder member = Member.builder();
member.id( memberEntity.getId() );
member.userKey( memberEntity.getUserKey() );
member.status( memberEntity.getStatus() );
member.linkType( memberEntity.getLinkType() );
member.email( memberEntity.getEmail() );
member.userName( memberEntity.getUserName() );
member.nickName( memberEntity.getNickName() );
member.phoneNum( memberEntity.getPhoneNum() );
member.gender( memberEntity.getGender() );
member.birthday( memberEntity.getBirthday() );
member.password( memberEntity.getPassword() );
member.lastLoginAt( memberEntity.getLastLoginAt() );
member.createdAt( memberEntity.getCreatedAt() );
member.updatedAt( memberEntity.getUpdatedAt() );
return member.build();
}
}