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

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) 데이터 검증의 단순화

    @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 요청을 받는 핸들러 메서드는 요청을 받는 것이 주 목적이므로 최대한 간결하게 작성하는 것이 좋다.

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

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

}

이메일 멤버 변수에 @Email 주석을 추가한 경우 이메일이 클라이언트의 요청 데이터에서 유효한 형식이 아닌 경우 유효성 검사가 실패하고 클라이언트의 요청이 거부됩니다.

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

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

}

DTO에 유효성 검사를 적용하면 위와 같이 컨트롤러 코드가 더 깔끔해집니다.

  • HTTP 요청 수를 줄이기 위해(잘못된 요청은 모두 차단됨)
  • 도메인 개체와의 분리

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

(1) HTTP 요청 본문이 JSON 형식이 아닌 경우

  • x-www-form-urlencoded 형식


(2) HTTP 요청 본문이 JSON 형식인 경우

@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", "[email protected]");
        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);
    }
}

  1. 구성원 정보를 받을 DTO 클래스를 만듭니다.
    • MemberController로부터 현재 멤버 정보로 받은 각 데이터 항목(이메일, 이름, 전화번호)을 DTO 클래스의 멤버 변수로 추가
  2. @RequestParam 어노테이션을 통해 클라이언트 측에서 전송한 요청 데이터를 받는 핸들러 메서드를 찾습니다.
  3. @RequestParam 측의 코드를 DTO 클래스의 객체로 수정합니다.
  4. Map 객체로 작성된 Response Body를 DTO 클래스의 객체로 변경합니다.

  • 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;
    
}

@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);
    }
}

추가 키워드