2 분 소요

이전에 객채 생성을 위해 Builder 패턴을 적용해봤다. 근데 프로젝트를 하다보면 엔티티 클래스 하나만 계속 참조하지 않고 필요에
따라 DTO를 정의해서 필요한 내용들만 담을 수 있도록 활용하며 때로는 DTO에서 DTO로 데이터를 넘겨줘야 하는 경우도 생긴다.
이 때 편리하게 객체를 생성할 수 있도록 도와주는게 MapStruct다!

MapStruct 적용하기

설정

build.gradle에서 다음 의존성을 추가해주자.

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

Mapper 클래스 생성

RequestLoginDto 정보를 기반으로 LoginDto를 생성할 수 있도록 해보자.
각 클래스 정보는 다음과 같다.

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RequestLoginDto {
    private String email;
    private String password;
}

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LoginDto {
    private String email;
    private String password;
}
  • 변환 전 객체(RequestLoginDto)는 값을 가져올 수 있도록 @Getter를 설정해줘야 함
  • 변환 후 객체(LoginDto)는 객체를 생성할 수 있도록 다음 중 1개라도 정의가 돼있어야 함
    • @Builder
    • @AllArgsConstructor
    • @Setter
  • @NoArgsConstructor@AllArgsConstructor를 같이 사용할 땐 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 사용해야 함

Mapper 클래스를 UserMapper라 하면, 다음처럼 작성해주면 된다.

@Mapper
public interface UserMapper {
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    LoginDto toLoginDto(RequestLoginDto requestLoginDto);
}
  • 함수명(toLoginDto)은 상관없음

RequestLoginDtoLoginDto는 서로 갖고있는 필드가 완전히 같아 위처럼 쉽게 설정해줄 수 있다.
빌드를 하면 build/classes/에서 UserMapper에 적어준 내용 기반으로 어떻게 매핑하는지 구현체 UserMapperImpl
생성된다. 아래는 UserMapperImpl 내용이다.

public class UserMapperImpl implements UserMapper {
  public UserMapperImpl() {
  }

  public LoginDto toLoginDto(RequestLoginDto requestLoginDto) {
    if (requestLoginDto == null) {
        return null;
    } else {
        LoginDto.LoginDtoBuilder loginDto = LoginDto.builder();
        loginDto.email(requestLoginDto.getEmail());
        loginDto.password(requestLoginDto.getPassword());
        return loginDto.build();
    }
  }
}
  • 내부적으로 Builder 패턴을 통해 객체를 생성해주고 있음

그런데 변환하려는 클래스의 필드에 값을 저장해야 하는데 참조할 필드가 없거나, 필드명이 다르거나, 참조하지 않는 등 여러 제약 조건
을 걸어야 할 때도 있다. 이 경우 @Mapping을 사용할 수 있으며 여러 @Mapping을 사용하는 경우 @Mappings로 감싸서
한번에 적용한다.

아래는 필드명이 다를 때 어떻게 설정하는지 그리고 어떻게 변환되는지에 대한 내용이다.

//변환할 클래스 정보
public class ResponseImage {
  private Long id;
  private String url;
}

//Mapper 설정
@Mapper
public interface ImageMapper {
  ImageMapper INSTANCE = Mappers.getMapper(ImageMapper.class);

  @Mapping(source = "imageUrl", target = "url") //imageUrl -> ResponseImage.url
  ResponseImage toResponseImage(String imageUrl);
}

//빌드 후 결과
public class ImageMapperImpl implements ImageMapper {
  public ImageMapperImpl() {
  }

  public ResponseImage toResponseImage(String imageUrl) {
    if (imageUrl == null) {
        return null;
    } else {
        ResponseImage.ResponseImageBuilder responseImage1 = ResponseImage.builder();
        responseImage1.url(imageUrl);
        return responseImage1.build();
    }
  }
}

만약 참조하고 싶지 않다면 ignore = true를 설정하면 되고, source에 빈 값이 들어오는 경우에는 설정해준 기본값을 입력
해줄 수 있다. 이 때는 defaultValue 또는 defaultExpression을 사용한다.

public class RequestImage {
  private Long id;
  private String url;
}

@Mapper
public interface ImageMapper {
  ImageMapper INSTANCE = Mappers.getMapper(ImageMapper.class);
  @Mappings({
    @Mapping(source = "requestImage.id", target = "id", ignore = true), //requestImage.id 매핑 안함
    @Mapping(source = "replacedUrl", target = "url", defaultValue = "defaultUrl") //기본값 replacedUrl
  })
  ResponseImage toResponseImage(RequestImage requestImage, String replacedUrl);
}

활용

바로 위에 toResponseImage()를 사용해서 ResponseImage를 만들고 싶다면 다음처럼 사용하면 된다.

RequestImage request = RequestImage.builder()
                                   .id(1L)
                                   .url("request")
                                   .build();
String replacedUrl = "replace";

//toResponseImage()로 ResponseImage 생성
ResponseImage response = ImageMapper.INSTANCE.toResponseImage(request, replacedUrl);

References

카테고리:

업데이트:

댓글남기기