관리 메뉴

피터의 개발이야기

[Kotlin] 코틀린 MapStruct 사용방법 본문

Programming/Kotlin

[Kotlin] 코틀린 MapStruct 사용방법

기록하는 백앤드개발자 2024. 7. 8. 10:10
반응형

ㅁ MapStruct란?

ㅇ MapStruct는 Java 객체 간 매핑을 쉽게 할 수 있게 해주는 코드 생성 라이브러리로, 객체 간의 매핑을 컴파일 시점에 자동으로 생성하여 런타임 오버헤드를 줄이고 안전한 매핑을 제공한다. 

ㅇ Kotlin에서도 MapStruct를 사용할 수 있지만, 몇 가지 추가 설정이 필요하다. 

ㅇ이 글에서는 Kotlin 프로젝트에서 MapStruct를 설정하고 사용하는 방법을 알아본다.

 

ㅁ MapStruct를 사용하는 이유

ㅇ Service에서 Dto로 많은 객체 변환하는 로직이 여러 곳에 퍼져있어서, 비지니스 로직의 간결성이 떨어진다.

ㅇ 객체 변환 로직이 너무 퍼져 있어서 재사용성이 떨어진다.

ㅇ 구체적으로 보면 Dto와 Entity 사이에 의존성이 크다.

ㅇ MapStruct는  객체 변환 메소드를 자동으로 만들어주고, 필요할 경우 직접 메소드를 만들어 사용할 수 있었다.

ㅇ Dto와 Entity 사이의 의존성을 줄이고, Service에서 변환 로직을 분리하여 MapStruct에 정리할 수 있게 되어, Service 로직의 간결성을 높일 수 있었다.

 

ㅁ 의존성 추가

//kotlin
plugins {
    kotlin("kapt") version "1.5.30"
}

dependencies {
    implementation("org.mapstruct:mapstruct:1.5.3.Final")
    kapt("org.mapstruct:mapstruct-processor:1.5.3.Final")
}

 

ㅁ 기본 사용법

import org.mapstruct.Mapper

@Mapper(componentModel = "spring")
interface MyMapper {
    fun mapFrom(requestDto: RequestDto): ResponseDto
}

ㅇ RequestDto를 ResponseDto로 매핑하는 기본적인 예제이다.

ㅇ Kotlin에서는 Java와 유사하게 @Mapper 어노테이션을 사용하여 매핑 인터페이스를 정의할 수 있다. 

 

ㅁ 이름이 다른 필드 매핑하기

@Mapper(componentModel = "spring")
interface MyMapper {
    @Mapping(source = "key", target = "title")
    @Mapping(source = "value", target = "content")
    fun mapFrom(mapDto: MapDto): ResponseDto
}

ㅇ 필드 이름이 다른 경우에는 @Mapping 어노테이션을 사용하여 명시적으로 매핑을 지정할 수 있다.

 

ㅁ 두 객체를 한 객체로 매핑하기

@Mapper(componentModel = "spring")
interface MyMapper {
    fun mapFrom(requestDto: RequestDto, mapDto: MapDto): DoubleDto
}

ㅇ 두 개의 객체를 하나의 객체로 매핑할 수도 있다.

ㅇ RequestDto와 MapDto를 DoubleDto로 매핑한다.

 

ㅁ 한 객체를 다른 객체로 매핑하기

import org.mapstruct.*

@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
interface ProductMapper {
    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    fun updateProductFromDto(dto: ProductUpdateDto, @MappingTarget entity: Product)
}

ㅇ MapStruct를 사용하여 DTO에서 Entity로 필요한 부분만 매핑하는 매퍼 인터페이스를 작성한다.

ㅇ 기존 entity를 받아서, null값이 아닌 updateDto의 값을 치환한다.

 

ㅁ 사용자 정의 메서드

@Mapper(componentModel = "spring")
interface MyMapper {
    fun mapFrom(requestDto: RequestDto): ResponseDto

    default fun mapFrom(requestDto: RequestDto): ResponseDto {
        return ResponseDto(requestDto.title, requestDto.content)
    }
}

ㅇ 복잡한 로직이 필요한 경우, default 키워드를 사용하여 사용자 정의 메서드를 작성할 수 있다.

 

ㅁ 심화 사용법

default

@Mapper(componentModel = "spring")
interface MyMapper {
    @Mapping(source = "text", target = "content", defaultValue = "content")
    fun mapFrom(title: String, text: String): ResponseDto
}

ㅇ 필드의 개수가 다른 객체를 매핑하거나 null 값을 처리할 때 default 값을 설정할 수 있다.

 

ignore = true 

@Mapper(componentModel = "spring")
interface MyMapper {
    @Mapping(target = "content", ignore = true)
    fun mapFrom(title: String): ResponseDto
}

ㅇ 특정 필드를 매핑하지 않으려면 ignore = true 옵션을 사용한다.

 

expression

@Mapper(componentModel = "spring")
interface MyMapper {
    @Mapping(target = "content", expression = "java(getContent())")
    fun mapFrom(title: String): ResponseDto

    fun getContent(): String {
        return "defaultContent"
    }
}

ㅇ 생성된 메서드를 사용하여 값을 매핑할 때는 expression을 사용한다.

ㅇ Source에 없는 값도 expression에서 초기값으로 세팅할 수 있다.

 

@Named

@Mapper(componentModel = "spring")
interface MyMapper {
    @Mapping(source = "num", target = "key", qualifiedByName = "toKey")
    fun mapFrom(num: Int, value: String): MapDto

    @Named("toKey")
    fun toKey(num: Int): String {
        return when (num) {
            1 -> "key1"
            2 -> "key2"
            else -> "defaultKey"
        }
    }
}

 

ㅇ 반복되는 연산을 처리하기 위해 메서드를 만들고 @Named 어노테이션을 사용하여 매핑할 수 있다.

ㅇ expression과 다르게 없는 필드에 대해서 매핑하지 못한다.

ㅇ mapper에 enum으로 매핑하는 코드는 재사용성이 커서 자주 사용하게 된다.

 

ㅁ 주의 사항

ㅇ Kotlin에서는 기본적으로 모든 클래스와 메서드가 final이므로, MapStruct가 구현 클래스를 생성할 수 없다. 

    이를 해결하기 위해 open 키워드를 사용하거나 all-open 플러그인을 사용해야 한다.
ㅇ Kotlin의 데이터 클래스는 기본 생성자가 없으므로, MapStruct가 인스턴스를 생성하는 데 문제가 될 수 있다. 

   이 경우 @BeanMapping(builder = @Builder(disableBuilder = true)) 어노테이션을 사용하여 빌더 사용을 비활성화할 수 있다.

ㅇ 스프링 부트 3 버전 이후에는 annotationProcessor로 동작하기 때문에 의존성 문제가 발생할 수 있다.

  ㄴ [피터의 개발이야기:티스토리] [Kotlin] Spring에 MapStruct와 Lombok을 함께 사용할 때 isCompleted 필드가 null로 넘어오는 문제

 

ㅁ 마무리 

Kotlin에서 MapStruct를 사용하면 객체 간 매핑을 쉽고 효율적으로 처리할 수 있다. 초기 설정에 약간의 추가 작업이 필요하지만, 한 번 설정해 놓으면 Java에서와 마찬가지로 편리하게 사용할 수 있다. MapStruct를 통해 반복적이고 오류가 발생하기 쉬운 매핑 코드 작성을 줄이고, 더 깔끔하고 유지보수가 쉬운 코드를 작성할 수 있다.

 

ㅁ 함께 보면 좋은 사이트

codinghejow - [BackEnd] MapStruct 사용기

baeldung - Custom Mapper with MapStruct

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

MapStruct 공식 홈페이지

Mapstruct 공식 github에서 mapstruct-kotlin

 

반응형
Comments