μ΄μ λ λ°°ν¬λ₯Ό μ€λΉνκΈ° μν΄
Controller κΈ°μ€μ μ΅μ ν
μ€νΈ μ½λλ₯Ό μμ±νλ€.
μ μ μμ²μ 보λ΄λ©΄
μ μ μλ΅μ΄ μ€λμ§ νμΈνλ ν
μ€νΈμλ€.
νμ§λ§ ν
μ€νΈ μ½λλ
μ±κ³΅νλ κ²½μ°λ§ νμΈνλ©΄ λΆμ‘±νλ€.
μ€μ μλΉμ€μμλ
μλͺ»λ μμ²λ λ€μ΄μ€κ³
μ‘΄μ¬νμ§ μλ λ°μ΄ν°λ₯Ό μ‘°ννλ κ²½μ°λ μκΈ΄λ€.
κ·Έλμ μ€λ λͺ©νλ λͺ ννλ€.
μ€ν¨νλ μν©λ ν μ€νΈνμ
κΈ°λ₯μ΄ μ μμ μΌλ‘ λμνλμ§ νμΈνλ κ²λ μ€μνμ§λ§
κ·Έλ§νΌ μ€μν κ² μλ€.
λ¬Έμ κ° μκ²Όμ λ μλ²κ° μ΄λ»κ² μλ΅νλκ°
μλ₯Ό λ€μ΄ μ‘΄μ¬νμ§ μλ μμ
μ μ‘°ννμ λ
μλ²κ° κ·Έλ₯ 500 μλ¬λ‘ ν°μ§λ κ²κ³Ό
λͺ ννκ² μλ¬ λ©μμ§λ₯Ό λ΄λ €μ£Όλ κ²μ λ€λ₯΄λ€.
GET /api/tasks/999
μ΄λ° μμ²μ΄ λ€μ΄μμ λ
"μμ μ μ°Ύμ μ μμ΅λλ€."
μ²λΌ μμΈ‘ κ°λ₯ν μλ΅μ λ΄λ €μ€μΌ νλ€.
Flowbitμλ μ΄λ―Έ μ μ μμΈ μ²λ¦¬ κ΅¬μ‘°κ° μμλ€.
GlobalExceptionHandlerμμ
Controller μ μμμ λ°μνλ μμΈλ₯Ό μ²λ¦¬νκ³ μμλ€.
νμ¬ κ΅¬μ‘°λ λ€μκ³Ό κ°λ€.
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) {
return new ErrorResponse("BAD_REQUEST", e.getMessage());
}
κ·Έλ¦¬κ³ μλ΅μ λ€μ κ΅¬μ‘°λ‘ λ΄λ €κ°λ€.
{
"code": "BAD_REQUEST",
"message": "μμ
μ μ°Ύμ μ μμ΅λλ€.",
"timestamp": "..."
}
μ¦ μ€λμ μ ꡬ쑰λ₯Ό λ§λλ κ²μ΄ μλλΌ
κΈ°μ‘΄ μμΈ μλ΅ κ΅¬μ‘°μ λ§μΆ° ν
μ€νΈλ₯Ό μΆκ°νλ μμ
μ΄μλ€.
λ¨Όμ μ‘΄μ¬νμ§ μλ νλ‘μ νΈλ₯Ό μ‘°ννλ ν μ€νΈλ₯Ό μΆκ°νλ€.
@Test
@DisplayName("μ‘΄μ¬νμ§ μλ νλ‘μ νΈ μ‘°ν μ 400μ λ°ννλ€")
void getProject_notFound() throws Exception {
when(projectService.getProject(999L))
.thenThrow(new IllegalArgumentException("νλ‘μ νΈλ₯Ό μ°Ύμ μ μμ΅λλ€."));
mockMvc.perform(get("/api/projects/999"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("BAD_REQUEST"))
.andExpect(jsonPath("$.message").value("νλ‘μ νΈλ₯Ό μ°Ύμ μ μμ΅λλ€."))
.andExpect(jsonPath("$.timestamp").exists());
verify(projectService).getProject(999L);
}
μ΄ ν
μ€νΈλ
/api/projects/999 μμ²μ΄ λ€μ΄μμ λ
μλ νλ‘μ νΈλΌλ©΄ BAD_REQUEST μλ΅μ΄ λ΄λ €μ€λμ§ νμΈνλ€.
Taskλ λμΌνκ² μ€ν¨ μΌμ΄μ€λ₯Ό μΆκ°νλ€.
@Test
@DisplayName("μ‘΄μ¬νμ§ μλ μμ
μ‘°ν μ 400μ λ°ννλ€")
void getTask_notFound() throws Exception {
when(taskService.getTask(999L))
.thenThrow(new IllegalArgumentException("μμ
μ μ°Ύμ μ μμ΅λλ€."));
mockMvc.perform(get("/api/tasks/999"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("BAD_REQUEST"))
.andExpect(jsonPath("$.message").value("μμ
μ μ°Ύμ μ μμ΅λλ€."))
.andExpect(jsonPath("$.timestamp").exists());
verify(taskService).getTask(999L);
}
μ μ μ‘°νλΏ μλλΌ
μλ μμ
μ μ‘°ννμ λμ μλ΅λ ν
μ€νΈνκ² λμλ€.
μμ
μ‘°νλΏ μλλΌ
μν λ³κ²½ APIλ μ€ν¨ μΌμ΄μ€λ₯Ό μΆκ°νλ€.
PATCH /api/tasks/999/start
PATCH /api/tasks/999/complete
PATCH /api/tasks/999/block
PATCH /api/tasks/999/delete
μ‘΄μ¬νμ§ μλ μμ
μ μμνκ±°λ
μλ£νκ±°λ
보λ₯νκ±°λ
μμ νλ €κ³ ν λ
λͺ¨λ κ°μ λ°©μμΌλ‘ μμΈ μλ΅μ΄ λ΄λ €μ€λμ§ νμΈνλ€.
μΆκ°λ‘ μλͺ»λ status κ°μ΄ λ€μ΄μ€λ κ²½μ°λ μ²λ¦¬νλ€.
GET /api/tasks?status=INVALID_STATUS
TaskStatusλ μ ν΄μ§ enum κ°λ§ λ°μ μ μλ€.
TODO
IN_PROGRESS
BLOCKED
DONE
DELETED
κ·Έ μΈμ κ°μ΄ λ€μ΄μ€λ©΄
νλΌλ―Έν° λ³ν κ³Όμ μμ μμΈκ° λ°μνλ€.
κ·Έλμ MethodArgumentTypeMismatchExceptionμ μ μ μμΈ μ²λ¦¬μ μΆκ°νλ€.
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ErrorResponse handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
return new ErrorResponse("BAD_REQUEST", "μμ² νλΌλ―Έν° κ°μ΄ μ¬λ°λ₯΄μ§ μμ΅λλ€.");
}
μ΄μ μλͺ»λ status μμ²λ
μΌκ΄λ μλ¬ μλ΅μΌλ‘ μ²λ¦¬λλ€.
ν μ€νΈ μμ± ν λ€μ μ€ννλ€.
./gradlew test
κ²°κ³Ό:

BUILD SUCCESSFUL
μ±κ³΅ μΌμ΄μ€λΏ μλλΌ
μ€ν¨ μΌμ΄μ€κΉμ§ ν¬ν¨ν ν
μ€νΈκ° ν΅κ³Όνλ€.
μ€λμ μλ‘μ΄ κΈ°λ₯μ λ§λ λ μ μλμλ€.
λμ κΈ°μ‘΄ APIκ°
μλͺ»λ μμ²μ λ°μμ λ
μ΄λ»κ² λ°μνλμ§ κ²μ¦νλ€.
μ€λ μμ ν λ΄μ©μ λ€μκ³Ό κ°λ€.
Project μ€ν¨ μΌμ΄μ€ ν
μ€νΈ μΆκ°
Task μ€ν¨ μΌμ΄μ€ ν
μ€νΈ μΆκ°
μν λ³κ²½ μ€ν¨ μΌμ΄μ€ ν
μ€νΈ μΆκ°
μλͺ»λ status νλΌλ―Έν° μμΈ μ²λ¦¬ μΆκ°
GlobalExceptionHandler ν
μ€νΈ μ μ©
Flowbitμ μ΄μ
μ μ μμ²μ΄ λ€μ΄μμ λ λμνκ³
μλͺ»λ μμ²μ΄ λ€μ΄μμ λλ μμΈ‘ κ°λ₯ν μλ΅μ λ΄λ €μ€λ€.
μ¦,
μ±κ³΅νλ APIμμ
μ€ν¨λ μ²λ¦¬ν μ μλ APIλ‘
ν λ¨κ³ λμ΄κ°λ€.
μ΄μ ν μ€νΈ μ½λλ μ΅μνμ μ±κ³΅/μ€ν¨ μΌμ΄μ€λ₯Ό κ°μ·λ€.
λ€μ λ¨κ³λ λ€μ λ°°ν¬λ€.
EC2 μλ² μμ±
Spring Boot μλ² λ°°ν¬
λλ©μΈ μ°κ²°
CI/CD ꡬμ±
λ‘컬μμ λμνλ νλ‘μ νΈλ₯Ό λμ΄
μ€μ νκ²½μμ μ κ·Ό κ°λ₯ν μλΉμ€λ‘ λ§λλ λ¨κ³λ‘ λ€μ΄κ°λ€.