MapStruct를 통한 Entity<->DTO간 변환

Dawon Seo·2023년 2월 17일
1

라이브러리를 사용하여 Entity, Dto 간 변환을 진행하는 이유

  • 필드의 개수가 늘어나거나 엔티티간 연관관계가 늘어날수록 가독성이 떨어진다
  • 코드를 작성하는 과정에서 개발자가 실수하여 다른 데이터를 넣을 수 있다
  • 반복적인 작업으로 피로도가 증가한다
  • 필드가 추가, 수정, 삭제가 일어날 경우 변환 로직에 대한 수정이 필요하다.
  • 결국, 생산성을 떨어뜨린다.

ModelMapper와 MapStruct

  • modelMapper는 modelMapper.map(member, MemberDTO.class) 매핑이 일어날 때마다 리플렉션이 발생한다
  • MapStruct는 컴파일 시점에 어노테이션을 읽어 구현체를 만들기 대문에 리플렉션 발생하지 않는다.

MapStruct

손으로 매핑 코드를 작성하는 것은 매우 지루하고 오류가 발생하기 쉽습니다.
MapStruct는 이러한 매핑 코드를 생성해 주어 시간을 절약해 줍니다.

  • 주의 : 만들어지는 대상은 Getter가, 만드는 대상은 Setter가 필요합니다.
    예를 들어 Entity -> DTO 변환은 Entity에는 각 필드 값을 읽을 Getter가, DTO에는
    필드값을 넣을 수 있는 Setter가 존재해야 합니다.
    단, Target 객체에 @Builder 어노테이션이 존재한다면 Builder 메소드를 우선 사용합니다.

1. 설정

...
dependencies {
    ...
    implementation "org.mapstruct:mapstruct:${mapstructVersion}"
    annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"

    // If you are using mapstruct in test code
    testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}
...

2. 매퍼 정의

2.1 기본 매핑

@Mapper
public interface CarMapper {
	@Mapping(target = "manufacturer", source = "make")
    @Mapping(target = "seatCount", source = "numberOfSeats")
    CarDto carToCarDto(Car car);
    
    @Mapping(target = "fullName", source = "name")
    PersonDto psersonToPsersonDto(Person person);
}

인터페이스를 생성합니다.
코드 내의 source type은 target type으로 변형됩니다.
(위의 코드에서는 source는 Car, target은 CarDto)

  • 속성이 대상 엔티티 대응 항목과 이름이 같으면 암시적으로 매핑됩니다.
  • 대상 엔티티에서 속성 이름이 다른 경우 @Mapping 주석을 통해 이름을 지정할 수 있습니다.

MapStruct은 다음과 같은 코드를 생성합니다.

// GENERATED CODE
public class CarMapperImpl implements CarMapper {
	
    @Override
    public CarDto carToCarDto(Car car) {
    	if (car == null) {
        	return null;
        }
        
        CarDto carDto = new CarDto();
        
        if (car.getFeatures() != null) {
        	carDto.setFeatures(new ArrayList<String>(car.getFeatures()));
        }
        carDto.setManufacouturer(car.getMake());
        carDto.setSeatCount(car.getNumberOfSeats());
        carDto.setDriver(personToPersonDto(car.getDriver()));
        carDto.setPrice(personToPsersonDto(car.getDriver()));
        if (car.getCategory() != null) {
        	carDto.setCategory(car.getCategory().toStirng());
        }
        carDto.setEngine(engineToEngineDto(car.getEngine()));
        
        return carDto;
    }
    
    @Override
    public PersonDto personToPersonDto(Person person) {
    	//...
    }
    
    private EngineDto engineToEngineDto(Engine engine) {
    	if (engine == null) {
        	return null;
        }
        
        EngineDto engineDto = new EngineDto();
        
        engineDto.setHoursePower(engine.getHorsePower());
        engineDto.setFuel(engine.getFuel());
        
        return engineDto;
    }
}

MapSturct의 철학은 가능한 손으로 직접 작성한 것처럼 보이는 코드를 생성하는 것입니다. reflection과 같은 방법 대신 일반적인 getter/setter를 통해 코드를 생성합니다.

@Node("Tweet")
@Builder
@Getter
public class Tweet {
    @Id
    @GeneratedValue(generatorClass = UUIDStringGenerator.class)
    private String id;

    @Property("content")
    private String content;

    @Property("created_at")
    private LocalDateTime createdAt;

    @Property("type")
    private String type;

    @Property("image_url")
    private List<String> imageUrl;

    @Property("deleted")
    private Boolean deleted;

    @Property("popular_count")
    private Integer popularCount;

    @Relationship(type = "WRITE", direction = Relationship.Direction.INCOMING)
    private Write write;

    @Relationship(type = "REPLY", direction = Relationship.Direction.INCOMING)
    private Set<Reply> replyTweets;

    @Relationship(type = "LIKE", direction = Relationship.Direction.INCOMING)
    private Set<User> likeUsers;

    @Relationship(type = "RETWEET", direction = Relationship.Direction.INCOMING)
    private Set<Retweet> retweetUsers;

    @Relationship(type = "MENTION", direction = Relationship.Direction.OUTGOING)
    private Set<Mention> mentionedUsers;

    @Relationship(type = "QUOTE", direction = Relationship.Direction.INCOMING)
    private Set<Quote> QuoteTweets;
    
    // 기존 Tweet 클래스 내에 존재하던 toDto
    public TweetInfoDTO toDto() {
        return TweetInfoDTO.builder()
                .id(id)
                .content(content)
                .createAt(createdAt.format(DateTimeFormatter.ofPattern("YYYY-MM-dd'T'HH:mm:ss")))
                .imageUrl(imageUrl)
                .type(type)
                .deleted(deleted)
                .build();
    }
}

Entity 객체 내에 toDto클래스를 두어 사용하는 경우 Entity 클래스와 DTO 클래스간의 의존관계가 생겨 좋지 않음

@Mapping(componentModel = "spring")
위에서 componentModel 사용하지 않으면 스프링 빈으로 등록되지 않음

dateFormat 사용

Tweet의 LocalDateTime 데이터를
DTO에 String값으로 넘기고자 했음

처음 시도한 방법

@Mapping(target = "createAt", expression = "java(tweet.getCreatedAt().format(DateTimeFormatter.ofPattern(\"YYYY-MM-dd'T'HH:mm:ss\")))")
TweetInfoDTO tweetToTweetInfoDTO

빌드 시 다음과 같은 문제 발생

다음 방법으로 사용

@Mapping(source = "createdAt", target = "createdAt", dateFormat = "YYYY-mm-dd'T'HH:mm:ss")
TweetInfoDTO tweetToTweetInfoDTO(Tweet tweet);

0개의 댓글