1. 개요

이 예제에서 우리는 MapStruct , 즉 간단히 말해서 Java Bean 매퍼의 사용을 탐구할 것입니다.

이 API에는 두 Java Bean 간에 자동으로 매핑되는 기능이 포함되어 있습니다. MapStruct를 사용하면 인터페이스만 생성하면 라이브러리가 컴파일 시간 동안 구체적인 구현을 자동으로 생성합니다.

2. MapStruct 및 Transfer 객체 패턴

대부분의 애플리케이션에서 POJO를 다른 POJO로 변환하는 상용구 코드를 많이 볼 수 있습니다.

예를 들어, 지속성 지원 엔터티와 클라이언트 측으로 나가는 DTO 간에 일반적인 유형의 변환이 발생합니다.

이것이 MapStruct가 해결하는 문제입니다. 수동으로 빈 매퍼를 만드는 것은 시간이 많이 걸립니다. 그러나 라이브러리 는 자동으로 빈 매퍼 클래스를 생성할 수 있습니다.

3. 메이븐

Maven pom.xml 에 아래 의존성을 추가해 보겠습니다 .

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.4.2.Final</version> 
</dependency>

MapStruct 의 안정적인 최신 릴리스 와 해당 프로세서 는 모두 Maven Central Repository에서 사용할 수 있습니다.

또한 maven-compiler-plugin 플러그인 의 구성 부분에 annotationProcessorPaths 섹션을 추가해 보겠습니다 .

mapstruct -processor 는 빌드 중에 매퍼 구현을 생성하는 데 사용됩니다.

<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>

4. 기본 매핑

4.1. POJO 만들기

먼저 간단한 Java POJO를 만들어 보겠습니다.

public class SimpleSource {
    private String name;
    private String description;
    // getters and setters
}
 
public class SimpleDestination {
    private String name;
    private String description;
    // getters and setters
}

4.2. 매퍼 인터페이스

@Mapper
public interface SimpleSourceDestinationMapper {
    SimpleDestination sourceToDestination(SimpleSource source);
    SimpleSource destinationToSource(SimpleDestination destination);
}

SimpleSourceDestinationMapper 에 대한 구현 클래스를 생성하지 않았음을 주목 하세요. MapStruct가 생성하기 때문입니다.

4.3. 새로운 매퍼

mvn clean install 을 실행하여 MapStruct 처리를 트리거할 수 있습니다 .

그러면 /target/generated-sources/annotations/ 아래에 구현 클래스가 생성 됩니다.

다음은 MapStruct가 자동으로 생성하는 클래스입니다.

public class SimpleSourceDestinationMapperImpl
  implements SimpleSourceDestinationMapper {
    @Override
    public SimpleDestination sourceToDestination(SimpleSource source) {
        if ( source == null ) {
            return null;
        }
        SimpleDestination simpleDestination = new SimpleDestination();
        simpleDestination.setName( source.getName() );
        simpleDestination.setDescription( source.getDescription() );
        return simpleDestination;
    }
    @Override
    public SimpleSource destinationToSource(SimpleDestination destination){
        if ( destination == null ) {
            return null;
        }
        SimpleSource simpleSource = new SimpleSource();
        simpleSource.setName( destination.getName() );
        simpleSource.setDescription( destination.getDescription() );
        return simpleSource;
    }
}

4.4. 테스트 케이스

마지막으로 생성된 모든 항목과 함께 SimpleSource 의 값이 SimpleDestination 의 값과 일치 함을 보여주는 테스트 사례를 작성해 보겠습니다 .

public class SimpleSourceDestinationMapperIntegrationTest {
    private SimpleSourceDestinationMapper mapper
      = Mappers.getMapper(SimpleSourceDestinationMapper.class);
    @Test
    public void givenSourceToDestination_whenMaps_thenCorrect() {
        SimpleSource simpleSource = new SimpleSource();
        simpleSource.setName("SourceName");
        simpleSource.setDescription("SourceDescription");
        SimpleDestination destination = mapper.sourceToDestination(simpleSource);
 
        assertEquals(simpleSource.getName(), destination.getName());
        assertEquals(simpleSource.getDescription(), 
          destination.getDescription());
    }
    @Test
    public void givenDestinationToSource_whenMaps_thenCorrect() {
        SimpleDestination destination = new SimpleDestination();
        destination.setName("DestinationName");
        destination.setDescription("DestinationDescription");
        SimpleSource source = mapper.destinationToSource(destination);
        assertEquals(destination.getName(), source.getName());
        assertEquals(destination.getDescription(),
          source.getDescription());
    }
}

5. 의존성 주입으로 매핑하기

다음으로, 단순히 Mappers.getMapper(YourClass.class) 를 호출하여 MapStruct에서 매퍼의 인스턴스를 얻습니다 .

물론 이는 인스턴스를 가져오는 매우 수동적인 방법입니다. 그러나 훨씬 더 나은 대안은 필요한 곳에 직접 매퍼를 주입하는 것입니다(프로젝트에서 의존성 주입 솔루션을 사용하는 경우).

운 좋게도 MapStruct는 Spring과 CDI ( Contexts and Dependency Injection )를 모두 지원합니다.

매퍼에서 Spring IoC를 사용하려면 spring 값 으로 @Mapper 에 componentModel 속성을 추가해야 하고 CDI의 경우 cdi 가 됩니다.

5.1. 매퍼 수정

SimpleSourceDestinationMapper 에 다음 코드를 추가합니다 .

@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper

5.2. 매퍼에 스프링 컴포넌트 주입

때로는 매핑 로직 내에서 다른 Spring 구성 요소를 활용해야 합니다. 이 경우 인터페이스 대신 추상 클래스를 사용해야 합니다 .

@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService

그런 다음 잘 알려진 @Autowired 어노테이션을 사용 하여 원하는 구성 요소를 쉽게 삽입 하고 코드에서 사용할 수 있습니다.

@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService {

    @Autowired
    protected SimpleService simpleService;

    @Mapping(target = "name", expression = "java(simpleService.enrichName(source.getName()))")
    public abstract SimpleDestination sourceToDestination(SimpleSource source);
}

주입된 빈을 비공개로 만들지 않도록 주의해야 합니다! MapStruct는 생성된 구현 클래스의 개체에 액세스해야 하기 때문입니다.

6. 필드 이름이 다른 필드 매핑

이전 예제에서 MapStruct는 필드 이름이 같기 때문에 빈을 자동으로 매핑할 수 있었습니다. 그렇다면 매핑하려는 빈의 필드 이름이 다른 경우에는 어떻게 될까요?

이 예제에서는 EmployeeEmployeeDTO 라는 새 빈을 생성할 것입니다 .

6.1. 새로운 POJO

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    // getters and setters
}
public class Employee {
    private int id;
    private String name;
    // getters and setters
}

6.2. 매퍼 인터페이스

다른 필드 이름을 매핑할 때 소스 필드를 대상 필드로 구성해야 하고 그렇게 하려면 각 필드에 @Mapping 어노테이션 을 추가해야 합니다.

MapStruct에서 점 표기법을 사용하여 bean의 구성원을 정의할 수도 있습니다.

@Mapper
public interface EmployeeMapper {
    
    @Mapping(target="employeeId", source="entity.id")
    @Mapping(target="employeeName", source="entity.name")
    EmployeeDTO employeeToEmployeeDTO(Employee entity);

    @Mapping(target="id", source="dto.employeeId")
    @Mapping(target="name", source="dto.employeeName")
    Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

6.3. 테스트 케이스

다시 말하지만 소스 및 대상 개체 값이 모두 일치하는지 테스트해야 합니다.

@Test
public void givenEmployeeDTOwithDiffNametoEmployee_whenMaps_thenCorrect() {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setEmployeeId(1);
    dto.setEmployeeName("John");

    Employee entity = mapper.employeeDTOtoEmployee(dto);

    assertEquals(dto.getEmployeeId(), entity.getId());
    assertEquals(dto.getEmployeeName(), entity.getName());
}

GitHub 프로젝트 에서 더 많은 테스트 사례를 찾을 수 있습니다 .

7. 빈을 자식 빈으로 매핑하기

다음으로 다른 빈에 대한 참조로 빈을 매핑하는 방법을 보여줍니다.

7.1. POJO 수정

Employee 객체 에 새 빈 참조를 추가해 보겠습니다 .

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    private DivisionDTO division;
    // getters and setters omitted
}
public class Employee {
    private int id;
    private String name;
    private Division division;
    // getters and setters omitted
}
public class Division {
    private int id;
    private String name;
    // default constructor, getters and setters omitted
}

7.2. 매퍼 수정

여기에 DivisionDivisionDTO 로 또는 그 반대로 변환하는 방법을 추가해야 합니다. MapStruct가 객체 유형을 변환해야 하고 변환할 메소드가 동일한 클래스에 존재하는 것을 감지하면 자동으로 사용합니다.

다음을 매퍼에 추가해 보겠습니다.

DivisionDTO divisionToDivisionDTO(Division entity);

Division divisionDTOtoDivision(DivisionDTO dto);

7.3. 테스트 케이스 수정

기존 테스트 케이스에 몇 가지 테스트 케이스를 수정하고 추가해 보겠습니다.

@Test
public void givenEmpDTONestedMappingToEmp_whenMaps_thenCorrect() {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setDivision(new DivisionDTO(1, "Division1"));
    Employee entity = mapper.employeeDTOtoEmployee(dto);
    assertEquals(dto.getDivision().getId(), 
      entity.getDivision().getId());
    assertEquals(dto.getDivision().getName(), 
      entity.getDivision().getName());
}

8. 유형 변환을 통한 매핑

MapStruct는 또한 미리 만들어진 몇 가지 암시적 유형 변환을 제공하며 이 예에서는 String 날짜를 실제 Date 객체로 변환하려고 시도합니다.

암시적 유형 변환에 대한 자세한 내용은 MapStruct 참조 사용방법(예제) 를 확인하세요 .

8.1. 빈 수정

직원의 시작 날짜를 추가합니다.

public class Employee {
    // other fields
    private Date startDt;
    // getters and setters
}
public class EmployeeDTO {
    // other fields
    private String employeeStartDt;
    // getters and setters
}

8.2. 매퍼 수정

매퍼를 수정 하고 시작 날짜에 대한 dateFormat 을 제공합니다.


  @Mapping(target="employeeId", source = "entity.id")
  @Mapping(target="employeeName", source = "entity.name")
  @Mapping(target="employeeStartDt", source = "entity.startDt",
           dateFormat = "dd-MM-yyyy HH:mm:ss")
EmployeeDTO employeeToEmployeeDTO(Employee entity);

  @Mapping(target="id", source="dto.employeeId")
  @Mapping(target="name", source="dto.employeeName")
  @Mapping(target="startDt", source="dto.employeeStartDt",
           dateFormat="dd-MM-yyyy HH:mm:ss")
Employee employeeDTOtoEmployee(EmployeeDTO dto);

8.3. 테스트 케이스 수정

변환이 올바른지 확인하기 위해 몇 가지 테스트 사례를 더 추가해 보겠습니다.

private static final String DATE_FORMAT = "dd-MM-yyyy HH:mm:ss";
@Test
public void givenEmpStartDtMappingToEmpDTO_whenMaps_thenCorrect() throws ParseException {
    Employee entity = new Employee();
    entity.setStartDt(new Date());
    EmployeeDTO dto = mapper.employeeToEmployeeDTO(entity);
    SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
 
    assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
      entity.getStartDt().toString());
}
@Test
public void givenEmpDTOStartDtMappingToEmp_whenMaps_thenCorrect() throws ParseException {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setEmployeeStartDt("01-04-2016 01:00:00");
    Employee entity = mapper.employeeDTOtoEmployee(dto);
    SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
 
    assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
      entity.getStartDt().toString());
}

9. 추상 클래스로 매핑하기

때로는 @Mapping 기능을 초과하는 방식으로 매퍼를 사용자 정의하고 싶을 수도 있습니다.

예를 들어, 유형 변환 외에도 아래 예제와 같이 어떤 방식으로든 값을 변환할 수 있습니다.

이러한 경우 추상 클래스를 만들고 사용자 정의하려는 메서드를 구현하고 MapStruct에서 생성해야 하는 추상은 그대로 둘 수 있습니다.

9.1. 기본 모델

이 예에서는 다음 클래스를 사용합니다.

public class Transaction {
    private Long id;
    private String uuid = UUID.randomUUID().toString();
    private BigDecimal total;

    //standard getters
}

및 일치하는 DTO:

public class TransactionDTO {

    private String uuid;
    private Long totalInCents;

    // standard getters and setters
}

여기서 까다로운 부분은 BigDecimal 금액을 Long totalInCents 로 변환하는 것 입니다.

9.2. 매퍼 정의

Mapper 를 추상 클래스로 생성하여 이를 달성할 수 있습니다 .

@Mapper
abstract class TransactionMapper {

    public TransactionDTO toTransactionDTO(Transaction transaction) {
        TransactionDTO transactionDTO = new TransactionDTO();
        transactionDTO.setUuid(transaction.getUuid());
        transactionDTO.setTotalInCents(transaction.getTotal()
          .multiply(new BigDecimal("100")).longValue());
        return transactionDTO;
    }

    public abstract List<TransactionDTO> toTransactionDTO(
      Collection<Transaction> transactions);
}

여기에서 단일 개체 변환을 위해 완전히 사용자 정의된 매핑 방법을 구현했습니다.

반면에 CollectionList 추상으로 매핑하기 위한 메서드는 남겨두었으므로 MapStruct 가 이를 구현합니다.

9.3. 생성된 결과

단일 TransactionTransactionDTO 에 매핑하는 메서드를 이미 구현했기 때문에 MapStruct 가 두 번째 메서드에서 이를 사용할 것으로 예상 합니다.

다음이 생성됩니다.

@Generated
class TransactionMapperImpl extends TransactionMapper {

    @Override
    public List<TransactionDTO> toTransactionDTO(Collection<Transaction> transactions) {
        if ( transactions == null ) {
            return null;
        }

        List<TransactionDTO> list = new ArrayList<>();
        for ( Transaction transaction : transactions ) {
            list.add( toTransactionDTO( transaction ) );
        }

        return list;
    }
}

12행에서 볼 수 있듯이 MapStruct 는 생성된 메서드에서 구현을 사용합니다.

10. 사전 매핑 및 매핑 후 어노테이션

@BeforeMapping@AfterMapping 어노테이션 을 사용하여 @Mapping 기능 을 사용자 정의하는 또 다른 방법은 다음 과 같습니다. 어노테이션은 매핑 논리 바로 앞과 뒤에 호출되는 메서드를 표시하는 데 사용됩니다.

매핑된 모든 수퍼 유형에 이 동작을 적용하려는 시나리오에서 매우 유용합니다 .

Car ElectricCarBioDieselCar 의 하위 유형을 CarDTO에 매핑하는 예를 살펴 보겠습니다 .

매핑하는 동안 유형 개념을 DTO의 FuelType 열거형 필드에 매핑하려고 합니다. 그런 다음 매핑이 완료된 후 DTO의 이름을 대문자로 변경하고 싶습니다.

10.1. 기본 모델

다음 클래스를 사용할 것입니다.

public class Car {
    private int id;
    private String name;
}

자동차 의 하위 유형 :

public class BioDieselCar extends Car {
}
public class ElectricCar extends Car {
}

열거 필드 유형 FuelType 인 CarDTO :

public class CarDTO {
    private int id;
    private String name;
    private FuelType fuelType;
}
public enum FuelType {
    ELECTRIC, BIO_DIESEL
}

10.2. 매퍼 정의

이제 CarCarDTO 에 매핑하는 추상 매퍼 클래스를 작성해 보겠습니다 .

@Mapper
public abstract class CarsMapper {
    @BeforeMapping
    protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) {
        if (car instanceof ElectricCar) {
            carDto.setFuelType(FuelType.ELECTRIC);
        }
        if (car instanceof BioDieselCar) { 
            carDto.setFuelType(FuelType.BIO_DIESEL);
        }
    }

    @AfterMapping
    protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) {
        carDto.setName(carDto.getName().toUpperCase());
    }

    public abstract CarDTO toCarDto(Car car);
}

@MappingTarget 은 @BeforeMapping 의 경우 매핑 로직이 실행되기 직전 , @AfterMapping annotated 메소드 의 경우 직후에 대상 매핑 ​​DTO를 채우는 매개변수 어노테이션이다

10.3. 결과

위에서 정의한 CarsMapper 는 구현을 생성 합니다 .

@Generated
public class CarsMapperImpl extends CarsMapper {

    @Override
    public CarDTO toCarDto(Car car) {
        if (car == null) {
            return null;
        }

        CarDTO carDTO = new CarDTO();

        enrichDTOWithFuelType(car, carDTO);

        carDTO.setId(car.getId());
        carDTO.setName(car.getName());

        convertNameToUpperCase(carDTO);

        return carDTO;
    }
}

어노테이션이 달린 메서드 호출 이 구현 의 매핑 논리를 둘러싸고 있는 방식에 주목 하세요.

11. 롬복 지원

최신 버전의 MapStruct에서 Lombok 지원이 발표되었습니다. 따라서 Lombok을 사용하여 소스 엔터티와 대상을 쉽게 매핑할 수 있습니다.

Lombok 지원을 활성화하려면 어노테이션 프로세서 경로에 의존성 을 추가해야 합니다. Lombok 버전 1.18.16부터 lombok-mapstruct-binding 에 대한 의존성을 추가해야 합니다 . 이제 Maven 컴파일러 플러그인에 Mapstruct 프로세서 와 Lombok이 있습니다.

<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>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
	        <version>1.18.4</version>
            </path>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok-mapstruct-binding</artifactId>
	        <version>0.2.0</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

Lombok 어노테이션을 사용하여 소스 엔터티를 정의해 보겠습니다.

@Getter
@Setter
public class Car {
    private int id;
    private String name;
}

그리고 대상 데이터 전송 객체:

@Getter
@Setter
public class CarDTO {
    private int id;
    private String name;
}

이에 대한 매퍼 인터페이스는 이전 예와 유사합니다.

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    CarDTO carToCarDTO(Car car);
}

12. defaultExpression 지원

버전 1.3.0부터 @Mapping 어노테이션의 defaultExpression 속성을 사용 하여 소스 필드가 null 인 경우 대상 필드의 값을 결정하는 표현식을 지정할 수 있습니다. 이것은 기존 defaultValue 속성 기능에 추가됩니다.

소스 엔티티:

public class Person {
    private int id;
    private String name;
}

대상 데이터 전송 개체:

public class PersonDTO {
    private int id;
    private String name;
}

소스 엔터티 id 필드가 null 이면 랜덤의 id 를 생성 하고 다른 속성 값을 있는 그대로 유지하여 대상에 할당하려고 합니다.

@Mapper
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
    
    @Mapping(target = "id", source = "person.id", 
      defaultExpression = "java(java.util.UUID.randomUUID().toString())")
    PersonDTO personToPersonDTO(Person person);
}

표현식 실행을 확인하기 위해 테스트 케이스를 추가해 보겠습니다.

@Test
public void givenPersonEntitytoPersonWithExpression_whenMaps_thenCorrect() 
    Person entity  = new Person();
    entity.setName("Micheal");
    PersonDTO personDto = PersonMapper.INSTANCE.personToPersonDTO(entity);
    assertNull(entity.getId());
    assertNotNull(personDto.getId());
    assertEquals(personDto.getName(), entity.getName());
}

13. 결론

이 기사에서는 MapStruct에 대한 소개를 제공했습니다. 매핑 라이브러리의 기본 사항과 응용 프로그램에서 사용하는 방법을 소개했습니다.

이러한 예제 및 테스트의 구현은 GitHub 프로젝트에서 찾을 수 있습니다. 메이븐 프로젝트이므로 그대로 임포트하고 실행하기 쉬워야 한다.

Generic footer banner