From de21b0d9db0f85cf90a6c123eb271cf74a58ee58 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Tue, 6 Aug 2024 16:50:26 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20feat:=20=ED=97=A4=EB=8D=94=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=20=EC=97=AC=EB=B6=80=20=EA=B2=80=EC=A6=9D=20(#207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 인터페이스와 구현체 어노테이션 일치 * feat: 헤더 검사 어노테이션 * feat: 헤더 밸리데이터 * feat: 컨트롤러에 헤더 검사 * feat: 헤더 검증 메시지 클라이언트에 전달 * fix: 누락된 `@Valid` 어노테이션 추가 * refactor: 요청이 null인 경우 핸들링 * chore: 테스트 이름 간결하게 변경 * chore: 소문자 컨벤션 * feat: ArgumentResolver를 활용한 헤더 검증 * feat: 공용 패키지로 이동 및 범용적으로 사용할 수 있도록 수정 * chore: 사용하지 않는 import 제거 * feat: ArgumentResolver 적용, Validator 삭제 * chore: 사용하지 않는 커스텀 예외 원복 * refactor: 헤더 존재하지 않는 경우 메시지 수정 * chore: 불필요한 `@Valid` 어노테이션 삭제 * refactor: 변수 추출 Co-authored-by: Yeongseo Na * fix: 컴파일 에러 해결 --------- Co-authored-by: Yeongseo Na --- .../main/java/reviewme/config/WebConfig.java | 16 ++++++ .../global/GlobalExceptionHandler.java | 6 +- .../java/reviewme/global/HeaderProperty.java | 18 ++++++ .../HeaderPropertyArgumentResolver.java | 32 +++++++++++ .../MissingHeaderPropertyException.java | 8 +++ .../review/controller/ReviewController.java | 13 +++-- .../InvalidGroupAccessCodeException.java | 10 ---- .../InvalidReviewRequestCodeException.java | 0 .../HeaderPropertyArgumentResolverTest.java | 56 +++++++++++++++++++ .../review/service/ReviewServiceTest.java | 1 - 10 files changed, 141 insertions(+), 19 deletions(-) create mode 100644 backend/src/main/java/reviewme/config/WebConfig.java create mode 100644 backend/src/main/java/reviewme/global/HeaderProperty.java create mode 100644 backend/src/main/java/reviewme/global/HeaderPropertyArgumentResolver.java create mode 100644 backend/src/main/java/reviewme/global/exception/MissingHeaderPropertyException.java delete mode 100644 backend/src/main/java/reviewme/review/service/exception/InvalidGroupAccessCodeException.java delete mode 100644 backend/src/main/java/reviewme/review/service/exception/InvalidReviewRequestCodeException.java create mode 100644 backend/src/test/java/reviewme/global/HeaderPropertyArgumentResolverTest.java diff --git a/backend/src/main/java/reviewme/config/WebConfig.java b/backend/src/main/java/reviewme/config/WebConfig.java new file mode 100644 index 000000000..423c8f0e5 --- /dev/null +++ b/backend/src/main/java/reviewme/config/WebConfig.java @@ -0,0 +1,16 @@ +package reviewme.config; + +import java.util.List; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import reviewme.global.HeaderPropertyArgumentResolver; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new HeaderPropertyArgumentResolver()); + } +} diff --git a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java index c5260df73..b3423e102 100644 --- a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java +++ b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java @@ -73,8 +73,10 @@ public ProblemDetail handleServletRequestBindingException(Exception ex) { return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "요청을 읽을 수 없습니다."); } - @ExceptionHandler({MethodValidationException.class, BindException.class, - TypeMismatchException.class, HandlerMethodValidationException.class}) + @ExceptionHandler({ + MethodValidationException.class, BindException.class, + TypeMismatchException.class, HandlerMethodValidationException.class + }) public ProblemDetail handleRequestFormatException(Exception ex) { return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "요청의 형식이 잘못되었습니다."); } diff --git a/backend/src/main/java/reviewme/global/HeaderProperty.java b/backend/src/main/java/reviewme/global/HeaderProperty.java new file mode 100644 index 000000000..86462c596 --- /dev/null +++ b/backend/src/main/java/reviewme/global/HeaderProperty.java @@ -0,0 +1,18 @@ +package reviewme.global; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface HeaderProperty { + + @AliasFor("headerName") + String value() default ""; + + @AliasFor("value") + String headerName() default ""; +} diff --git a/backend/src/main/java/reviewme/global/HeaderPropertyArgumentResolver.java b/backend/src/main/java/reviewme/global/HeaderPropertyArgumentResolver.java new file mode 100644 index 000000000..5c825e3de --- /dev/null +++ b/backend/src/main/java/reviewme/global/HeaderPropertyArgumentResolver.java @@ -0,0 +1,32 @@ +package reviewme.global; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import reviewme.global.exception.MissingHeaderPropertyException; + +public class HeaderPropertyArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(HeaderProperty.class); + } + + @Override + public String resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + + HeaderProperty parameterAnnotation = parameter.getParameterAnnotation(HeaderProperty.class); + String headerName = parameterAnnotation.headerName(); + String headerProperty = request.getHeader(headerName); + + if (headerProperty == null) { + throw new MissingHeaderPropertyException(headerName); + } + return headerProperty; + } +} diff --git a/backend/src/main/java/reviewme/global/exception/MissingHeaderPropertyException.java b/backend/src/main/java/reviewme/global/exception/MissingHeaderPropertyException.java new file mode 100644 index 000000000..1da1ae3f0 --- /dev/null +++ b/backend/src/main/java/reviewme/global/exception/MissingHeaderPropertyException.java @@ -0,0 +1,8 @@ +package reviewme.global.exception; + +public class MissingHeaderPropertyException extends BadRequestException { + + public MissingHeaderPropertyException(String headerName) { + super("요청에 %s이(가) 존재하지 않아요.".formatted(headerName)); + } +} diff --git a/backend/src/main/java/reviewme/review/controller/ReviewController.java b/backend/src/main/java/reviewme/review/controller/ReviewController.java index a56198ea6..3e338f34e 100644 --- a/backend/src/main/java/reviewme/review/controller/ReviewController.java +++ b/backend/src/main/java/reviewme/review/controller/ReviewController.java @@ -1,6 +1,5 @@ package reviewme.review.controller; -import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import java.net.URI; import lombok.RequiredArgsConstructor; @@ -11,6 +10,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import reviewme.global.HeaderProperty; import reviewme.review.dto.request.CreateReviewRequest; import reviewme.review.dto.response.ReceivedReviewsResponse; import reviewme.review.dto.response.ReviewDetailResponse; @@ -32,8 +32,9 @@ public ResponseEntity createReview(@Valid @RequestBody CreateReviewRequest } @GetMapping("/reviews") - public ResponseEntity findReceivedReviews(HttpServletRequest request) { - String groupAccessCode = request.getHeader(GROUP_ACCESS_CODE_HEADER); + public ResponseEntity findReceivedReviews( + @HeaderProperty(GROUP_ACCESS_CODE_HEADER) String groupAccessCode + ) { ReceivedReviewsResponse response = reviewService.findReceivedReviews(groupAccessCode); return ResponseEntity.ok(response); } @@ -45,9 +46,9 @@ public ResponseEntity findReviewCreationSetup(@RequestParam } @GetMapping("/reviews/{id}") - public ResponseEntity findReceivedReviewDetail(@PathVariable long id, - HttpServletRequest request) { - String groupAccessCode = request.getHeader(GROUP_ACCESS_CODE_HEADER); + public ResponseEntity findReceivedReviewDetail( + @PathVariable long id, + @HeaderProperty(GROUP_ACCESS_CODE_HEADER) String groupAccessCode) { ReviewDetailResponse response = reviewService.findReceivedReviewDetail(groupAccessCode, id); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/reviewme/review/service/exception/InvalidGroupAccessCodeException.java b/backend/src/main/java/reviewme/review/service/exception/InvalidGroupAccessCodeException.java deleted file mode 100644 index c392eb315..000000000 --- a/backend/src/main/java/reviewme/review/service/exception/InvalidGroupAccessCodeException.java +++ /dev/null @@ -1,10 +0,0 @@ -package reviewme.review.service.exception; - -import reviewme.global.exception.BadRequestException; - -public class InvalidGroupAccessCodeException extends BadRequestException { - - public InvalidGroupAccessCodeException() { - super("올바르지 않은 확인 코드예요."); - } -} diff --git a/backend/src/main/java/reviewme/review/service/exception/InvalidReviewRequestCodeException.java b/backend/src/main/java/reviewme/review/service/exception/InvalidReviewRequestCodeException.java deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/src/test/java/reviewme/global/HeaderPropertyArgumentResolverTest.java b/backend/src/test/java/reviewme/global/HeaderPropertyArgumentResolverTest.java new file mode 100644 index 000000000..fdaae95df --- /dev/null +++ b/backend/src/test/java/reviewme/global/HeaderPropertyArgumentResolverTest.java @@ -0,0 +1,56 @@ +package reviewme.global; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.NativeWebRequest; +import reviewme.global.exception.MissingHeaderPropertyException; + +class HeaderPropertyArgumentResolverTest { + + private final HeaderPropertyArgumentResolver resolver = new HeaderPropertyArgumentResolver(); + private final MethodParameter parameter = mock(MethodParameter.class); + private final HeaderProperty headerProperty = mock(HeaderProperty.class); + + @BeforeEach + void setUp() { + given(parameter.hasParameterAnnotation(HeaderProperty.class)).willReturn(true); + given(parameter.getParameterAnnotation(HeaderProperty.class)).willReturn(headerProperty); + } + + @Test + void 검증값이_헤더에_존재하지_않으면_검증에_실패한다() { + // given + NativeWebRequest request = mock(NativeWebRequest.class); + given(request.getNativeRequest()).willReturn(new MockHttpServletRequest()); + given(headerProperty.headerName()).willReturn("test"); + + // when, then + assertThatThrownBy(() -> resolver.resolveArgument(parameter, null, request, null)) + .isInstanceOf(MissingHeaderPropertyException.class); + } + + @Test + void 검증값이_헤더에_존재하면_값을_반환한다() { + // given + String headerName = "test"; + String headerValue = "1234"; + NativeWebRequest request = mock(NativeWebRequest.class); + MockHttpServletRequest mockRequest = (new MockHttpServletRequest()); + mockRequest.addHeader(headerName, headerValue); + given(request.getNativeRequest()).willReturn(mockRequest); + given(headerProperty.headerName()).willReturn(headerName); + + // when + String actual = resolver.resolveArgument(parameter, null, request, null); + + // then + assertThat(actual).isEqualTo(headerValue); + } +} diff --git a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java index e5c5d3663..be62e6f80 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java @@ -31,7 +31,6 @@ import reviewme.review.repository.ReviewContentRepository; import reviewme.review.repository.ReviewKeywordRepository; import reviewme.review.repository.ReviewRepository; -import reviewme.review.service.exception.InvalidGroupAccessCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.support.ServiceTest;