[Spring MVC] DTO

최우형·2023년 4월 12일
1

Spring MVC

목록 보기
2/4

📌DTO(Data Tranfer Object)란?

Transfer라는 의미에서 알 수 있듯이 데이터를 전송하기 위한 용도의 객체 정도로 생각할 수 있다.


DTO가 필요한 이유?

DTO 클래스를 이용한 코드의 간결성

예시
@RestController
@RequestMapping("/v1/members")
public class MemberController {
    @PostMapping
    public ResponseEntity postMember(@RequestParam("email") String email,
                                     @RequestParam("name") String name,
                                     @RequestParam("phone") String phone) {
        Map<String, String> map = new HashMap<>();
        map.put("email", email);
        map.put("name", name);
        map.put("phone", phone);

        return new ResponseEntity<Map>(map, HttpStatus.CREATED);
    }

		...
		...
}

여기서 @RequestParam은 계속 늘어날 수 밖에 없다.

이랬던 코드를 DTO 클래스를 이용해서 코드를 간결하게할 수 있다.

@RestController
@RequestMapping("/v1/members")
public class MemberController {
    @PostMapping
    public ResponseEntity postMember(MemberDto memberDto) {
        return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
    }

		...
		...
}
  • 아직 비즈니스 로직은 없는 상태

@RequestParam은 사라지고 MemberDto memberDto가 추가되었다.

데이터 유효성 검증의 단순화

email 주소를 이메일 주소 형식이 아닌 단순 문자열로 전송해도 정상적으로 핸들러 메서드 쪽에서 전달 받을 수 있다.

이 처럼 서버 쪽에서 유효한 데이터를 전달 받기 위해 데이터를 검증하는 것을 유효성(Validation)검증이라고 한다.

예시
@RestController
@RequestMapping("/no-dto-validation/v1/members")
public class MemberController {
    @PostMapping
    public ResponseEntity postMember(@RequestParam("email") String email,
                                     @RequestParam("name") String name,
                                     @RequestParam("phone") String phone) {
				// (1) email 유효성 검증
        if (!email.matches("^[a-zA-Z0-9_!#$%&'\\*+/=?{|}~^.-]+@[a-zA-Z0-9.-]+$")) {
            throw new InvalidParameterException();
        }
        Map<String, String> map = new HashMap<>();
        map.put("email", email);
        map.put("name", name);
        map.put("phone", phone);

        return new ResponseEntity<Map>(map, HttpStatus.CREATED);
    }
		...
		...
}

하지만 이 방법은 핸들러 메서드네에 직접적으로 유효성을 검증하는 로직이 포함된 것을 보면 좋지 않은 방식이다.

이 때 DTO 클래스를 이용하자

public class MemberDto {
    @Email
    private String email;
    private String name;
    private String phone;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }
}

email 멤버 변수에 @Email 에너테이션을 추가하면 이메일 주소가 포함되어 있지 않을 경우 유효성 검증에 실패하기에 클라이언트 요청은 거부(reject) 된다.

@RestController
@RequestMapping("/v1/members")
public class MemberController {
    @PostMapping
    public ResponseEntity postMember(@Valid MemberDto memberDto) {
        return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
    }

		...
		...
}

이렇게나 간단해진다.

MemberDto 앞에 붙은 @Valid 애너테이션은 emberDto객체에 유효성 검증을 적용하게 해주는 애너테이션이다.

DTO의 가장 중요한 목적은 비용이 많이 드는 작업인 HTTP 요청의 수를 줄이기 위함이다.


HTTP 요청 / 응답 데이터에 DTO 적용하기

HTTP Request Body가 JSON 형식이 아닐 경우

이제까지 전달하는 요청 데이터는 x-www-form-urlencoded 형식 데이터였다.

프런트엔드 쪽 웹앱과 통신하려면 대부분 JSON이다.

HTTP Request Body가 JSON 형식일 경우

예시
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/v1/members")
public class MemberController {
    // 회원 정보 등록
    @PostMapping
    public ResponseEntity postMember(@RequestParam("email") String email,
                                     @RequestParam("name") String name,
                                     @RequestParam("phone") String phone) {
        Map<String, String> body = new HashMap<>();
        body.put("email", email);
        body.put("name", name);
        body.put("phone", phone);

        return new ResponseEntity<Map>(body, HttpStatus.CREATED);
    }

    // 회원 정보 수정
    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(@PathVariable("member-id") long memberId,
                                      @RequestParam String phone) {
        Map<String, Object> body = new HashMap<>();
        body.put("memberId", memberId);
        body.put("email", "hgd@gmail.com");
        body.put("name", "홍길동");
        body.put("phone", phone);

        // No need Business logic

        return new ResponseEntity<Map>(body, HttpStatus.OK);
    }
    
    // 한명의 회원 정보 조회
    @GetMapping("/{member-id}")
    public ResponseEntity getMember(@PathVariable("member-id") long memberId) {
        System.out.println("# memberId: " + memberId);

        // not implementation
        return new ResponseEntity<Map>(HttpStatus.OK);
    }

    // 모든 회원 정보 조회
    @GetMapping
    public ResponseEntity getMembers() {
        System.out.println("# get Members");

        // not implementation

        return new ResponseEntity<Map>(HttpStatus.OK);
    }
    
    // 회원 정보 삭제
    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(@PathVariable("member-id") long memberId) {
        // No need business logic

        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}

DTO가 적용되지 않은 레거시 MemberController 코드이다.

DTO 클래스 적용을 위한 코드 리팩토링 절차는 다음과 같다.

  • 회원 정보를 전달 받을 DTO 클래스를 생성한다.
    • MemberController에서 현재 회원 정보로 전달받는 각 데이터 항목(email, name, phone)들을 DTO 클래스의 멤버 변수로 추가하면 된다.
  • 클라이언트 쪽 전달하는 요청 데이터를 @RequestParam 애너테이션으로 전달 받는 핸들러 메서드를 찾는다.
    • Request Body가 필요한 핸들러는 HTTP POST, PATCH, PUT 같이 리소스의 추가나 변경이 발생할 때다. HTTP GET은 리소스를 조회하는 용도이기에 Request Body가 필요 없다.
    • 결국 @PostMapping, @PatchMapping 애너테이션이 붙은 핸들러 메서드를 찾는 것과 동일하다고 볼 수 있다.
  • @RequestParam 쪽 코드를 DTO 클래스의 객체로 수정한다.
  • Map 객체로 작성되어 있는 Response Body를 DTO 클래스의 객체로 변경한다.

✔️ MemberPostDto 및 MemberPatchDto 클래스 생성

DTO 클래스 생성 / Request Body를 전달 받을 때 사용
public class MemberPostDto {
    private String email;
    private String name;
    private String phone;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }
}
회원 정보 수정 시 사용하는 MemberPatchDto

public class MemberPatchDto {
    private long memberId;
    private String name;
    private String phone;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public long getMemberId() {
        return memberId;
    }

    public void setMemberId(long memberId) {
        this.memberId = memberId;
    }
}

DTO 클래스를 만들 때, getter 메서드가 있어야한다.

✔️ MemberController에 DTO 클래스 적용

import com.codestates.member.MemberPatchDto;
import com.codestates.member.MemberPostDto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/v1/members")
public class MemberController {
    // 회원 정보 등록
    @PostMapping
    public ResponseEntity postMember(@RequestBody MemberPostDto memberPostDto) {
        return new ResponseEntity<>(memberPostDto, HttpStatus.CREATED);
    }

    // 회원 정보 수정
    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(@PathVariable("member-id") long memberId,
                                      @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);
        memberPatchDto.setName("홍길동");

        // No need Business logic

        return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
    }

    // 한명의 회원 정보 조회
    @GetMapping("/{member-id}")
    public ResponseEntity getMember(@PathVariable("member-id") long memberId) {
        System.out.println("# memberId: " + memberId);

        // not implementation
        return new ResponseEntity<>(HttpStatus.OK);
    }

    // 모든 회원 정보 조회
    @GetMapping
    public ResponseEntity getMembers() {
        System.out.println("# get Members");

        // not implementation

        return new ResponseEntity<>(HttpStatus.OK);
    }

    // 회원 정보 삭제
    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(@PathVariable("member-id") long memberId) {
        // No need business logic

        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}

이렇게 코드를 친 후에


Postman에서 JSON 형식으로 보내면 올바르게 작동한다.


⭐핵심 포인트

  • DTO는 Data Transfer Object의 약자로 마틴 파울러(Martin Fowler)가 'Patterns of Enterprise Application Architecture' 라는 책에서 처음 소개한 엔터프라이즈 애플리케이션 아키텍쳐 패턴의 하나이다.

  • DTO는 주로 클라이언트에서 서버 쪽으로 전송하는 요청 데이터를 전달 받을 때, 서버에서 클라이언트 쪽으로 전송하는 응답 데이터를 전송하기 위한 용도로 사용된다.

  • DTO가 필요한 이유

    • 클라이언트의 Request Body를 하나의 객체로 모두 전달 받을 수 있기때문에 코드 자체가 간결해진다.
    • Request Body의 데이터 유효성(Validation) 검증이 단순해진다.
  • JSON 형식의 Request Body를 전달 받기 위해서는 DTO 객체에 @ResponseBody 애너테이션을 붙여야한다.

  • Response Body를 JSON 형식으로 전달하기 위해서는 @ResponseBody 애너테이션을 메서드 앞에 붙여 주어야하지만 ResponseEntity 객체를 리턴 값으로 사용할 경우 @ResponseBody를 생략할 수 있다.

  • 클라이언트 쪽에서 JSON 형식의 데이터를 서버 쪽으로 전송하면 서버 쪽의 웹 애플리케이션은 전달 받은 JSON 형식의 데이터를 DTO 같은 Java의 객체로 변환하는데 이를 역직렬화(Deserialization)이라고 한다.

  • 서버 쪽에서 클라이언트에게 응답 데이터를 전송하기 위해서 DTO 같은 Java의 객체를 JSON 형식으로 변환하는 것을 직렬화(Serialization)라고 한다.

profile
프로젝트, 오류, CS 공부, 코테 등을 꾸준히 기록하는 저만의 기술 블로그입니다!

0개의 댓글