Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dev-cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
--memory="700m" \
--memory-swap="1400m" \
--cpus="1.5" \
--health-cmd="wget --no-verbose --tries=1 --spider http://localhost:8080/api/v1/actuator/health || exit 1" \
--health-cmd="wget --no-verbose --tries=1 --spider http://localhost:8080/api/actuator/health || exit 1" \
--health-interval=30s \
--health-timeout=5s \
--health-retries=3 \
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-restclient")
implementation("org.springframework.boot:spring-boot-starter-validation")
// implementation("org.springframework.boot:spring-boot-starter-security")
// implementation("org.springframework.boot:spring-boot-starter-security-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-webmvc")
Expand Down
3 changes: 0 additions & 3 deletions infra/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ USER spring:spring

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1

ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=50.0 \
Expand Down
2 changes: 1 addition & 1 deletion infra/dev/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ lokit.co.kr {
}

reverse_proxy localhost:8080 {
health_uri /api/v1/actuator/health
health_uri /api/actuator/health
health_interval 10s
health_timeout 5s

Expand Down
73 changes: 73 additions & 0 deletions src/main/kotlin/kr/co/lokit/api/common/dto/ApiResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package kr.co.lokit.api.common.dto

import jakarta.servlet.http.HttpServletRequest
import kr.co.lokit.api.common.exception.ErrorCode
import org.springframework.http.HttpStatus

data class ApiResponse<T>(
val code: Int,
val message: String,
val data: T,
) {
companion object {
data class ErrorDetail(
val errorCode: String,
val detail: String,
val instance: String,
val errors: Map<String, String>? = null,
)

fun <T> success(
code: Int,
data: T,
): ApiResponse<T> =
ApiResponse(
code = code,
message = "success",
data = data,
)

fun failure(
exception: Exception,
request: HttpServletRequest,
errorCode: ErrorCode,
errors: Map<String, String>? = null,
): ApiResponse<ErrorDetail> {
val errorDetail =
ErrorDetail(
errorCode = errorCode.code,
detail = exception.message ?: errorCode.message,
instance = request.requestURI,
errors = errors,
)

return ApiResponse(
code = errorCode.status.value(),
message = errorCode.status.reasonPhrase,
data = errorDetail,
)
}

fun failure(
status: HttpStatus,
detail: String,
request: HttpServletRequest,
errorCode: String,
errors: Map<String, String>? = null,
): ApiResponse<ErrorDetail> {
val errorDetail =
ErrorDetail(
errorCode = errorCode,
detail = detail,
instance = request.requestURI,
errors = errors,
)

return ApiResponse(
code = status.value(),
message = status.reasonPhrase,
data = errorDetail,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package kr.co.lokit.api.common.exception

sealed class BusinessException(
val errorCode: ErrorCode,
override val message: String = errorCode.message,
override val cause: Throwable? = null,
) : RuntimeException(message, cause) {
class InvalidInputException(
message: String = ErrorCode.INVALID_INPUT.message,
cause: Throwable? = null,
) : BusinessException(ErrorCode.INVALID_INPUT, message, cause)

class ResourceNotFoundException(
message: String = ErrorCode.RESOURCE_NOT_FOUND.message,
cause: Throwable? = null,
) : BusinessException(ErrorCode.RESOURCE_NOT_FOUND, message, cause)

class ResourceAlreadyExistsException(
message: String = ErrorCode.RESOURCE_ALREADY_EXISTS.message,
cause: Throwable? = null,
) : BusinessException(ErrorCode.RESOURCE_ALREADY_EXISTS, message, cause)

class UnauthorizedException(
message: String = ErrorCode.UNAUTHORIZED.message,
cause: Throwable? = null,
) : BusinessException(ErrorCode.UNAUTHORIZED, message, cause)

class ForbiddenException(
message: String = ErrorCode.FORBIDDEN.message,
cause: Throwable? = null,
) : BusinessException(ErrorCode.FORBIDDEN, message, cause)

class BusinessRuleViolationException(
message: String = ErrorCode.BUSINESS_RULE_VIOLATION.message,
cause: Throwable? = null,
) : BusinessException(ErrorCode.BUSINESS_RULE_VIOLATION, message, cause)
}
29 changes: 29 additions & 0 deletions src/main/kotlin/kr/co/lokit/api/common/exception/ErrorCode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package kr.co.lokit.api.common.exception

import org.springframework.http.HttpStatus

enum class ErrorCode(
val status: HttpStatus,
val code: String,
val message: String,
) {
// Common
INVALID_INPUT(HttpStatus.BAD_REQUEST, "COMMON_001", "잘못된 입력값입니다"),
INVALID_TYPE(HttpStatus.BAD_REQUEST, "COMMON_002", "잘못된 타입입니다"),
MISSING_PARAMETER(HttpStatus.BAD_REQUEST, "COMMON_003", "필수 파라미터가 누락되었습니다"),
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "COMMON_001", "지원하지 않는 HTTP 메서드입니다"),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON_001", "서버 내부 오류가 발생했습니다"),

// Authentication & Authorization
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "AUTH_001", "인증이 필요합니다"),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_002", "유효하지 않은 토큰입니다"),
EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_003", "만료된 토큰입니다"),
FORBIDDEN(HttpStatus.FORBIDDEN, "AUTH_001", "접근 권한이 없습니다"),

// Resource
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "RESOURCE_001", "요청한 리소스를 찾을 수 없습니다"),
RESOURCE_ALREADY_EXISTS(HttpStatus.CONFLICT, "RESOURCE_001", "이미 존재하는 리소스입니다"),

// Business
BUSINESS_RULE_VIOLATION(HttpStatus.BAD_REQUEST, "BUSINESS_001", "비즈니스 규칙 위반입니다"),
}
55 changes: 55 additions & 0 deletions src/main/kotlin/kr/co/lokit/api/config/advice/ApiResponseAdvice.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package kr.co.lokit.api.config.advice

import kr.co.lokit.api.common.dto.ApiResponse
import org.springframework.core.MethodParameter
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ProblemDetail
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.http.server.ServerHttpRequest
import org.springframework.http.server.ServerHttpResponse
import org.springframework.http.server.ServletServerHttpResponse
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice

@RestControllerAdvice(basePackages = ["kr.co.lokit.api"])
class ApiResponseAdvice : ResponseBodyAdvice<Any> {
companion object {
private val EXCLUDED_TYPES =
setOf(
ProblemDetail::class.java,
ApiResponse::class.java,
ResponseEntity::class.java,
String::class.java,
Void.TYPE,
Unit::class.java,
)
}

override fun supports(
returnType: MethodParameter,
converterType: Class<out HttpMessageConverter<*>>,
): Boolean = returnType.parameterType !in EXCLUDED_TYPES

override fun beforeBodyWrite(
body: Any?,
returnType: MethodParameter,
selectedContentType: MediaType,
selectedConverterType: Class<out HttpMessageConverter<*>>,
request: ServerHttpRequest,
response: ServerHttpResponse,
): Any? {
val statusCode =
if (response is ServletServerHttpResponse) {
response.servletResponse.status
} else {
HttpStatus.OK.value()
}

return ApiResponse.success(
code = statusCode,
data = body,
)
}
}
Loading