Mapstruct로 데이터 변환 적용하기
Mapstruct란?
Spring Framework에서 개발할 때, 비즈니스 로직, 객체와 객체 간의 Mapping 등을 할 때 get, set을 일일이 넣어주는 것은 실수가 쉽고 생산성이 떨어진다. 특히나 Setter를 사용하는 것은 좋지 않다.
Setter를 사용하면 안되는 이유는 Setter함수 호출을 통해 Entity의 값을 변경할 수 있다면, DB 데이터값 변경에 대한 히스토리를 코드를 보고 파악하기 어려워진다. 즉, Entity의 값을 변경한 의도를 파악하기 어렵다.
get, set을 일일이 넣어주는 것을 대신 해주는 것이 Object Mapping 라이브러리이다.
Object Mapping에는 여러 종류가 있는데,Mapstruct, ModelMapper, jmapper, orika 등이 있다.
주로 쓰이는건 MapStruct와 ModelMapper인데 MapStruct가 ModelMapper와 비교했을 때 컴파일시 미리 생성된 구현체를 통해 Mapping하기 때문에 속도적인 측면에서 이점이 있어 MapStruct를 선택하는 것이 좋다.
그 중 가장 많이 사용하고 있는 MapStruct를 알아보았다.
MapStruct란
자바에서 객체 간 매핑에 대한 코드를 자동으로 생성해주는 매핑 라이브러리다.
즉, dto와 Entity의 변환을 쉽게 도와주는 라이브러리이다.
MapStruct의 장점은
- 컴파일시 오류 확인이 가능하다.
- reflection(리플렉션)을 사용하지 않아 매핑 속도가 빠르다. (참고로 리플렉션은 구체적인 클래스 타입을 알지 못해도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API이다.)
- 디버깅이 쉽다.
- 생성된 매핑 코드를 눈으로 확인할 수 있다.
Reflection이란?
구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API
런타임에 지금 실행되고 있는 클래스를 가져와서 실행해야하는 경우
동적으로 객체를 생성하고 메서드를 호출하는 방법
자바의 리플렉션은 클래스, 인터페이스, 메소드들을 찾을 수 있고, 객체를 생성하거나 변수를 변경하거나 메소드를 호출할 수 있다.
어떤 경우에 사용되나?
코드를 작성할 시점에는 어떤 타입의 클래스를 사용할지 모르지만, 런타임 시점에 지금 실행되고 있는 클래스를 가져와서 실행해야 하는 경우
프레임워크나 IDE에서 이런 동적인 바인딩을 이용한 기능을 제공한다. intelliJ의 자동완성 기능, 스프링의 어노테이션이 리플렉션을 이용한 기능이다.
Mapstruct 적용하기
기존에
dto → Entity
Entity → dto
변환 작업은 아래와 같이 진행했다.
기존에 dto → entity는 아래처럼 entity의 생성자 생성하고 save했다.
//List<Category> categories = categoryRepository.findAllByPostId(postId);
//if(categoryRequestDto.getCategory() != null && categories.size()<2 ){
Category category = new Category(categoryRequestDto.getCategory(), post);
categoryRepository.save(category);
//} else {
// log.info("~~~ category is null");
//}
기존에 entity → dto는 아래처럼 dto의 생성자를 선언해서 return 했다.
List<PostLikes> postLikes = postLikesRepository.findAllByPostId(postId);
List<PostLikeClickersResponseDto> postLikeClickersResponseDtoList = new ArrayList<>();
for (PostLikes postLikesTemp : postLikes) {
postLikeClickersResponseDtoList.add(new PostLikeClickersResponseDto(postLikesTemp));
}
// ...
return new PostDetailResponseDto(post, postLikeClickersResponseDtoList, bookmarkClickUserKeyResDtoList,
paragraphResDtoList, commentResDtoList, categoryResDtoList, postLikesCnt, postUsername);
dto와 entity간 변환 방법을 MapStruct로 적용해보았다.
UserReqMapper
UserResMapper 를 나눠서 request와 response의 Mapper를 나누었고
둘 다 GenericMapper를 상속받았다.
UserReqMapper 인터페이스
@Mapper(componentModel = "spring")
public interface UserReqMapper extends GenericMapper<UserJoinReqDto, UserEntity> {
}
UserResMapper 인터페이스
@Mapper(componentModel = "spring")
public interface UserResMapper extends GenericMapper<UserJoinResDto, UserEntity> {
}
GenericMapper 인터페이스
public interface GenericMapper <D, E> {
// Dto, Entity
D toDto(E e);
E toEntity(D d);
List<D> toDtoList(List<E> entityList);
List<E> toEntityList(List<D> dtoList);
}
진행중 Mapstruct 사용시 에러가 발생했다.
다음과 같은 에러인데
D:\Bitnine\mockup\DashBoardA\src\main\java\com\example\dashboarda\model\entity\mapper\UserReqMapper.java:8: error: Ambiguous constructors found for creating com.example.dashboarda.dto.requestDto.UserJoinReqDto: UserJoinReqDto(com.example.dashboarda.dto.requestDto.UserJoinReqDto), UserJoinReqDto(java.lang.String, int, java.lang.String). Either declare parameterless constructor or annotate the default constructor with an annotation named @Default. Occured at 'D toDto(E e)' in 'GenericMapper'. public interface UserReqMapper extends GenericMapper<UserJoinReqDto, UserEntity> { ^
에러 내용중에 declare parameterless constructor 라는 것을 봐서
일단 UserJoinReqDto 에 매개변수 없는 생성자를 선언해주기 위해
@NoArgsConstructor 어노테이션 붙였다.
그 후 실행해보니 정상적으로 작동했다.
결론
중요한점은
• @Mapper : MapStruct Code Generator가 해당 인터페이스의 구현체를 생성해준다.
• componentModel = "spring" : spring에 맞게 bean으로 등록해준다
이 두가지 어노테이션을 사용하고 싶은 매퍼에 붙이면 bean으로 등록 되고 인터페이스의 구현체를 생성해 준다.
구현체를 보면 아래 처럼 MapperImpl 클래스가 생긴것을 볼수있다.
MapperImpl에서 결국 set을하는데
일반적으로 사용자가 setter를 사용하지 않는 이유는
Setter함수 호출을 통해 Entity의 값을 변경할 수 있다면, DB 데이터값 변경에 대한 히스토리를 코드를 보고 파악하기 어려워진다. 즉, Entity의 값을 변경한 의도를 파악하기 어렵다.
그러므로 Mapstruct로 사용해 변환할 때는 구현체가 직접 set을 사용했다.
UserReqMapperImpl에 보면 toEntity 메서드는 아래처럼 set을 사용한다.
@Override
public UserEntity toEntity(UserJoinReqDto d) {
if ( d == null ) {
return null;
}
UserEntity userEntity = new UserEntity();
userEntity.setUsername( d.getUsername() );
userEntity.setAge( d.getAge() );
userEntity.setPsword( d.getPsword() );
return userEntity;
}