24.05.29. MapStruct 적용

develemon·2024년 6월 20일
0

Doran

목록 보기
8/13
post-thumbnail

이전 포스팅에서 ModelMapperMapStruct에 대해 비교하며 MapStruct가 어떠한 지점에서 유리한지를 살펴봤다. 이번에는 실제 프로젝트에 MapStruct를 적용한 코드들을 살펴보기로 한다.

의존성 설정


빌드툴로 Maven을 사용하였기에 xml 방식으로 의존성을 설정하였다.

pom.xml

	<properties>
		<java.version>17</java.version>
		<spring-cloud.version>2023.0.1</spring-cloud.version>
		<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
	</properties>
	...
	<dependencies>
      	...
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
      	...
		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct</artifactId>
			<version>${org.mapstruct.version}</version>
		</dependency>
		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct-processor</artifactId>
			<version>${org.mapstruct.version}</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok-mapstruct-binding</artifactId>
			<version>0.2.0</version>
		</dependency>
	</dependencies>

여기서 주의할 점은 MapStructLombok보다 dependency 선언이 뒤에 되어야 한다는 것이다. MapStructLombok@Getter, @Setter, @Builder를 이용하므로 Lombok보다 먼저 선언되는 경우 정상적으로 실행할 수 없다.

참고로 MapStruct 의존성 설정에 있어서 ChatGPT에 물어보면 다음 코드도 추가하라고 나오는데,

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version> <!-- 원하는 Maven Compiler Plugin 버전으로 설정 -->
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.24</version> <!-- Lombok의 최신 버전으로 설정 -->
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

문제는 QueryDSL을 사용할 때 위 설정을 추가하면 Q클래스 생성이 되지 않는다. 확인해보니 위 설정은 deprecated 된 것으로, ChatGPT를 너무 믿어서는 안된다. 그러나 내가 작업하던 중에는 위 설정이 있든 없든 라이브러리가 제대로 추가되지 않아 @Mapper 어노테이션을 사용할 수 없어 너무 많은 시간을 버리게 되었는데, IDE가 라이브러리를 제대로 불러오지 못하는 경우도 있다는 것을 알았다. 너무 어이없지만 아픈 경험이었으므로 잊지 않고 메모해둔다...

Java 코드


앞선 포스팅을 통해서도 코드를 보였지만, 여기서 다시 한번 정리해본다.

@Mapper(componentModel = "spring", imports = UUID.class)
public interface BookMapper {

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

    Book toBook(BookDto bookDto);
    
    BookDto toBookDto(Book book);
    
    @Mapping(target = "itemUuid", source = "itemUuid", defaultExpression = "java(UUID.randomUUID().toString())")
    @Mapping(target = "category", source = "category", defaultExpression = "java(Category.BOOK)")
    BookDto toBookDto(BookCreateRequest request, String itemUuid, Category category);
    
    ItemSimpleResponse toItemSimpleResponse(BookDto bookDto);
}

여기서 잊지 말아야 할 건, @Mapper 어노테이션의 옵션 설정이다. componentModel = "spring"을 설정하지 않으면 Spring에서 위의 코드를 컴포넌트로서 인식하지 못한다.

그리고 매핑 메서드는 @Mapping 어노테이션의 targetsource 옵션을 통해 파라미터를 전달받아 대상 객체로 자동으로 매핑해주게 되는데, 이 MapStruct의 장점은 이렇게 메서드 선언과 어노테이션 설정만 지정해주면 컴파일 타임에 이 인터페이스의 구현체를 자동으로 만들어주므로 구현에는 신경쓰지 않아도 된다는 점이다. 이대로 끝.

그럼 서비스 계층에서 이 인터페이스를 의존성 주입받아 객체로서 바로 사용할 수 있다.

@Service
@RequiredArgsConstructor
public class ItemServiceImpl implements ItemService {

    private final ItemRepository itemRepository;
    private final BookMapper bookMapper;
    private final MessageUtil messageUtil;

	// ...

    @Override
    @Transactional
    public void saveBook(BookCreateRequest request) {
        BookDto bookDto = bookMapper.toBookDto(request, UUID.randomUUID().toString(), Category.BOOK);
        Item item = bookMapper.toBook(bookDto);
        try {
            itemRepository.save(item);
        } catch (Exception e) {
            throw new RuntimeException(messageUtil.getItemCreateErrorMessage());
        }
    }
    
    // ...
}

MapStruct는 보다 다양한 기능을 지원하므로 몇몇 예시들을 더 덧붙여서 소개하자면,

@Mapper(imports = UUID.class)
public interface MessageMapper {
	MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);

        @Mapping(source = "messageId", target = "messageId", defaultExpression = "java(UUID.randomUUID().toString())")
        @Mapping(source = "requestDto.type", target = "type", defaultValue = "SMS")
        @Mapping(source = "requestDto.sender", target="sender", ignore=true)
        MessageListServiceDto toMessageListServiceDto(String messageId, Integer count, RequestDto requestDto);
}

defaultExpression 옵션 뿐만 아니라 defaultValue로 기본값 지정과 ignore을 통해 특정 필드를 무시할 수도 있다.

나아가 보다 복잡하거나 까다로운 설정이 필요하다면 구현체에서 직접 매핑 메서드를 커스텀할 수도 있다. 예를 들어

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2024-05-30T16:22:17+0900",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.10 (Oracle Corporation)"
)
@Component
public class BookMapperImpl implements BookMapper {

    @Override
    public Book toBook(BookDto bookDto) {
        if ( bookDto == null ) {
            return null;
        }

        Book.BookBuilder book = Book.builder();

        book.itemUuid( bookDto.getItemUuid() );
        book.itemName( bookDto.getItemName() );
        book.price( bookDto.getPrice() );
        book.stockQuantity( bookDto.getStockQuantity() );
        book.itemImageUrl( bookDto.getItemImageUrl() );
        book.category( bookDto.getCategory() );
        book.author( bookDto.getAuthor() );
        book.isbn( bookDto.getIsbn() );
        book.pages( bookDto.getPages() );
        book.publicationDate( bookDto.getPublicationDate() );
        book.contentsTable( bookDto.getContentsTable() );
        book.bookReview( bookDto.getBookReview() );

        return book.build();
    }
    
    // ...
}

위의 코드는 자동으로 생성된 코드인데, category 필드에 기본값 설정이 필요하다면 아래와 같이 default를 붙여 메소드를 만들어주고 구현 메서드 대신 default로 정의한 메서드를 사용할 수 있다.

    @Override
    public default Book toBook(BookDto bookDto) {
        // ...
        String categoryType = Optional.ofNullable(bookDto.getType()).orElse("book").toUpperCase();
        Category category = Category.BOOK;
        if (categoryType.equals("BOOK")) {
			category = Category.BOOK;
		} else if (categoryType.equals("ALBUM")){
			category = Category.ALBUM;
		}
        
        return book.builder()
				.itemUuid( bookDto.getItemUuid() )
				.itemName( bookDto.getItemName() )
				// ...
				.category( category )
				// ...
				.bookReview( bookDto.getBookReview() )
				.build();
	}

이 외에도 더 다양한 설정들이 있으나, MapStruct 적용기는 여기까지 정리하기로 한다.

참고

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

profile
유랑하는 백엔드 개발자 새싹 블로그

0개의 댓글