mapstruct는 객체간 매핑한 코드를 생성 해주는 library 이다.
이번 포스팅에서는 mapstruct를 사용하는 이유와 그 사용법에 대해 자세히 알아보도록 하겠다.
mapstruct 공식 홈페이지 : https://mapstruct.org/
mapstruct를 사용해야 하냐, 하지 말아야 하냐에 대해 의견은 꽤나 분분한 것 같다.
mapstruct 대신 stream과 정적팩토리 메소드를 이용한 방식을 추구하시는 분도 계셨고(참고 블로그), modelmapper를 선택 한 분들도 있었다.
결론적으로 나는 mapstruct를 사용하기로 결정하였다.
그 이유는 간편함과 속도에 있었다.
앞서 언급한 stream과 정적팩토리 메소드를 이용한 방식도 꽤 유용해 보였지만, 한 DTO의 필드가 15개가 넘어가는... 코드에 정적 팩토리 메소드를 이용하게 되면 가독성이 심각하게 떨어지고, 너무 복잡해 진다.
또한, 다른 mapping library들 중 속도가 빠르고, compile중 에러를 확인 할 수 있다는 점이 주요하게 작용했다.
MapStruct 사용법
[ 기본 사용법 ]
사용법은 꽤나 간단하다.
1. build.gradle dependency 설정을 해준다.
나는 아래처럼 설정 해주었다. (공식문서 참조)
// https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor
annotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final")
// https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor
testAnnotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final")
// https://mvnrepository.com/artifact/org.mapstruct/mapstruct
implementation("org.mapstruct:mapstruct:1.5.3.Final")
// https://mvnrepository.com/artifact/org.projectlombok/lombok-mapstruct-binding
implementation("org.projectlombok:lombok-mapstruct-binding:0.2.0")
2. mapper Interface 작성
mapstruct는 우리가 mapping interface만 작성하면 그 implementation 코드를 generate해주는 라이브러리이다.
interface를 작성하는 방법은 아래와 같다. 바로 코드와 함께 살펴보자.
변환 해볼 class는 아래 Student와 StudentDTO 클래스이다.
//StudentDTO.java
import lombok.Data;
@Data
public class StudentDTO {
private String name;
private String email;
private String address;
private String password;
private String internationalAge;
private String species;
}
//Student.java
import lombok.Data;
@Data
public class Student {
private String name;
private String email;
private String password;
private Long age;
}
이제, 정말 인터페이스를 작성해 보자.
//StudentMapper.java
//@Mapper를 이용하여 mapper임을 알려주고, MapStruct를 이용해 코드를 generate해야 함을 알려준다.
//componentModel="spring"을 통해서 spring container에 Bean으로 등록 해 준다.
//unmappedTargetPolicy IGNORE 만약, target class에 매핑되지 않는 필드가 있으면, null로 넣게 되고, 따로 report하지 않는다.
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface StudentMapper {
// mapStruct가 코드를 만들 메소드를 선언해준다.
// 아래 메소드는 Student 클래스를 받아, StudentDTO에 매핑하여 리턴하는 함수이다.
public StudentDTO toDTO(Student student);
}
이렇게 인터페이스를 작성하고 난 뒤, 저장 하게 되면
/bin아래에 자동으로 매핑하는 코드가 생성되게 된다. 내용은 아래와 같다.
//StudentMapperImpl.java
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-05-09T13:06:25+0900",
comments = "version: 1.5.3.Final, compiler: Eclipse JDT (IDE) 3.34.0.v20230413-0857, environment: Java 17.0.6 (Homebrew)"
)
@Component
public class StudentMapperImpl implements StudentMapper {
@Override
public StudentDTO toDTO(Student student) {
if ( student == null ) {
return null;
}
StudentDTO studentDTO = new StudentDTO();
studentDTO.setEmail( student.getEmail() );
studentDTO.setName( student.getName() );
studentDTO.setPassword( student.getPassword() );
return studentDTO;
}
}
즉, 우리는 Student 클래스를 다른 클래스로 매핑 하고 싶을 때, 더 이상 하나하나 매핑할 필요 없이, 아래처럼 인터페이스에 간단히 메소드 하나만 추가 해주면 된다.
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface StudentMapper {
public StudentDTO toDTO(Student student);
// 리턴 타입만 다른 메소드를 선언해주면 자동으로 코드를 생성 해 준다.
public OtherDTO toOther(Student student);
}
[ custom method ]
근데, 간혹 가다 필드 명이 다르지만 매핑을 해주고 싶다 던가, 특정 필드는 항상 상수로 넣어주고 싶을 때가 있다.
예를 들어, StudentDTO에는 email이라는 필드 대신 address라는 필드가 있다고 해보자.
그리고 우리는 Student의 email을 StudentDTO의 address에 매핑 해주고 싶다.
아래처럼 @Mapping을 이용하여 코드를 작성하면 된다.
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface StudentMapper {
//source Student.email을 target StudentDTO.address 에 매핑 하겠다는 뜻
@Mapping(target = "address", source = "email")
public StudentDTO toDTO(Student student);
}
특정 함수를 이용해서 구한 값을 매핑하고 싶다면 아래처럼 @Named를 이용하여 작성하면 된다.
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface StudentMapper {
@Mapping(target = "address", source = "email")
// target의 internationalAge 필드는 getInternationalAge(age)의 값으로 매핑한다.
@Mapping(target = "internationalAge", source = "age", qualifiedByName = "getInternationalAge")
public StudentDTO toDTO(Student student);
@Named("getInternationalAge")
public static Long getInternationalAge(Long age){
return age - 2;
}
}
필드에 특정 상수를 넣고 싶다면 아래처럼 @Mapping(constant) 를 이용한다.
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface StudentMapper {
@Mapping(target = "address", source = "email")
@Mapping(target = "internationalAge", source = "age", qualifiedByName = "getInternationalAge")
//source와 무관하게, target의 species라는 필드에 "human"을 넣어준다.
@Mapping(target = "species", constant = "human")
public StudentDTO toDTO(Student student);
@Named("getInternationalAge")
public static Long getInternationalAge(Long age){
return age - 2;
}
}
여기까지 mapstruct 사용법에 대해 알아보았다.
필드가 많은 class일 수록 mapstruct를 이용하여 코드를 자동생성하는 것은 큰 이점을 주는 것 같다.
'java,springboot' 카테고리의 다른 글
[WebFlux] saveAll(Iterable) vs saveAll(Flux) 뭘 써야 할까? (0) | 2023.05.23 |
---|---|
[Solved] DataBufferLimitException 보낼 때, 받을 때 둘 다 해결 (1) | 2023.05.22 |
[Springboot] Error:IllegalStateException: Required property b not found for class (0) | 2023.04.21 |
mariaDB(mysql) Java(R2dbc) 타입 매핑 (TinyInt(1) -> Boolean) (1) | 2023.04.20 |
Spring - DI 세가지 방법 (생성자 방식이 좋은 이유 with code) (0) | 2023.04.19 |