ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • MapStruct 자동 매핑이 안 될 때 확인할 것들(Lombok)
    오답노트/오류 해결 2025. 4. 23. 00:33

    최근 개인 프로젝트에서 MapStruct를 사용해 Entity → DTO 매핑을 구현하던 중 이상한 현상을 겪었다.
    @AfterMapping은 잘 동작하지만, 나머지 필드들이 전혀 매핑되지 않는 상황이었다.

    @Generated(
        value = "org.mapstruct.ap.MappingProcessor",
        date = "2025-04-22T23:40:01+0900",
        comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.12 (Oracle Corporation)"
    )
    @Component
    public class PostMapperImpl implements PostMapper {
    
        @Override
        public PostSummaryDTO toSummaryDto(Post post) {
            if ( post == null ) {
                return null;
            }
    
            PostSummaryDTO postSummaryDTO = new PostSummaryDTO();
    		
            // Post -> PostSummaryDTO 자동 매핑 코드가 없음!!
            
            // AfterMapping 만 호출됨
            enrichPostSummary( postSummaryDTO, post );
    
            return postSummaryDTO;
        }
    
        @Override
        public List<PostSummaryDTO> toSummaryDtoList(List<Post> post) {
            if ( post == null ) {
                return null;
            }
    
            List<PostSummaryDTO> list = new ArrayList<PostSummaryDTO>( post.size() );
            for ( Post post1 : post ) {
                list.add( toSummaryDto( post1 ) );
            }
    
            return list;
        }
    
        @Override
        public PostDetailDTO toDto(Post post) {
            if ( post == null ) {
                return null;
            }
    
            PostDetailDTO postDetailDTO = new PostDetailDTO();
    
            return postDetailDTO;
        }
    }

     

    문제 현상

    • PostMapperImpl 구현체가 생성되긴 했지만…
    • postSummaryDTO.setTitle(...) 같은 필드 매핑 코드가 전혀 없고, @AfterMapping만 호출되고 있었다.

    원인 분석

    1. 필드명이 다르지 않음 → 이름은 일치
    2. @Getter, @Setter도 있음 → Lombok 사용 중

    1. @Mapper(componentModel = "spring")도 정상 적용
    @Mapper(componentModel = "spring")
    public interface PostMapper {
        @AfterMapping
        default void enrichPostSummary(@MappingTarget PostSummaryDTO dto, Post post) {
            dto.setCommentCount(post.getComments() != null ? post.getComments().size() : 0);
            dto.setUsername(post.getUser() != null ? post.getUser().getName() : "");
            dto.setUserId(post.getUser() != null ? post.getUser().getId() : -1);
            dto.setCreatedAt(post.getCreatedAt());
        }
    
        PostSummaryDTO toSummaryDto(Post post);
    
        List<PostSummaryDTO> toSummaryDtoList(List<Post> post);
    
    
        PostDetailDTO toDto(Post post);
    }

    그런데도 자동 매핑이 안 된다?

    Lombok의 annotation processor가 MapStruct 컴파일 시점에 인식되지 않고 있었던 것이었다.

     

    문제 해결

    build.gradle 에서 mapstruct 보다 lombok 이 먼저 빌드되어서 Getter, Setter 를 인식하지 못하고 있었고 의존성 부분의 순서를 정리해주었다.

    // build.gradle before
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-security'
        implementation 'org.mariadb.jdbc:mariadb-java-client:3.1.2'
        implementation 'com.squareup.okhttp3:okhttp:4.11.0'
        implementation 'org.mapstruct:mapstruct:1.5.5.Final'
    
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    
        compileOnly 'org.projectlombok:lombok:1.18.24'
        annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' 
        annotationProcessor 'org.projectlombok:lombok:1.18.24' <- Lombok 이 후순위!!!
        testCompileOnly 'org.projectlombok:lombok:1.18.24'
        testAnnotationProcessor 'org.projectlombok:lombok:1.18.24'
    }
    
    // build.gradle after
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-security'
        implementation 'org.mariadb.jdbc:mariadb-java-client:3.1.2'
        implementation 'com.squareup.okhttp3:okhttp:4.11.0'
        implementation 'org.mapstruct:mapstruct:1.5.5.Final'
    
        // lombok
        compileOnly 'org.projectlombok:lombok:1.18.24'
        annotationProcessor 'org.projectlombok:lombok:1.18.24'
    
        // mapstruct processor
        annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
    
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
        testCompileOnly 'org.projectlombok:lombok:1.18.24'
        testAnnotationProcessor 'org.projectlombok:lombok:1.18.24'
    }

     

    이후 구현체를 수동으로 삭제해주고 빌드를 다시 하니 구현체에 자동 매핑 코드가 생성되어있었다.

    -> 수동 삭제시 Gradle, IDE 캐시가 이전 구현체를 계속 참조해서 돌아가는 상황이 발생하면 강제로 캐시를 사용하지 않는 리빌드 명령어를 실행해야한다. 또는 캐시 초기화도 해주어야함

    더보기

     

    📝 덧붙이며 – 구현체가 생성되어도 주입되지 않는다면?

    이번 문제에서 가장 헷갈렸던 점은 MapStruct의 구현체(PostMapperImpl.java)가 분명히 build/generated/... 아래 생성되었음에도, Spring에서는 No bean of type 'PostMapper' 오류가 발생했다는 점이었다.

    원인을 추적해보니 다음과 같은 조건이 충족되지 않으면 Spring은 해당 구현체를 Bean으로 인식하지 못하고 주입도 하지 않는다.

    💥 핵심 원인

    • MapStruct가 생성한 .java 파일이 컴파일되지 않음 (→ .class 파일이 없음)
    • 이유는 해당 경로(build/generated/sources/annotationProcessor/java/main)가 IntelliJ에서 소스 루트로 등록되지 않았기 때문

    ✅ 해결 방법 요약

    1. IntelliJ의 Project 패널에서경로를 우클릭 → Mark Directory as → Generated Sources Root 선택
    2. build/generated/sources/annotationProcessor/java/main
    3. build.gradle에 아래 설정 포함
    4. sourceSets { main { java { srcDirs += 'build/generated/sources/annotationProcessor/java/main' } } }
    5. 캐시 초기화 후 재빌드
    6. ./gradlew clean build --no-build-cache --refresh-dependencies

    📌 교훈

    MapStruct가 아무리 잘 작동하더라도, IDE가 해당 소스를 컴파일 대상으로 인식하지 않으면 결국 주입도, 실행도 되지 않는다.
    Lombok과 마찬가지로, MapStruct는 annotationProcessor 기반 도구이기 때문에 IDE와의 연동이 매우 중요하다는 걸 다시 한 번 느꼈다.

    @Generated(
        value = "org.mapstruct.ap.MappingProcessor",
        date = "2025-04-23T00:08:19+0900",
        comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.11.1.jar, environment: Java 17.0.12 (Oracle Corporation)"
    )
    @Component
    public class PostMapperImpl implements PostMapper {
    
        @Override
        public PostSummaryDTO toSummaryDto(Post post) {
            if ( post == null ) {
                return null;
            }
    
            PostSummaryDTO postSummaryDTO = new PostSummaryDTO();
    
            postSummaryDTO.setTitle( post.getTitle() );
            postSummaryDTO.setId( post.getId() );
            postSummaryDTO.setCategory( post.getCategory() );
            postSummaryDTO.setViews( post.getViews() );
            postSummaryDTO.setLikes( post.getLikes() );
            postSummaryDTO.setSpoiler( post.isSpoiler() );
            postSummaryDTO.setNotice( post.isNotice() );
            postSummaryDTO.setCreatedAt( post.getCreatedAt() );
    
            enrichPostSummary( postSummaryDTO, post );
    
            return postSummaryDTO;
        }
    
        @Override
        public List<PostSummaryDTO> toSummaryDtoList(List<Post> post) {
            if ( post == null ) {
                return null;
            }
    
            List<PostSummaryDTO> list = new ArrayList<PostSummaryDTO>( post.size() );
            for ( Post post1 : post ) {
                list.add( toSummaryDto( post1 ) );
            }
    
            return list;
        }
    
        @Override
        public PostDetailDTO toDto(Post post) {
            if ( post == null ) {
                return null;
            }
    
            PostDetailDTO postDetailDTO = new PostDetailDTO();
    
            return postDetailDTO;
        }
    }

     

Designed by Tistory.