[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개의 댓글

관련 채용 정보