지금까지 느꼈던 'Project'의 최적화에는 3단계가 있다.
- 코드의 최적화
- 코드의 줄 수를 줄이고, visibility가 높은 메서드로 바꾸고, Logic을 깔끔하게 짜는 것
- DB의 최적화
- DB에 왔다갔다 하는 비용을 줄이고, 가능한 한 번에 내가 원하는 Data만을 가지고 오는 것
- 변환의 최적화
- application level에서, DB에서 가지고 왔던 리소스들을 더욱 빠르게 변환하는 것
이번 프로젝트는 객체 간의 매핑으로 손수 mapping을 해 주었던 지난 프로젝트와는 다르게,
mapping framework를 이용해 매핑을 진행해보려고 한다.
Mapping framework란?
매핑 프레임워크는 한 형식이나 구조에서 다른 형식이나 구조로 데이터를 쉽게 변환하는 도구 또는 라이브러리입니다. 한 객체 유형에서 다른 객체 유형으로 데이터를 변환하는 프로세스를 자동화하며 종종 서로 다른 클래스 또는 데이터 구조 간의 매핑을 포함합니다. 매핑 프레임워크의 주요 목적은 데이터 변환 프로세스를 단순화하고 능률화하여 수동 코딩의 필요성과 잠재적인 오류 원인을 줄이는 것입니다.
결국, 어떤 엔티티에서 DTO로의 변환을 쉽게 해주는 프레임워크다.
널리 사용되는 mapping framework의 장, 단점을 알아보자.
- ModelMapper
- 장점 : 런타임에 리플렉션을 사용하여 자동으로 매핑되므로 설정이 간편하고 유연하다.
- 단점 : 리플렉션을 사용하기 때문에 상대적으로 성능이 느리고, 컴파일 시점에 오류를 알아채기 어렵다. (굉장히 큰 단점이라고 생각한다. JPQL이 아닌 Querydsl을 사용하는 이유에도 이런 이슈들이 있어서이다.)
- MapStruct
- 장점 : 컴파일 타임에 생성되는 mapping code로 인해 성능이 좋고, 디버깅이 쉽다. 또, 생성자 주입, Lombok과의 호환성 등 다양한 기능을 제공한다. (그러나 lombok에 의존하기 때문에 gradle / maven 설정에 lombok 하위에 dependency 추가를 해 주어야 한다.)
- 단점 : 설정이 복잡하고 (내가 이 글을 쓰는 이유 - 블로그마다 설명이 너무 다르다.) 컴파일타임에 코드 생성이 필요해서 빌드가 느려진다.
- Orika
- 장점 : 런타임에 Byte code를 생성하여 매핑되므로, 리플렉션보다 빠른 성능을 제공한다. 또한 Mapstruct보다 더 빠른 성능을 제공하고, "유연"하다.
- 단점 : 디버깅이 어렵고, 성능이 Mapstruct보다 더 떨어진다.
- Dozer
- 장점 : 런타임에 리플렉션을 사용하여 자동으로 매핑되어, 설정이 간편하고 유연하다. XML 및 어노테이션 기반의 매핑 설정을 제공한다.
- 단점 : 리플렉션을 사용하여 성능이 상대적으로 느리고, 더이상 지원하지 않는다. dozer github에 가보면 다음과 같은 말이 있다.
The project is currently not active and will more than likely be deprecated in the future. If you are looking to use Dozer on a greenfield project, we would discourage that. If you have been using Dozer for a while, we would suggest you start to think about migrating onto another library, such as :
[mapstruct](https://github.com/mapstruct/mapstruct)
[modelmapper](https://github.com/modelmapper/modelmapper)
For those moving to mapstruct, the community has created a [Intellij plugin](https://plugins.jetbrains.com/plugin/20853-dostruct) that can help with the migration.
우리 서비스 이제 중단하니까 mapstruct나 modelmapper 써라
- Jmapper
- 장점 : 바이트 코드 생성을 사용하여, 성능이 빠르고 설정이 간단하다.
- 단점 : 디버깅이 어렵고, reference가 많이 없다.
그래서 보통 2가지의 mapping framework를 사용하거나, java lang으로 손수 변환해준다.
추후에 실험하겠지만, ModelMapper보다는 Mapstruct가 압도적으로 빠른 속도와 성능을 자랑한다.
둘 다 external library를 이용하지만, ModelMapper는 매핑을 할 때 리플렉션을 발생시키고, MapStruct는 컴파일 시점에서 annotation을 이용해 구현체를 만들어내기 때문에 리플렉션을 발생시키지 않아서 차이가 나는 것이다.
또, 개인적으로 엔티티 클래스에 Setter 어노테이션을 넣는것을 많이 지양하는 편이라, Mapstruct를 사용하기로 했다.
설정
build.gradle
dependencies{
//mapstruct
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
}
먼저, 쉬운 예제부터 해보자.
comment > entity > Comment
@Getter
@NoArgsConstructor
@Entity
public class Comment extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long commentId;
@Column(nullable = false)
private String comment;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ticketId")
private Ticket ticket;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "userId")
private User user;
public Comment(User user, Ticket ticket, CommentRequestDto commentList) {
this.commentId = getCommentId();
this.user = user;
this.ticket = ticket;
this.comment = commentList.getComment();
}
public void update(CommentRequestDto commentRequestDto) {
this.comment = commentRequestDto.getComment();
}
}
Mapstruct는 보통 Java Beans 규약을 따르므로, setter가 있어야 Mapstruct가 이를 이용해 속성을 정의할 수 있다. 그러나 MapStruct 1.3버전 이상에서는 생성자 주입을 지원한다.
comment > dto > CommentResponseDto
@Getter
@NoArgsConstructor
public class CommentResponseDto {
private Long commentId;
private String comment;
private String email;
@QueryProjection
public CommentResponseDto(Long commentId, String comment, String email) {
this.commentId = commentId;
this.comment = comment;
this.email = email;
}
public CommentResponseDto(Comment c) {
this.commentId = c.getCommentId();
this.comment = c.getComment();
this.email = c.getUser().getEmail();
}
}
Dto <-> Entity간의 변환 수행시 다음과 같은 사항들을 고려해야 한다.
- Property name : MapStruct는 Source와 Target간 동일한 이름을 가진 속성을 매핑하려고 한다. 그래서Entity와 DTO가 다른 Attributes를 가지고 있는 경우
@Mapping
으로 설정을 지정해 줄 수 있다. Querydsl같은 경우 allias로 지정했던 것과 비슷하게 생각하면 된다. - Proeprty type : MapStruct는 java의 primitive, boxed, 그리고 standart type(String, LocalDate 등)에 대해서는 자동으로 변환해 줄 수 있다. 그러나 다른 복잡한 타입같은 경우,
@Mapping
이나@Mapper
어노테이션들로 설정을 해 주어야 한다. - Null값 핸들링 : MapStruct는 source 객체의 Null값 처리를 디폴트로 할 수 있다. null handling을 따로 해주고 싶으면,
NullValueMappingStrategy
나NullValueCheckStrategy
구성으로 바꿔줄 수 있다. - 중첩 매핑 : 엔티티나 DTO가 중첩된 객체나 컬렉션을 포함하는 경우
@Mapping
이나@InheritInverseConfiguration
어노테이션을 이용하여 중첩된 객체에 대한 매핑 규칙을 구성할 수 있다. - 매핑하기 싫은 속성 : DTO나 엔티티에 서로 없는 속성이 있는 경우
@Mapping(target = "", ignore = true)
를 통해 속성 매핑을 건너뛸 수 있다. 다량의 ignore가 있을 경우@Mapping
어노테이션을 두 번 사용해주면 된다. - 불변성(immutability) : DTO가 불변 속성인 경우(생성자 기반의 initialization등을 사용하는 경우)
@Builder
등으로 MapStruct configuration을 통해 생성자 또는 빌더 패턴을 사용하는 매핑 코드를 생성할 수 있다.
그럼, Mapping을 시작해 보자!
주의할 점
- Comment Entity에 있는 ticket field은 commentResponseDto에서 나타나지 않는다.
- Comment Entity에 있는 user field는 commentResponseDto에서 user의 email 주소로 나타나야 한다.
comment > dto > CommentMapper
@Mapper(componentModel = "spring")
public interface CommentMapper{
@Mappings({
@Mapping(target = "ticket", ignore = true)
@Mapping(target = "email", source = "comment.user.email")
})
CommentResponseDto mapToResponseDto(Comment comment);
}
@Mapper(componentModel = "spring")
: Mapper 어노테이션은 Mapstruct Code generator가 해당 인터페이스의 구현체를 생성해준다. componentModel = "Spring"은, spring에 맞게 bean으로 등록해준다는 뜻을 가지고 있다.@Mapper(componentModel = "spring")
을 써주지 않으면 Instance를 생성해야 한다. 우리는 TicketMapper니까 interface 안에INSTANCE = Mappers.getMapper(TicketMapper.class);
를 써주면 된다.
- 여기서 target 객체는 CommentResponseDto, source 객체는 comment이다. 그래서 target 먼저, source를 나중에 써 주면 좋다.
- comment(source)에 있는 ticket field는 dto에 field로 존재하지 않아서, MapStruct에서 알아서 걸러준다.
build를 하면, 자동으로 build > generated를 열어보자. 자동으로 구현체가 생겨있는 것을 알 수 있다.
build > generated > sources > annotationProcessor > java > main > dir > comment > dto > CommentMapperImpl
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-04-04T03:36:44+0900",
comments = "version: 1.5.3.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.5.1.jar, environment: Java 11.0.17 (Azul Systems, Inc.)"
)
@Component
public class CommentMapperImpl implements CommentMapper {
@Override
public CommentResponseDto mapToResponseDto(Comment comment) {
if ( comment == null ) {
return null;
}
CommentResponseDto.CommentResponseDtoBuilder commentResponseDto = CommentResponseDto.builder();
commentResponseDto.email( commentUserEmail( comment ) );
commentResponseDto.commentId( comment.getCommentId() );
commentResponseDto.comment( comment.getComment() );
return commentResponseDto.build();
}
private String commentUserEmail(Comment comment) {
if ( comment == null ) {
return null;
}
User user = comment.getUser();
if ( user == null ) {
return null;
}
String email = user.getEmail();
if ( email == null ) {
return null;
}
return email;
}
}
builder annotation을 기준으로, 생성되어있음을 알 수 있다. Test를 짜서 실험해보도록 하자!
test > CommentMapperTest
@Test
public void commentMapperTest() {
//given
User user1 = mock(User.class);
when(user1.getEmail()).thenReturn("pengooseDev@daum.net");
Comment comment = mock(Comment.class);
when(comment.getCommentId()).thenReturn(1L);
when(comment.getComment()).thenReturn("test");
when(comment.getTicket()).thenReturn(mock(Ticket.class));
when(comment.getUser()).thenReturn(user1);
//when
CommentResponseDto commentResponseDto = commentMapper.mapToResponseDto(comment);
//then
assertThat(commentResponseDto.getCommentId()).isEqualTo(1L);
assertThat(commentResponseDto.getEmail()).isEqualTo("pengooseDev@daum.net");
}
잘 변환되고 있음을 알 수 있다.
다시 돌아가서, 다음으로는 dto를 엔티티로 변환해보자.
comment > dto > commentRequestDto
@Getter
@NoArgsConstructor
public class CommentRequestDto{
@Schema(name = "ticket Id")
@NotNull(message = "ticket Id is required")
private Long ticketId;
private String comment;
}
comment > entity > Comment : Constructor
@Builder
public Comment(User user, Ticket ticket, CommentRequestDto commentList) {
this.commentId = getCommentId();
this.user = user;
this.ticket = ticket;
this.comment = commentList.getComment();
}
comment > dto > CommentMapper
@Mapper(componentModel = "spring")
public interface CommentMapper{
@Mappings({
@Mapping(target = "commentId", ignore = true),
@Mapping(target = "comment", source = "commentRequestDto.comment"),
@Mapping(target = "ticket", source = "ticket"),
@Mapping(target = "user", source = "user")
})
Comment mapToEntity(CommentRequestDto commentRequestDto, Ticket ticket, User user);
}
- CommentId는 ResponseDto, Ticket, User 어디에도 없는 Auto-generated되는 field이다. 사실 이런 값은 Mapper에서 알아서 걸러주지만, 이번에는 무시한다고 적었다.
TC를 짜보자.
test > CommentMapperTest
@Test
public void toEntityTest() {
//given
User user = mock(User.class);
when(user.getEmail()).thenReturn("pengooseDev@daum.net");
CommentRequestDto commentRequestDto = mock(CommentRequestDto.class);
when(commentRequestDto.getComment()).thenReturn("test");
when(commentRequestDto.getTicketId()).thenReturn(1L);
Ticket ticket = mock(Ticket.class);
when(ticket.getTicketId()).thenReturn(1L);
when(ticket.getTicketTitle()).thenReturn("test Ticket");
//when
Comment comment = commentMapper.mapToEntity(user, ticket, commentRequestDto);
//then
assertThat(comment.getComment()).isEqualTo("test");
assertThat(comment.getTicket().getTicketId()).isEqualTo(1L);
assertThat(comment.getUser().getEmail()).isEqualTo("pengooseDev@daum.net");
assertThat(comment.getCreatedAt().toString()).isNotNull();
}
그러나, assertThat(comment.getCreatedAt().toString()).isNotNull(); 이 실패하고, null로 나온다. 이를 검증하기 위해 서버를 한 번 띄워보자.
commentService
@Service
@RequiredArgsConstructor
@Transactional
public class CommentService {
private final CommentMapper commentMapper;
// 댓글 작성
public ResponseEntity<SuccessResponseDto> createComment(User user,
CommentRequestDto commentRequestDto) {
user = validateUserByEmail(user.getEmail());
Ticket ticket = TicketValidation(commentRequestDto.getTicketId());
// comment 작성
// Comment comment = new Comment(user, ticket, commentRequestDto);
Comment comment = commentMapper.mapToEntity(user, ticket, commentRequestDto);
commentRepository.save(comment);
// 상태 반환
return SuccessResponseDto.toResponseEntity(SuccessCode.CREATED_SUCCESSFULLY);
}
... 나머지 메소드
}
원래 comment를 생성하던 생성자를 주석처리하고, Mapper 메소드를 새로 새겨 넣었다.
postman에서는 성공적으로 만들어졌다고 나오고, 이제 db를 확인 해 보자.
DB에도 id와 auditAware가 정상적으로 반환되었음을 알 수 있다.
조금 더 난이도를 올려서!
이번엔, Comment와 @ManyToOne관계에 있는 Ticket를 매핑해보자.
TicketMapper interface를 만들어보자. Entity를 DTO로 변형하는것부터 시작하자!
ticket > entity > Ticket
@Getter
@NoArgsConstructor
@Entity
@AllArgsConstructor
@DynamicUpdate
public class Ticket extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long ticketId;
private String ticketTitle;
private String ticketDescription;
private int priority;
private int difficulty;
private LocalDate expiredAt;
private LocalDate completedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "task_Id")
private Task task;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_Id")
private User user;
@Enumerated(value = EnumType.STRING)
private TicketStatus status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "label_Id")
private Label label;
@OneToMany(mappedBy = "ticket", cascade = CascadeType.REMOVE)
private List<Comment> comment = new ArrayList<>();
... // 생성자 및 연관관계 편의 메소드
}
난이도가 확 올라갔다. 이제는 primitive type을 제외하고도, 다른 Class type이 들어있다.
ticket > dto > TicketResponseDto
@Getter
@NoArgsConstructor
public class TicketResponseDto {
private Long ticketId;
private String title;
private String description;
private TicketStatus status;
private int priority;
private int difficulty;
private String assigned;
private LocalDate expiredAt;
private LocalDate completedAt;
private String label;
private List<CommentResponseDto> commentList;
@Builder
public TicketResponseDto(Ticket ticket) {
ticketId = ticket.getTicketId();
title = ticket.getTicketTitle();
description = ticket.getTicketDescription();
ticketStatus = ticket.getStatus();
priority = ticket.getPriority();
difficulty = ticket.getDifficulty();
assigned = ticket.getUser() == null ? null : ticket.getUser().getEmail();
expiredAt = ticket.getExpiredAt();
label = ticket.getLabel() == null ? null : ticket.getLabel().getLabelTitle();
commentList = ticket.getComment().stream().map(CommentResponseDto::new).collect(Collectors.toList());
}
}
ticket > dto > TicketMapper
@Mapper(componentModel = "spring")
public interface TicketMapper {
@Mappings({
@Mapping(target = "ticketId", source = "ticket.ticketId"),
@Mapping(target = "title", source = "ticket.ticketTitle"),
@Mapping(target = "description", source = "ticket.ticketDescription"),
@Mapping(target = "status", source = "ticket.status"),
@Mapping(target = "priority", source = "ticket.priority"),
@Mapping(target = "difficulty", source = "ticket.difficulty"),
@Mapping(target = "assigned", source = "ticket.user.email"),
@Mapping(target = "expiredAt", source = "ticket.expiredAt"),
@Mapping(target = "completedAt", source = "ticket.completedAt"),
@Mapping(target = "label", source = "ticket.label.labelTitle"),
@Mapping(target = "commentList", ignore = true),
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
})
TicketResponseDto mapToResponseDto(Ticket ticket);
}
- description같은 부분은 entity에서 Ticketdescription, responseDto같은 경우에는 description으로 나간다. 그래서 source(지금은 entity, 꺼내올 객체)에는 ticketDescription, target(지금은 dto, 만들 객체)에는 description으로 binding 해주는 작업이 필요하다.
@BeanMapping
으로, null값에 대해서는 무시하겠다는 이야기이다.- BeanMapping을 활용할때는 주의해야 한다. 프론트엔드와 협업을 할 때, 프론트엔드에서는 null에 대해
?
로 체이닝을 걸어줄 수 있는데, 아예 필드가 안나간다면 곤란하기 때문이다.
- BeanMapping을 활용할때는 주의해야 한다. 프론트엔드와 협업을 할 때, 프론트엔드에서는 null에 대해
- 문제는 CommentList를 반환하려면 commentList가 comment 형태가 아닌 CommentResponseDto 형태로 나가야하는데, 이것은 어떻게 처리해 줘야 할까? 일단은 ignore=true로 놓아보자. Comment형태로 반환하기엔 DTO를 만든 의미가 없어진다.
- 이 때 이용하는것이,
@Mapper(uses={CommentMapper.class})
이다. commentMapper를 이용해 자동으로 Comment를 CommentDto로 변환해주는 로직을 작성한다.
MapStruct를 활용해 매핑을 처리하다 보면, 의도하지 않았던 필드에 매핑되는 경우가 있다. 매핑 메서드를 생성한 이후에는 생성된 MapperImpl을 꼭 확인해서 의도한대로 필드들이 잘 매핑되었는지 확인하는 것이 좋다. Querydsl로 예를 들자면, Queryprojection을 이용해서 바인딩을 할 때 ResponseDto와 순서가 일치하지 않으면 (allias를 붙여주지 않는다면 일어날 수 있다) 나중에 결과가 꼬여 title이 description 자리에, description이 title 자리에 들어가서 return 될 수 있다!
이제, TC를 짜보자!
test > TicketMapperTest
@Test
public void ticketToDtoTest() {
//given
User user1 = mock(User.class);
when(user1.getEmail()).thenReturn("pengooseDev@daum.net");
Label label1 = mock(Label.class);
when(label1.getLabelId()).thenReturn(1L);
when(label1.getLabelTitle()).thenReturn("testLabel");
when(label1.getLabelLeader()).thenReturn("pengooseDev@daum.net");
Ticket ticket = mock(Ticket.class);
when(ticket.getTicketId()).thenReturn(1L);
when(ticket.getTicketTitle()).thenReturn("testTicket");
when(ticket.getTicketDescription()).thenReturn("testTicketDescription");
when(ticket.getStatus()).thenReturn(TicketStatus.TODO);
when(ticket.getUser()).thenReturn(user1);
// 일부러 ticket.assigned를 설정 안함
when(ticket.getExpiredAt()).thenReturn(null);
when(ticket.getCompletedAt()).thenReturn(null);
//when
TicketResponseDto ticketResponseDto = ticketMapper.mapToResponseWithoutComment(ticket);
//then
assertThat(ticketResponseDto.getTicketId()).isEqualTo(ticket.getTicketId());
assertThat(ticketResponseDto.getTitle()).isEqualTo("testTicket");
Assertions.assertNull(ticketResponseDto.getCommentList(), "commentList should be null");
}
잘 통과했다! 그럼 실제로 서버를 돌려서 dto가 commentList를 어떻게 반환하는지 확인하자.
아쉽게도, commentList field를 완전히 지우는 데는 실패했다.
그렇다면, 이를 해결하는 방법은 두 가지가 남았다.
- 필요한 정보만을 가지는 클래스를 작성 후, 이를 상속받아 더 자세한 정보를 제공하는 클래스
- @JsonView로 basic한 view와 detail한 view를 나누기
그렇다면, 이제 남은 것은 Mapstruct가 왜 좋은지에 대한 성능 테스트이다.
다음과 같이 10만번의 mapping 결과
@Test
public void speedMapStruct() {
//given
Ticket ticket = mock(Ticket.class);
Comment comment = mock(Comment.class);
ticket.getComment().add(comment);
//when
long startMapstruct = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
TicketResponseDto ticketResponseDto = ticketMapper.toDto(ticket);
}
long takenTimeWithMapStruct = System.currentTimeMillis() - startMapstruct;
//then
System.out.println("MapStruct로 걸린 시간: " + takenTimeWithMapStruct);
}
@Test
public void speedWithManual() {
//given
Ticket ticket = mock(Ticket.class);
Comment comment = mock(Comment.class);
ticket.getComment().add(comment);
//when
long startManual = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
TicketResponseDto ticketResponseDto = new TicketResponseDto(ticket);
}
long takenTimeWithManual = System.currentTimeMillis() - startManual;
//then
System.out.println("Manual로 걸린 시간: " + takenTimeWithManual);
}
출력 결과, Mapstruct는 5.8초, java 수동 변환은 5.1초가 걸렸다.
그런데 생각을 해 보면 Mapstruct도 java-based 외부 라이브러리기 때문에
대용량 데이터에서도 둘간의 성능 차이는 비슷할 것으로 예상한다.
더 단점을 가져와 보시오
구글링을 통해, spring의 대가 김영한 선생님이 생각하는 Mapping framework의 단점을 찾았다.
- 모델이 단순하면 상관이 없는데, 매핑해야 하는 모델이 복잡하거나 서로 차이가 많이 나면... 머리속으로 생각을 좀 많이 해야 합니다. 이게 어느정도 복잡해지면 생각하시는 시간 때문에 비용이 더 들어가더라구요.
- 직접 수동으로 할 때는 컴파일 시점에 오류를 잡을 수 있는데, 이건 실행을 해봐야 오류를 찾을 수 있습니다.
- ModelMapper는 동시성 성능 이슈가 있습니다. 수천 TPS의 리엑티브 모델에서는 이 부분이 명확하게 병목으로 나왔습니다. 물론 수천 TPS가 안되는 상황에서는 상관이 없습니다. (MapStruct는 모르겠네요)
결국 장단점이 있는데요. 저는 사용을 안합니다. ㅎㅎ(사용하다가 사용을 안하게 되었으니, 언젠가는 바뀔지도 모르겠습니다.)
복잡한 실무에서 엔티티를 DTO로 변경하는게 이상적으로 딱딱 맞아 떨어지는 경우만 있는 것도 아니고, 수동으로 작업하면 결국 컴파일 시점에 오류를 잡을 수 있다는 장점도 있구요. 그리고 무엇보다! 수동으로 해도 손까락만 약간 힘들지 몇분 안걸립니다. ㅋㅋ
그럼 왜 씀?
위의 단점에도 불구하고, Mapstruct를 사용하는 이유는 다음과 같다.
- 객체간의 변환 로직을 분리해서, 코드의 가독성과 유지 보수성이 향상된다.
- 다른 mapping library와 다르게, 실행 할 때 코드를 생성하여, 런타임 오류를 줄일 수 있다.
- framework 사용 시, 협업에 있어서 조금 더 '일관된 코드' 작성이 가능하다.
- 개발자가 직접 변환 로직 짜는 시간을 줄여준다.
그럼 님 씀?
ㅋㅋ...전 안쓸듯..
-참고-
https://velog.io/@cham/Java-MapStruct-%EC%82%AC%EC%9A%A9%EB%B2%95
https://huisam.tistory.com/entry/mapStruct#Dependecy%--%EC%--%A-%EC%A-%---%EC%-D%--%EC%A-%B-%EC%--%B-%--%EC%--%A-%EC%A-%---
https://mein-figur.tistory.com/entry/mapstruct-1
https://ryumodrn.tistory.com/26
https://meetup.nhncloud.com/posts/213
- MapStruct: https://mapstruct.org/
- MapStruct 가이드 문서: https://mapstruct.org/documentation/stable/reference/html/
'스프링' 카테고리의 다른 글
@Valid, ExceptionHandling을 어떻게 해 주어야 할까? (0) | 2023.03.24 |
---|---|
[스프링 부트] Junit5를 이용한 Test에서 @RequiredArgsConstructor가 안되는 이유 (0) | 2023.03.15 |