Skip to content

Commit

Permalink
클라이언트 예외를 위한 Validator 추가 (#117)
Browse files Browse the repository at this point in the history
* refactor: ClientRequestException 일반 클래스로 변경

* refactor: ErrorResponse 에서 httpStatus 제거

* refactor: RunnerPostUpdateRequest record로 변경

* refactor: ClientErrorCode 내용 추가

* refactor: record로 생긴 변화 적용

* feat: NotNullValid 어노테이션 추가

* refactor: ErrorResponse 에 getter 추가

* feat: Controller valid 추가

* feat: ValidFuture 어노테이션 추가

* feat: 어노테이션 이름 변경

* feat: Max validator 추가

* feat: request dto에 validation 적용

* refactor: 어노테이션 target 조정

* refactor: tag 값이 없을 때 Bad Request 보내도록 수정
  • Loading branch information
shb03323 authored Jul 27, 2023
1 parent 9e55b40 commit 63b978d
Show file tree
Hide file tree
Showing 15 changed files with 242 additions and 103 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package touch.baton.domain.common;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import touch.baton.domain.common.exception.ClientRequestException;
import touch.baton.domain.common.response.ErrorResponse;

@RestControllerAdvice
public class GlobalControllerAdvice {

@ExceptionHandler(ClientRequestException.class)
public ResponseEntity<ErrorResponse> handleClientRequest(ClientRequestException e) {
return ResponseEntity.status(e.getHttpStatus().value()).body(ErrorResponse.from(e));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@
import org.springframework.http.HttpStatus;

public enum ClientErrorCode implements ErrorCode {
;

TITLE_IS_NULL(HttpStatus.BAD_REQUEST, "RP001", "제목을 입력해주세요."),
PULL_REQUEST_URL_IS_NULL(HttpStatus.BAD_REQUEST, "RP002", "PR 주소를 입력해주세요."),
DEADLINE_IS_NULL(HttpStatus.BAD_REQUEST, "RP003", "마감일을 입력해주세요."),
CONTENTS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP004", "내용을 입력해주세요."),
CONTENTS_OVERFLOW(HttpStatus.BAD_REQUEST, "RP005", "내용은 1000자 까지 입력해주세요."),
PAST_DEADLINE(HttpStatus.BAD_REQUEST, "RP006", "마감일은 오늘보다 과거일 수 없습니다."),
CONTENTS_NOT_FOUND(HttpStatus.NOT_FOUND, "RP007", "존재하지 않는 게시물입니다."),
TAGS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP008", "태그 목록을 빈 값이라도 입력해주세요.");

private final HttpStatus httpStatus;
private final String errorCode;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package touch.baton.domain.common.exception;

public abstract class ClientRequestException extends BaseException {
public class ClientRequestException extends BaseException {

public ClientRequestException(final ClientErrorCode errorCode) {
super(errorCode);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package touch.baton.domain.common.exception.validator;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import touch.baton.domain.common.exception.ClientErrorCode;
import touch.baton.domain.common.exception.ClientRequestException;

public class NotNullValidator implements ConstraintValidator<ValidNotNull, Object> {

private ClientErrorCode errorCode;

@Override
public void initialize(final ValidNotNull constraintAnnotation) {
errorCode = constraintAnnotation.clientErrorCode();
}

@Override
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
if (value == null) {
throw new ClientRequestException(errorCode);
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package touch.baton.domain.common.exception.validator;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import touch.baton.domain.common.exception.ClientErrorCode;

import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ FIELD, PARAMETER })
@Retention(RUNTIME)
@Repeatable(ValidNotNull.List.class)
@Documented
@Constraint(validatedBy = NotNullValidator.class)
public @interface ValidNotNull {

String message() default "null 값이 존재합니다.";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

ClientErrorCode clientErrorCode();

@Target({ FIELD, PARAMETER })
@Retention(RUNTIME)
@Documented
@interface List {

ValidNotNull[] value();
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
package touch.baton.domain.common.response;

import org.springframework.http.HttpStatus;
import lombok.Getter;
import touch.baton.domain.common.exception.BaseException;

@Getter
public class ErrorResponse {

private final HttpStatus status;
private final String errorCode;
private final String message;

private ErrorResponse(final HttpStatus status, final String errorCode, final String message) {
this.status = status;
private ErrorResponse(final String errorCode, final String message) {
this.errorCode = errorCode;
this.message = message;
}

public static ErrorResponse from(final BaseException e) {
return new ErrorResponse(e.getHttpStatus(), e.getErrorCode(), e.getMessage());
return new ErrorResponse(e.getErrorCode(), e.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package touch.baton.domain.runnerpost.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand Down Expand Up @@ -32,7 +33,7 @@ public class RunnerPostController {
private final RunnerService runnerService;

@PostMapping
public ResponseEntity<Void> createRunnerPost(@RequestBody RunnerPostCreateRequest request) {
public ResponseEntity<Void> createRunnerPost(@Valid @RequestBody RunnerPostCreateRequest request) {
// TODO 07/19 로그인 기능 개발시 1L 변경 요망
Runner runner = runnerService.readRunnerWithMember(1L);

Expand All @@ -46,7 +47,7 @@ public ResponseEntity<Void> createRunnerPost(@RequestBody RunnerPostCreateReques
}

@PostMapping("/test")
public ResponseEntity<Void> createRunnerPostVersionTest(@RequestBody RunnerPostCreateTestRequest request) {
public ResponseEntity<Void> createRunnerPostVersionTest(@Valid @RequestBody RunnerPostCreateTestRequest request) {
// TODO 07/19 로그인 기능 개발시 1L 변경 요망
Runner runner = runnerService.readRunnerWithMember(1L);

Expand Down Expand Up @@ -84,7 +85,7 @@ public ResponseEntity<Void> deleteByRunnerPostId(@PathVariable final Long runner

@PutMapping("/{runnerPostId}")
public ResponseEntity<Void> update(@PathVariable final Long runnerPostId,
@RequestBody final RunnerPostUpdateRequest request
@Valid @RequestBody final RunnerPostUpdateRequest request
) {
final Long updatedId = runnerPostService.updateRunnerPost(runnerPostId, request);
final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/posts/runner")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package touch.baton.domain.runnerpost.exception.validator;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import touch.baton.domain.common.exception.ClientErrorCode;
import touch.baton.domain.common.exception.ClientRequestException;

import java.time.LocalDateTime;

public class FutureValidator implements ConstraintValidator<ValidFuture, LocalDateTime> {

private ClientErrorCode errorCode;

@Override
public void initialize(final ValidFuture constraintAnnotation) {
errorCode = constraintAnnotation.clientErrorCode();
}

@Override
public boolean isValid(final LocalDateTime value, final ConstraintValidatorContext context) {
if (value.isBefore(LocalDateTime.now())) {
throw new ClientRequestException(errorCode);
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package touch.baton.domain.runnerpost.exception.validator;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import touch.baton.domain.common.exception.ClientErrorCode;
import touch.baton.domain.common.exception.ClientRequestException;

public class MaxLengthValidator implements ConstraintValidator<ValidMaxLength, String> {

private ClientErrorCode errorCode;
private int max;

@Override
public void initialize(final ValidMaxLength constraintAnnotation) {
errorCode = constraintAnnotation.clientErrorCode();
max = constraintAnnotation.max();
}

@Override
public boolean isValid(final String value, final ConstraintValidatorContext context) {
if (value.length() > max) {
throw new ClientRequestException(errorCode);
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package touch.baton.domain.runnerpost.exception.validator;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import touch.baton.domain.common.exception.ClientErrorCode;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ FIELD, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = FutureValidator.class)
public @interface ValidFuture {

String message() default "마감일은 오늘보다 과거일 수 없습니다.";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

ClientErrorCode clientErrorCode();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package touch.baton.domain.runnerpost.exception.validator;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import touch.baton.domain.common.exception.ClientErrorCode;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ FIELD, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = MaxLengthValidator.class)
public @interface ValidMaxLength {

String message() default "길이가 잘못되었습니다.";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

ClientErrorCode clientErrorCode();

int max();
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,10 @@ public Long updateRunnerPost(final Long runnerPostId, final RunnerPostUpdateRequ
// TODO: 메소드 분리
final RunnerPost runnerPost = runnerPostRepository.findById(runnerPostId)
.orElseThrow(() -> new IllegalArgumentException("잘못된 runnerPostId 입니다."));
runnerPost.updateTitle(new Title(request.getTitle()));
runnerPost.updateContents(new Contents(request.getContents()));
runnerPost.updatePullRequestUrl(new PullRequestUrl(request.getPullRequestUrl()));
runnerPost.updateDeadLine(new Deadline(request.getDeadline()));
runnerPost.updateTitle(new Title(request.title()));
runnerPost.updateContents(new Contents(request.contents()));
runnerPost.updatePullRequestUrl(new PullRequestUrl(request.pullRequestUrl()));
runnerPost.updateDeadLine(new Deadline(request.deadline()));

final List<RunnerPostTag> presentRunnerPostTags =
runnerPostTagRepository.joinTagByRunnerPostId(runnerPost.getId());
Expand All @@ -162,7 +162,7 @@ public Long updateRunnerPost(final Long runnerPostId, final RunnerPostUpdateRequ

// TODO: 새로운 tag 로 교체 메소드 분리
final List<RunnerPostTag> removedRunnerPostTags = new ArrayList<>(presentRunnerPostTags);
for (String tagName : request.getTags()) {
for (String tagName : request.tags()) {
final Optional<RunnerPostTag> existRunnerPostTag = presentRunnerPostTags.stream()
.filter(presentRunnerPostTag -> presentRunnerPostTag.isSameTagName(tagName))
.findFirst();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
package touch.baton.domain.runnerpost.service.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import touch.baton.domain.common.exception.ClientErrorCode;
import touch.baton.domain.common.exception.validator.ValidNotNull;
import touch.baton.domain.runnerpost.exception.validator.ValidFuture;
import touch.baton.domain.runnerpost.exception.validator.ValidMaxLength;

import java.time.LocalDateTime;
import java.util.List;

public record RunnerPostCreateRequest(String title,
public record RunnerPostCreateRequest(@ValidNotNull(clientErrorCode = ClientErrorCode.TITLE_IS_NULL)
String title,
@ValidNotNull(clientErrorCode = ClientErrorCode.TAGS_ARE_NULL)
List<String> tags,
@ValidNotNull(clientErrorCode = ClientErrorCode.PULL_REQUEST_URL_IS_NULL)
String pullRequestUrl,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm", timezone = "Asia/Seoul")
@ValidNotNull(clientErrorCode = ClientErrorCode.DEADLINE_IS_NULL)
@ValidFuture(clientErrorCode = ClientErrorCode.PAST_DEADLINE)
LocalDateTime deadline,
@ValidNotNull(clientErrorCode = ClientErrorCode.CONTENTS_ARE_NULL)
@ValidMaxLength(clientErrorCode = ClientErrorCode.CONTENTS_OVERFLOW, max = 1000)
String contents
) {
}
Loading

0 comments on commit 63b978d

Please sign in to comment.