DTO 계층을 분리해보자 (feat. MSA)

sonjiseokk·2024년 9월 14일
0
post-thumbnail

📌 개요

MSA로 팀프로젝트를 진행중인데, 여기서 한 가지 의문이 생겼었다.

멀티모듈을 도입하지 않는 이상 각 서비스마다 중복된 DTO 저장은 필연적인데 이걸 서비스 계층에서 조작하는게 맞나?

쉽게 말하면 이런 느낌이다

➡️ 상황 설명

만약 MemberService에서 특정 로직을 수행한 후 이걸 MemberController에서 반환하는 로직이라고 가정하자.

MemberDto

public class MemberDto {  
    private Long memberId;  
    private String memberName;  
}

MemberService

// 가상의 로직
MemberDto memberDto = memberService.findById(id);

MemberController

@GetMapping("/detail")  
public ResponseEntity<?> logic(@RequestParam Long memberId) {  
	MemberDto memberDto = memberService.logic(memberId)
    return ResponseEntity.ok(memberDto);  
}

그리고 다른 Order 마이크로서비스는 이 로직의 결과값이 필요하다
OrderController

@FeignClient(name = "member-service", url = "http://member-service:8080")
public interface MemberClient {

    @GetMapping("/api/member/detail")
    MemberResponseDto getMemberDetail(@RequestParam("memberId") Long memberId);
}

이때 Order 마이크로서비스는 이 MemberReponseDto를 다음과 같이 만들었을 것이다.
MemberResponseDto

public class MemberDto {  
    private Long memberId;  
    private String memberName;  
}

❓ 이게 뭐가 문젠데

이때 멤버 서비스는 로직의 변경으로 인해 MemberDto에 이메일 정보를 추가했다.

public class MemberDto {  
    private Long memberId;  
    private String memberName;
    private String memberEmail;  
}

그럼 어떻게 될까?

이 로직 변경으로 인해 다른 마이크로서비스들은 기존의 DTO에 매핑할 수 없어, 오류가 나거나 email에 대한 정보가 누락될 것이다.

이유는 다른 마이크로서비스는 이렇게 알고있기 때문

public class MemberDto {  
    private Long memberId;  
    private String memberName;  
}

그래서 생각한 구조는 Service - Controller 계층간 DTO 분리 이다.


😀 관심사의 분리

먼저 멤버 서비스에서는 로직에 필요한 DTO와 반환에 필요한 DTO를 분리한다.

서비스에서 사용될 DTO

public class MemberDto {  
    private Long memberId;  
    private String memberName;  
}

컨트롤러에서 사용될 DTO

public class MemberResponse {  
    private Long memberId;  
    private String memberName;  
}

그리고 이를 ModelMapper 라이브러리를 통해 매핑한다

@GetMapping("/detail")  
public ResponseEntity<?> logic(@RequestParam Long memberId) { 
	MemberDto memberDto = memberService.logic(memberId);
	MemberResponse response = modelMapper.map(memberDto, MemberResponse.class);
    return ResponseEntity.ok(response);  
}

이렇게 사용한다면 장점은 다음과 같음

  1. 다른 마이크로서비스들은 API 요청으로부터 원하던 값을 좀 더 신뢰성있게 얻을 수 있음.

만약 개발과정에서 DTO를 함부로 막 건든다면 다른 마이크로서비스는 이런 에러를 만날 수 있기 때문

Exception in thread "main" org.springframework.http.converter.HttpMessageNotReadableException: 
    JSON parse error: Cannot deserialize value of type `java.lang.String` from Object value (token `JsonToken.START_OBJECT`);
    nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `java.lang.String` from Object value (token `JsonToken.START_OBJECT`)
  1. 서비스와 컨트롤러 간의 관심사 분리를 할 수 있음

✈️ 결론

그러나 근본적으로 DTO의 신뢰성 문제를 해결해주진 못함

A 서비스가 애초에 반환되는 DTO를 바꿨다면 B 서비스는 그걸 절대 알 수가 없기 때문

이를 해결하기 위한 방법이 바로 "멀티 모듈"

정확히는 내가 프로젝트를 하면서 체감해봐야하기 때문에 구조만을 보여주면 다음과 같다

├── common-dto-module
│   ├── src/main/java/com/example/dto/MemberDto.java
├── service-a
├── service-b

하나의 레포지토리에 여러 마이크로서비스가 존재하는 모노레포 방식의 최적화된 방법이다.

profile
Software Engineer

0개의 댓글