[MapStruct] 객체 간의 매핑

lkdcode·2023년 11월 17일

SpringBoot

목록 보기
1/4
post-thumbnail

Spring Boot 프로젝트 중 객체 간의 매핑을 도와주는
MapStruct 를 활용하자.
간단한 설정으로 entity <-> dto 매핑을 도와줌으로써 보일러 플레이트를 해결해 준다!

💡 ex) 요구 사항
게시글 CRUD 중 ....
게시글의 ID, 제목, 내용 을 포함해서 Response 한다.

위의 요구 사항을 만족하기 위한 응답으로,
Entity 대신 DTO 로 응답하는 이유가 있다.

보안 문제

Entity 는 민감한 정보를 포함, 노출 시 보안 취약점이 발생할 수 있음.

의미적 분리

Entity 데이터베이스의 구조를 반영하고 비즈니스 로직에 종속적일 수 있음.
API 응답은 클라이언트와의 의사소통을 위해 조정된 데이터 구조가 필요하며
Entity 를 그대로 반환하면 클라이언트에게 불필요한 정보가 노출될 수 있음.

불필요한 데이터 전송 최소화

클라이언트가 필요한 데이터만 전송할 수 있음.

변경 용이성

Entity 의 구조를 변경할 경우, API 응답도 같이 변경됨.
직접 반환하는 경우 클라이언트에게 영향을 미침.

버전 관리

API 응답의 구조를 명시적으로 관리함으로써 더 유연한 버전관리가 가능함.

위의 이유가 전부는 아니겠지만 Entity 대신 DTO 를 활용하여 클라이언트에게 응답해보자!

# Dependencies

Gradle 의존성을 추가해주자.
Lombok 아래에 추가를 해주자.

    // --------- mapper
    implementation 'org.mapstruct:mapstruct:1.5.3.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
    annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'

# 게시글 Entity

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SamplePostEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String content;
}

간단한 게시글이다.
Id, Title, Content 3개의 필드를 가지고 있는 Entity 다.

💡 클라이언트의 요청으로 Title, Content 2개의 응답을 원한다고 가정하자!!

# 게시글 DTO

public sealed interface SamplePostDTO permits CreateResponseDTO, CreateRequestDTO {
    @Builder
    record CreateResponseDTO(
            String DTOtitle,
            String DTOcontent
    ) implements SamplePostDTO {
    }

    @Builder
    record CreateRequestDTO(
            String DTOtitle,
            String DTOcontent
    ) implements SamplePostDTO {
    }
}
  1. 자바 14에서 등장한 record 를 활용하여 DTO 를 만들자.
    데이터를 다룰 때 적합한 것 같다

  2. 게시글 생성 요청에 대한 RequestDTO 와 생성 응답에 대한 ResponseDTO 2개만 있다.

Controller, Service...

간단하게 테스트한다.

🎯 Controller 의 코드다

@RestController
@RequestMapping("/api/sample-posts")
@RequiredArgsConstructor
public class SampleController {

	private final SampleService sampleService;

    @PostMapping
    public SamplePostDTO.CreateResponseDTO sampleCreate(@RequestBody SamplePostDTO.CreateRequestDTO dto) {
        return sampleService.sampleMethod(dto);
    }
}

🎯 Service 의 코드다

@Service
public class SampleService {
    public SamplePostDTO.CreateResponseDTO sampleMethod(SamplePostDTO.CreateRequestDTO dto) {
        SamplePostEntity entity = SamplePostMapper.INSTANCE.createRequestDTOToEntity(dto);
        /*
         * 로직...
         */
        return SamplePostMapper.INSTANCE.entityToCreateResponseDTO(entity);
    }
}

맵핑만 되는지 확인을 위해 다른 로직은 제거한 상태다.

# Mapper Interface

매핑을 위한 맵퍼 코드다.

@Mapper
public interface SamplePostMapper {
    SamplePostMapper INSTANCE = Mappers.getMapper(SamplePostMapper.class);

    @Mapping(source = "DTOtitle", target = "title")
    @Mapping(source = "DTOcontent", target = "content")
    SamplePostEntity createRequestDTOToEntity(SamplePostDTO.CreateRequestDTO dto);

    @Mapping(source = "title", target = "DTOtitle")
    @Mapping(source = "content", target = "DTOcontent")
    SamplePostDTO.CreateResponseDTO entityToCreateResponseDTO(SamplePostEntity entity);
}

@Mapping 어노테이션을 붙여주고 매핑할 필드를 적어준다.

source : 매개변수로 넘어오는 필드값이다.
target : 변환할 객체의 필드값이다.

    @Mapping(source = "DTOtitle", target = "title")
    @Mapping(source = "DTOcontent", target = "content")
    SamplePostEntity createRequestDTOToEntity(SamplePostDTO.CreateRequestDTO dto);

위의 코드는 dtoentity 를 매핑한다.
SamplePostDTO.CreateRequestDTO dtoString DTOtitle
SamplePostEntityString title 로 매핑한다.

SamplePostDTO.CreateRequestDTO dtoString DTOcontent
SamplePostEntityString content 로 매핑한다.

💡 만약, source = "title"target = "title" 의 필드명이 같다면 일일이 다 매핑해줄 필요없다.
dto 의 필드명을 entity 와 통일 시키면 Mapping 어노테이션 1개만 해도 나머지는 알아서 매핑이 된다.

# List 매핑

여러개의 게시글을 불러오는 경우에도 매핑이 가능하다.
이미 매핑이 되어 있다면 List<> 만 붙여주면 된다.

ex)

List<PostResponseDTO.Get> postToPostListDTO(List<Post> posts);

Entity 의 필드명을 그대로 가져와서 DTO 로 담으면 쉽게 매핑이 가능한 것 같다.
여러개의 매개변수를 받는 경우에도 매핑을 잘 해주면 된다.

profile
되면 한다

0개의 댓글