API 계층 - DTO

jungseo·2023년 6월 12일
0

Spring

목록 보기
4/23

HTTP 요청/응답 DTO

DTO(Data Transfer Object)

  • 데이터를 전송하기 위한 객체
  • API Layer - Controller 예시 코드의 경우 요청 데이터가 늘어날 경우 @RequestParam 애너테이션의 개수도 증가함
  • DTO를 통해 클라이언트의 요청 데이터를 하나의 객체로 모두 전달 가능
  • 유효성 검사를 DTO내에서 실행해 핸들러 메서드의 간결함을 유지 가능
  • HTTP 요청 수를 줄이기 위해 사용

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

  • MemberPostDTO class
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 class
public class MemberPatchDTO {
    private long memberId;
    private String name;
    private String phone;

    public long getMemberId() {
        return memberId;
    }

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

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

  • MemberController class
// 해당 클래스가 REST API의 리소스를 처리하기 위한 API 엔드포인트로 동작함을 정의
@RestController

// 클라이언트 요청과 요청을 처리하는 핸들러 메서드를 매핑
// 클래스 전체에 사용되는 공통 URL 설정
@RequestMapping("/v1/members")
public class MemberController {

//    회원 정보 등록
    @PostMapping // @RequestBody : 클라이언트 쪽에서 전송한 JSON 형식의 Request Body를 DTO 클래스의 객체로 변환
    public ResponseEntity postMember(@RequestBody MemberPostDTO memberPostDTO) {
        return new ResponseEntity<>(memberPostDTO, HttpStatus.CREATED);
    }

    //    회원 정보 수정
    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(@PathVariable("member-id") long memberId,
                                      @Valid @RequestBody MemberPatchDTO memberPatchDTO) {
        memberPatchDTO.setMemberId(memberId);

        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) {

        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}
  • 각 멤버 변수에 해당하는 getter 메서드가 있어야 함
  • HTTP Request Body가 JSON 형식이어야 하기 때문에 클라이언트 쪽에서 전달하는 Request Body도 JSON 형식으로 입력
  • @ReuestBody 애너테이션
    • JSON 형식의 Request Body를 DTO 클래스의 객체로 변환시켜줌(역직렬화)
  • @ResponseBody 애너테이션
    • DTO 클래스의 객체를 JSON 형식의 Response Body로 변환(직렬화)
    • 핸들러 메서드의 반환값이 ResponseEntity인 경우 생략 가능
    • @ResponseBody 애너테이션이 붙거나 핸들러 메서드의 반환 값이 ResponseEntity인 경우 내부적으로 HTTPMessageConverter가 동작하여 응답 객체를 JSON 형식으로 바꿔줌

DTO 유효성 검증(Validation)

Jakarta Bean Validation

  • API가 아닌 스펙
  • 해당 스펙의 구현체가 Hibernate Validator
  • Java Bean 스펙을 준수하는 Java 클래스라면 사용 가능

1. 예시 코드

  • 유효성 검증을 적용한 MemberPostDTO class
public class MemberPatchDTO {
    private long memberId;

    @Pattern(regexp = "^\\S+(\\s?\\S+)*$", message = "회원 이름은 공백이 아니어야 합니다.")
    private String name;

    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
            message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
    private String phone;

    public long getMemberId() {
        return memberId;
    }

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

    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 class
public class MemberPatchDTO {
    private long memberId;

    @Pattern(regexp = "^\\S+(\\s?\\S+)*$", message = "회원 이름은 공백이 아니어야 합니다.")
    private String name;

    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
            message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
    private String phone;

    public long getMemberId() {
        return memberId;
    }

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

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

  • 유효성 검증을 적용한 MemberController class
@RestController
@RequestMapping("/v1/members")
@Validated
public class MemberController {

//    회원 정보 등록
    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDTO memberPostDTO) {
        return new ResponseEntity<>(memberPostDTO, HttpStatus.CREATED);
    }

    //    회원 정보 수정
    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(@PathVariable("member-id") @Min(1) long memberId,
                                      @Valid @RequestBody MemberPatchDTO memberPatchDTO) {
        memberPatchDTO.setMemberId(memberId);

        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) {

        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}

2. 필드 유효성 검증 애너테이션

  • @Valid : 핸들러 메서드에 추가하여 유효성 검증
  • @Email
    • 유효한 이메일 형식인지 검증
  • @NotBlank
    • 데이터가 비어있지 않은지
    • null, 공백(""), 스페이스(" ") 허용하지 않음
  • @Pattern
    • @Pattern(regexp = "^\S+(\s?\S+)*$")
    • 정규표현식에 매치되는지 검증
    • null인 경우 유효성 검사 수행하지 않음
    • 문자열 필드에 적용
    • 정규표현식
      • '^' : 문자열의 시작
      • '$' : 문자열의 끝
      • '*' : 앞에 평가할 대상이 0개 또는 1개 이상인지
      • '\s' : 앞에 평가할 대상이 공백 문자열인지
      • '\S' : 앞에 평가할 대상이 공백이 아닌 문자열인지
      • '?' : 앞에 평가할 대상이 0개 또는 1개 인지
      • '+' : 앞에 평가할 대상이 1개인지
    • 정규표현식 예시
      • 숫자 검사 : "^[0-9]*$"
      • 영어 검사 : "^[a-zA-Z]*$"
      • 한글 검사 : "[가-힣]*$"
  • 정수형일 경우 Integer 타입으로 @Min, @Max 혹은 @Range(min = 1, max = 10) 등으로 제한 가능

3. 쿼리 파라미터, @Pathvariable 유효성 검증

  • @Validated : 클래스 레벨에 사용하여 유효성 검증
  • 핸들러 메서드에 @Min(1) 등으로 제약 가능

Custom Validator 유효성 검사

공백을 허용하지 않는 Custom Validator 구현

1. Custom Validator를 사용하기 위한 Custom Annotation을 정의

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {NotSpaceValidator.class}) // (1)
public @interface NotSpace {
    String message() default "공백이 아니어야 합니다"; // (2)
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  • (1) : 해당 애너테이션이 멤버 변수에 추가됐을때 동작할 Custom Validator를 추가
  • (2) : 검증 실패 시, 표시되는 디폴트 메시지

2. 정의한 Custom Annotation에 바인딩되는 Custom Validator를 구현

import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class NotSpaceValidator implements ConstraintValidator<NotSpace, String> {

    @Override
    public void initialize(NotSpace constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value == null || StringUtils.hasText(value);
    }
}
  • ConstraintValidator<NotSpace, String>
    • NotSpace는 CustomValidator와 매핑된 Custom Annotation을 의미
    • String은 Custom Annotation으로 검증할 대상의 멤버 변수 타입

3. 유효성 검증이 필요한 DTO 클래스의 멤버 변수에 Custom Annotation을 추가한다.

public class MemberPatchDto {
    private long memberId;

    @NotSpace(message = "회원 이름은 공백이 아니어야 합니다") // (1)
    private String name;

    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
            message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다")
    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;
    }
}
  • @Pattern(regexp ="^\S+(\s?\S+)*$") 대신 구현한 Custom Annotation @NotSpace 추가

0개의 댓글