Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 컨트롤러 초기 설정 구현 #40

Merged
merged 11 commits into from
Jun 17, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.few.api.web.controller.hello

import com.few.api.web.controller.hello.request._HelloBody
import com.few.api.web.controller.hello.request._HelloParam
import com.few.api.web.support.ApiResponse
import com.few.api.web.support.ApiResponseGenerator
import com.few.api.web.support.MessageCode
import org.springframework.http.HttpStatus
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*

@Validated
@RestController
@RequestMapping("/api/v1/hello")
class _HelloController {

/**
* @param param 객체로 파라미터를 받는 경우
* @param club RequestParam을 사용하는 경우
*/
@GetMapping
fun helloGet(
param: _HelloParam?,
@RequestParam(required = true) club: String
): ApiResponse<ApiResponse.SuccessBody<Map<String, String>>> {
val name = param?.name ?: "few"
val age = param?.age ?: 0
val club = club
val data =
mapOf("hello" to "world", "name" to name, "age" to age.toString(), "club" to club)
return ApiResponseGenerator.success(data, HttpStatus.OK)
}

@PostMapping
fun helloPost(
@RequestBody body: _HelloBody
): ApiResponse<ApiResponse.SuccessBody<Map<String, String>>> {
val data = mapOf("hello" to "world", "name" to body.name)
return ApiResponseGenerator.success(data, HttpStatus.OK, MessageCode.RESOURCE_CREATED)
}

@GetMapping("/error")
fun helloError(): ApiResponse<ApiResponse.Success> {
throw RuntimeException("Hello Error")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.web.controller.hello.request

data class _HelloBody(
val name: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.web.controller.hello.request

data class _HelloParam(
val name: String?,
val age: Int?
)
44 changes: 44 additions & 0 deletions api/src/main/kotlin/com/few/api/web/filter/MDCFilter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.few.api.web.filter

import com.fasterxml.jackson.databind.ObjectMapper
import org.jboss.logging.MDC
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono

@Component
class MDCFilter(private val mapper: ObjectMapper) : WebFilter {
private val log: Logger = LoggerFactory.getLogger(MDCFilter::class.java)
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val requestStartTime = System.currentTimeMillis()
MDC.put("Type", "Request MDC Info")
MDC.put("TraceId", exchange.request.id)
MDC.put("Request-Remote-Address", exchange.request.remoteAddress)
MDC.put("Request-URL", exchange.request.uri)
MDC.put("Request-Method", exchange.request.method)
MDC.put(HttpHeaders.REFERER, exchange.request.headers.getFirst(HttpHeaders.REFERER))
MDC.put(HttpHeaders.USER_AGENT, exchange.request.headers.getFirst(HttpHeaders.USER_AGENT))

val request = mapOf(
"Type" to "Request Info",
"TraceId" to exchange.request.id,
"Request-Remote-Address" to exchange.request.remoteAddress.toString(),
"Request-URL" to exchange.request.uri.toString(),
"Request-Method" to exchange.request.method.toString()
)
log.info("{}", mapper.writeValueAsString(request))

return chain.filter(exchange).doFinally {
val requestEndTime = System.currentTimeMillis()
val elapsedTime = requestEndTime - requestStartTime
MDC.put("ElapsedTime", elapsedTime.toString() + "ms")
log.info("{}", mapper.writeValueAsString(MDC.getMap()))
MDC.clear()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.few.api.web.handler

import com.few.api.web.support.ApiResponse
import com.few.api.web.support.ApiResponseGenerator
import jakarta.validation.ConstraintViolationException
import org.springframework.beans.TypeMismatchException
import org.springframework.core.codec.DecodingException
import org.springframework.http.HttpStatus
import org.springframework.http.converter.HttpMessageNotReadableException
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.bind.support.WebExchangeBindException
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException
import org.springframework.web.server.ServerWebInputException
import java.nio.file.AccessDeniedException

/** API 요청 처리 중 발생하는 예외를 처리하는 핸들러 */
@RestControllerAdvice
class ApiControllerExceptionHandler(
private val loggingHandler: LoggingHandler
) {

@ExceptionHandler(IllegalArgumentException::class)
fun handleBadRequest(
ex: IllegalArgumentException
): ApiResponse<ApiResponse.FailureBody> {
return ApiResponseGenerator.fail(ExceptionMessage.FAIL.message, HttpStatus.BAD_REQUEST)
}

@ExceptionHandler(
MethodArgumentTypeMismatchException::class,
TypeMismatchException::class,
WebExchangeBindException::class,
BindException::class,
MethodArgumentNotValidException::class,
DecodingException::class,
ConstraintViolationException::class,
ServerWebInputException::class,
HttpMessageNotReadableException::class
)
fun handleBadRequest(
ex: Exception
): ApiResponse<ApiResponse.FailureBody> {
return handleRequestDetails(ex)
}

private fun handleRequestDetails(ex: Exception): ApiResponse<ApiResponse.FailureBody> {
if (ex is MethodArgumentTypeMismatchException) {
return handleRequestDetail(ex)
}
if (ex is MethodArgumentNotValidException) {
return handleRequestDetail(ex)
}
return ApiResponseGenerator.fail(
ExceptionMessage.FAIL_REQUEST.message,
HttpStatus.BAD_REQUEST
)
}

private fun handleRequestDetail(ex: MethodArgumentTypeMismatchException): ApiResponse<ApiResponse.FailureBody> {
val messageDetail = ExceptionMessage.REQUEST_INVALID_FORMAT.message + " : " + ex.name
return ApiResponseGenerator.fail(messageDetail, HttpStatus.BAD_REQUEST)
}

private fun handleRequestDetail(ex: MethodArgumentNotValidException): ApiResponse<ApiResponse.FailureBody> {
val filedErrors = ex.fieldErrors.toList()
val messageDetail = ExceptionMessage.REQUEST_INVALID.message + " : " + filedErrors
return ApiResponseGenerator.fail(
messageDetail,
HttpStatus.BAD_REQUEST
)
}

@ExceptionHandler(IllegalStateException::class)
fun handleIllegalState(
ex: Exception
): ApiResponse<ApiResponse.FailureBody> {
return ApiResponseGenerator.fail(
ExceptionMessage.FAIL.message,
HttpStatus.INTERNAL_SERVER_ERROR
)
}

@ExceptionHandler(NoSuchElementException::class)
fun handleForbidden(
ex: AccessDeniedException
): ApiResponse<ApiResponse.FailureBody> {
return ApiResponseGenerator.fail(
ExceptionMessage.ACCESS_DENIED.message,
HttpStatus.FORBIDDEN
)
}

@ExceptionHandler(Exception::class)
fun handleInternalServerError(
ex: Exception
): ApiResponse<ApiResponse.FailureBody> {
return ApiResponseGenerator.fail(
ExceptionMessage.FAIL.message,
HttpStatus.INTERNAL_SERVER_ERROR
)
}
}
13 changes: 13 additions & 0 deletions api/src/main/kotlin/com/few/api/web/handler/ExceptionMessage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.few.api.web.handler

enum class ExceptionMessage(val code: String, val message: String) {
FAIL("fail", "알 수 없는 오류가 발생했어요."),
FAIL_NOT_FOUND("fail.notfound", "일치하는 결과를 찾을 수 없어요."),
FAIL_AUTHENTICATION("fail.authentication", "인증이 필요해요."),
FAIL_REQUEST("fail.request", "잘못된 요청입니다."),
RESOURCE_NOT_FOUND("resource.notfound", "요청과 일치하는 결과를 찾을 수 없어요."),
RESOURCE_DELETED("resource.deleted", "요청에 대한 응답을 찾을 수 없어요."),
ACCESS_DENIED("access.denied", "접근 권한이 없어요."),
REQUEST_INVALID_FORMAT("request.%s.invalid", "잘못된 요청입니다."),
REQUEST_INVALID("request.invalid", "잘못된 요청입니다.")
}
Loading
Loading