MapStruct 적용 해보자잇

Kevin·2023년 6월 1일
1

Spring

목록 보기
5/11
post-thumbnail

나는 기존 글에서 다뤘던 DTO에게 변환의 책임을 주는게 맞는지에 대한 고민의 답으로 변환만을 책임지는 객체를 두기로 했었다.

참고 링크 : DTO? 내 생각에는!

그리고 변환을 책임지는 객체를 MapStruct라는 라이브러리를 통해 생성하고자 하였고, 해당 라이브러리의 간단한 특징들과 사용법, 사용시에 주의할 점에 대해서 이야기 해보고자 한다.

MapStruct 특징

  • 다른 매핑 라이브러리보다 속도가 빠르다.
  • 컴파일 시점에 코드를 생성하기에, 런타임시 안정성 보장
  • 사용이 쉽다.

MapStruct 사용법

  1. 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'
        
    }
    ```

  2. 매핑을 위한 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);

Lombok 라이브러리가 MapStruct 라이브러리 보다 먼저 의존성 추가가 되어 있어야 하는 이유


그 이유를 아래의 코드 예시를 통해 알아보자.

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는 buildersetter를 이용해서 생성된 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();
        }
    }
    • Dto에 선언한 Builder 어노테이션과 getter 어노테이션을 통해서 객체간 바인딩을 하는 코드를 볼 수 있다.

아래의 코드는 @Setter를 선언해준 코드이다.

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class MentoResponseDto {
    private Long id;
    private String username;
    private String password;

}
  • Setter 어노테이션을 적용하였을 때 생기는 Mapper 구현체
    @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;
        
    • Entity에 선언한 Setter 어노테이션과 Getter 어노테이션을 통해서 객체간 바인딩을 하는 코드를 볼 수 있다.


정리하자면 질문에 대한 답은 다음과 같다.

MapStruct는 Lombok의 getter, setter, builder를 이용해서 객체를 생성하기 때문이다.


아니 그러면 직접 getter, setter 메서드를 만드는 방법은 안된답니까?

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
profile
Hello, World! \n

0개의 댓글