1. 개요

이 사용방법(예제)에서는 MapStruct를 사용하여 개체 컬렉션을 매핑하는 방법을 배웁니다.

이 기사는 이미 MapStruct에 대한 기본적인 이해를 가정하고 있으므로 초보자는 먼저 MapStruct에 대한 빠른 사용방법(예제)를 확인해야 합니다 .

2. 컬렉션 매핑

일반적으로 MapStruct를 사용한 매핑 컬렉션은 단순 유형과 동일한 방식으로 작동합니다 .

기본적으로 간단한 인터페이스나 추상 클래스를 만들고 매핑 메서드를 선언해야 합니다. 선언에 따라 MapStruct는 매핑 코드를 자동으로 생성합니다. 일반적으로 생성된 코드는 소스 컬렉션을 반복하고 각 요소를 대상 유형으로 변환하고 각 요소를 대상 컬렉션에 포함합니다 .

간단한 예를 살펴보겠습니다.

2.1. 매핑 List

먼저 매퍼의 매핑 소스로 간단한 POJO를 고려합니다.

public class Employee {
    private String firstName;
    private String lastName;

    // constructor, getters and setters
}

대상은 간단한 DTO입니다.

public class EmployeeDTO {

    private String firstName;
    private String lastName;

    // getters and setters
}

다음으로 매퍼를 정의합니다.

@Mapper
public interface EmployeeMapper {
    List<EmployeeDTO> map(List<Employee> employees);
}

마지막으로 EmployeeMapper 인터페이스 에서 생성된 코드 MapStruct를 살펴보겠습니다 .

public class EmployeeMapperImpl implements EmployeeMapper {

    @Override
    public List<EmployeeDTO> map(List<Employee> employees) {
        if (employees == null) {
            return null;
        }

        List<EmployeeDTO> list = new ArrayList<EmployeeDTO>(employees.size());
        for (Employee employee : employees) {
            list.add(employeeToEmployeeDTO(employee));
        }

        return list;
    }

    protected EmployeeDTO employeeToEmployeeDTO(Employee employee) {
        if (employee == null) {
            return null;
        }

        EmployeeDTO employeeDTO = new EmployeeDTO();

        employeeDTO.setFirstName(employee.getFirstName());
        employeeDTO.setLastName(employee.getLastName());

        return employeeDTO;
    }
}

주목해야 할 한 가지 중요한 점은 MapStruct가 Employee에서 EmployeeDTO로의 매핑을 자동으로 생성 했다는입니다 .

이것이 불가능한 경우가 있습니다. 예를 들어 Employee 모델을 다음 모델에 매핑한다고 가정해 보겠습니다.

public class EmployeeFullNameDTO {

    private String fullName;

    // getter and setter
}

이 경우 Employee List에서 EmployeeFullNameDTO List으로의 매핑 방법을 선언하면 컴파일 타임 오류 또는 경고표시 됩니다.

Warning:(11, 31) java: Unmapped target property: "fullName". 
  Mapping from Collection element "com.baeldung.mapstruct.mappingCollections.model.Employee employee" to 
  "com.baeldung.mapstruct.mappingCollections.dto.EmployeeFullNameDTO employeeFullNameDTO".

기본적으로 이것은 이 경우 MapStruct가 매핑을 자동으로 생성할 수 없음을 의미합니다 . 따라서 Employee 와 EmployeeFullNameDTO 간의 매핑을 수동으로 정의해야 합니다 .

이러한 점을 감안하여 수동으로 정의해 보겠습니다.

@Mapper
public interface EmployeeFullNameMapper {

    List<EmployeeFullNameDTO> map(List<Employee> employees);

    default EmployeeFullNameDTO map(Employee employee) {
        EmployeeFullNameDTO employeeInfoDTO = new EmployeeFullNameDTO();
        employeeInfoDTO.setFullName(employee.getFirstName() + " " + employee.getLastName());

        return employeeInfoDTO;
    }
}

생성된 코드는 소스 List 의 요소를 대상 List 에 매핑하기 위해 정의한 메서드를 사용합니다 .

이는 일반적으로도 적용됩니다. 소스 요소 유형을 대상 요소 유형에 매핑하는 메서드를 정의한 경우 MapStruct에서 이를 사용합니다.

2.2. 매핑 세트 및 맵

MapStruct를 사용한 매핑 세트는 List과 동일한 방식으로 작동합니다. 예를 들어 Employee 인스턴스 집합EmployeeDTO 인스턴스 집합 에 매핑한다고 가정해 보겠습니다 .

이전과 마찬가지로 매퍼가 필요합니다.

@Mapper
public interface EmployeeMapper {

    Set<EmployeeDTO> map(Set<Employee> employees);
}

그러면 MapStruct가 적절한 코드를 생성합니다.

public class EmployeeMapperImpl implements EmployeeMapper {

    @Override
    public Set<EmployeeDTO> map(Set<Employee> employees) {
        if (employees == null) {
            return null;
        }

        Set<EmployeeDTO> set = 
          new HashSet<EmployeeDTO>(Math.max((int)(employees.size() / .75f ) + 1, 16));
        for (Employee employee : employees) {
            set.add(employeeToEmployeeDTO(employee));
        }

        return set;
    }

    protected EmployeeDTO employeeToEmployeeDTO(Employee employee) {
        if (employee == null) {
            return null;
        }

        EmployeeDTO employeeDTO = new EmployeeDTO();

        employeeDTO.setFirstName(employee.getFirstName());
        employeeDTO.setLastName(employee.getLastName());

        return employeeDTO;
    }
}

Map에도 동일하게 적용됩니다. Map<String, Employee>Map <String, EmployeeDTO> 에 매핑한다고 가정해 봅시다 .

이전과 동일한 단계를 따를 수 있습니다.

@Mapper
public interface EmployeeMapper {

    Map<String, EmployeeDTO> map(Map<String, Employee> idEmployeeMap);
}

그리고 MapStruct는 다음 작업을 수행합니다.

public class EmployeeMapperImpl implements EmployeeMapper {

    @Override
    public Map<String, EmployeeDTO> map(Map<String, Employee> idEmployeeMap) {
        if (idEmployeeMap == null) {
            return null;
        }

        Map<String, EmployeeDTO> map = new HashMap<String, EmployeeDTO>(Math.max((int)(idEmployeeMap.size() / .75f) + 1, 16));

        for (java.util.Map.Entry<String, Employee> entry : idEmployeeMap.entrySet()) {
            String key = entry.getKey();
            EmployeeDTO value = employeeToEmployeeDTO(entry.getValue());
            map.put(key, value);
        }

        return map;
    }

    protected EmployeeDTO employeeToEmployeeDTO(Employee employee) {
        if (employee == null) {
            return null;
        }

        EmployeeDTO employeeDTO = new EmployeeDTO();

        employeeDTO.setFirstName(employee.getFirstName());
        employeeDTO.setLastName(employee.getLastName());

        return employeeDTO;
    }
}

3. 컬렉션 매핑 전략

종종 부모-자식 관계가 있는 데이터 유형을 매핑해야 합니다. 일반적으로 다른 데이터 유형(하위) 의 컬렉션 을 필드로 갖는 데이터 유형(상위)이 있습니다 .

이러한 경우에 MapStruct는 자식을 부모 유형에 설정하거나 추가하는 방법을 선택하는 방법을 제공합니다. 특히 @Mapper 어노테이션에는 ACCESSOR_ONLY , SETTER_PREFERRED , ADDER_PREFERRED 또는 TARGET_IMMUTABLE 일 수 있는 collectionMappingStrategy 속성이 있습니다 .

이러한 모든 값은 자식이 설정되거나 부모 유형에 추가되는 방식을 나타냅니다. 기본값은 ACCESSOR_ONLY이며 이는 접근자만 자식 컬렉션 을 설정하는 데 사용할 수 있음을 의미합니다.

이 옵션은 Collection 필드의 세터를 사용할 수 없지만 가산기가 있는 경우에 유용합니다. 이것이 유용한 또 다른 경우 컬렉션 이 상위 유형에서 변경 불가능한 경우 입니다. 일반적으로 이러한 경우는 생성된 대상 유형에서 발생합니다.

3.1. ACCESSOR_ONLY 컬렉션 매핑 전략

이것이 어떻게 작동하는지 더 잘 이해하기 위해 예를 살펴보겠습니다.

매핑 소스로 Company 클래스를 생성합니다 .

public class Company {

    private List<Employee> employees;

   // getter and setter
}

매핑 대상은 간단한 DTO입니다.

public class CompanyDTO {

    private List<EmployeeDTO> employees;

    public List<EmployeeDTO> getEmployees() {
        return employees;
    }

    public void setEmployees(List<EmployeeDTO> employees) {
        this.employees = employees;
    }

    public void addEmployee(EmployeeDTO employeeDTO) {
        if (employees == null) {
            employees = new ArrayList<>();
        }

        employees.add(employeeDTO);
    }
}

setter setEmployees 와 adder addEmployee를 모두 사용할 수 있습니다. 또한 adder의 경우 컬렉션 초기화를 담당합니다.

이제 회사CompanyDTO에 매핑하려고 한다고 가정해 보겠습니다. 그런 다음 이전과 마찬가지로 매퍼가 필요합니다.

@Mapper(uses = EmployeeMapper.class)
public interface CompanyMapper {
    CompanyDTO map(Company company);
}

EmployeeMapper 와 기본 collectionMappingStrategy 를 재사용 했습니다.

이제 MapStruct가 생성한 코드를 살펴보겠습니다.

public class CompanyMapperImpl implements CompanyMapper {

    private final EmployeeMapper employeeMapper = Mappers.getMapper(EmployeeMapper.class);

    @Override
    public CompanyDTO map(Company company) {
        if (company == null) {
            return null;
        }

        CompanyDTO companyDTO = new CompanyDTO();

        companyDTO.setEmployees(employeeMapper.map(company.getEmployees()));

        return companyDTO;
    }
}

보시다시피 MapStruct는 Setter setEmployees를 사용하여 EmployeeDTO 인스턴스 List 설정 합니다 . 이는 기본 collectionMappingStrategy인 ACCESSOR_ONLY를 사용했기 때문에 발생합니다.

MapStruct는 또한 EmployeeMapper에서 List<Employee>List < EmployeeDTO > 에 매핑하는 메서드를 찾아 재사용했습니다.

3.2. ADDER_PREFERRED 컬렉션 매핑 전략

반대로 ADDER_PREFERREDcollectionMappingStrategy 로 사용했다고 가정해 보겠습니다 .

@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
        uses = EmployeeMapper.class)
public interface CompanyMapperAdderPreferred {
    CompanyDTO map(Company company);
}

다시, 우리는  EmployeeMapper 를 재사용하고 싶습니다 . 그러나 먼저 단일  Employee 를  EmployeeDTO 로 변환할 수 있는 메서드를 명시적으로 추가해야 합니다 .

@Mapper
public interface EmployeeMapper {
    EmployeeDTO map(Employee employee);
    List map(List employees);
    Set map(Set employees);
    Map<String, EmployeeDTO> map(Map<String, Employee> idEmployeeMap);
}

이는 MapStruct가 가산기를 사용하여 EmployeeDTO 인스턴스를 대상 CompanyDTO 인스턴스에 하나씩 추가하기 때문입니다 .

public class CompanyMapperAdderPreferredImpl implements CompanyMapperAdderPreferred {

    private final EmployeeMapper employeeMapper = Mappers.getMapper( EmployeeMapper.class );

    @Override
    public CompanyDTO map(Company company) {
        if ( company == null ) {
            return null;
        }

        CompanyDTO companyDTO = new CompanyDTO();

        if ( company.getEmployees() != null ) {
            for ( Employee employee : company.getEmployees() ) {
                companyDTO.addEmployee( employeeMapper.map( employee ) );
            }
        }

        return companyDTO;
    }
}

가산기를 사용할 수 없으면 세터가 사용됩니다.

MapStruct의 참조 문서 에서 모든 컬렉션 매핑 전략에 대한 완전한 설명을 찾을 수 있습니다 .

4. 대상 수집 구현 유형

MapStruct는 컬렉션 인터페이스를 매핑 방법에 대한 대상 유형으로 지원합니다.

이 경우 일부 기본 구현이 생성된 코드에 사용됩니다. 예를 들어 List 의 기본 구현 은 위의 예제에서 알 수 있듯이 ArrayList 입니다.

참조 문서 에서 MapStruct가 지원하는 인터페이스의 전체 List과 각 인터페이스에 대해 사용하는 기본 구현을 찾을 수 있습니다 .

5. 결론

이 기사에서는 MapStruct를 사용하여 컬렉션을 매핑하는 방법을 살펴보았습니다.

먼저 다양한 유형의 컬렉션을 매핑하는 방법을 살펴보았습니다. 그런 다음 컬렉션 매핑 전략을 사용하여 부모-자식 관계 매퍼를 사용자 지정하는 방법을 배웠습니다.

그 과정에서 MapStruct를 사용하여 컬렉션을 매핑하는 동안 명심해야 할 핵심 사항과 사항을 강조했습니다.

평소와 같이 전체 코드는 GitHub 에서 사용할 수 있습니다 .

Generic footer banner