보라코딩
MapStruct 본문
MapStruct란?
- Java bean 유형 간의 매핑 구현을 단순화하는 코드 생성기
- 매핑 코드를 자동으로 생성해주어 반복적인 매핑 작업 효율적으로 처리
- DTO와 Entity간의 매핑을 편리하게 도와주는 도구
MapStruct 사용 방법
* 의존성 추가
- ⭐ 주의 : Lombok 뒤에 MapStruct dependency 선언 필요
- MapStruct는 Lombok의 getter, setter, builder를 이용해서 생성되기 때문
dependencies {
...
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
...
}
1. 기본 사용 방법
- Car ➡️ CarDto Mapping 하기
⭐ Car
- @Data 사용하는 이유
- 매핑 시, 필드에 접근하기 위해 Getter, Setter, Constructor 필요
@Data
@AllArgsConstructor
public class Car {
private String name;
private int numberOfSeats; // 변수명 다름
private String type;
}
⭐ CarDto
@Data
public class CarDto {
private String name;
private int seatCount; // 변수명 다름
private String type;
}
🎉 CarMapper
- Car과 CarDto의 변수명이 다른 경우 매핑
- Mapper interface에 @Mapper 어노테이션 붙이면 자동으로 Mapper 구현체 생성
- instance 선언 시 매퍼에 접근 가능
- @Mapping 을 통해 다른 변수명 매핑
@Mapper // MapStruct가 자동으로 CarMapper 구현체 생성
public interface CarMapper {
// 매퍼 클래스에서 CarMapper 찾을 수 있게 하는 방법
// 매퍼 interface에서 위와 같이 Instance를 선언해주면 매퍼에 대한 접근이 가능
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
// Car -> CarDto 매핑
@Mapping(source = "numberOfSeats", target = "seatCount") // 다른 변수명 매핑
CarDto toCarDto(Car car);
}
❗ Mapper interface 빌드하면 자동으로 구현체 만들어줌
build/classes/java/main/에 구현체 생성
public class CarMapperImpl implements CarMapper {
public CarMapperImpl() {
}
public CarDto toCarDto(Car car) {
if (car == null) {
return null;
} else {
CarDto carDto = new CarDto();
carDto.setSeatCount(car.getNumberOfSeats());
carDto.setName(car.getName());
carDto.setType(car.getType());
return carDto;
}
}
}
❓ 테스트 코드로 확인
public class CarMapperTest {
@Test
void mapstruct로_car을_carDto로_mapping_할_수_있다() {
//given
Car car = new Car("Morris", 5, "SEDAN");
//when
CarDto carDto = CarMapper.INSTANCE.toCarDto(car);
//then
assertThat( carDto ).isNotNull();
assertThat( carDto.getName() ).isEqualTo( "Morris" );
assertThat( carDto.getSeatCount() ).isEqualTo( 5 );
assertThat( carDto.getType() ).isEqualTo( "SEDAN" );
}
}
2. 여러 객체를 하나의 객체에 매핑
- One, Two ➡️ Three Mapping 하기
⭐ One
@Data
@AllArgsConstructor
public class One {
private boolean a;
private int b;
private String c;
private String d;
}
⭐ Two
@Data
@AllArgsConstructor
public class Two {
private List<String> e;
private String f;
}
⭐ Three
- One에 있는 변수 c가 없음
- Two에 있는 변수명이 다름
@Data
public class Three {
private boolean a;
private int b;
// c 없음
private String d;
private List<String> eee; // 변수 다름
private String fff; // 변수 다름
}
🎉 FourMapper
- @Mapping을 사용하여 변수명 매핑
- One, Two에 있는 변수가 Three에 없는 것은 문제 없음
// 여러_객체를_하나의_객체에_매핑할_수_있다
@Mapper
public interface FourMapper {
FourMapper Instance = Mappers.getMapper(FourMapper.class);
// One, Two -> Three 매핑
@Mapping(source = "two.e", target = "eee")
@Mapping(source = "two.f", target = "fff")
Three toThree(One one, Two two);
}
❓ 테스트 코드로 확인
public class FourMapperTest {
@Test
void 여러_객체를_하나의_객체에_매핑할_수_있다() {
// given
One one = new One(true, 2, "c", "d");
Two two = new Two(new ArrayList<>(), "f");
// when
Three three = FourMapper.Instance.toThree(one, two);
// then
assertThat( three ).isNotNull();
assertThat( three.isA() ).isEqualTo(true);
assertThat( three.getB() ).isEqualTo(2);
assertThat( three.getD() ).isEqualTo("d");
assertThat( three.getEee() ).isEqualTo(new ArrayList<>());
assertThat( three.getFff() ).isEqualTo("f");
}
}
3. 여러 인자 값을 매핑
- Five ➡️ Six Mapping 하기
⭐ Five
@Data
@AllArgsConstructor
public class Five {
private boolean a;
private int b;
private String c;
private String d;
private Date creationDate;
}
⭐ Six
- Five에 있는 c 가 없음
- Six에 e, f 가 추가됨
@Data
@AllArgsConstructor
public class Six {
private boolean a;
private int b;
// c 없음
private String d;
private String e; // 추가
private Long f; // 추가
private Date creationDate;
}
🎉 SevenMapper
- Six에만 있는 e, f 에 대한 파라미터 추가 가능
- 필수는 아니며 생략 시 null 적용됨
// 여러가지_다른_인자_값도_매핑할_수_있다
@Mapper
public interface SevenMapper {
SevenMapper INSTANCE = Mappers.getMapper(SevenMapper.class);
// Five -> Six 매핑
Six toSix(Long f, Five five); // Six에만 있는 e 매핑 안함
}
❓ 테스트 코드로 확인
- 매핑하지 않은 e는 null
public class SevenMapperTest {
@Test
void 여러가지_다른_인자_값도_매핑할_수_있다(){
// given
Five five = new Five(true, 2, "c", "d", new Date());
// when
Six six = SevenMapper.INSTANCE.toSix(6L, five);
// then
assertThat(six).isNotNull();
assertThat(six.isA()).isTrue();
assertThat(six.getB()).isEqualTo(2);
assertThat(six.getD()).isEqualTo("d");
assertThat(six.getE()).isNull(); // mapping 안한 e는 null
assertThat(six.getF()).isEqualTo(6L);
}
}
4. 추가 매핑 방법 (Custom)
- default 값 지정
- 객체에 빈 값이 들어오는 경우 또는 특정 값을 지정해야 하는 경우
- defaultValue(단순한 기본값), defaultExpression(동적값) 사용하여 값 지정 가능
- 특정 필드 매핑 무시
- ignore 사용해서 특정 필드 제외 가능
// 매핑시_default값_지정_가능하며_매핑_무시할_수_있다
@Mapper
public interface EightMapper {
EightMapper INSTANCE = Mappers.getMapper(EightMapper.class);
// Five -> Six 매핑
@Mapping(source = "five.b", target = "b", ignore = true) // b는 둘다 있어
@Mapping(source = "five.d", target = "d", defaultValue = "default_d") // d는 둘다 있어
@Mapping(source = "five.creationDate", target = "creationDate", defaultExpression = "java(new Date())")
@Mapping(source = "e", target = "e", defaultValue = "default_e") // e는 six만 있어
Six toSix(String e, Long f, Five five);
}
❓ 테스트 코드로 확인
- 변수 b : ignore로 무시했기 때문에 int면 0, String이면 null
- 변수 d, e : 값이 없을 경우에 defaultValue 적용됨
- 변수 createDate : 동적값 적용됨
@Test
void ignore_사용하여_매핑_무시할_수_있다(){
// given
Five five = new Five(true, 222, "c", "d값_지정", new Date());
// when
Six six = EightMapper.INSTANCE.toSix("e값_지정", 6L, five);
// then
assertThat(six).isNotNull();
assertThat(six.isA()).isTrue();
assertThat(six.getB()).isEqualTo(0); // b는 ignore로 제외해서 int면 0, String이면 null
assertThat(six.getD()).isEqualTo("d값_지정");
assertThat(six.getE()).isEqualTo("e값_지정");
assertThat(six.getF()).isEqualTo(6L);
//assertThat(six.getCreationDate()).isEqualTo(new Date());
}
@Test
void 매핑시_null인_경우_default_지정된_값을_사용할_수_있다(){
// given
Five five = new Five(true, 2, "c", null, new Date());
// when
Six six = EightMapper.INSTANCE.toSix(null, 6L, five);
// then
assertThat(six).isNotNull();
assertThat(six.isA()).isTrue();
assertThat(six.getB()).isEqualTo(0);
assertThat(six.getD()).isEqualTo("default_d"); // null이면 defaultValue
assertThat(six.getE()).isEqualTo("default_e"); // null이면 defaultValue
assertThat(six.getF()).isEqualTo(6L);
//assertThat(six.getCreationDate()).isEqualTo(new Date());
}
5. 별도 메서드를 통해 매핑
- Nine ➡️ Ten Mapping 하기
⭐ Nine
- animal type 다름 (String)
@Data
@AllArgsConstructor
public class Nine {
private String name;
private int age;
private String animal; // type 다름
}
⭐ Ten
- animal type 다름 (Animal)
@Data
@AllArgsConstructor
public class Ten {
private String name;
private int age;
private Animal animal; // type 다름
}
⭐ Animal
public enum Animal {
DOG,
CAT,
PIG
}
🎉 방법1_EnumMapper
- qualifiedByName : 매핑할 때 이용할 메서드 지정
- @Named : 매핑에 이용될 메서드 명시
// 타입이_다른_경우_별도의_메서드를_이용해_매핑할_수_있다
@Mapper
public interface EnumMapper {
EnumMapper INSTANCE = Mappers.getMapper(EnumMapper.class);
// Nine -> Ten 매핑
// qualifiedByName : 매핑 시 이용할 메서드 지정
@Mapping(source = "nine.animal", target = "animal", qualifiedByName = "stringToEnum")
Ten toTen(Nine nine);
// 매핑에 이용될 메서드 명시
@Named("stringToEnum")
static Animal stringToEnum(String animal) {
return switch (animal.toUpperCase()) {
case "CAT" -> Animal.CAT;
case "DOG" -> Animal.DOG;
default -> Animal.PIG;
};
}
}
🎉 방법2_EnumMapper2
- default 사용하여 동일하게 사용 가능
- 변수명은 달라도 되며 타입은 같아야 매핑됨
// 타입이_다른_경우_별도의_메서드를_이용해_매핑할_수_있다
@Mapper
public interface EnumMapper2 {
EnumMapper2 INSTANCE = Mappers.getMapper(EnumMapper2.class);
// Nine -> Ten 매핑
Ten toTen(Nine nine);
default Animal stringToEnum(String animal) {
return switch (animal.toUpperCase()) {
case "CAT" -> Animal.CAT;
case "DOG" -> Animal.DOG;
default -> Animal.PIG;
};
}
}
❓ 테스트 코드로 확인
- 방법1_qualifiedByName 사용한 메서드
@Test
void 타입이_다른_경우_별도의_qualifiedByName로_메서드를_이용해_매핑할_수_있다(){
// given
Nine nine = new Nine("name", 123, "dog");
// when
Ten ten = EnumMapper.INSTANCE.toTen(nine);
// then
assertThat(ten).isNotNull();
assertThat(ten.getName()).isEqualTo("name");
assertThat(ten.getAge()).isEqualTo(123);
assertThat(ten.getAnimal()).isEqualTo(Animal.DOG); // string -> Animal Enum 타입
}
- 방법2_default 사용한 메서드
@Test
void 타입이_다른_경우_별도의_default_메서드를_이용해_매핑할_수_있다(){
// given
Nine nine = new Nine("name", 123, "dog");
// when
Ten ten = EnumMapper2.INSTANCE.toTen(nine);
// then
assertThat(ten).isNotNull();
assertThat(ten.getName()).isEqualTo("name");
assertThat(ten.getAge()).isEqualTo(123);
assertThat(ten.getAnimal()).isEqualTo(Animal.DOG); // string -> Animal Enum 타입
}
6. 사용자 정의 매퍼 매서드
- 매핑이 까다로운 경우, 직접 매핑 메서드를 구현 가능
- default 를 붙여 메서드를 만들어주면 구현 메서드 대신 default로 정의한 메서드 사용 가능
default MessageListServiceDto toMessageListServiceDto(String messageId, Integer count, RequestDto requestDto) {
String messageType = Optional.ofNullable(requestDto.getType()).orElse("sms").toUpperCase();
Type msgType = Type.SMS;
if (messageType.equals("LMS")) {
msgType = Type.LMS;
} else if (messageType.equals("MMS")){
msgType = Type.MMS;
}
return MessageListServiceDto.builder()
.messageId(Optional.ofNullable(messageId).orElse(UUID.randomUUID().toString()))
.count(Optional.ofNullable(count).orElse(0))
.title(requestDto.getTitle())
.content(requestDto.getContent())
.sender(requestDto.getSender())
.receiver(Optional.ofNullable(requestDto.getReceiver()).orElse(Collections.EMPTY_LIST))
.requestTime(LocalDateTime.now())
.type(msgType)
.build();
}
7.MapStruct Processor 옵션 및 매핑 정책 등
- ComponentModel : 매퍼를 빈으로 만들어야 하는 경우
@Mapper(componentModel = "spring")
public interface MessageMapper {
...
}
- unmmappedTargetPolicy : Target 필드는 존재하는데 source 필드가 없는 경우에 대한 정책
- ERROR : 매핑 대상이 없는 경우, 매핑 코드 생성 시 error 가 발생
- WARN : 매핑 대상이 없는 경우, 빌드 시 warn 이 발생
- IGNORE : 매핑 대상이 없는 경우 무시하고 매핑
@Mapper(unmmapedTargetPolicy = ReportingPolicy.{ERROR,WARN,IGNORE})
public interface MessageMapper {
MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);
MessageBodyDto toMessageBodyDto(RequestDto requestDto);
}
- nullValueMappingStrategy : source가 null인 경우 제어할 수 있는 정책
- RETURN_NULL : source가 null 일 경우, target을 null 로 설정
- RETURN_DEFAULT : source가 null 일 경우, default 값으로 설정
@Mapper(
nullValueMapMappingStrategy = NullValueMappingStrategy.{RETURN_NULL,RETURN_DEFAULT},
nullValueIterableMappingStrategy = NullValueMappingStrategy.{RETURN_NULL,RETURN_DEFAULT}
)
public interface MessageMapper {
MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);
MessageBodyDto toMessageBodyDto(RequestDto requestDto);
}
8. INSTANCE 없이 Mapper 사용하기
➡️ 두 가지 방법 중 편리한 것 선택하여 사용하자
- ⭐ 첫번째, entity 또는 dto 클래스에 추가로 메서드 구현하기
@Getter
@Builder
public class WatcherFileInfo {
private String type;
private String filePath;
public FileKeyInfoEntity toEntity() {
return FileMapper.INSTANCE.toEntity(this);
}
}
@Mapper
public interface FileMapper {
FileMapper INSTANCE = Mappers.getMapper(FileMapper.class);
@Mapping(target = "key", ignore = true)
FileKeyInfoEntity toEntity(WatcherFileInfo watcherFileInfo);
}
🎉 사용 방법
FileKeyInfoEntity entity = watcherFileInfo.toEntity();
- ⭐ 두번째, Mapper를 빈으로 만들기
- @Mapper(componentModel = "spring") 이 부분이 필요!
@Mapper(componentModel = "spring")
public interface FileMapper {
@Mapping(target = "key", ignore = true)
FileKeyInfoEntity toEntity(WatcherFileInfo watcherFileInfo);
}
🎉 사용 방법
fileMapper.toEntity(watcherFileInfo); 이렇게 사용 가능!
@Autowired
private final FileMapper fileMapper;
@Override
public void sendFileInfoToParser(WatcherFileInfo watcherFileInfo) {
...
FileKeyInfoEntity entity = fileMapper.toEntity(watcherFileInfo);
...
}
9. 객체 안에 객체도 매핑 가능
- 어떤 객체끼리 매칭되는지 기입 필요
- 객체가 다른 경우 default로 메서드 작성 필요
@Mapping(source = "cat.table", target = "notTable")
Dog toDog(Cat cat);
default NotTable transfer(Table table123){
return new NotTable(table123.getId(), table123.getTitle());
}
마무리
- MapStruct 사용 시, 구현 메서드를 자동으로 만들기에 편리함
- 단, 매핑이 원하는대로 동작하는지 확인하기 위해 테스트 코드 작성 필요
- 반복 작업 줄어들며 에러 발생 확률 감소
- 간단한 매핑 작업은 MapStruct 사용하고, 복잡한 매핑 작업은 직접 코드를 작성
참고
'코딩 > Spring' 카테고리의 다른 글
| JPA N+1 문제 (0) | 2024.06.24 |
|---|---|
| AOP, 프록시, Transactional (0) | 2024.06.20 |
| Spring Security Config 버전별 정보 (0) | 2024.03.03 |
| 스프링부트 (JPA) 정리하기 좋은 강의 (0) | 2024.03.03 |
| 자바 테스트코드 강의 추천! (0) | 2024.02.14 |