본문 바로가기
JAVA/요점정리

[Spring] MapStruct 라이브러리 사용해서 dto to entity 매핑 간단화시키기

by 진짠 2024. 11. 18.
728x90
배경

 
새로 프로젝트를 제작하게 되었습니다. 레이어드 아키텍처 기반이며 계층 간의 모듈화를 좀 더 엄격하게 관리하고 싶어 dto와 entity를 별도로 관리하게 되었습니다. 기존에는 빌더 패턴을 통해 각각의 값들을 매핑해주었습니다.
 

@Controller
public class ExController {
...
	@PostMapping("/testUrl")
    @ResponseBody
    public List<TestDto> getWorkPlanList(@RequestBody TestSearch testSearch) {
        List<Test> testList = testService.findSpYjSelect(testSearch);
        return getTestListEntityToDto(testList);
    }
    
    public List<TestDto> getTestListEntityToDto(List<Test> testList) {
        return testList.stream()
                .map(test -> TestDto.builder()
                        .rowNum(test.getRowNum())
                        .col1(test.getCol1())
                        .col2(test.getCol2())
                        // 생략
                        .build())
                .collect(Collectors.toList());
    }
...
}

 
 

@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TestDto {
    ...
}

 
Dto 쪽은 롬복 어노테이션을 통해 다음과 같이 생성했습니다. 빌더 패턴을 생성하기 위해 생성자 조건인 AllArgsConstructor와 NoArgsConstructor를 붙혀주었습니다. 
 
데이터를 객체에 매핑하기 위해 빌더 패턴을 사용했지만 모든 객체들을 다 매핑해줘야 되기 때문에 컬럼 개수가 10개만 넘어가도 꽤나 피곤한 작업이었습니다. 이런 방식으로 모든 객체를 매핑해주는 시간을 단축시키고 싶었습니다.
 
그래서 이 문제를 해결함과 동시에 객체 매핑, 그 외 로직의 책임을 분리할 수 있는 MapStruct를 적용하게 되었습니다.
 

적용

 
MapStruct를 선택한 이유는 다음과 같습니다.
 
1. 매핑 코드를 컴파일 시점에서 생성합니다. 이는 컴파일 단계에서 오류를 감지하기 때문에 런타임 오류를 줄일 수 있습니다.
2. 빠릅니다. 매핑 코드를 자바 코드로 생성하기 때문에 다른 라이브러리와 비교했을 때 더 빠릅니다.
3.커스터마이징. 기본적인 매핑 외에도 개발자가 직접 커스텀 할수 있습니다.
 
지금이야 간단한 매핑만 사용하지만 확장성을 고려해봤을 때 좋은 선택이 될 수 있을 것 같습니다.
 
제일 먼저 의존성 등록을 해줍니다. 
https://mvnrepository.com/search?q=mapstruct
다음 페이지에서 최신버전을 등록합니다. 저의 경우에는 1.5.3final 을 사용했습니다.

<!-- pom.xml -->
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.3.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.3.Final</version>
    <scope>provided</scope>
</dependency>

<!-- gradle -->
// https://mvnrepository.com/artifact/org.mapstruct/mapstruct
implementation group: 'org.mapstruct', name: 'mapstruct', version: '1.5.3.Final'
// https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor
implementation group: 'org.mapstruct', name: 'mapstruct-processor', version: '1.5.3.Final'

 
core와 processor 모두 추가해줍니다. core는 실행(runtime)과 관련된 의존성을 제공하는 부분이고 processor는 컴파일 단계에서 매핑 코드를 생성하는 역할을 담당합니다.
 
그리고 애너테이션 프로세서로 등록해줍니다. mapStruct는 컴파일 단계에서 어노테이션을 확인하여 매핑 코드를 자동으로 생성해줍니다. 그래서 애너테이션 프로세서에 등록을 해줘야 mapStruct 관련 어노테이션을 인식하고 코드를 추가할 수 있습니다.
 
참고로 lombok 또한 등록해줘야 합니다. 롬복 만세
 
추가해주는 방법은 pom.xml 에서

<configuration>
    ...
    <annotationProcessorPaths>
        <path>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>1.5.3.Final</version>
        </path>
        <path>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </path>
    </annotationProcessorPaths>
</configuration>

 
다음과 같이 <configuration> 안에 추가해주면 됩니다. gradle 의 경우는 ' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final' 과 같은 식으로 추가할 수 있습니다.
 
자세한 설명은 깃허브 홈페이지에서도 볼 수 있습니다.

 

GitHub - mapstruct/mapstruct: An annotation processor for generating type-safe bean mappers

An annotation processor for generating type-safe bean mappers - mapstruct/mapstruct

github.com

 
그래도 프로세서가 작동하지 않으면 IDE 단계에서 애너테이션 프로세서를 활성화 해줍니다. 다음은 인텔리제이 기준 방법입니다.

1. File &amp;amp;gt; Settings

 

2. Build, Execution, Deployment &amp;amp;gt; Compiler &amp;amp;gt; Annotation Processors
3. Enable annotation processing Obtain processors from project classpath 체크

의존성을 추가했으면 매핑 관련 인터페이스를 만들어줍니다. 

@Mapper(componentModel = "spring")
public interface TestMapper {
	TestMapper INSTANCE = Mappers.getMapper(TestMapper.class);
    TestDto map(Test entity);

    List<TestDto> toDtoList(List<Test> entityList);
}

 
스프링 부트 기준, (componentModel = "spring") 을 사용하여 스프링 빈에 등록해줄 수 있습니다. 위 코드는 Entity -> DTO, 그리고 컬렉션 list를 매핑하는 인터페이스 이며 DTO -> Entity가 필요하다면 따로 선언해줘야 합니다.
 
두 객체의 이름이 같을 경우에만 위처럼 생성할 수 있고 객체의 이름이 다르다면 @Mapping 어노테이션을 통해 별도로 매핑하는 작업이 필요합니다.
 
또 매핑 객체는 생성자를 선언해줘야 매핑 코드를 만들 수 있습니다.
위 상황을 예로 들면 TestDto 객체엔 생성자를 만들어줘야 합니다.
 
여기까지 끝이지만 더 간소화하고 싶다면 BaseMapper 인터페이스를 생성하여 오버라이딩 해주는 방법을 사용합니다.

public interface BaseMapper<E, D> {
    D map(E entity);
    List<D> toDtoList(List<E> entityList);
}

 
다음과 같이 기본 인터페이스를 만들어주고 기존에 만든 인터페이스는
 

@Mapper(componentModel = "spring")
public interface TestMapper extends BaseMapper<Test, TestDto> {
    TestMapper INSTANCE = Mappers.getMapper(TestMapper.class);
    @Override
    TestDto map(Test entity);

    @Override
    List<TestDto> toDtoList(List<Test> entityList);
}

 
BaseMapper를 extends 하여 Generate - Override method 를 사용하여 오버라이딩 했습니다. 단축키는 윈도우 기준 'Alt+Insert' 맥은 'cmd+shift+N' 입니다. 이제 두 딸깍만 하면 매핑 완성입니다.
 
클라이언트 코드는 

@Controller
@RequiredArgsConstructor
@RequestMapping("/your/path")
public class TestController {
	private final TestMapper testMapper;
    ...
    
    public ... {
    	return testMapper.toDtoList(testList);
    }
}

 
다음과 같이 빈으로 생성된 매퍼를 주입하여 사용하면 됩니다.
 
기존 롬복의 빌더 패턴을 이용하여 컬럼을 하나하나 세팅해주던 방식에서 MapStruct 라이브러리의 자동 매핑 방식을 사용하니 매우 간편하게 수정이 가능했습니다.
 
물론 이렇게 자동으로 매핑하기 위해서는 변수명이 모두 같아야 합니다. 값이 다를 경우 @Mapping 어노테이션을 통해 별도로 매핑 작업이 필요합니다.
 
그 외에 유용한 어노테이션이 있다면
 
@InheritInverseConfiguration - 매핑 메서드의 역매핑을 자동으로 생성

@Mapper
public interface UserMapper {
    @Mapping(source = "id", target = "userId")
    UserDTO userToUserDTO(User user);

    @InheritInverseConfiguration
    User userDTOToUser(UserDTO userDTO);
}

 
@InheritConfiguration - 매핑 메서드의 매핑 설정을 재사용

@Mapper
public interface UserMapper {
    @Mapping(source = "name", target = "username")
    UserDTO userToUserDTO(User user);

    @InheritConfiguration(name = "userToUserDTO")
    void updateUserFromDTO(UserDTO userDTO, @MappingTarget User user);
}

 
@AfterMapping / @BeforeMapping - 매핑 전, 후 커스텀 로직 추가

@Mapper
public interface UserMapper {
    @Mapping(source = "name", target = "username")
    UserDTO userToUserDTO(User user);

    @AfterMapping
    default void afterMapping(@MappingTarget UserDTO userDTO) {
        userDTO.setExtraField("Extra Value");
    }
}

 
NullValueMappingStrategy - Null 값을 처리하는 전략 정의

@Mapper(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT)
public interface UserMapper {
    UserDTO userToUserDTO(User user);
}

 
 
저는 @Mapping 어노테이션 외에는 당장 필요하진 않지만 알아두면 나중에 쓸 때 도움이 될 것 같네요.

 

---

24.12.19 추가

IDE에서 다른 프로젝트를 작업하다가 다시 넘어와서 프로젝트를 로드하면 인식하지 못하는 경우가 있는 것 같습니다. maven일 경우 확인하여 maven clean 후 install 해주면 다시 정상적으로 사용 가능합니다.

728x90

댓글