🐰 [flowbit] #9. μ‹€νŒ¨ μΌ€μ΄μŠ€ ν…ŒμŠ€νŠΈ - κΉ¨μ§€λŠ” μƒν™©κΉŒμ§€ κ²€μ¦ν•˜κΈ°

bean8080πŸ«›Β·2026λ…„ 5μ›” 4일

flowbit 🐰☘️

λͺ©λ‘ 보기
10/15

☘️ 1. 였늘 λͺ©ν‘œ

μ–΄μ œλŠ” 배포λ₯Ό μ€€λΉ„ν•˜κΈ° μœ„ν•΄
Controller κΈ°μ€€μ˜ μ΅œμ†Œ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν–ˆλ‹€.

정상 μš”μ²­μ„ 보내면
정상 응닡이 μ˜€λŠ”μ§€ ν™•μΈν•˜λŠ” ν…ŒμŠ€νŠΈμ˜€λ‹€.

ν•˜μ§€λ§Œ ν…ŒμŠ€νŠΈ μ½”λ“œλŠ”
μ„±κ³΅ν•˜λŠ” 경우만 ν™•μΈν•˜λ©΄ λΆ€μ‘±ν•˜λ‹€.

μ‹€μ œ μ„œλΉ„μŠ€μ—μ„œλŠ”
잘λͺ»λœ μš”μ²­λ„ λ“€μ–΄μ˜€κ³ 
μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 데이터λ₯Ό μ‘°νšŒν•˜λŠ” κ²½μš°λ„ 생긴닀.

κ·Έλž˜μ„œ 였늘 λͺ©ν‘œλŠ” λͺ…ν™•ν–ˆλ‹€.

μ‹€νŒ¨ν•˜λŠ” 상황도 ν…ŒμŠ€νŠΈν•˜μž


☘️ 2. μ™œ μ‹€νŒ¨ μΌ€μ΄μŠ€ ν…ŒμŠ€νŠΈκ°€ ν•„μš”ν•œκ°€

κΈ°λŠ₯이 μ •μƒμ μœΌλ‘œ λ™μž‘ν•˜λŠ”μ§€ ν™•μΈν•˜λŠ” 것도 μ€‘μš”ν•˜μ§€λ§Œ
그만큼 μ€‘μš”ν•œ 게 μžˆλ‹€.

λ¬Έμ œκ°€ 생겼을 λ•Œ μ„œλ²„κ°€ μ–΄λ–»κ²Œ μ‘λ‹΅ν•˜λŠ”κ°€

예λ₯Ό λ“€μ–΄ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μ„ μ‘°νšŒν–ˆμ„ λ•Œ
μ„œλ²„κ°€ κ·Έλƒ₯ 500 μ—λŸ¬λ‘œ ν„°μ§€λŠ” 것과

λͺ…ν™•ν•˜κ²Œ μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό λ‚΄λ €μ£ΌλŠ” 것은 λ‹€λ₯΄λ‹€.

GET /api/tasks/999

이런 μš”μ²­μ΄ 듀어왔을 λ•Œ

"μž‘μ—…μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."

처럼 예츑 κ°€λŠ₯ν•œ 응닡을 λ‚΄λ €μ€˜μ•Ό ν•œλ‹€.


☘️ 3. κΈ°μ‘΄ μ˜ˆμ™Έ 처리 ꡬ쑰 확인

Flowbitμ—λŠ” 이미 μ „μ—­ μ˜ˆμ™Έ 처리 ꡬ쑰가 μžˆμ—ˆλ‹€.

GlobalExceptionHandlerμ—μ„œ
Controller μ „μ—­μ—μ„œ λ°œμƒν•˜λŠ” μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•˜κ³  μžˆμ—ˆλ‹€.

ν˜„μž¬ κ΅¬μ‘°λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

@ExceptionHandler(IllegalArgumentException.class)
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) {
    return new ErrorResponse("BAD_REQUEST", e.getMessage());
}

그리고 응닡은 λ‹€μŒ ꡬ쑰둜 λ‚΄λ €κ°„λ‹€.

{
  "code": "BAD_REQUEST",
  "message": "μž‘μ—…μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.",
  "timestamp": "..."
}

즉 μ˜€λŠ˜μ€ μƒˆ ꡬ쑰λ₯Ό λ§Œλ“œλŠ” 것이 μ•„λ‹ˆλΌ
κΈ°μ‘΄ μ˜ˆμ™Έ 응닡 ꡬ쑰에 맞좰 ν…ŒμŠ€νŠΈλ₯Ό μΆ”κ°€ν•˜λŠ” μž‘μ—…μ΄μ—ˆλ‹€.


☘️ 4. μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” Project ν…ŒμŠ€νŠΈ

λ¨Όμ € μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν”„λ‘œμ νŠΈλ₯Ό μ‘°νšŒν•˜λŠ” ν…ŒμŠ€νŠΈλ₯Ό μΆ”κ°€ν–ˆλ‹€.

@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 응닡이 λ‚΄λ €μ˜€λŠ”μ§€ ν™•μΈν•œλ‹€.


☘️ 5. μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” Task ν…ŒμŠ€νŠΈ

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);
}

정상 쑰회뿐 μ•„λ‹ˆλΌ
μ—†λŠ” μž‘μ—…μ„ μ‘°νšŒν–ˆμ„ λ•Œμ˜ 응닡도 ν…ŒμŠ€νŠΈν•˜κ²Œ λ˜μ—ˆλ‹€.


☘️ 6. μƒνƒœ λ³€κ²½ μ‹€νŒ¨ μΌ€μ΄μŠ€

μž‘μ—… 쑰회뿐 μ•„λ‹ˆλΌ
μƒνƒœ λ³€κ²½ API도 μ‹€νŒ¨ μΌ€μ΄μŠ€λ₯Ό μΆ”κ°€ν–ˆλ‹€.

PATCH /api/tasks/999/start
PATCH /api/tasks/999/complete
PATCH /api/tasks/999/block
PATCH /api/tasks/999/delete

μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μ„ μ‹œμž‘ν•˜κ±°λ‚˜
μ™„λ£Œν•˜κ±°λ‚˜
보λ₯˜ν•˜κ±°λ‚˜
μ‚­μ œν•˜λ €κ³  ν•  λ•Œ

λͺ¨λ‘ 같은 λ°©μ‹μœΌλ‘œ μ˜ˆμ™Έ 응닡이 λ‚΄λ €μ˜€λŠ”μ§€ ν™•μΈν–ˆλ‹€.


☘️ 7. 잘λͺ»λœ status νŒŒλΌλ―Έν„° 처리

μΆ”κ°€λ‘œ 잘λͺ»λœ 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 μš”μ²­λ„
μΌκ΄€λœ μ—λŸ¬ μ‘λ‹΅μœΌλ‘œ μ²˜λ¦¬λœλ‹€.


8. ν…ŒμŠ€νŠΈ μ‹€ν–‰

ν…ŒμŠ€νŠΈ μž‘μ„± ν›„ λ‹€μ‹œ μ‹€ν–‰ν–ˆλ‹€.

./gradlew test

κ²°κ³Ό:

Gradle ν…ŒμŠ€νŠΈ μ‹€ν–‰ κ²°κ³Ό

BUILD SUCCESSFUL

성곡 μΌ€μ΄μŠ€λΏ μ•„λ‹ˆλΌ
μ‹€νŒ¨ μΌ€μ΄μŠ€κΉŒμ§€ ν¬ν•¨ν•œ ν…ŒμŠ€νŠΈκ°€ ν†΅κ³Όν–ˆλ‹€.


☘️ 9. 였늘 정리

μ˜€λŠ˜μ€ μƒˆλ‘œμš΄ κΈ°λŠ₯을 λ§Œλ“  날은 μ•„λ‹ˆμ—ˆλ‹€.

λŒ€μ‹  κΈ°μ‘΄ APIκ°€
잘λͺ»λœ μš”μ²­μ„ λ°›μ•˜μ„ λ•Œ
μ–΄λ–»κ²Œ λ°˜μ‘ν•˜λŠ”μ§€ κ²€μ¦ν–ˆλ‹€.

였늘 μž‘μ—…ν•œ λ‚΄μš©μ€ λ‹€μŒκ³Ό κ°™λ‹€.

Project μ‹€νŒ¨ μΌ€μ΄μŠ€ ν…ŒμŠ€νŠΈ μΆ”κ°€
Task μ‹€νŒ¨ μΌ€μ΄μŠ€ ν…ŒμŠ€νŠΈ μΆ”κ°€
μƒνƒœ λ³€κ²½ μ‹€νŒ¨ μΌ€μ΄μŠ€ ν…ŒμŠ€νŠΈ μΆ”κ°€
잘λͺ»λœ status νŒŒλΌλ―Έν„° μ˜ˆμ™Έ 처리 μΆ”κ°€
GlobalExceptionHandler ν…ŒμŠ€νŠΈ 적용


☘️ 10. ν˜„μž¬ μƒνƒœ

Flowbit은 이제

정상 μš”μ²­μ΄ 듀어왔을 λ•Œ λ™μž‘ν•˜κ³ 
잘λͺ»λœ μš”μ²­μ΄ 듀어왔을 λ•Œλ„ 예츑 κ°€λŠ₯ν•œ 응닡을 λ‚΄λ €μ€€λ‹€.

즉,

μ„±κ³΅ν•˜λŠ” APIμ—μ„œ

μ‹€νŒ¨λ„ μ²˜λ¦¬ν•  수 μžˆλŠ” API둜

ν•œ 단계 λ„˜μ–΄κ°”λ‹€.


☘️ 11. λ‹€μŒ λͺ©ν‘œ

이제 ν…ŒμŠ€νŠΈ μ½”λ“œλ„ μ΅œμ†Œν•œμ˜ 성곡/μ‹€νŒ¨ μΌ€μ΄μŠ€λ₯Ό κ°–μ·„λ‹€.

λ‹€μŒ λ‹¨κ³„λŠ” λ‹€μ‹œ 배포닀.

EC2 μ„œλ²„ 생성
Spring Boot μ„œλ²„ 배포
도메인 μ—°κ²°
CI/CD ꡬ성

λ‘œμ»¬μ—μ„œ λ™μž‘ν•˜λŠ” ν”„λ‘œμ νŠΈλ₯Ό λ„˜μ–΄
μ‹€μ œ ν™˜κ²½μ—μ„œ μ ‘κ·Ό κ°€λŠ₯ν•œ μ„œλΉ„μŠ€λ‘œ λ§Œλ“œλŠ” λ‹¨κ³„λ‘œ λ“€μ–΄κ°„λ‹€.

0개의 λŒ“κΈ€