-
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만 호출되고 있었다.

원인 분석
- 필드명이 다르지 않음 → 이름은 일치
- @Getter, @Setter도 있음 → Lombok 사용 중

- @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에서 소스 루트로 등록되지 않았기 때문
✅ 해결 방법 요약
- IntelliJ의 Project 패널에서경로를 우클릭 → Mark Directory as → Generated Sources Root 선택
- build/generated/sources/annotationProcessor/java/main
- build.gradle에 아래 설정 포함
- sourceSets { main { java { srcDirs += 'build/generated/sources/annotationProcessor/java/main' } } }
- 캐시 초기화 후 재빌드
- ./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; } }'오답노트 > 오류 해결' 카테고리의 다른 글
moviemoa 프로젝트 배포 중 문제 해결 (0) 2025.07.20 Spring boot - 인텔리제이에서 JPA 환경 구축 중 접속 불가 (0) 2024.12.28 Spring - 라이브러리를 불러오지 못하는 문제 발생(Maven Denpendencies) (0) 2024.04.08 Spring - Deployment Assembly 에서 The given project is not a virtual component project 오류 해결 (0) 2024.03.24 Spring - pom.xml 파일에서 <packaging>war</packaging> 에 에러가 생기는 상황 (0) 2024.03.24