노션으로 다시 돌아갔습니다 😅

HTTP 요청/응답에서의 DTO(Data Transfer Object)

by mignon25

DTO(Data Transfer Object)란?

  • 엔터프라이즈 애플리케이션 아키텍처 패턴의 하나
  • 데이터를 전송하기 위한 용도의 객체
  • 클라이언트의 요청 데이터, 서버의 응답 데이터에 사용될 수 있다.

 

DTO가 필요한 이유 (1)  코드가 간결해진다. 

  • 여러 정보를 전달 받아야 할 때, 각각의 데이터를 모두 각각의 파라미터로 전달받게 되면 코드가 복잡해진다.
  • 그러한 파라미터들을 하나의 객체로 모은 DTO 클래스를 적용하면 다음과 같이 코드가 굉장히 간결해진다.
@RestController
@RequestMapping("/dto/members")
public class DTOMemberController {
    
    @PostMapping
    public ResponseEntity postMember(MemberDto memberDto) {
        return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
    }
    
}

 

 

DTO가 필요한 이유 (2) 데이터 유효성(Validation) 검증의 단순화

<DTO 미적용시 유효성 검증>

    @PostMapping
    public ResponseEntity postMember(
            @RequestParam("email") String email,
            @RequestParam("name") String name,
            @RequestParam("phone") String phone
    ) {

        // DTO 미적용 - email 유효성 검증
        if(!email.matches("^[a-zA-Z0-9_!#$%&'\\*+/=?{|}~^.-]+@[a-zA-Z0-9.-]+$")) {
            throw new InvalidParameterException();
        }
        
        ...
    }
  • 유효성 검증 로직이 핸들러 메서드 내에 직접적으로 포함되어 있다.
  • 다른 프로퍼티에 대한 유효성 검증도 필요하다면 핸들러 내의 코드는 유효성 검증 로직으로 넘쳐날 것이고, 그만큼 코드의 복잡도가 높아지게 된다.
HTTP 요청을 전달 받는 핸들러 메서드는 요청을 전달받는 것이 주 목적이기 때문에 최대한 간결하게 작성되는 것이 좋다.

 

<DTO 적용>

implementation 'org.springframework.boot:spring-boot-starter-validation'
@Getter
@Setter
public class MemberDto {

    @Email
    private String email;
    private String name;
    private String phone;

}

email 멤버 변수에 @Email 애너테이션을 추가하면 클라이언트의 요청 데이터에서 email이 유효한 형식이 아닐 경우 유효성 검증에 실패하고, 클라이언트의 요청은 거부된다.

@RestController
@RequestMapping("/dto/members")
public class DTOMemberController {

    @PostMapping
    public ResponseEntity postMember(@Valid MemberDto memberDto) { // @Valid 추가
        return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
    }

}

DTO에 유효성 검증을 적용함으로써 Controller의 코드가 위와 같이 깔끔해졌다.

 

<그 밖의 DTO 사용 목적>

  • HTTP 요청의 수를 줄이기 위함 (유효하지 않은 요청은 아예 차단된다)
  • 도메인 객체와의 분리

 

 

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

(1) HTTP Request Body 가 JSON 형식이 아닌 경우

  • x-www-form-urlencoded 형식

 

(2) HTTP Request Body 가 JSON 형식인 경우

<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, 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", "asdf@email.com");
        body.put("name", "네임1");
        body.put("phone", phone);

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

    // 1명의 회원 정보 조회
    @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("MemberController.getMembers");

        // not implementation

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

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

        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}

 

<Refactoring>

  1. 회원 정보를 전달받을 DTO 클래스를 생성한다.
    • MemberController 에서 현재 회원 정보로 전달받는 각 데이터 항목(email, name, phone)들을 DTO 클래스의 멤버 변수로 추가
  2. 클라이언트 쪽에서 전달하는 요청 데이터를 @RequestParam 애너테이션으로 전달받는 핸들러 메서드를 찾는다.
  3. @RequestParam 쪽 코드를 DTO 클래스의 객체로 수정한다.
  4. Map 객체로 작성되어 있는 Response Body 를 DTO 클래스의 객체로 변경한다.

 

<MemberPostDto 및 MemberPatchDto 클래스 생성>

  • DTO 클래스를 만들 때 getter 메서드가 반드시 있어야 한다. 
  • setter 메서드는 개발자의 필요에 따라 사용
@Setter
@Getter
public class MemberPostDto {
    
    private String email;
    private String name;
    private String phone;
    
}
@Getter
@Setter
public class MemberPatchDto {
    
    private long memberId;
    private String name;
    private String phone;
    
}

 

<MemberController에 DTO 적용>

@RestController
@RequestMapping("/dto/members")
public class DTOMemberController {

    // 회원 정보 등록
    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberPostDto) {
		// 회원 등록 로직
        return new ResponseEntity<MemberPostDto>(memberPostDto, HttpStatus.CREATED);
    }
    
    // 회원 정보 수정
    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(
            @PathVariable("member-id") long memberId,
            @Valid @RequestBody MemberPatchDto memberPatchDto) {

        // 회원 정보 수정 로직
        return new ResponseEntity<MemberPatchDto>(memberPatchDto, HttpStatus.OK);
    }
    

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

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

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

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

    // 회원 정보 삭제
    @DeleteMapping("/{member-id}")
    public ResponseEntity deleteMember(@PathVariable("member-id") long memberId) {
        System.out.println("DTOMemberController.deleteMember");
        System.out.println("memberId = " + memberId);

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

 

 

 

Additional Keywords

 

 

블로그의 정보

Mignon'S Dev Log

mignon25

활동하기