
이번 프로젝트에서 처음에는 DTO <-> Entity 간의 변환을 정적팩토리 메서드로 작성했었는데, 해당 메서드를 엔티티에 두자니 책임이 다르다고 생각하였고, DTO에 두자니 서비스로직에서 지저분하게 매핑을 해주는 코드를 작성하거나 DTO를 만들어 엔티티를 DTO로 변환하고 그걸 다시 엔티티에 매핑해야 하는 상황이 발생하였다. 어떻게 해결할까 고민하다가 mapper에 대해서 알게 되었고, 그중 mapstruct가 컴파일 단계에서 실행되어 리플렉션을 사용하는 ModelMapper보다 성능 면에서 우수하다는 장점이 있어 사용하게 되었다.
MapStruct 사용가능 환경
Java전용 라이브러리(Java 8 이상)
Maven, Gradle 등의 빌드 도구 사용 프로젝트
MapStruct는 Lombok의 getter, setter를 사용해서 매핑을 처리하기 때문에 의존성을 추가할 때 Lombok이 MapStruct보다 먼저 선언되어야 한다. 그렇지 않으면 MapStruct가 Lombok이 생성한 메서드들을 인식하지 못할 수 있다.
이후로는 mapstruct를 사용하면서 겪었던 문제이다.
PDF파일을 처리하는 기능을 개발하던 중, 파일에서 추출한 내용과 메타데이터를 엔티티로 변환하는 매퍼가 필요했다. DTO와 엔티티 간 변환을 MapStruct로 구현하려고 했는데, 다음과 같이 Enum 타입을 매핑하는 과정에서 문제가 발생했다.
public enum FileContentType {
PDF
}
public enum FileStatus {
UPLOADED
}
@Mapper(componentModel = "spring")
public interface FileMapper {
@Mapping(target = "name", source = "pdfFile.name")
@Mapping(target = "size", expression = "java((int) pdfFile.length())")
@Mapping(target = "path", source = "pdfFile.absolutePath")
@Mapping(target = "fileContentType", constant = "FileContentType.PDF")
@Mapping(target = "fileStatus", constant = "FileStatus.UPLOADED")
@Mapping(target = "extractedText", source = "extractedText")
File pdfToFile(java.io.File pdfFile, String extractedText);
}
이렇게 작성 후 애플리케이션을 실행하자 아래와 같은 메세지가 발생하였다.
error: Constant "FileContentType.PDF" doesn't exist in enum type FileContentType for property "fileContentType".
error: Can't map "FileContentType.PDF" to "FileContentType fileContentType".
error: Constant "FileStatus.UPLOADED" doesn't exist in enum type FileStatus for property "fileStatus".
error: Can't map "FileStatus.UPLOADED" to "FileStatus fileStatus".
또한 MapStruct가 생성한 구현 클래스에서는 이런 오류가 발생
cannot find symbol
symbol: variable FileStatus
location: class FileMapperImpl
constant 속성에 문자열로 enum 값을 지정할 때 MapStruct가 이를 실제 enum 타입으로 변환하는 과정에서 문제가 발생("FileContentType.PDF" 자체를 Enum 상수로 해석하려 한것이다.)
MapStruct는 @Mapper 어노테이션 처리 시 필요한 import 구문을 자동으로 생성하려고 시도한다. 하지만 constant 속성을 사용하면 해당 Enum타입을 명시적으로 참조하지 않기 때문에 import 구문을 생성하지 못한다.(수동으로 import 구문을 추가해도 MapStruct가 다시 코드를 생성할 때 해당 import 구문이 유지되지 않는다.)
@Mapper(componentModel = "spring", imports = {
FileContentType.class, FileStatus.class
})
public interface FileMapper {
@Mapping(target = "name", source = "pdfFile.name")
@Mapping(target = "size", expression = "java((int) pdfFile.length())")
@Mapping(target = "path", source = "pdfFile.absolutePath")
@Mapping(target = "fileContentType", expression = "java(FileContentType.PDF)")
@Mapping(target = "fileStatus", expression = "java(FileStatus.UPLOADED)")
@Mapping(target = "extractedText", source = "extractedText")
File pdfToFile(java.io.File pdfFile, String extractedText);
}
그리고 테스트 코드를 추가해주었다.
@Test
void testPdfToFile() throws IOException {
임시 PDF 파일 생성
Path tempFile = tempDir.resolve("test.pdf");
String testContent = "This is a test PDF content";
Files.write(tempFile, testContent.getBytes());
java.io.File pdfFile = tempFile.toFile();
String extractedText = "Extracted content from PDF";
File result = fileMapper.pdfToFile(pdfFile, extractedText);
assertNotNull(result);
assertEquals("test.pdf", result.getName());
assertEquals(testContent.getBytes().length, result.getSize());
assertEquals(pdfFile.getAbsolutePath(), result.getPath());
assertEquals(FileContentType.PDF, result.getFileContentType());
assertEquals(FileStatus.UPLOADED, result.getFileStatus());
assertEquals(extractedText, result.getExtractedText());
}
java.io.File 객체와 추출된 텍스트를 입력받아 File 엔티티로 매핑되는지 확인하는 테스트다. @TempDir를 활용하여 실제 파일 I/O 동작을 시뮬레이션했다.