[사이드 프로젝트] 기도제목 CRUD API를 만들며 - Kotlin & Spring Boot 개발 회고

박경희·2025년 4월 22일

프로젝트

목록 보기
19/22

사이드 프로젝트 첫 서비스, 기도제목 API 만들기

사이드 프로젝트를 시작하며 첫 번째로 완성한 서비스는 기도제목을 작성하고 관리하는 API였습니다.
이 기능은 크지 않지만, 백엔드의 기본 흐름(CRUD, Entity-DTO-Service-Controller 구조, 인증 처리)을 연습하는 데 아주 좋은 출발점이었습니다.


프로젝트 개요

  • 목표: 사용자가 기도제목을 작성하고 수정/삭제할 수 있는 간단한 API

  • 기간: 2024년 2월 ~ 3월

  • 역할: 백엔드 개발

  • 기술 스택: Kotlin, Spring Boot, JPA, PostgreSQL


기능 구현 흐름

  • 전체 조회 GET /api/v1/prayers - 모든 기도제목 리스트 반환

  • 작성 POST /api/v1/prayers - 요청 바디에서 사용자 ID 포함

  • 수정 PUT /api/v1/prayers/{id} - X-USER-ID 헤더로 본인 여부 체크

  • 삭제 DELETE /api/v1/prayers/{id} - 마찬가지로 사용자 확인 후 삭제 가능


고민했던 점들

  1. ❓ 사용자 인증 - JWT 없이 처리할 수 있을까?
  • 원래는 JWT를 붙일 계획이었지만, 서비스 구조가 아직 초기라 간단하게 X-USER-ID 헤더를 통해 사용자를 구분했습니다.

  • 팀원은 “굳이 헤더로 받을 필요 없이 내부에서 임의 처리해도 된다”고 했지만, 실제 인증 흐름을 고려해 습관을 들이기 위해 헤더 방식으로 구현했습니다.

  • 나중에 JWT를 붙일 때, 구조적으로 Controller → Service까지 흐름이 그대로 유지되므로 리팩토링이 쉬울 것이라고 판단했습니다.

  1. ⚠️ "본인만 수정/삭제 가능" 체크
    가장 기본적인 but 중요한 인증 로직.
  • Prayer 엔티티에 userId 필드를 추가하고, Service에서 currentUserId와 비교하여 권한 체크.

  • 추후 연관관계를 맺게 되면 이 부분도 리팩토링 예정.


테스트 코드 작성

테스트 환경 구성

  • 테스트용 데이터베이스도 H2가 아닌 PostgreSQL을 별도로 구성했습니다.
  • 이유는 실제 서비스에서 PostgreSQL을 사용하기 때문에,
    테스트에서도 동일한 환경에서의 쿼리 동작과 트랜잭션 흐름을 검증하고 싶었기 때문입니다.
  • 특히 JPA에서는 DB에 따라 동작 방식이나 SQL 방언이 달라질 수 있어,
    “로컬 개발 환경 = 테스트 환경 = 운영 환경”을 가깝게 만드는 것이 중요하다고 생각했습니다.

Controller 테스트 (MockMvc 사용)

주요 목적: API가 의도한 대로 동작하는지, HTTP 응답과 body를 검증합니다.

@Test
@DisplayName("기도제목 저장 api가 정상 동작한다")
fun savePrayer() {
    val request = PrayerCreateRequest("기도제목1")
    val requestJson = jacksonObjectMapper().writeValueAsString(request)

    mockMvc.perform(
        post("/api/v1/prayers")
            .contentType(MediaType.APPLICATION_JSON)
            .content(requestJson)
    )
        .andExpect(status().isCreated)

    then(prayerService).should().savePrayer(request)
}

Service 테스트 (실제 DB 연동)

주요 목적: 실제 비즈니스 로직과 데이터베이스 처리 흐름을 검증합니다.

@Test
@DisplayName("본인이 작성한 기도제목만 수정할 수 있다.")
fun updatePrayerExceptionTest() {
    val prayer = prayerRepository.save(Prayer("내용", userId = 1L))
    val otherUserId = 2L
    val request = PrayerUpdateRequest("수정 시도")

    assertThatThrownBy {
        prayerService.updatePrayer(prayer.id!!, request, otherUserId)
    }.isInstanceOf(IllegalArgumentException::class.java)
     .hasMessage("본인이 작성한 기도제목만 수정할 수 있습니다.")
}

💥 트러블슈팅

No value present 예외

  • .orElseThrow()를 사용할 때, 엔티티가 없을 경우 예외가 터졌고, 이를 커스텀 예외로 바꾸려고 고민 중입니다.

  • 현재는 간단하게 IllegalArgumentException을 사용 중이며, 공통 예외 처리 모듈은 추후 적용 예정입니다.


📈 배운 점 & 느낀 점

  • 간단한 CRUD라고 해도, 테이블 설계 → Entity 작성 → DTO 흐름 → 테스트 코드까지 정리하려면 꽤 많은 생각이 필요했습니다.

  • “작고 확실한 기능부터 완성해보기”가 정말 중요하다는 걸 느꼈습니다.

  • 또한, 일단 완성된 기능 하나를 기준으로 다음 기능들을 설계해갈 수 있어, 이 구조는 팀 전체의 기준이 되기도 합니다.


다음 계획

  • Redis를 적용한 새로운 기능(콘티 서비스) 진행 예정

마무리

이번 사이드 프로젝트는 작은 기능부터 차근차근 시작해보자는 취지에서 출발했어요.
그만큼 아직 완성된 기능은 많지 않지만, “작업 하나하나에 대한 고민과 기록”이 확실하게 남는다는 점에서 매우 만족스럽습니다.

0개의 댓글