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

chore: Global Exception Handling #3

Merged
merged 7 commits into from
Mar 15, 2024
13 changes: 8 additions & 5 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
## <i>PULL REQUEST</i>

### ๐ŸŽ‹ ์ž‘์—… ์ค‘์ธ ๋ธŒ๋žœ์น˜
### ๐ŸŽ‹ Issue Ticket
-

### ๐Ÿ”‘ ์ฃผ์š” ์ž‘์—…์‚ฌํ•ญ
-

### ๐Ÿž (Optional) ์ฐธ๊ณ  ์ž๋ฃŒ
- ์Šคํฌ๋ฆฐ์ƒท์„ ์ฒจ๋ถ€ํ•ด์ฃผ์„ธ์š”.

### ๊ด€๋ จ ์ด์Šˆ
-

### (์ค‘์š”) ์„œ๋ธŒ๋ชจ๋“ˆ์ด ์ˆ˜์ •๋˜์—ˆ๋‚˜์š”?
- ๊ธฐ์กด ์ปค๋ฐ‹ :
- ๋ณ€๊ฒฝ ์ปค๋ฐ‹ :
- ๋ณ€๊ฒฝ ์‚ฌํ•ญ :
- [] ํ•ด๋‹น ์„œ๋ธŒ๋ชจ๋“ˆ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด PR์— ์ž˜ ๋ฐ˜์˜๋˜์—ˆ๋‚˜์š”?

### ๊ผญ ํ™•์ธํ•ด ์ฃผ์„ธ์š”!!
-
-
33 changes: 13 additions & 20 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -1,42 +1,35 @@
name: ๋นŒ๋“œ ํ…Œ์ŠคํŠธ / ๋ฆฌ๋ทฐ์–ด ํ• ๋‹น
name: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋ฐ ๋นŒ๋“œ

on:
pull_request:
types: [opened, synchronize, closed]
types: [ opened, synchronize, closed ]
branches:
- 'develop'
- 'master'

jobs:
build_and_review_assign:
name: "[CI] Check Build/Testcases and Assign Reviewer"
test:
name: "[CI] Check Tests"
runs-on: ubuntu-latest

steps:
- name: (Set Up) checkout
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.GIT_TOKEN }}
submodules: true

- name: (Set Up) Set up JDK 17
- name: Setup Java 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
distribution: 'adopt'

- name: (Set Up) Grant Execute permission for gradlew
run: chmod 777 gradlew
- name: Update Git submodules
run: git submodule update --remote --recursive

- name: (Build) Build with Gradle
id: build
run: ./gradlew test -i
- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: (Assign Reviewer)
if: steps.build.outcome == 'success'
uses: hkusu/review-assign-action@v1
with:
assignees: ${{ github.actor }}
reviewers: HyungJu, h-beeen
ready-comment: '์ฝ”๋“œ ๋ฆฌ๋ทฐ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค ๐Ÿ™† <reviewers>'
merged-comment: '์„ฑ๊ณต์ ์œผ๋กœ Merge ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Shout out to <reviewers> :wink:'
- name: Test with Gradle
run: ./gradlew test
21 changes: 21 additions & 0 deletions src/main/kotlin/com/vacgom/backend/domain/member/Member.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.vacgom.backend.domain.member

import com.vacgom.backend.global.auditing.BaseEntity
import jakarta.persistence.*

@Entity
@Table(name = "t_member")
class Member(nickname: String) : BaseEntity() {

@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null

@Column(name = "nickname")
val nickname: String

init {
this.nickname = nickname
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.vacgom.backend.global.exception

import com.vacgom.backend.global.exception.error.BusinessException
import com.vacgom.backend.global.exception.error.ErrorCode
import com.vacgom.backend.global.exception.error.ErrorResponse
import com.vacgom.backend.global.exception.error.GlobalError
import com.vacgom.backend.global.logger.CommonLogger
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.MissingServletRequestParameterException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class ApiExceptionHandler {
companion object : CommonLogger();

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleMethodArgumentNotValidException(ex: MethodArgumentNotValidException): ErrorResponse? {
return ErrorResponse(ex.fieldErrors)
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException::class)
fun missingServletRequestParameterException(): ErrorResponse? {
return ErrorResponse(GlobalError.INVALID_REQUEST_PARAM)
}

@ExceptionHandler(BusinessException::class)
fun handleBusinessException(
exception: BusinessException
): ResponseEntity<ErrorResponse?> {
logBusinessException(exception)
return convert(exception.errorCode)
}

private fun convert(
errorCode: ErrorCode
): ResponseEntity<ErrorResponse?> {
return ResponseEntity
.status(errorCode.status)
.body(ErrorResponse(errorCode))
}

private fun logBusinessException(exception: BusinessException) {
if (exception.errorCode.status.is5xxServerError) {
log.error("", exception)
} else {
log.error("Error Message = {}", exception.message)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.vacgom.backend.global.exception

import com.fasterxml.jackson.databind.ObjectMapper
import com.vacgom.backend.global.exception.error.BusinessException
import com.vacgom.backend.global.exception.error.ErrorResponse
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletException
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import java.io.IOException


@Component
class ApiExceptionHandlingFilter(
private val om: ObjectMapper
) : OncePerRequestFilter() {

@Throws(ServletException::class, IOException::class)
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
chain: FilterChain
) {
try {
chain.doFilter(request, response)
} catch (exception: BusinessException) {
setErrorResponse(response, exception)
}
}

private fun setErrorResponse(
response: HttpServletResponse,
exception: BusinessException
) = try {
response.contentType = MediaType.APPLICATION_JSON_VALUE
response.status = HttpStatus.UNAUTHORIZED.value()
om.writeValue(response.outputStream, ErrorResponse(exception.errorCode))
} catch (exception: IOException) {
throw RuntimeException(exception)
}
Comment on lines +38 to +44
Copy link
Member Author

Choose a reason for hiding this comment

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

try-catch๋„ ์‹ ๊ธฐํ•œ ๋ฌธ๋ฒ•์œผ๋กœ ์“ฐ๋”๊ตฐ์š”.. ์‹ ์„ธ๊ณ„๋ฅผ ๋ง›๋ณด๋Š”์ค‘

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.vacgom.backend.global.exception.error


class BusinessException(
val errorCode: ErrorCode
) : RuntimeException(errorCode.message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.vacgom.backend.global.exception.error

import org.springframework.http.HttpStatus

interface ErrorCode {
val message: String
val status: HttpStatus
val code: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.vacgom.backend.global.exception.error

import org.springframework.validation.FieldError
import java.time.LocalDateTime

data class ErrorResponse(
val timeStamp: String = LocalDateTime.now().toString(),
val errorCode: String,
val errorMessage: String,
val details: Any? = null
) {
constructor(
errorCode: ErrorCode
) : this(
errorCode = errorCode.code,
errorMessage = errorCode.message
)

constructor(
errorCode: ErrorCode,
details: Any?
) : this(
errorCode = errorCode.code,
errorMessage = errorCode.message,
details = details
)
Comment on lines +12 to +26
Copy link
Member Author

Choose a reason for hiding this comment

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

์ €๋Š” java๋ฅผ ์ฝ”๋”ฉํ•  ๋•Œ, 2๊ฐœ ์ด์ƒ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๋Š” ๋ฉ”์†Œ๋“œ์—
์ค„๊ณง ์ด๋Ÿฐ ๋Š๋‚Œ์˜ ๊ฐœํ–‰์„ ํ•ด์˜ค๋ฉฐ ์ฝ”๋”ฉํ–ˆ์—ˆ์–ด์š”.

์ฝ”ํ‹€๋ฆฐ์€ ์ด๋Ÿฐ ๋ถ€๋ถ„์— ๋Œ€ํ•ด์„œ ์–ด๋–ค์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š”๊ฒŒ ๊ฐ€์žฅ ์ž์—ฐ์Šค๋Ÿฌ์šธ๊นŒ์š”?
์ด ๋ถ€๋ถ„๋„ ํ˜•์ฃผ๋‹˜์˜ ์˜๊ฒฌ์„ ๋“ค์–ด๋ณด๊ณ  ์‹ถ๊ตฐ์š”!!

public void singleParameter(Long id){
        id++;
}

public void multipleParameter(
        Long id1,
        Long id2,
        Long id3
) {
        id1++;
        id2--;
        id3 -= 1;
}


constructor(
fieldError: FieldError?
) : this(
errorCode = fieldError?.code ?: "",
errorMessage = fieldError?.defaultMessage ?: ""
)

constructor(fieldErrors: List<FieldError>) : this(
GlobalError.INVALID_REQUEST_PARAM,
fieldErrors.associate {
it.field to (it.defaultMessage ?: "null")
}
)
Comment on lines +12 to +40
Copy link
Member Author

Choose a reason for hiding this comment

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

์ฝ”ํ‹€๋ฆฐ์—์„œ ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์†Œ๋“œ์˜ ์‚ฌ์šฉ์€ ์–ด๋–ป๊ฒŒ ์ƒ๊ฐํ•˜์„ธ์š”?
java์—์„œ๋Š” new ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€์‹œ์ ์œผ๋กœ ๊ตฌ๋ถ„์ด ํฌ๊ฒŒ ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋Š”๋ฐ, Kotlin์—์„œ๋Š” ํฐ ํ•„์š”๋ฅผ ๋Š๋ผ์ง€ ๋ชปํ–ˆ๋˜ ๊ฒƒ ๊ฐ™์•„์š”.

๊ทธ๋ž˜์„œ of๋กœ ์˜ค๋ฒ„๋กœ๋”ฉํ•ด์„œ ์‚ฌ์šฉํ•˜๋˜ ๋ฉ”์†Œ๋“œ๋ฅผ ์ „๋ถ€ ์ƒ์„ฑ์ž๋กœ ๋ฐ”๊ฟ” ์„ค๊ณ„ํ•ด๋ดค์–ด์š”.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.vacgom.backend.global.exception.error

import org.springframework.http.HttpStatus

enum class GlobalError(
override val message: String,
override val status: HttpStatus,
override val code: String
) : ErrorCode {
GLOBAL_NOT_FOUND("๋ฆฌ์†Œ์Šค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND, "G_001"),
INVALID_REQUEST_PARAM("์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST, "G_002");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.vacgom.backend.global.logger

import org.slf4j.LoggerFactory

abstract class CommonLogger {
val log = LoggerFactory.getLogger(this.javaClass)!!
}
16 changes: 0 additions & 16 deletions src/main/kotlin/com/vacgom/backend/member/domain/Member.kt

This file was deleted.

Loading