Object mapping
객체 간 매핑을 뜻하며, 엔티티를 DTO로 변환하는 등과 같이 다른 객체로 변환하거나 합치는 경우에 사용한다.
보통 이 경우엔 코드 중복이 발생하기 쉽고, 실수하기도 쉽고, 필드 수정이나 삭제가 일어날 경우 역시 수정이 필요하고, 비즈니스 로직에 섞이기 때문에 생산성이 떨어진다. 따라서 객체 매핑 라이브러리를 이용하여 이를 해결하며, 대표적으로 ModelMapper와 MapStruct 가 있다.
두 라이브러리 중에서는 MapStruct가 더욱 장점이 많아서 이를 쓰며, 둘 간의 차이점은 다음과 같다.
- 컴파일 시점에서 어노테이션을 읽어 구현체를 만들어내서 리플렉션이 발생하지 않음.
- 처리속도가 더 빠름 (원본 글에선 단위가 m/s 인데 이게 머선 단위지..)
- 컴파일 시 오류를 확인 가능
- 디버깅 쉬움
- 생산된 매핑 코드를 확인 가능
MapStruct
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// mapStruct
implementation "org.mapstruct:mapstruct:1.5.5.Final"
annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final"
// If you are using mapstruct in test code
// testAnnotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final"
우선 build.gradle 에 다음과 같이 의존성을 설정해두면 된다. 밑의 한 줄은 테스트 코드 작성시 주석을 풀어서 사용하면 된다.
지금은 1.5.5.Final 이 최신 버전이지만 이후 버전은 여기서 확인 후 사용하면 된다.
다만 Lombok 라이브러리에 먼저 의존성이 추가되어 있어야 하며, mapStruct 보다 위에 Lombok 의존성 추가 코드를 작성해야 한다.
@Mapper(
componentModel = "spring", // 빈으로 등록해줌
unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface PostMapper {
// PostMapper INSTANCE = Mappers.getMapper(PostMapper.class);
PostDetailResponseDto toPostDetailResponseDto(Post post);
PostPreviewResponseDto toPostPreviewResponseDto(Post post);
}
우선 매퍼 인터페이스를 만들어 주어야 한다.
componentModel = "spring" 을 하면 스프링 빈으로 등록을 해준다. 안 하고 싶으면 따로 주석처럼 인스턴스를 만들면 되는 듯 하다.
@Getter
@Entity
@Table(name = "post") // 테이블명을 명시적으로 알림
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA는 기본 생성자를 필요하므로 최소의 접근제어자로 생성
public class Post extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 30) // 제목은 1이상 30 글자 이하
private String title;
@Column(nullable = false, length = 20) // 작성자명은 2이상 20 글자 이하
private String author;
@Column(nullable = false, length = 128) // 비밀번호는 4이상 50이하 이나, 암호화로 128자 까지 저장
private String password;
@Column(nullable = false, columnDefinition = "text") // 글내용은 1이상 65,535 byte 이하
private String contents;
}
우선 Post 엔티티이고,
@Getter
@Setter
public class PostPreviewResponseDto {
private Long id;
private String title;
private String author;
private LocalDateTime createdAt;
}
응답 DTO 이다. DTO의 값 변경을 위해 getter, setter가 필요하다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PostService {
private final PostMapper postMapper;
private final PostRepository postRepository;
private final PasswordEncoder passwordEncoder;
/**
* 레포지토리로부터 생성일자를 기준으로 내림차순 리스트를 받아와 간소화 게시글 DTO 리스트로 변환 후 리턴
*/
public List<PostPreviewResponseDto> getPosts() {
return postRepository.findAllByOrderByCreatedAtDesc()
.stream()
.map(postMapper::toPostPreviewResponseDto)
.toList();
}
/**
* 레포지토리로부터 특정 id의 게시글 세부 정보를 반환
* @param id 게시글 id
*/
public PostDetailResponseDto getPost(Long id) {
Post post = postRepository.findById(id)
.orElseThrow(PostNotFoundException::new);
return postMapper.toPostDetailResponseDto(post);
}
}
그리고 엔티티를 DTO로 변환하려면, 필드에 PostMapper 선언 해두고, 매퍼 인터페이스에서 만들어둔 메서드를 이용하면 된다.
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-11-21T00:35:51+0900",
comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.4.jar, environment: Java 17.0.9 (Azul Systems, Inc.)"
)
@Component
public class PostMapperImpl implements PostMapper {
@Override
public PostDetailResponseDto toPostDetailResponseDto(Post post) {
if ( post == null ) {
return null;
}
PostDetailResponseDto postDetailResponseDto = new PostDetailResponseDto();
postDetailResponseDto.setId( post.getId() );
postDetailResponseDto.setTitle( post.getTitle() );
postDetailResponseDto.setAuthor( post.getAuthor() );
postDetailResponseDto.setContents( post.getContents() );
postDetailResponseDto.setCreatedAt( post.getCreatedAt() );
postDetailResponseDto.setModifiedAt( post.getModifiedAt() );
return postDetailResponseDto;
}
@Override
public PostPreviewResponseDto toPostPreviewResponseDto(Post post) {
if ( post == null ) {
return null;
}
PostPreviewResponseDto postPreviewResponseDto = new PostPreviewResponseDto();
postPreviewResponseDto.setId( post.getId() );
postPreviewResponseDto.setTitle( post.getTitle() );
postPreviewResponseDto.setAuthor( post.getAuthor() );
postPreviewResponseDto.setCreatedAt( post.getCreatedAt() );
return postPreviewResponseDto;
}
}
참고로 매퍼 인터페이스로 구현된 구현체는 다음과 같다.
참고 링크
- https://dev-splin.github.io/spring/Spring-ModelMapper,MapStruct/
- https://medium.com/naver-cloud-platform/%EA%B8%B0%EC%88%A0-%EC%BB%A8%ED%85%90%EC%B8%A0-%EB%AC%B8%EC%9E%90-%EC%95%8C%EB%A6%BC-%EB%B0%9C%EC%86%A1-%EC%84%9C%EB%B9%84%EC%8A%A4-sens%EC%9D%98-mapstruct-%EC%A0%81%EC%9A%A9%EA%B8%B0-8fd2bc2bc33b
- https://mapstruct.org/
'TIL ✍️' 카테고리의 다른 글
23년 11월 23일(목요일) - 38번째 TIL (0) | 2023.11.23 |
---|---|
23년 11월 22일(수요일) - 37번째 TIL (0) | 2023.11.22 |
23년 11월 20일(월요일) - 35번째 TIL (0) | 2023.11.20 |
23년 11월 17일(금요일) - 34번째 TIL (2) | 2023.11.17 |
23년 11월 16일(목요일) - 33번째 TIL (0) | 2023.11.16 |