최근에 ? DTO ➡️ A Entity 로 변환해야 하는 로직을 개발해야 했다.
단순하게 하나의 DTO에서 하나의 Entity로 변환하는 것이었다면 쉬웠겠지만, 다양한 형태의 DTO를 하나의 Entity로 변환해야 했다.
이때 고려해본 것이 ObjectMapper와 MapStruct였는데, 고민 했던 내용을 작성해보려고 한다.
? DTO ➡️ A Entity
? DTO의 depth는 1이상일 수 있다
? DTO의 속성명은 A Entity와 다를 수 있다
? DTO를 가공시켜야 A Entity로 변환이 가능할 수도 있다
일단 기본적으로 depth가 1이며, 속성명이 같은 DTO만 변환해보려고 한다
Jackson 라이브러리의 ObjectMapper 클래스 알아보기
➡️ https://www.baeldung.com/jackson-object-mapper-tutorial
ObjectMapper는 Json ↔️ Java 객체 간 변환할 때 사용한다.
ObjectMapper의 convertValue는 자바 객체 간 변환에 사용된다.
public class AEntityConverter {
/**
* DTO를 Entity로 변환
*/
public static <T> AEntity convertToAEntity(T dto) {
ObjectMapper objectMapper = new ObjectMapper();
// dto -> JSON -> AEntity
return objectMapper.convertValue(dto, AEntity.class);
}
}
변환할 결과는 AEntity로 정해져 있으므로 AEntity.class를 넣어주고, 변환의 대상은 어떤 DTO 객체인지 알 수 없으므로 Generic type으로 넣어준다.
동아리, 인턴, 등등 프로젝트에서 MapStruct를 사용해 DTO to Entity, Entity to DTO 등의 변환을 해왔다.
그래서 좀 더 익숙하고 도입에 긍정적이었던 것 같다.
MapStruct를 처음 사용하려고 했을 때 많이 읽었던 포스트는 NHN 블로그의 글이었다.
최근엔 네이버 클라우드 플랫폼 글을 읽어보았다.
MapStruct를 사용할 경우, Mapper interface를 만들어 사용한다.
@Mapper
public interface AEntityMapper {
AEntity toAEntity(BDTO bDto);
}
이렇게 interface에 메서드를 정의해주면, 빌드 시점에 구현체 코드가 생성된다.
BDTO의 형태가 아래와 같다고 해보자
public class BDTO {
private String id;
private CDTO beforeDTO;
private CDTO afterDTO;
}
public class CDTO {
private String name;
private String email;
}
name, email 정보 변경을 beforeDTO, afterDTO로 나타낸다고 생각해보자.
이때 최신 데이터만 AEntity로 변환하고자 한다. (id + afterDTO)
ObjectMapper의 convertValue를 사용하여 Map으로 변환한다.
public class AEntityConverter {
/**
* DTO를 Entity로 변환
*/
public static <T> AEntity convertToAEntity(T dto) {
ObjectMapper objectMapper = new ObjectMapper();
// dto -> JSON -> Map
Map<String, Object> map = objectMapper.convertValue(dto, Map.class);
// return MapToEntity; ??
}
}
Map으로 변경하면 아래와 같이 변경될 것이다.
map = {
"id": "id값",
"beforeDTO": {"name": "이름1", "email": "email1"},
"afterDTO": {"name": "이름2", "email": "email2"}
}
이것을 AEntity로 변환한다면?
// MapToEntity
// 타입 변환코드는 생략
AEntity.builder
.id(map.id)
.name(map.afterDTO.name)
.email(map.afterDTO.email)
.build();
이렇게 될 경우 문제는, map에서 일일히 name과 email의 위치를 알려줘야 한다는 것이다. 이러면 convert method에서 generic을 쓰는 이점이 사라진다.
@Mapper
public interface AEntityMapper {
@Mapping(source = "afterDTO.name", target = "name")
@Mapping(source = "afterDTO.email", target = "email")
AEntity toAEntity(BDTO bdto);
}
좀 더 간결하고 코드 짜는 시간이 줄어든다. 물론 MapStruct도 일일히 source를 적어주어야 한다.
일단 depth 1에 대해서는 objectMapper + generic으로 한번에 처리하도록 하고, depth가 2이상이거나 전처리가 필요한 DTO는 일일히 mapping 방법을 정해주도록 했다.
더 좋은 방법이 있으면 댓글로 알려주세요 😊