-
Notifications
You must be signed in to change notification settings - Fork 3
Convention
feat/<issue-num>-<summary>
# Example
feat/1-init-project
feat/2-implement-performance
Only Allow Squash Merge
to Main Branch
- reference 뱅크샐러드: 하루에 1000번 배포하는 조직 되기
- 해당 레퍼런스가 경험했던 많은 문제점을 해결한다고 느끼기에 선택했습니다.
- master => main
- feature => feat
Issue: CI: commit message convention #14
Git actions와 Local Git Hook을 통해 CI 진행
# <type>: <subject>
##### Subject 50 characters ################# -> |
# Body Message
######## Body 72 characters ####################################### -> |
# Issue Tracker Number or URL
# --- COMMIT END ---
# Type can be
# feat : new feature
# fix : bug fix
# refactor: refactoring production code
# style : formatting, missing semi colons, etc; no code change
# docs : changes to documentation
# test : adding or refactoring tests
# no productin code change
# chore : updating grunt tasks etc
# no production code change
Issue: CI: 코드 컨벤션 체크 #6
Git actions와 Local Git Hook을 통해 CI 진행
root = true
[*]
charset=utf-8
end_of_line=lf
indent_style=space
indent_size=4
insert_final_newline=true
disabled_rules=no-wildcard-imports,import-ordering,comment-spacing
[*.{kt,kts}]
ktlint_code_style = ktlint_official
camelCase
를 기본적으로 사용하며 상수와 열거형(Enum)을 정의할 때는 대문자를 사용한다.
Issue: 레이어간 데이터 전달 형식 정의
API Docs: https://f-lab-clone.github.io/ticketing-backend/
HTTP_STATUS: 200, 201 ...
{
"timestamp": "2023-10-13T14:21:54.3281858Z",
"message": "success",
"data": {
controller가 반환한 값이 들어가있다.
},
"path": "/events/1",
"totalElements": null
}
HTTP_STATUS: 400, 401, 403, 404, 500 ...
// 500 상황에서도 해당 format은 준수되어야 한다.
{
"timestamp": "2023-10-13T14:43:22.1755822Z",
"errorCode": 40000,
"message": "해당 레코드를 찾을수 없습니다.",
"path": "/reservations"
}
애플리케이션 동작시 발생하는 에러에 대해 서버에서 의도된 response값을 반환하기 위하여 예상되는 오류들은 Eum class인 ErrorCodes에 정의되어있음.
enum class ErrorCodes(val status: HttpStatus, val message: String, val errorCode: Int) {
// 400 Bad Request
VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "유효성 검증에 실패하였습니다.", 10001),
MESSAGE_NOT_READABLE(HttpStatus.BAD_REQUEST, "올바른 형식의 요청이 아닙니다", 10002),
// 404 Not Found
ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 레코드를 찾을수 없습니다.", 40000),
...
}
ErrorCodes를 필드로 갖는 CustomException을 정의하여 비지니스 로직에서 문제가 생겼을때 Serivce Layer에서 errorCode와 함께 CustomException을 호출한다.
userRepository.findByEmail(username) ?: let { throw CustomException(ErrorCodes.ENTITY_NOT_FOUND) }
위에서 발생한 CustomException은 RestControllerAdbice가 Handle하여 의도한 response를 반환한다.
Controller에서 매 메소드마다 try-catch로 발생한 Exception을 처리 하는거 보다 훨씬 깔끔하다.
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(value = [CustomException::class])
fun handleCustomException(exception: CustomException, request: HttpServletRequest): ResponseEntity<ErrorResponseDTO> {
val errorCode = exception.errorCode
val errorDto = ErrorResponseDTO(
errorCode = exception.errorCode.errorCode,
message = exception.errorCode.message,
path = request.requestURI
)
return ResponseEntity(errorDto, errorCode.status)
}
...
}
기본적으로 모든 통신은 application/json
형식으로 이루어지며 Gson을 사용한다.
코틀린에서 json (de)serializer은 kotlinx(jetbrains)와 gson(google)이 대표적으로 사용된다. Jetbrains사에서 만든 kotlinx를 채택하지 않은 이유는
data class
로 정의한 dto 객체에@serializable
이라는 어노테이션을 선언해야 하며,kotlinx.LocalDateTime
객체가 따로 존재하여java.time.OffesetDateTime
로 컨버팅시 필요없는 보일러 플레이트 코드가 발생하여 직관적인Gson
을 사용하기로 했다.
HTTP 통신 중 JSON 포맷은 Gson을 통해 Serialization/Deserialization 된다. 이때 외부에서 들어오는 ISO8601 Datetime String 중 Timezone offset이 UTC(+00:00)이 아닌 경우 UTC 시간으로 자동 변경된다.
{
“datetime”: “2000-01-01T09:00:00.001+09:00”
}
위의 DatetimeString은 아래와 같은 UTC 시간으로 변경되어 Serialization 된다.
DatetimeObj = OffsetDatetime.of(2000,1,1,0,0,0,1,”00:00”)
세부 내용은
config/WebConfig.kt
,config/GsonConfig.kt
를 확인한다. 2000-01-01T00:00:00.001Z는 2000-01-01T00:00:00.001+00:00 과 같은 의미이며 Z는 UTC timezone을 의미한다. **만약 Timezone Offset 정보(+00:00)가 Datetime String에 존재하지 않는다면 파싱시IllegalArgumentationException
을 발생하게 된다.
java.time.OffesetDateTime
객체를 직렬화(역직렬화) 할때 ISO8601 포맷으로 변환(파싱) 해야 한다. 아래 로직은 Gson의 JsonSerializer<T>, JsonDeserializer<T>
를 상속받아 처리한다.
class DateTimeConverter : JsonSerializer< OffesetDateTime?>, JsonDeserializer< OffesetDateTime?> {
private val FORMATTER = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"
.let { DateTimeFormatter.ofPattern(it) }
override fun serialize(src: OffesetDateTime?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
return JsonPrimitive(FORMATTER.format(src))
}
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): OffesetDateTime? {
return FORMATTER.parse(json!!.asJsonPrimitive.asString) as LocalDateTime?
}
}
@Test
fun `All API only returns OffsetDateTime in UTC without any offset info`() {
val performanceCreateRequest = "{\"title\":\"test title\"," +
"\"date\":\"2022-09-01T21:00:00.001+09:00\"," +
"\"bookingStartTime\":\"2022-09-01T22:00:00.001+09:00\"," +
"\"bookingEndTime\":\"2022-09-01T23:00:00.001+09:00\"," +
"\"maxAttendees\":10}"
mockMvc.perform(
post("/performances")
.contentType(MediaType.APPLICATION_JSON)
.content(performanceCreateRequest)
)
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.date").value("2022-09-01T12:00:00.001Z"))
.andExpect(jsonPath("$.bookingStartTime").value("2022-09-01T13:00:00.001Z"))
.andExpect(jsonPath("$.bookingEndTime").value("2022-09-01T14:00:00.001Z"))
}
java.time.OffesetDateTime
을 기본적으로 사용하며 문자열로 변환시 포맷은 ISO8601 형식을 따르고 마이크로세컨드 단위까지 표시한다.
java.time.OffesetDateTime.now(clock)
메서드를 통해 UTC 표준시를 사용한다.
문자열로 포맷으로 변환 및 LocalDatetime
객체로의 변환은 아래와 같이 사용할 수 있다.
val FORMATTER = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX".let { DateTimeFormatter.ofPattern(it) }
// OffesetDateTime to String
DateTimeString = FORMATTER.format(DateTimeObj)
// String to OffesetDateTime
DateTimeObj = FORMATTER.parse(DateTimeString)
각 레이어의 테스트는 본질적으로 Application Context
를 구성하기에 순수한 단위 테스트가 아니라고 생각할 수 있다. 하지만 실행되는 코드에 집중하지 않고 검증하는 테스트에 집중한다면 단위 테스트이다. 이러한 모호함을 없애기 위해, 구글의 테스트 구분 방법을 사용해 해당 테스트를 구분한다.
구글은 단위테스트와 통합테스트 대신 작은/중간/큰 테스트라고 정의한다. 작은 테스트는 단 하나의 프로세스에서 실행되는 테스트를 의미한다. 또한 Sleep, I/O 연산, 네트워크에 접근해선 안된다. 중간 크기 테스트는 여러 프로세스와 스레드 이용이 가능하다 즉 데이터베이스 인스턴스의 실행이 가능하다. 테스트를 크기로 구분하는 것은 명확하지만, 범위로 구분하는 것은 모호한 면이 있다. 구글의 정의에 따르면 단위 테스트는 대체로 작은 테스트에 속한다. - 구글 엔지니어는 어떻게 일하는가?
1. Controller Layer Test - 중간 크기 테스트
- 컨트롤러 레이어는 웹과 맞붙어 있는 레이어로써, 외부 요인 차단이 불가능하다.
- 통합테스트와 유닛 테스트 경계에 위치해 있다 (슬라이스 테스트)
-
src/test
하위 폴더에 작성
2. Service Layer Test - 작은 크기 테스트
- Repository 객체 Mock up 후 테스트한다
-
src/test
하위 폴더에 작성
3. Repository Layer Test - 중간 크기 테스트
- H2 DB가 아닌 Testcontainer를 이용
- DB 인스턴스와의 상호작용도 테스트하기 때문에 통합 테스트
-
src/integrationTest
하위 폴더에 작성
참고