나는 기존 글에서 다뤘던 DTO에게 변환의 책임을 주는게 맞는지에 대한 고민의 답으로 변환만을 책임지는 객체를 두기로 했었다.
참고 링크 : DTO? 내 생각에는!
그리고 변환을 책임지는 객체를 MapStruct라는 라이브러리를 통해 생성하고자 하였고, 해당 라이브러리의 간단한 특징들과 사용법, 사용시에 주의할 점에 대해서 이야기 해보고자 한다.
MapStruct 의존성 추가하기
1. Lombok 라이브러리가 MapStruct 라이브러리 보다 먼저 의존성 추가가 되어 있어야 한다. 그 이유는 MapStruct는 Lombok의 getter, setter, builder를 이용하여 생성되기 때문이다. → 이거는 아래에서 더 정확히 예시와 함께 알아보자.
```
dependencies {
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
}
```
매핑을 위한 Interface를 만들어준다.
@Mapper
public interface MentoMapper {
// 매퍼 클래스에서 MentoMapper를 찾을 수 있도록 하는 코드
MentoMapper INSTANCE = Mappers.getMapper(MentoMapper.class);
// MentoRequestDto -> Mento 매핑
Mento toEntity(MentoRequestDto mentoRequestDto);
}
Binding 될 객체 toBinding 될 객체(Binding 할 객체 변수명);
DTO를 Mento로 매핑할 때 Mento의 noteMenties와 id 필드들을 빼고 매핑하고 싶다면 아래와 같이 ignore
를 사용해서 제외할 수 있다.
// MentoRequestDto -> Mento 매핑
@Mapping(target="noteMenties", ignore=true)
@Mapping(target="id", ignore=true)
Mento toEntity(MentoRequestDto mentoRequestDto);
source
= Binding 할 객체의 변수, 변환할 필드target
= Binding 될 객체의 변수, 저장될 필드ignore
= 해당 값은 바인딩하지 않도록 하는 설정Note 엔티티에 2개의 DTO 값들을 매핑 시키고 싶다면 아래와 같이 코드를 작성해주면 된다.
// MentoRequestDto, MentiRequestDto -> Note 매핑
@Mapping(source="mentoRequestDto.noteMenties", target="noteMenties")
@Mapping(source="mentiRequestDto.noteMentoes", target="noteMentoes")
Note toEntity(MentoRequestDto mentoRequestDto, MentiRequestDto mentiRequestDto);
파라미터를 통해 Note의 필드들과 매핑시키고 싶다면 아래와 같이 코드를 작성 해주면 된다.
// Mento, Menti-> Note 매핑
Note toEntity(Mento mento, Menti menti);
바인딩 할 값을 메서드를 통해 변경한 후 보내고 싶다면 아래와 같이 코드를 작성하면 된다.
@Mapping(source="MentoRequestDto.type", target="type", qualifiedByName="typetoEnum")
Mento toEntity(MentoRequestDto mentoRequestDto)
@Named("typeToEnum")
static Type typeToEnum(String type) {
switch (type.toUpperCase()) {
case "activate":
return Type.ACTIVATE;
case "banned":
return Type.BANNED;
default:
return Type.ACTIVATE;
}
}
qualifiedByName
: 매핑할 때 이용할 메서드를 지정해준다.@Named
: 매핑에 이용될 메서드라는 것을 명시해준다.Service에서는 아래와 같이 호출해서 사용합니다.
// Entity -> DTO
MentoResponseDto mentoResponseDto = MentoMapper.INSTANCE.toDto(mento);
// DTO -> Entity
Mento mentoEntity = MentoMapper.INSTANCE.toEntity(mentoRequestDto);
그 이유를 아래의 코드 예시를 통해 알아보자.
DTO
@Getter
public class MentiResponseDto {
private Long id;
private String username;
private String password;
}
Mapper
@Mapper
public interface MentoMapper {
// 매퍼 클래스에서 MentoMapper를 찾을 수 있도록 하는 코드
MentoMapper INSTANCE = Mappers.getMapper(MentoMapper.class);
// MentoRequestDto -> Mento 매핑
Mento toEntity(MentoRequestDto mentoRequestDto);
// Mento -> MentoResponseDto 매핑
MentoResponseDto toDto(Mento mento);
}
Test Code
@Test
@Transactional
public void mapper_test1() throws Exception {
// Entity -> ResponseDto
// Mento mento = em.find(Mento.class, 1L);
Mento mento = repo.findById(1L).orElseThrow(Exception::new);
MentoResponseDto mentoResponseDto = MentoMapper.INSTANCE.toDto(mento);
System.out.println("testing ans : " + mentoResponseDto.getUsername());
System.out.println("testing ans : " + mentoResponseDto.getId());
}
}
Testing 결과는 아래와 같다.
testing ans : null
testing ans : null
타입을 출력해본 결과 객체의 Type은 성공적으로 MentoResponseDto로 변환은 되었다. 문제는 데이터가 제대로 바인딩 되지 않았다는 것이다.
생성된 Mapper 구현체 코드를 한번 봐보자.
아래는 Mapper 구현체 중 해당 toDto 메서드 코드 일부이다.
@Override
public MentoResponseDto toDto(Mento mento) {
if ( mento == null ) {
return null;
}
MentoResponseDto mentoResponseDto = new MentoResponseDto();
return mentoResponseDto;
}
위 코드를 보면 분명 객체를 생성하는 것은 있어도, Mento 엔티티의 필드의 값을 MentoResponseDto의 필드의 값에 매핑 시켜주는 코드가 없는 것을 볼 수 있다.
도대체 무슨 문제인걸까?
MapStruct는 builder
와 setter
를 이용해서 생성된 MentoResponseDto 객체의 값에 getter
를 통해서 가져온 Mento 엔티티의 값을 넣어준다.
위 DTO 코드를 다시 보면, @Getter 어노테이션만 선언되어있는 것을 볼 수 있다. 그렇기에 Mento 엔티티의 값을 정상적으로 넣어주지 못했던 것이다.
그러면 이제 @Builder
와 @Setter
어노테이션을 선언한 각각 2개의 케이스를 살펴보자.
아래의 코드는 @Builder
를 선언해준 코드이다.
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class MentoResponseDto {
private Long id;
private String username;
private String password;
}
@Builder
어노테이션을 적용하였을 때 생기는 Mapper 구현체 @Override
public MentoResponseDto toDto(Mento mento) {
if ( mento == null ) {
return null;
}
MentoResponseDto.MentoResponseDtoBuilder mentoResponseDto = MentoResponseDto.builder();
mentoResponseDto.id( mento.getId() );
mentoResponseDto.username( mento.getUsername() );
mentoResponseDto.password( mento.getPassword() );
return mentoResponseDto.build();
}
}
아래의 코드는 @Setter
를 선언해준 코드이다.
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class MentoResponseDto {
private Long id;
private String username;
private String password;
}
@Override
public MentoResponseDto toDto(Mento mento) {
if ( mento == null ) {
return null;
}
MentoResponseDto mentoResponseDto = new MentoResponseDto();
mentoResponseDto.setId( mento.getId() );
mentoResponseDto.setUsername( mento.getUsername() );
mentoResponseDto.setPassword( mento.getPassword() );
return mentoResponseDto;
MapStruct는 Lombok의 getter, setter, builder를 이용해서 객체를 생성하기 때문이다.
public class MentoResponseDto {
private Long id;
private String username;
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
당연히 된다.
testing ans : kevin
testing ans : 1