[프로젝트] MapStruct 적용

공부하는 감자·2024년 2월 1일
0

F-Lab 프로젝트

목록 보기
7/11

들어가는 말

프로젝트에서 헥사고날 아키텍처를 중요하며 가장 중요한 것은 결합도를 떨어뜨리는 일이다.

도메인 계층과 외부 계층 혹은 외부 계층 간에 있어서, 각 계층은 다른 계층에 데이터를 전달할 때 DTO를 이용하여 데이터를 교환한다.

즉, 도메인 계층에서 사용하는 도메인 모델을 그대로 영속성 계층에 전달하는 것이 아니라, 두 계층 사이에 DTO를 두고 해당 DTO로 변환하여 전달해야 한다.

그러기 위해 각 계층에서 사용하는 데이터를 DTO로 변환하는 로직(mapper)이 필요했다.

내가 직접 코드를 짤 수도 있지만, 매퍼 클래스를 자동으로 생성해주는 라이브러리가 있다고 해서 그걸 적용하기로 했다.

MapStruct 적용하기

MapStruct란

공식 사이트에 따르면 다음과 같다.

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 타입 간의 매핑 구현을 단순화하는 코드 생성기이다.

사용하는 이유

  • Multi-layered 애플리케이션은 서로 다른 개체 모델(예: Entity 와 DTO) 간에 매핑해야 하는 경우가 많다.
  • 이러한 매핑 코드를 작성하는 건 지루하고 오류가 발생하기 쉬운 작업이다.
  • MapStruct는 이 작업을 가능한 한 자동화하여 단순화한다.
    • Annotation processor를 이용
  • 컴파일 타임에 매핑 코드를 생성하므로, 오타나 오류를 런타임 이전에 잡을 수 있다.
    • 생성된 코드를 직접 확인할 수 있다.
  • 다른 매핑 라이브러리보다 속도가 빠르다.

주의할 점

  • Lombok 라이브러리에 의존성 추가가 되어 있어야 한다.
  • Lombok의 @Getter @Setter @Builder 를 이용해 생성되므로, Lombok 보다 먼저 의존성이 선언된 경우 실행할 수 없다.

의존성 추가

build.gradle 에 의존성을 추가한다.

  • 이때, MapStruct는 lombok 아래에 위치해야 한다.
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 객체

나는 빌더 패턴을 사용하여 매퍼 구현체가 생성되었으면 하므로, 도메인 계층에서 사용하는 Member 객체에 어노테이션을 추가했다.

  • Member 객체를 DTO로 매핑할 때, Member의 변수 값을 읽어와야 하므로 @Getter 추가
  • DTO를 Member 객체로 매핑할 때, Setter 가 아닌 Builder 패턴을 사용하도록 @Builder 추가
@Builder
@Getter
public class Member {
    private Long id;
    private String userKey;
    private MemberStatus status;
    ...
}

영속성 계층: Entity

  • 위와 같은 이유로 @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 인터페이스 생성

매핑을 위한 인터페이스는 내가 생성해 줘야 한다.

@Mapper(componentModel = "spring")
public interface MemberPersistenceMapper {

    MemberPersistenceMapper INSTANCE = Mappers.getMapper(MemberPersistenceMapper.class);

    MemberEntity toMemberEntity(Member member);

    Member toMember(MemberEntity memberEntity);
}
  • interface를 생성한 후, @Mapper 어노테이션을 붙이면 MapStruct가 자동으로 해당 인터페이스 매퍼의 구현체를 생성해준다.
  • 스프링 빈으로 등록해야 할 경우, @Mapper(componentModel = "spring") 로 지정해주어야 한다.
  • 관례적으로 인터페이스는 구현체를 가져올 수 있는 INSTANCE 를 선언해야 한다.
  • 메서드의 네이밍은 to변환할타입 으로 선언한다.
    • Member 객체로 변환할 거면 toMember
  • 만약, 변환할 속성의 이름이 다른 경우 @Mapping 을 이용하여 설정할 수 있다.
    • 예를 들어 Member의 status 가 MemberEntity에선 statusCode 라면

      @Mapping(source = "status", target = "statusCode")
      MemberEntity toMemberEntity(Member member);

정적 팩토리 패턴으로 감추기

호출하는 법

인터페이스에서 선언했던 INSTANCE 변수를 이용해 메소드를 호출한다.

  • Member → MemberCreateResponse 로 변환할 경우
MemberMapper.INSTANCE.toMember(this);

정적 팩토리 패턴

처음엔 Adapter에서 호출해서 사용했었는데, 그러면 매퍼가 수정되면 Adapter 들도 수정되어야 한다는 피드백을 들었다. (SRP를 위반하는 경우겠지?)

따라서, 정적 팩토리 패턴을 이용해 각 객체 안에 매퍼를 호출하는 코드(생성 코드)를 숨겨두었다.

  • public static 변환후타입 from(변환전타입 객체) 메서드를 만들고, 그 안에서 매퍼를 통해 객체를 생성 후 반환한다.
  • 도메인 계층은 외부 계층에 의존하면 안되므로, MemberEntity 안에서 도메인 객체로 변환하는 메서드도 추가했다.
    • 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();
    }
}

Reference

참고 사이트

MapStruct – Java bean mappings, the easy way!

편리한 객체 간 매핑을 위한 MapStruct 적용기 (feat. SENS)

profile
책을 읽거나 강의를 들으며 공부한 내용을 정리합니다. 가끔 개발하는데 있었던 이슈도 올립니다.

0개의 댓글

관련 채용 정보