MapStruct의 mapping방식과 Lobmok 함께 사용 시 값이 mapping되지 않는 이유
web/Spring

MapStruct의 mapping방식과 Lobmok 함께 사용 시 값이 mapping되지 않는 이유

반응형

저번에 작성했던 MapStruct를 사용하면서 겪은 이야기를 정리해본다.

MapStruct를 사용하면 값을 매핑해야하는 여러 경우에서 편리하게 값을 매핑할 수 있다. 특히 Entity값을 외부로 내보내려고 값객체에 값을 저장할 때 유용하게 사용된다.

 

객체 생성 및 값 주입 방법

 

기본적으로 MapStruct에 경우 setter, constructor, builder를 사용하여 객체를 생성하고 값을 넣는다. 먼저 setter를 사용하는 경우를 살펴보자.

 

1. setter 

package com.wedul.mapstructtest.entity;

import lombok.Builder;
import lombok.Getter;

import java.util.List;

@Getter
public class AccountEntity {

    private String name;
    private int age;
    private String address;
    private String homeTel;
    private List<AccountImage> images;
}


package com.wedul.mapstructtest.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
public class AccountResponse {

    private String name;
    private int age;
    private String location;
    private String homeNumber;
    private List<AccountResponseImage> images;
}


package com.wedul.mapstructtest.mapper;

import com.wedul.mapstructtest.dto.AccountResponse;
import com.wedul.mapstructtest.entity.AccountEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(componentModel = "spring")
public interface AccountMapper {

    @Mapping(source = "address", target = "location")
    @Mapping(source = "homeTel", target = "homeNumber")
    AccountResponse domainToEntity(AccountEntity accountEntity);

}

위와 같이 AccountEntity에서 AccountResponse로 값을 매핑하고 싶어서 Mapper를 선언했을 때 Annotation processor를 통해 생성된 코드는 아래와 같이 setter를 사용해서 객체를 생성하고 값을 넣어준다.

package com.wedul.mapstructtest.mapper;

import com.wedul.mapstructtest.dto.AccountResponse;
import com.wedul.mapstructtest.dto.AccountResponseImage;
import com.wedul.mapstructtest.dto.AccountResponseImage.AccountResponseImageBuilder;
import com.wedul.mapstructtest.entity.AccountEntity;
import com.wedul.mapstructtest.entity.AccountImage;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-03-27T21:57:30+0900",
    comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.3.3.jar, environment: Java 11.0.11 (AdoptOpenJDK)"
)
@Component
public class AccountMapperImpl implements AccountMapper {

    @Override
    public AccountResponse domainToEntity(AccountEntity accountEntity) {
        if ( accountEntity == null ) {
            return null;
        }

        AccountResponse accountResponse = new AccountResponse();

        accountResponse.setLocation( accountEntity.getAddress() );
        accountResponse.setHomeNumber( accountEntity.getHomeTel() );
        accountResponse.setName( accountEntity.getName() );
        accountResponse.setAge( accountEntity.getAge() );
        accountResponse.setImages( accountImageListToAccountResponseImageList( accountEntity.getImages() ) );

        return accountResponse;
    }
...
}

 

하지만 값에 변화를 주지 않도록 setter를 지양하도록 하는 요새 추세하고는 맞지 않다.

 

 

2. constructor

setter를 명시해주어도 기본생성자 이외에 생성자를 생성하였을경우 생성한 생성자를 사용하여 객체를 생성하고 값을 넣어준다.

package com.wedul.mapstructtest.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
public class AccountResponse {

    private String name;
    private int age;
    private String location;
    private String homeNumber;
    private List<AccountResponseImage> images;

    public AccountResponse(String name, int age, String location, String homeNumber, List<AccountResponseImage> images) {
        this.name = name;
        this.age = age;
        this.location = location;
        this.homeNumber = homeNumber;
        this.images = images;
    }
}



// 생성된 값
package com.wedul.mapstructtest.mapper;

import com.wedul.mapstructtest.dto.AccountResponse;
import com.wedul.mapstructtest.dto.AccountResponseImage;
import com.wedul.mapstructtest.dto.AccountResponseImage.AccountResponseImageBuilder;
import com.wedul.mapstructtest.entity.AccountEntity;
import com.wedul.mapstructtest.entity.AccountImage;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-03-27T22:04:05+0900",
    comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.3.3.jar, environment: Java 11.0.11 (AdoptOpenJDK)"
)
@Component
public class AccountMapperImpl implements AccountMapper {

    @Override
    public AccountResponse domainToEntity(AccountEntity accountEntity) {
        if ( accountEntity == null ) {
            return null;
        }

        String location = null;
        String homeNumber = null;
        String name = null;
        int age = 0;
        List<AccountResponseImage> images = null;

        location = accountEntity.getAddress();
        homeNumber = accountEntity.getHomeTel();
        name = accountEntity.getName();
        age = accountEntity.getAge();
        images = accountImageListToAccountResponseImageList( accountEntity.getImages() );

        AccountResponse accountResponse = new AccountResponse( name, age, location, homeNumber, images );

        return accountResponse;
    }
...
}

만약 생성자에 일부 필드가 빠져 있을 경우 setter가 있으면 생성자에 없는 값은 setter를 사용해서 값을 넣어준다.

package com.wedul.mapstructtest.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
public class AccountResponse {

    private String name;
    private int age;
    private String location;
    private String homeNumber;
    private List<AccountResponseImage> images;

    // age값이 빠진 생성자
    public AccountResponse(String name, String location, String homeNumber, List<AccountResponseImage> images) {
        this.name = name;
        this.location = location;
        this.homeNumber = homeNumber;
        this.images = images;
    }
}

package com.wedul.mapstructtest.mapper;

import com.wedul.mapstructtest.dto.AccountResponse;
import com.wedul.mapstructtest.dto.AccountResponseImage;
import com.wedul.mapstructtest.dto.AccountResponseImage.AccountResponseImageBuilder;
import com.wedul.mapstructtest.entity.AccountEntity;
import com.wedul.mapstructtest.entity.AccountImage;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-03-27T22:04:50+0900",
    comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.3.3.jar, environment: Java 11.0.11 (AdoptOpenJDK)"
)
@Component
public class AccountMapperImpl implements AccountMapper {

    @Override
    public AccountResponse domainToEntity(AccountEntity accountEntity) {
        if ( accountEntity == null ) {
            return null;
        }

        String location = null;
        String homeNumber = null;
        String name = null;
        List<AccountResponseImage> images = null;

        location = accountEntity.getAddress();
        homeNumber = accountEntity.getHomeTel();
        name = accountEntity.getName();
        images = accountImageListToAccountResponseImageList( accountEntity.getImages() );

        AccountResponse accountResponse = new AccountResponse( name, location, homeNumber, images );

        // 빠진 값은 setter를 사용해서 추가
        accountResponse.setAge( accountEntity.getAge() );

        return accountResponse;
    }
...
}

하지만 생성자가 allArgument를 선언한게 아니고 setter가 빠져있다면 해당값은 매핑이 되지 않기 때문에 일부 필드를 빼고 생성자를 생성하면 안된다.

 

package com.wedul.mapstructtest.mapper;

import com.wedul.mapstructtest.dto.AccountResponse;
import com.wedul.mapstructtest.dto.AccountResponseImage;
import com.wedul.mapstructtest.dto.AccountResponseImage.AccountResponseImageBuilder;
import com.wedul.mapstructtest.entity.AccountEntity;
import com.wedul.mapstructtest.entity.AccountImage;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-03-27T22:06:49+0900",
    comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.3.3.jar, environment: Java 11.0.11 (AdoptOpenJDK)"
)
@Component
public class AccountMapperImpl implements AccountMapper {

    @Override
    public AccountResponse domainToEntity(AccountEntity accountEntity) {
        if ( accountEntity == null ) {
            return null;
        }

        String location = null;
        String homeNumber = null;
        List<AccountResponseImage> images = null;
        String name = null;

        location = accountEntity.getAddress();
        homeNumber = accountEntity.getHomeTel();
        images = accountImageListToAccountResponseImageList( accountEntity.getImages() );
        name = accountEntity.getName();

        AccountResponse accountResponse = new AccountResponse( name, location, homeNumber, images );
		// age값 매핑이 빠진걸 확인 가능하다.
        return accountResponse;
    }
    ...
}

그럼 생성자를 여러개 생성한다면 어떻게 될까?

package com.wedul.mapstructtest.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
public class AccountResponse {

    private String name;
    private int age;
    private String location;
    private String homeNumber;
    private List<AccountResponseImage> images;

    public AccountResponse(String name, String location, String homeNumber, List<AccountResponseImage> images) {
        this.name = name;
        this.location = location;
        this.homeNumber = homeNumber;
        this.images = images;
    }

    public AccountResponse(String homeNumber, List<AccountResponseImage> images) {
        this.homeNumber = homeNumber;
        this.images = images;
    }
}

위 처럼 생성자를 여러개 생성할 경우에는 생성자가 많다는 에러가 발생한다. 

/Users/jeongcheol/Documents/test/mapstructtest/src/main/java/com/wedul/mapstructtest/mapper/AccountMapper.java:13: error: Ambiguous constructors found for creating com.wedul.mapstructtest.dto.AccountResponse. Either declare parameterless constructor or annotate the default constructor with an annotation named @Default.
    AccountResponse domainToEntity(AccountEntity accountEntity);
                    ^

 

 

3. builder

3번째 옵션으로 builder를 사용할 수 있다. 생성자 위에 builder를 선언하면 mapper generated 코드는 값을 builder로 생성한다.

package com.wedul.mapstructtest.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
public class AccountResponse {

    private String name;
    private int age;
    private String location;
    private String homeNumber;
    private List<AccountResponseImage> images;

    @Builder
    public AccountResponse(String name, int age, String location, String homeNumber, List<AccountResponseImage> images) {
        this.name = name;
        this.age = age;
        this.location = location;
        this.homeNumber = homeNumber;
        this.images = images;
    }
}


package com.wedul.mapstructtest.mapper;

import com.wedul.mapstructtest.dto.AccountResponse;
import com.wedul.mapstructtest.dto.AccountResponse.AccountResponseBuilder;
import com.wedul.mapstructtest.dto.AccountResponseImage;
import com.wedul.mapstructtest.dto.AccountResponseImage.AccountResponseImageBuilder;
import com.wedul.mapstructtest.entity.AccountEntity;
import com.wedul.mapstructtest.entity.AccountImage;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-03-27T22:12:29+0900",
    comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.3.3.jar, environment: Java 11.0.11 (AdoptOpenJDK)"
)
@Component
public class AccountMapperImpl implements AccountMapper {

    @Override
    public AccountResponse domainToEntity(AccountEntity accountEntity) {
        if ( accountEntity == null ) {
            return null;
        }

        AccountResponseBuilder accountResponse = AccountResponse.builder();

        accountResponse.location( accountEntity.getAddress() );
        accountResponse.homeNumber( accountEntity.getHomeTel() );
        accountResponse.name( accountEntity.getName() );
        accountResponse.age( accountEntity.getAge() );
        accountResponse.images( accountImageListToAccountResponseImageList( accountEntity.getImages() ) );

        return accountResponse.build();
    }
...
}

2번 생성자로 생성하는 것과 동일하게 생성자에 필드가 빠져있을경우 값 누락이 발생할 수 있다. 다만 생성자로 생성하는 경우 setter가 만들어져 있으면 setter로 값을 매핑해주지만 builder는 만들어 주지 않는다. 또한 생성자 여러개에 builder를 붙을 경우 아래와 같은 문제가 발생한다.

package com.wedul.mapstructtest.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
public class AccountResponse {

    private String name;
    private int age;
    private String location;
    private String homeNumber;
    private List<AccountResponseImage> images;

    @Builder
    public AccountResponse( String homeNumber, List<AccountResponseImage> images) {
        this.homeNumber = homeNumber;
        this.images = images;
    }

    @Builder
    public AccountResponse(String name, String location, String homeNumber, List<AccountResponseImage> images) {
        this.name = name;
        this.location = location;
        this.homeNumber = homeNumber;
        this.images = images;
    }


}

코드는 맨 아래 생성자를 보고 만들어졌으나 builder호출 시 첫번째 선언된 생성자를 바라보고 있어서 컴파일 에러가 발생한다. 물론 여러 생성자에 builder를 붙이지 않을것이지만 주의해서 사용은 필요하다.

 

builder 사용 시 gradle library 선언 순서에 따른 생성 이슈

처음 mapstruct를 사용할 때는 lombok 밑에 MapStruct 라이브러리를 붙여서 사용했어서 문제가 없었다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    implementation 'org.mapstruct:mapstruct:1.4.2.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}

 

하지만 새로운 프로젝트에 추가할 때 순서를 반대로 MapStruct를 선언하여 사용했었는데 이때 아래처럼 builder를 사용하여 값을 넣는 코드를 정상적으로 생성하지 못했다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation 'org.mapstruct:mapstruct:1.4.2.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

}
package com.wedul.mapstructtest.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
public class AccountResponse {

    private String name;
    private int age;
    private String location;
    private String homeNumber;
    private List<AccountResponseImage> images;

    @Builder
    public AccountResponse(String name, int age, String location, String homeNumber, List<AccountResponseImage> images) {
        this.name = name;
        this.age = age;
        this.location = location;
        this.homeNumber = homeNumber;
        this.images = images;
    }
}



// 생성된 코드
package com.wedul.mapstructtest.mapper;

import com.wedul.mapstructtest.dto.AccountResponseImage;
import com.wedul.mapstructtest.entity.AccountImage;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-03-27T22:21:41+0900",
    comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.3.3.jar, environment: Java 11.0.11 (AdoptOpenJDK)"
)
@Component
public class AccountImageMapperImpl implements AccountImageMapper {

    @Override
    public AccountResponseImage domainToEntity(AccountImage accountEntity) {
        if ( accountEntity == null ) {
            return null;
        }

        long id = 0L;
        String path = null;
        String imageName = null;

        AccountResponseImage accountResponseImage = new AccountResponseImage( id, path, imageName );

        return accountResponseImage;
    }
}

위에 코드를 보면 builder를 생성자위에 붙였음에도 라이브러리 선언 순서가 바꼈다는 이유로 builder를 사용하지 못했고 값도 모두 가져와서 매핑하지 못하고 있다.

 

정확한 이유는 모르겠지만 annotion processor에서 코드를 generated하는 과정에서 순서 이슈가 있는 것 같다. 자세한건 아래 링크를 참고하면 좋을 것 같고 반드시 lombok -> mapstruct순서대로 사용하기를 권장한다.

 

https://github.com/mapstruct/mapstruct/issues/1997

https://github.com/mapstruct/mapstruct/issues/1581

https://github.com/projectlombok/lombok/issues/1538

반응형