저번에 작성했던 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
'web > Spring' 카테고리의 다른 글
lettuce pipeline코드 사용시 커넥션 풀 사용 필요 (0) | 2024.06.08 |
---|---|
spring batch에서 파라미터 시 - 사용 주의 (0) | 2024.05.07 |
Mapstruct 사용 시 collection 내부에 이름이 다른경우 (0) | 2023.03.25 |
dynamoDbEnhancedClient range query condition 사용 시 The provided starting key does not match the range key predicate 에러 발생 (0) | 2022.11.05 |
dynamoDbEnhancedClient에서 QueryConditional에서 sort key range 조회하기 (0) | 2022.11.04 |