Skip to content
Merged
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ repositories {

dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-security")
// implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
// implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.example.log4u.common.advice;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import com.example.log4u.common.exception.ApiErrorResponse;
import com.example.log4u.common.exception.CommonErrorCode;
import com.example.log4u.common.exception.base.ErrorCode;
import com.example.log4u.common.exception.base.ServiceException;

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException e,
@NonNull HttpHeaders headers,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

headers랑 status 파라미터 안 쓰는 걸로 보이는데 맞나요?
맞으면 혹시 이 파라미터는 왜 필요한가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 headers와 status 파라미터는 직접 사용하고 있지 않지만, 이 메서드는 ResponseEntityExceptionHandler가 정의한 오버라이드 대상 메서드입니다. 따라서 시그니처 그대로 맞춰서 오버라이드하기 위해 작성이 필요합니다.

커스텀 응답 객체를 반환하고 있어서 직접 활용하고 있지 않지만,필요 시 응답 헤더를 추가하거나 상태 코드를 조정할 수 있도록 하기 위해 시그니처에 포함되어 있는거로 알고 있습니다.

@NonNull HttpStatusCode status,
@NonNull WebRequest request) {
HttpServletRequest servletRequest = ((ServletWebRequest)request).getRequest();

String requestUrl = servletRequest.getRequestURI();
String httpMethod = servletRequest.getMethod();
List<String> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.toList());

log.warn("Validation failed for request to {} {}. Errors: {}",
httpMethod, requestUrl, errors);
CommonErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER;
return handleExceptionInternal(e, errorCode);
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiErrorResponse> handleIllegalArgument(IllegalArgumentException e) {
String location = getExceptionLocation(e);

log.warn("Illegal argument encountered at {}: {}", location, e.getMessage());

CommonErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER;
return handleExceptionInternal(errorCode);
}

@ExceptionHandler(ServiceException.class)
public ResponseEntity<ApiErrorResponse> handleGiveMeTiConException(ServiceException e) {
String location = getExceptionLocation(e);
log.warn("Error invoke in our app at {}: {} ErrorCode: {}", location, e.getMessage(), e.getErrorCode());
ErrorCode errorCode = e.getErrorCode();
return handleExceptionInternal(errorCode);
}

@ExceptionHandler({Exception.class})
public ResponseEntity<ApiErrorResponse> handleAllException(Exception e) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 경우 로그에 HttpMethod나 API 경로 같은게 포함되면 더 로그 파악이 좋을 것 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그럴 수 있겠네요. 확인 감사합니다

String location = getExceptionLocation(e);
log.warn("Unhandled exception occurred at {}: {}", location, e.getMessage());

CommonErrorCode errorCode = CommonErrorCode.INTERNAL_SERVER_ERROR;
return handleExceptionInternal(errorCode);
}

private ResponseEntity<ApiErrorResponse> handleExceptionInternal(ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(errorCode));
}

private ApiErrorResponse makeErrorResponse(ErrorCode errorCode) {
return ApiErrorResponse.builder()
.errorMessage(errorCode.getErrorMessage())
.errorCode(errorCode.getHttpStatus().value())
.build();
}

private ResponseEntity<Object> handleExceptionInternal(BindException e, ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(e, errorCode));
}

private ApiErrorResponse makeErrorResponse(BindException e, ErrorCode errorCode) {
List<ApiErrorResponse.ValidationError> validationErrorList = e.getBindingResult()
.getFieldErrors()
.stream()
.map(ApiErrorResponse.ValidationError::of)
.collect(Collectors.toList());

return ApiErrorResponse.builder()
.errorMessage(errorCode.getErrorMessage())
.errorCode(errorCode.getHttpStatus().value())
.errors(validationErrorList)
.build();
}

private String getExceptionLocation(Exception e) {
StackTraceElement element = e.getStackTrace()[0];
return element.getClassName() + "." + element.getMethodName() + ":" + element.getLineNumber();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.log4u.common.exception;

import java.util.List;

import org.springframework.validation.FieldError;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@Builder
@RequiredArgsConstructor
public class ApiErrorResponse {
private final String errorMessage;
private final int errorCode;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
private final List<ValidationError> errors;

public record ValidationError(String field, String message) {

public static ValidationError of(final FieldError fieldError) {
return new ValidationError(fieldError.getField(), fieldError.getDefaultMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.log4u.common.exception;

import org.springframework.http.HttpStatus;

import com.example.log4u.common.exception.base.ErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum CommonErrorCode implements ErrorCode {

INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 요청입니다"),
UNAUTHENTICATED(HttpStatus.UNAUTHORIZED,"로그인이 필요한 기능입니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "접근 권한이 없습니다"),
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "요청 정보를 찾을 수 없습니다"),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다. 관리자에게 문의하세요.")
;

private final HttpStatus httpStatus;
private final String message;

@Override
public HttpStatus getHttpStatus() {
return this.httpStatus;
}

@Override
public String getErrorMessage() {
return this.message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.log4u.common.exception.base;

import org.springframework.http.HttpStatus;

public interface ErrorCode {
String name();
HttpStatus getHttpStatus();
String getErrorMessage();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.log4u.common.exception.base;


import lombok.Getter;

@Getter
public class ServiceException extends RuntimeException {

private final ErrorCode errorCode;

public ServiceException(ErrorCode errorCode) {
super(errorCode.getErrorMessage());
this.errorCode = errorCode;
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/example/log4u/common/external/ClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.log4u.common.external;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

import com.example.log4u.common.external.hanlder.ApiResponseErrorHandler;

@Configuration
public class ClientConfig {

@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new ApiResponseErrorHandler());
return restTemplate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.log4u.common.external.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class ExternalApiRequestException extends RuntimeException{

private final String statusCode;
private final String message;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.log4u.common.external.hanlder;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.stream.Collectors;

import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;

import com.example.log4u.common.external.exception.ExternalApiRequestException;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ApiResponseErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return !response.getStatusCode().is2xxSuccessful();
}

@Override
public void handleError(ClientHttpResponse response) throws IOException {
String body;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.getBody()))) {
body = reader.lines().collect(Collectors.joining("\n"));
}

log.error("API 호출 중 에러 발생: HTTP 상태 코드: {}, 응답 본문: {}", response.getStatusCode().value(), body);

throw new ExternalApiRequestException(response.getStatusCode().toString(),
"API 호출 중 에러 발생: " + response.getStatusCode().value() + " 응답 본문: " + body);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.log4u.domain.comment.exception;

import org.springframework.http.HttpStatus;

import com.example.log4u.common.exception.base.ErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum CommentErrorCode implements ErrorCode {

NOT_FOUND_COMMENT(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다.");

private final HttpStatus httpStatus;
private final String message;

@Override
public HttpStatus getHttpStatus() {
return httpStatus;
}

@Override
public String getErrorMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.log4u.domain.comment.exception;

import com.example.log4u.common.exception.base.ErrorCode;
import com.example.log4u.common.exception.base.ServiceException;

public class CommentException extends ServiceException {
public CommentException(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.log4u.domain.comment.exception;

public class NotFoundCommentException extends CommentException {
public NotFoundCommentException() {
super(CommentErrorCode.NOT_FOUND_COMMENT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.log4u.domain.comment.testController;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.log4u.domain.comment.testDto.TestRequest;
import com.example.log4u.domain.comment.exception.NotFoundCommentException;

import jakarta.validation.Valid;

@RestController
@RequestMapping("/test")
public class TestController {

@PostMapping("/valid")
public ResponseEntity<Void> testValidation(@RequestBody @Valid TestRequest request) {
return ResponseEntity.ok().build();
}

@GetMapping("/illegal")
public String testIllegalArgument() {
throw new IllegalArgumentException("잘못된 인자입니다!");
}

@GetMapping("/log4u")
public String testLog4uException() {
throw new NotFoundCommentException(); // 또는 임의의 ServiceException
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.log4u.domain.comment.testDto;

// dto/TestRequest.java
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class TestRequest {

@NotBlank
private String name;

@Min(value = 18, message = "나이는 18세 이상이어야 합니다.")
private int age;

}