JavaRush /Java Blog /Random-KO /Mapstruct란 무엇이며 SpringBoot 애플리케이션에서 단위 테스트를 위해 Mapstruct를...
Roman Beekeeper
레벨 35

Mapstruct란 무엇이며 SpringBoot 애플리케이션에서 단위 테스트를 위해 Mapstruct를 올바르게 구성하는 방법

Random-KO 그룹에 게시되었습니다

배경

안녕하세요, 사랑하는 친구와 독자 여러분! 기사를 쓰기 전에 약간의 배경 지식이 있습니다... 최근에 Mapstruct 라이브러리를 사용하면서 문제가 발생했습니다. 이에 대해 여기 내 텔레그램 채널에서 간략하게 설명했습니다 . 게시물의 문제는 댓글을 통해 해결되었으며 이전 프로젝트의 동료가 이를 도와주었습니다. Mapstruct란 무엇이며 SpringBoot 애플리케이션에서 단위 테스트를 위해 Mapstruct를 올바르게 구성하는 방법입니다.  파트 1 - 1그 후, 나는 이 주제에 대한 기사를 쓰기로 결정했지만, 물론 좁은 관점을 취하지 않고 먼저 속도를 높이고 Mapstruct가 무엇이며 왜 필요한지 이해하고 실제 예를 사용하여 앞서 발생한 상황과 해결 방법을 분석합니다. 따라서 실제로 모든 것을 경험하려면 기사를 읽는 것과 동시에 모든 계산을 수행하는 것이 좋습니다. 시작하기 전에 내 텔레그램 채널을 구독하고 거기에서 내 활동을 수집하고 Java 및 IT 전반의 개발에 대한 생각을 씁니다. 구독하셨나요? 엄청난! 자, 이제 가자!

맵스트럭트, 자주 묻는 질문?

빠른 유형 안전 빈 매핑을 위한 코드 생성기입니다. 우리의 첫 번째 임무는 Mapstruct가 무엇이고 왜 필요한지 알아내는 것입니다. 일반적으로 공식 웹 사이트에서 해당 내용을 읽을 수 있습니다. 사이트의 메인 페이지에는 질문에 대한 세 가지 답변이 있습니다. 그것은 무엇입니까? 무엇을 위해? 어떻게? 우리도 이렇게 하려고 노력할 것입니다:

그것은 무엇입니까?

Mapstruct는 인터페이스를 통해 설명되는 구성을 기반으로 생성된 코드를 사용하여 일부 엔터티의 객체를 다른 엔터티의 객체로 매핑(일반적으로 매핑, 매핑 등)하는 데 도움이 되는 라이브러리입니다.

무엇을 위해?

대부분의 경우 우리는 다중 계층 애플리케이션(데이터베이스 작업을 위한 계층, 비즈니스 논리 계층, 애플리케이션과 외부 세계의 상호 작용을 위한 계층)을 개발하며 각 계층에는 데이터를 저장하고 처리하기 위한 자체 개체가 있습니다. . 그리고 이 데이터는 한 엔터티에서 다른 엔터티로 전송되어 레이어에서 레이어로 전송되어야 합니다. 이 접근 방식을 사용해 본 적이 없는 사람들에게는 이 방법이 약간 복잡해 보일 수 있습니다. 예를 들어 학생 데이터베이스에 대한 엔터티가 있습니다. 이 엔터티의 데이터가 비즈니스 로직(서비스) 계층으로 이동하면 Student 클래스에서 StudentModel 클래스로 데이터를 전송해야 합니다. 다음으로, 비즈니스 로직을 통한 모든 조작이 끝나면 데이터를 외부로 공개해야 합니다. 이를 위해 StudentDto 클래스가 있습니다. 물론 StudentModel 클래스의 데이터를 StudentDto로 전달해야 합니다. 전송될 방법을 매번 손으로 작성하는 것은 노동 집약적입니다. 또한 이는 유지 관리해야 하는 코드 베이스의 추가 코드입니다. 실수할 수 있습니다. 그리고 Mapstruct는 컴파일 단계에서 이러한 메서드를 생성하고 생성된 소스에 저장합니다.

어떻게?

주석을 사용합니다. 이 인터페이스의 메소드를 사용하여 한 객체에서 다른 객체로 변환할 수 있음을 라이브러리에 알려주는 기본 Mapper 주석이 있는 주석을 생성하기만 하면 됩니다. 앞서 학생에 대해 말했듯이, 우리의 경우 이것은 한 레이어에서 다른 레이어로 데이터를 전송하는 여러 가지 방법을 포함하는 StudentMapper 인터페이스가 될 것입니다.
public class Student {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}

public class StudentDTO {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}

public class StudentModel {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}
이러한 클래스에 대해 우리는 매퍼를 생성합니다(이후 우리가 전송하려는 대상과 위치를 설명하는 인터페이스라고 부릅니다).
@Mapper
public interface StudentMapper {
   StudentModel toModel(StudentDTO dto);
   Student toEntity(StudentModel model);
   StudentModel toModel(Student entity);
   StudentDTO toDto(StudentModel model);
}
이 접근 방식의 장점은 필드의 이름과 유형이 다른 클래스에서 동일한 경우(우리의 경우처럼) Mapstruct에 대한 설정이 컴파일 단계에서 StudentMapper 인터페이스를 기반으로 필요한 구현을 생성하기에 충분하다는 것입니다. 번역합니다. 그러면 벌써 더 명확해졌죠? 더 나아가 실제 예제를 사용하여 Spring Boot 애플리케이션의 작업을 분석해 보겠습니다.

Spring Boot와 Mapstruct가 작동하는 예

가장 먼저 필요한 것은 Spring Boot 프로젝트를 생성하고 여기에 Mapstruct를 추가하는 것입니다. 이 문제에 대해 GitHub에 리포지토리용 템플릿이 있는 조직이 있고 Spring Boot의 시작이 그 중 하나입니다. 이를 기반으로 새 프로젝트를 만듭니다. 다음으로 프로젝트를Mapstruct란 무엇이며 SpringBoot 애플리케이션에서 단위 테스트를 위해 Mapstruct를 올바르게 구성하는 방법입니다.  파트 1 - 2 가져옵니다 . 예, 친구 여러분, 프로젝트가 유용하다고 생각하시면 별점을 주세요. 그러면 제가 이 일을 헛되이 하고 있지 않다는 것을 알게 될 것입니다. 이번 프로젝트에서는 제가 직장에서 받고 텔레그램 채널에 올린 글을 통해 설명한 상황을 공개하겠습니다 . 잘 모르는 사람들을 위해 상황을 간략하게 설명하겠습니다. 매퍼에 대한 테스트를 작성할 때(즉, 이전에 설명한 인터페이스 구현에 대한) 테스트가 최대한 빨리 통과되기를 원합니다. 매퍼를 사용하는 가장 간단한 옵션은 테스트를 실행할 때 SpringBootTest 주석을 사용하는 것입니다. 이는 Spring Boot 애플리케이션의 전체 ApplicationContext를 선택하고 테스트 내부에 테스트에 필요한 매퍼를 주입합니다. 하지만 이 옵션은 리소스 집약적이고 시간도 훨씬 많이 걸리기 때문에 우리에게는 적합하지 않습니다. 원하는 매퍼를 생성하고 해당 메서드가 예상한 대로 정확히 작동하는지 확인하는 단위 테스트를 작성해야 합니다. 더 빠르게 실행하기 위해 테스트가 필요한 이유는 무엇입니까? 테스트에 오랜 시간이 걸리면 전체 개발 프로세스가 느려집니다. 새 코드에 대한 테스트가 통과될 때까지 이 코드는 올바른 것으로 간주될 수 없으며 테스트용으로 사용되지 않습니다. 이는 해당 코드가 프로덕션에 적용되지 않으며 개발자가 작업을 완료하지 않았음을 의미합니다. 작동이 의심할 여지가 없는 라이브러리에 대한 테스트를 작성하는 이유는 무엇일까요? 하지만 우리는 매퍼를 얼마나 정확하게 설명했는지, 그리고 그것이 우리가 기대한 대로 작동하는지 테스트하고 있기 때문에 테스트를 작성해야 합니다. 우선, 작업을 더 쉽게 하기 위해 pom.xml에 또 다른 종속성을 추가하여 프로젝트에 Lombok을 추가하겠습니다.
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <scope>provided</scope>
</dependency>
우리 프로젝트에서는 모델 클래스(비즈니스 로직 작업에 사용됨)에서 외부 세계와 통신하는 데 사용하는 DTO 클래스로 전환해야 합니다. 단순화된 버전에서는 필드가 변경되지 않고 매퍼가 단순하다고 가정합니다. 그러나 원하는 경우 Mapstruct를 사용하여 작업하는 방법, 구성하는 방법 및 이점을 활용하는 방법에 대한 보다 자세한 기사를 작성하는 것이 가능할 것입니다. 그런데 그러면 이 글이 꽤 길어질 것 같거든요. 강의 목록과 그가 참석하는 강사 목록을 가진 학생이 있다고 가정해 보겠습니다. 모델 패키지를 만들어 보겠습니다 . 이를 바탕으로 간단한 모델을 만들겠습니다.
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

import java.util.List;

@Data
public class StudentDTO {

   private Long id;

   private String name;

   private List<LectureDTO> lectures;

   private List<LecturerDTO> lecturers;
}
그의 강의
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LectureDTO {

   private Long id;

   private String name;
}
그리고 강사
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LecturerDTO {

   private Long id;

   private String name;
}
그리고 모델 패키지 옆에 dto 패키지를 만듭니다 .
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

import java.util.List;

@Data
public class StudentDTO {

   private Long id;

   private String name;

   private List<LectureDTO> lectures;

   private List<LecturerDTO> lecturers;
}
강의
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LectureDTO {

   private Long id;

   private String name;
}
그리고 강사
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LecturerDTO {

   private Long id;

   private String name;
}
이제 강의 모델 모음을 DTO 강의 모음으로 변환하는 매퍼를 만들어 보겠습니다. 가장 먼저 할 일은 프로젝트에 Mapstruct를 추가하는 것입니다. 이를 위해 우리는 공식 웹사이트를 사용할 것입니다 . 거기에 모든 것이 설명되어 있습니다. 즉, 메모리에 하나의 종속성과 플러그인을 추가해야 합니다(메모리가 무엇인지 궁금하신 경우 여기 Article1Article2 를 참조하세요 ).
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct</artifactId>
   <version>1.4.2.Final</version>
</dependency>
그리고 <build/> 블록의 메모리에 있습니다. 우리가 아직 경험하지 못한 것:
<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.5.1</version>
           <configuration>
               <source>1.8</source>
               <target>1.8</target>
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>1.4.2.Final</version>
                   </path>
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>
다음으로 dtomodel 옆에 매퍼 패키지를 만들어 보겠습니다 . 이전에 보여준 클래스를 기반으로 5개의 매퍼를 더 만들어야 합니다.
  • 매퍼 LectureModel <-> LectureDTO
  • 매퍼 목록<LectureModel> <-> 목록<LectureDTO>
  • 매퍼 LecturerModel <-> LecturerDTO
  • 매퍼 목록<LecturerModel> <-> 목록<LecturerDTO>
  • 매퍼 StudentModel <-> StudentDTO
가다:

강의지도자

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.dto.LecturerDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface LectureMapper {
   LectureDTO toDTO(LectureModel model);

   LectureModel toModel(LecturerDTO dto);
}

강의목록맵퍼

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper(componentModel = "spring", uses = LectureMapper.class)
public interface LectureListMapper {
   List<LectureModel> toModelList(List<LectureDTO> dtos);
   List<LectureDTO> toDTOList(List<LectureModel> models);
}

강사맵퍼

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface LectureMapper {
   LectureDTO toDTO(LectureModel model);

   LectureModel toModel(LectureDTO dto);
}

강사목록매퍼

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LecturerDTO;
import com.github.romankh3.templaterepository.springboot.model.LecturerModel;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper(componentModel = "spring", uses = LecturerMapper.class)
public interface LecturerListMapper {
   List<LecturerModel> toModelList(List<LecturerDTO> dloList);
   List<LecturerDTO> toDTOList(List<LecturerModel> modelList);
}

학생매퍼

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.StudentDTO;
import com.github.romankh3.templaterepository.springboot.model.StudentModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring", uses = {LectureListMapper.class, LecturerListMapper.class})
public interface StudentMapper {
   StudentDTO toDTO(StudentModel model);
   StudentModel toModel(StudentDTO dto);
}
매퍼에서는 다른 매퍼를 참조한다는 점을 별도로 언급해야 합니다. 이는 StudentMapper에서 수행되는 것처럼 Mapper 주석의 사용 필드를 통해 수행됩니다.
@Mapper(componentModel = "spring", uses = {LectureListMapper.class, LecturerListMapper.class})
여기서는 강의 목록과 강사 목록을 정확하게 매핑하기 위해 두 개의 매퍼를 사용합니다. 이제 코드를 컴파일하고 거기에 무엇이 있고 어떻게 있는지 확인해야 합니다. 이는 mvn clean 컴파일 명령을 사용하여 수행할 수 있습니다 . 그러나 알고 보니 매퍼의 Mapstruct 구현을 생성할 때 매퍼 구현이 필드를 덮어쓰지 않았습니다. 왜? Lombok에서는 데이터 주석을 선택할 수 없는 것으로 나타났습니다. 그리고 뭔가를 해야 했습니다... 따라서 기사에 새로운 섹션이 있습니다.

Lombok과 Mapstruct 연결하기

몇 분 동안 검색한 끝에 Lombok과 Mapstruct를 특정 방식으로 연결해야 한다는 사실이 밝혀졌습니다. Mapstruct 문서에 이에 대한 정보가 있습니다 . Mapstruct에서 개발자가 제안한 예제를 검토한 후 pom.xml을 업데이트해 보겠습니다. 별도의 버전을 추가해 보겠습니다.
​​<properties>
   <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
   <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>
누락된 종속성을 추가해 보겠습니다.
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok-mapstruct-binding</artifactId>
   <version>${lombok-mapstruct-binding.version}</version>
</dependency>
그리고 Lombok과 Mapstruct를 연결할 수 있도록 컴파일러 플러그인을 업데이트해 보겠습니다.
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-compiler-plugin</artifactId>
   <version>3.5.1</version>
   <configuration>
       <source>1.8</source>
       <target>1.8</target>
       <annotationProcessorPaths>
           <path>
               <groupId>org.mapstruct</groupId>
               <artifactId>mapstruct-processor</artifactId>
               <version>${org.mapstruct.version}</version>
           </path>
           <path>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok</artifactId>
               <version>${lombok.version}</version>
           </path>
           <path>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok-mapstruct-binding</artifactId>
               <version>${lombok-mapstruct-binding.version}</version>
           </path>
       </annotationProcessorPaths>
   </configuration>
</plugin>
그 후에는 모든 것이 잘 될 것입니다. 프로젝트를 다시 컴파일해 보겠습니다. 하지만 Mapstruct가 생성한 클래스는 어디서 찾을 수 있나요? 생성된 소스에 있습니다: ${projectDir}/target/generated-sources/annotations/ Mapstruct란 무엇이며 SpringBoot 애플리케이션에서 단위 테스트를 위해 Mapstruct를 올바르게 구성하는 방법입니다.  파트 1 - 3 이제 Mapstruct 게시물에 대한 실망감을 느낄 준비가 되었으므로 매퍼를 위한 테스트를 만들어 보겠습니다.

우리는 매퍼를 위한 테스트를 작성합니다.

통합 테스트를 생성하고 완료 시간에 대해 걱정하지 않는 경우 매퍼 중 하나를 테스트하는 빠르고 간단한 테스트를 생성하겠습니다.

강의맵퍼테스트

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class LectureMapperTest {

   @Autowired
   private LectureMapper mapperUnderTest;

   @Test
   void shouldProperlyMapModelToDto() {
       //given
       LectureModel model = new LectureModel();
       model.setId(11L);
       model.setName("lecture name");

       //when
       LectureDTO dto = mapperUnderTest.toDTO(model);

       //then
       Assertions.assertNotNull(dto);
       Assertions.assertEquals(model.getId(), dto.getId());
       Assertions.assertEquals(model.getName(), dto.getName());
   }

   @Test
   void shouldProperlyMapDtoToModel() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(11L);
       dto.setName("lecture name");

       //when
       LectureModel model = mapperUnderTest.toModel(dto);

       //then
       Assertions.assertNotNull(model);
       Assertions.assertEquals(dto.getId(), model.getId());
       Assertions.assertEquals(dto.getName(), model.getName());
   }
}
여기에서는 SpringBootTest 주석을 사용하여 전체 applicationContext를 시작하고 여기에서 Autowired 주석을 사용하여 테스트에 필요한 클래스를 추출합니다. 속도와 테스트 작성 용이성 측면에서 보면 매우 좋습니다. 테스트가 성공적으로 통과되었으며 모든 것이 정상입니다. 하지만 우리는 다른 방법으로 LectureListMapper와 같은 매퍼에 대한 단위 테스트를 작성하겠습니다.
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

class LectureListMapperTest {

   private final LectureListMapper lectureListMapper = new LectureListMapperImpl();

   @Test
   void shouldProperlyMapListDtosToListModels() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(12L);
       dto.setName("I'm BATMAN!");

       List<LectureDTO> dtos = Collections.singletonList(dto);

       //when
       List<LectureModel> models = lectureListMapper.toModelList(dtos);

       //then
       Assertions.assertNotNull(models);
       Assertions.assertEquals(1, models.size());
       Assertions.assertEquals(dto.getId(), models.get(0).getId());
       Assertions.assertEquals(dto.getName(), models.get(0).getName());
   }
}
Mapstruct가 생성하는 구현은 우리 프로젝트와 동일한 클래스에 있으므로 테스트에서 쉽게 사용할 수 있습니다. 모든 것이 멋져 보입니다. 주석이 필요 없으며 가장 간단한 방법으로 필요한 클래스를 생성하면 그게 전부입니다. 그러나 테스트를 실행하면 충돌이 발생하고 콘솔에 NullPointerException이 발생한다는 것을 이해하게 됩니다... 이는 LectureListMapper 매퍼의 구현이 다음과 같기 때문입니다.
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Generated(
   value = "org.mapstruct.ap.MappingProcessor",
   date = "2021-12-09T21:46:12+0300",
   comments = "version: 1.4.2.Final, compiler: javac, environment: Java 15.0.2 (N/A)"
)
@Component
public class LectureListMapperImpl implements LectureListMapper {

   @Autowired
   private LectureMapper lectureMapper;

   @Override
   public List<LectureModel> toModelList(List<LectureDTO> dtos) {
       if ( dtos == null ) {
           return null;
       }

       List<LectureModel> list = new ArrayList<LectureModel>( dtos.size() );
       for ( LectureDTO lectureDTO : dtos ) {
           list.add( lectureMapper.toModel( lectureDTO ) );
       }

       return list;
   }

   @Override
   public List<LectureDTO> toDTOList(List<LectureModel> models) {
       if ( models == null ) {
           return null;
       }

       List<LectureDTO> list = new ArrayList<LectureDTO>( models.size() );
       for ( LectureModel lectureModel : models ) {
           list.add( lectureMapper.toDTO( lectureModel ) );
       }

       return list;
   }
}
NPE(NullPointerException의 줄임말)를 보면 LectureMapper 변수에서 가져오는데 , 이는 초기화되지 않은 것으로 드러납니다. 그러나 우리 구현에는 변수를 초기화할 수 있는 생성자가 없습니다. 이것이 바로 Mapstruct가 매퍼를 이런 방식으로 구현한 이유입니다! Spring에서는 여러 가지 방법으로 클래스에 빈을 추가할 수 있고, 위에서 수행한 것처럼 Autowired 주석과 함께 필드를 통해 빈을 삽입하거나 생성자를 통해 빈을 삽입할 수 있습니다. 테스트 실행 시간을 최적화해야 할 때 직장에서 문제가 되는 상황에 처하게 되었습니다. 어쩔 수 없다고 생각하고 텔레그램 채널에 제 아픔을 토로했습니다. 그리고 댓글로 도움을 주셨고 주입 전략을 맞춤 설정할 수 있다고 말씀해 주셨어요. Mapper 인터페이스에는 FIELDCONSTRUCTOR 라는 두 가지 값을 갖는 주입 전략 이름 만 허용하는 주입 전략 필드가 있습니다 . 이제 이를 알고 있으므로 매퍼에 이 설정을 추가해 보겠습니다. 예를 들어 LectureListMapper 를 사용하여 보여드리겠습니다 .
@Mapper(componentModel = "spring", uses = LectureMapper.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface LectureListMapper {
   List<LectureModel> toModelList(List<LectureDTO> dtos);
   List<LectureDTO> toDTOList(List<LectureModel> models);
}
제가 추가한 부분을 굵게 표시해 두었습니다. 다른 모든 항목에 대해 이 옵션을 추가하고 새 줄로 매퍼가 생성되도록 프로젝트를 다시 컴파일해 보겠습니다. 이 작업이 완료되면 LectureListMapper 에 대한 매퍼 구현이 어떻게 변경되었는지 살펴보겠습니다 (필요한 부분은 굵게 강조 표시됨).
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Generated(
   value = "org.mapstruct.ap.MappingProcessor",
   date = "2021-12-09T22:25:37+0300",
   comments = "version: 1.4.2.Final, compiler: javac, environment: Java 15.0.2 (N/A)"
)
@Component
public class LectureListMapperImpl implements LectureListMapper {

   private final LectureMapper lectureMapper;

   @Autowired
   public LectureListMapperImpl(LectureMapper lectureMapper) {

       this.lectureMapper = lectureMapper;
   }

   @Override
   public List<LectureModel> toModelList(List<LectureDTO> dtos) {
       if ( dtos == null ) {
           return null;
       }

       List<LectureModel> list = new ArrayList<LectureModel>( dtos.size() );
       for ( LectureDTO lectureDTO : dtos ) {
           list.add( lectureMapper.toModel( lectureDTO ) );
       }

       return list;
   }

   @Override
   public List<LectureDTO> toDTOList(List<LectureModel> models) {
       if ( models == null ) {
           return null;
       }

       List<LectureDTO> list = new ArrayList<LectureDTO>( models.size() );
       for ( LectureModel lectureModel : models ) {
           list.add( lectureMapper.toDTO( lectureModel ) );
       }

       return list;
   }
}
이제 Mapstruct는 생성자를 통해 매퍼 주입을 구현했습니다. 이것이 바로 우리가 달성하려고 했던 것입니다. 이제 테스트 컴파일이 중지됩니다. 업데이트하여 다음을 얻습니다.
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

class LectureListMapperTest {

   private final LectureListMapper lectureListMapper = new LectureListMapperImpl(new LectureMapperImpl());

   @Test
   void shouldProperlyMapListDtosToListModels() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(12L);
       dto.setName("I'm BATMAN!");

       List<LectureDTO> dtos = Collections.singletonList(dto);

       //when
       List<LectureModel> models = lectureListMapper.toModelList(dtos);

       //then
       Assertions.assertNotNull(models);
       Assertions.assertEquals(1, models.size());
       Assertions.assertEquals(dto.getId(), models.get(0).getId());
       Assertions.assertEquals(dto.getName(), models.get(0).getName());
   }
}
이제 테스트를 실행하면 모든 것이 예상대로 작동할 것입니다. LectureListMapperImpl에서 필요한 LectureMapper를 전달하기 때문입니다. 승리하세요! 여러분에게는 어렵지 않지만 기쁩니다. 친구 여러분, 모든 것이 평소와 같습니다. 내 GitHub 계정 , 내 Telegram 계정을 구독하세요 . 거기에 내 활동 결과가 게시되는데 정말 유용한 것들이 있습니다.) 특히 텔레그램 채널의 토론 그룹에 참여하도록 초대합니다 . 누군가 기술적인 질문이 있으면 그곳에서 답변을 얻을 수 있습니다. 이 형식은 모든 사람에게 흥미롭습니다. 누가 무엇을 알고 있는지 읽고 경험을 얻을 수 있습니다.

결론

이 기사의 일부로 우리는 Mapstruct와 같이 필요하고 자주 사용되는 제품에 대해 알게되었습니다. 우리는 그것이 무엇인지, 이유와 방법을 알아냈습니다. 실제 사례를 통해 우리는 무엇을 할 수 있고 어떻게 바꿀 수 있는지 느꼈습니다. 또한 매퍼를 올바르게 테스트할 수 있도록 생성자를 통해 Bean 주입을 설정하는 방법도 살펴보았습니다. Mapstruct의 동료들은 자사 제품 사용자가 매퍼를 삽입하는 방법을 정확하게 선택할 수 있도록 허용했으며 이에 대해 의심할 여지 없이 감사드립니다. 그러나 Spring이 생성자를 통해 빈 주입을 권장한다는 사실에도 불구하고 Mapstruct의 사람들은 기본적으로 필드를 통한 주입을 설정했습니다. 왜 그런 겁니까? 대답이 없습니다. 나는 우리가 모르는 이유가 있을 수 있다고 생각하며, 그것이 바로 그들이 이런 식으로 한 이유입니다. 그리고 그들에게서 알아보기 위해 공식 제품 저장소에 GitHub 문제를 만들었습니다 .
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION