백엔드/스프링, 스프링부트, JPA, Spring Webflux

Request와 Response에서 어떻게(어디에서) DTO로 변환하는걸까? 왜 DTO로 변환할까?

Kangjieun11 2023. 1. 6. 01:22
728x90

 

학습하면서 정리한 내용입니다. 

 

 


 

 

 

✅ Request와 Response에서 어떻게(어디에서) DTO를 변환하는걸까?

 

 

 

선행지식으로 요청을 보냈을 때 컨트롤러로 오고, 다시 return되는 과정을 함께 알아야한다. 

 

 

아래 그림은 request를 보냈을때,  Spring Context에서의 동작과정을 나타냅니다

 

 

  1. 클라이언트로부터, 요청메세지가 들어오면 DispatcherServlet에서 요청 URI를 통해 HandlerMapping에서 컨트롤러의 정보를 검색, 리턴받는다. 
  2. 리턴받은정보를 DispatcherServlet은 HandlerAdapter에게 보내며 컨트롤러의 호출을 위임한다.
  3. HandlerAdapter는 Controller를 호출한다.   (여기에서 JSON데이터가 DTO로 변환된다.)
  4. 컨트롤러가 로직 처리를 마치고 return이 되면, HandlerAdapter에서 DispatcherServlet으로 넘어갈 때 리턴된 데이터를 JSON으로 변환한다.

 

 

 

⏺ Request  ( Json to DTO  )

 

✔️ @RestController

 

일단 @RestController의 역할을 알아야한다.

@RestController = @Controller + @ResponseBody

 

Json 형태로 객체 데이터를 반환하는 것이 @RestController의 주 목적이다.

 

 

 

 

✔️ HttpMessageConverter

 

위의 그림을 보면 HandlerAdapter는 Controller를 호출하면서, 보유한 핸들러 메서드의 정보를 바탕으로

요청메세지의 BODY- JSON을  DTO에 넣어주게 된다.

 

 

메세지를 객체로, 객체를 메세지로 변환하기 위해서  HttpMessageConverter가 동작한다.

 

 

아래는 HttpMessageConverter의 read()가 동작하는 곳인데

 

hasBody() 바디가 존재하면,

msgToUse = getAdvice.berforeBodyRead()    :  Body를 직접적으로 읽기전에 어떤 전처리를 해주고

read() 메서드 안쪽 어딘가에서 body를 만들고 변환할 대상 객체 (DTO) 를 생성해 body변수에 할당한다. 

 

이때 body의 타입이 Object라서 어떤 DTO의 타입도 상관없다!

 

 

 

HttpMessageConverter에는 아래 3가지가 존재한다.

 

스프링에서 메시지 컨버터를 선정할 때엔 대상 클래스 타입과 미디어 타입을 체크 후 사용 여부를 결정하고,

이를 만족하지 않을 경우 다음 우선순위에 있는 메시지 컨버터로 넘어가 체크하게 된다. 

 

  • ByteArrayHttpMessageConverter
    • byte [] 데이터 처리
    • 클래스 타입: byte []
    • 미디어 타입: */*
    • HTTP 요청: @RequestBody byte [] example
    • HTTP 응답: @ResponeBody return byte[] (쓰기 MediaType: application/octet-stream)
  • StringHttpMessageConverter
    • String 문자열로 데이터 처리
    • 클래스 타입: String
    • 미디어 타입: */*
    • HTTP 요청: @RequestBody String s
    • HTTP 응답: @ResponseBody return s (쓰기 MediaType: text/plain)
  • MappingJackson2HttpMessageConverter
    • application/json 처리
    • 클래스 타입: 객체 또는 HashMap
    • 미디어 타입: application/json
    • HTTP 요청: @RequestBody Example example
    • HTTP 응답: @ResponseBody return example (쓰기 MediaType: application/json)

 

 

우리는 RestController를 사용하고 있으므로, JSON과 객체간의 변환이 이루어지고 따라서 MappingJackson2HttpMessageConverter를 이용하게 된다. 

 

 

 

Jackson2HttpMessageConverter 가 선택되어 read()가 동작하고, 

동작 이후에 해당 DTO로 매핑된 객체가 다시 read()가 호출되었던  HttpMessageConverter로 반환되어 body에 들어가는것이다.

 

 

>> 직접 요청메세지 보내서 어디로 들어가는지 확인도 해봤다 😃

 

 

 

 

⏺ Response  ( Object  to  JSON  )

 

✔️ 객체 응답과 ResponseEntity 응답의 비교

@RestController가 JSON으로 요청 응답을 해주기 때문에, Controller에서 DTO, Entity 클래스 자체를 리턴해도

자동적으로 적용이 되어 응답메세지에 맞는 형태로 반환이 된다!

 

(이 경우도 객체의 JSON변환이라 AbstractJackson2HttpMessageConverter에서 해준다.)

 

  • 이때 ResponseEntity로 묶어주지 않으면, defualt status는 200이다 (ok)

 

 

아래 코드는 DTO와 Entity를 반환하는 코드인데 둘다 상태코드는 defualt 200으로 반환된다.

// DTO 반환
@PostMapping
public MemberDto.response postMember2(@Valid @RequestBody MemberDto.Post requestBody) {
	Member member = mapper.memberPostToMember(requestBody);
	member.setStamp(new Stamp());

	Member createdMember = memberService.createMember(member);
	return  mapper.memberToMemberResponse(createdMember);

}

//엔티티 반환
@PostMapping
public Member postMember2(@Valid @RequestBody MemberDto.Post requestBody) {
    Member member = mapper.memberPostToMember(requestBody);
    member.setStamp(new Stamp());
    Member createdMember = memberService.createMember(member);

    return createdMember;
}

 

 

 

 위에서 엔티티나 DTO 구별없이 객체를 리턴하게 되면, 객체의 필드를 JSON으로 바꿔준다고 했다.

이경우는 결국 Http Status를 설정할 수 없다는 단점이 된다.

 

상태코드를 의도적으로 바꿔 주려면 ResponseEntity를 통해 응답을 해주면 된다.

 

 

 

✔️ 객체 생성시 (POST) 200 vs 201

 

먼저 Restful하게 접근하면,  201이 사용되어야 한다.

 

 

아래 글에선 객체 생성시에 201(Created)과 함께 URL을 보내주는 과정을 설명하고 있다.

 

 

201 Created

The HTTP 201 Created success status response code indicates that the request has succeeded and has led to the creation of a resource. The new resource, or a description and link to the new resource, is effectively created before the response is sent back and the newly created items are returned in the body of the messagelocated at either the URL of the request, or at the URL in the value of the Location header.

The common use case of this status code is as the result of a POST request.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201

 

 

 

생성을 요청받은 상황에서  응답으로 객체를 그대로 반환하면, HttpStatus를 설정해줄 수 없어서 200을 상태코드로 응답하게 된다.

 

Restful한 응답을 위해 201을 보내려면 결국, 객체를 상황에 맞는 ResponseEntity로 감싸서 반환해주어야한다.

여기에 URL 까지 함께 반환하게 되면 훨씬 더 해야할 일이 많아진다.

 

@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberDto.Post requestBody) {
    Member member = mapper.memberPostToMember(requestBody);
    member.setStamp(new Stamp());

    Member createdMember = memberService.createMember(member);
    URI location = UriCreator.createUri(MEMBER_DEFAULT_URL, createdMember.getMemberId());

    return ResponseEntity.created(location).build();
}

 

 

( 좀더 restful 함에도 불구하고 ) 

201 created는 해주어야하는 작업이 조금 더 많다는 이유로 많은 개발자들이 200을 반환하는 경우가 생각보다 많다고 한다.

 

 

 

어떤게 좋은지는 개발자가 직접 선택하면 된다.

 

 


 

✅ 요청과 응답메세지는 왜  DTO로 변환할까?

 

우리는 위에서  엔티티와 DTO를 리턴하는 코드를 보았다.

 

왜 근데 요청과 응답을 DTO로 변환해서 작업하는걸까?

엔티티를 사용하면 안될까?

 

 

 

이전엔 단순히 역할의 분리를 위해서 정도만 이해를 했었다.

 

 

엔티티는 데이터베이스 영속성(persistent)의 목적으로 사용되는 객체이고 
요청, 응답을 전달하는 클래스로 함께 사용될 경우 변경될 가능성이 크며,
데이터베이스에 직접적인 영향을 줘서 DTO를 분리한다

 

 

 

오늘 Entity 객체 자체를 응답으로 보내보는 과정속에서 한가지 이유를 더 깨달았다.

 

 

⏺  엔티티를 Response할 경우

 

✔️  String으로 변하며 생기는 순환참조 문제

 

엔티티는 연관관계를 맺어주어야하는 엔티티에 대해

필드값으로 엔티티클래스를 보유하고 있다

 

우리가 엔티티를 응답메세지로 직렬화해서 JSON으로 만들어주게 되면, String을 만들어주게 되면서

양방향 매핑시 순환참조가 벌어진다.

 

>>  @Data, @ToString, @EqualsAndHashCode 을 사용하면서 두 객체가 서로의 필드를 계속 참조하며 순환참조 발생

@Getter
@Setter
@NoArgsConstructor
@Entity
public class Stamp extends Auditable {

	//생략 
    
    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
	
    //생략
}
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member extends Auditable {
	
    //생략 
    
    @OneToOne(mappedBy = "member", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    private Stamp stamp;
    
    //생략
}

 

 

 

Member와 Stamp가 양방향 매핑이 적용되어 응답메세지가 무한으로 순환참조 되고 있는것을 확인했다

Entity를 사용하면 무조건 객체를 보유하기 때문에 순환참조가 발생할 수밖에 없다.

 

 

 

 

 

⏺  DTO를 Response할 경우

 

  • 요청과 응답으로 DTO를 사용할 경우, 객체를 보유하지 않으면 순환참조 가능성이 사라진다. (객체를 보유하지 않도록 만들어준다면)
  • 만약 객체가 있다면 -> getStamp()와 같이 숫자를 반환하도록 처리해줘야한다.
    • Jackson의 직렬화 방식은 다음과 같이 동작한다.
    • 기본적으로 public 필드만 직렬화를 시도함
    • private 필드를 직렬화하기 위해 getter를 선언함
    • @Getter 어노테이션이 각 필드의 게터메서드를 만들어주지만, 미리 getStamp()를 만들어주어, Stamp에 대해서는 count 값을 반환하도록한다.
    • 결국 객체를 직접 읽어 직렬화하는게 아닌, 숫자값으로 직렬화를 해서 순환참조가 발생하지 않는다. 
@AllArgsConstructor
@Getter
public static class Response {
    private long memberId;
    private String email;
    private String name;
    private String phone;
    private Member.MemberStatus memberStatus;
    private Stamp stamp;

    public String getMemberStatus() {
        return memberStatus.getStatus();
    }
    public int getStamp() {
        return stamp.getStampCount();
    }
}

 

  • 데이터를 전달한다는 역할에도 명확해진다 (객체지향에 부합해짐)
  •  

 

 

 

 


 

 

references 

더보기

https://jaimemin.tistory.com/1823

https://mangkyu.tistory.com/49

https://data-make.tistory.com/727

전체 피드백 - 상운님

DTO의 객체보유/순환참조 관련 추가 피드백 - 태희님