문제 상황
호텔 검색 기능 개발을 완료한 후, HTTP 요청을 보내니 필드 값이 모두 null로 반환되었다.
{
"content": [
{
"hotelId": null,
"name": null,
"country": null,
"city": null,
"address": null,
"description": null,
"charge": 0
}
],
"page": {
"size": 10,
"number": 0,
"totalElements": 1,
"totalPages": 1
}
}
처음에는 검색 조건으로 받는 요청 파라미터나 DB에서 불러오는 데이터에 문제가 있다고 생각하고 로그를 확인해봤으나, 요청 파라미터와 엔티티 값은 모두 제대로 작성되어 있었다.
ResponseDto에 null 값이 들어간 원인
원인은 빌드 과정에서 Mapper 인터페이스의 정보를 기반으로 HotelMapperImpl을 생성하는 과정에서, 타겟 클래스의 프로퍼티를 매핑할 수 없다는 에러가 발생한 것이었습니다.
// MapStruct Mapper 클래스
@Mapper
public interface HotelMapper {
HotelMapper HOTEL_MAPPER = Mappers.getMapper(HotelMapper.class);
@Mapping(target = "hotelId", source = "hotelId")
HotelResponse hotelToResponse(Hotel hotel);
List<HotelResponse> hotelToResponses(List<Hotel> hotels);
}
// 에러 로그
warning: Unmapped target properties: "hotelId, name, city, country, address, description". Mapping from Collection element "Hotel hotel" to "HotelResponse hotelResponse". ^
1 warning
Property를 찾을 수 없어서 자동 생성된 구현체가 null로 초기화된 후 값을 제대로 넣지 못해 변환 과정에서 응답 객체의 필드가 모두 null로 채워지게 되었습니다.
// 자동 생성된 Mapper 클래스 구현체
import com.marriot.hotel.infrastructure.persistence.entity.Hotel;
import com.marriot.hotel.presentation.rest.dto.response.HotelResponse;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2025-01-02T10:08:22+0900",
comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.11.1.jar, environment: Java 17.0.7 (Azul Systems, Inc.)"
)
public class HotelMapperImpl implements HotelMapper {
@Override
public List<HotelResponse> hotelToResponses(List<Hotel> hotels) {
if ( hotels == null ) {
return null;
}
List<HotelResponse> list = new ArrayList<HotelResponse>( hotels.size() );
for ( Hotel hotel : hotels ) {
list.add( hotelToHotelResponse( hotel ) );
}
return list;
}
protected HotelResponse hotelToHotelResponse(Hotel hotel) {
if ( hotel == null ) {
return null;
}
// null로 초기화되고 아무 값도 안들어 감
Long hotelId = null;
String name = null;
String country = null;
String city = null;
String address = null;
String description = null;
int charge = 0;
HotelResponse hotelResponse = new HotelResponse( hotelId, name, country, city, address, description, charge );
return hotelResponse;
}
}
왜 MapStruct는 프로퍼티값을 찾지 못한 것일까?
바로 본론부터 이야기 하자면 build.gradle의 annotationProcessor 의존성 순서 문제였다. mapstruct는 lombok을 이용하기 때문에 Lombok을 명시해주고 mapStruct를 명시해주어야 한다.
dependencies {
// MapStruct
implementation 'org.mapstruct:mapstruct:1.6.3'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
// QueryDSL
implementation "com.querydsl:querydsl-jpa:${querydslVersion}:jakarta"
annotationProcessor "com.querydsl:querydsl-apt:${querydslVersion}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
문제가 생겼을 당시의 build.gradle
dependencies {
implementation 'org.mapstruct:mapstruct:1.6.3'
implementation "com.querydsl:querydsl-jpa:${querydslVersion}:jakarta"
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
// annotationProcessor 순서 유지 필요
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
annotationProcessor "com.querydsl:querydsl-apt:${querydslVersion}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
build.gradle annotationProcessor의 순서 조정 이후 해당 이슈 해결
(추가) Lombok 버전 1.18.16 이상을 사용하는 경우 lombok-mapstruct-binding 의존성을 추가하게 되면 순서와 상관없이 사용할 수 있게 된다.
'Spring 단기심화 2기' 카테고리의 다른 글
TIL_Annotation_241221 (1) | 2024.12.21 |
---|---|
TIL_MapStruct_241219 (1) | 2024.12.20 |
TIL_JVM 구조와 동작 원리_241208 (3) | 2024.12.09 |
TIL_HTTP 메서드의 멱등성_241205 (0) | 2024.12.05 |
TIL_대규모 스트림 처리에서의 데이터 일관성 유지_241205 (0) | 2024.12.05 |