Kotlin + Spring Boot 입문(5/6)
Kotlin/Spring

REST API 만들기 - Controller, Service, Repository

실제 유저 API를 만들면서 Controller, Service, Repository 역할을 이해한다.

2026-03-18
10 min read
#Spring Boot#Kotlin#REST API#Controller#Service

REST API란?

REST(Representational State Transfer) 는 서버와 클라이언트가 데이터를 주고받는 방식에 대한 규칙이다. REST 규칙을 따르는 API를 REST API 또는 RESTful API라고 부른다.

핵심 아이디어는 단순하다. URL은 자원(resource)을 나타내고, HTTP 메서드는 그 자원에 할 행동을 나타낸다.

자원(무엇을)    +    메서드(어떻게)
  /users/1      +       GET        → 1번 유저 조회
  /users        +       POST       → 유저 생성
  /users/1      +       PUT        → 1번 유저 수정
  /users/1      +       DELETE     → 1번 유저 삭제

HTTP 메서드

메서드의미주로 쓰는 상황
GET조회데이터를 가져올 때
POST생성새 데이터를 만들 때
PUT전체 수정데이터 전체를 교체할 때
PATCH부분 수정일부 필드만 바꿀 때
DELETE삭제데이터를 지울 때

요청(Request) 구조

클라이언트가 서버에 보내는 요청은 이런 구조다.

POST /users HTTP/1.1
Host: localhost:8080
Content-Type: application/json    ← 헤더: 보내는 데이터 형식

{                                  ← 바디: 실제 데이터 (JSON)
  "name": "김철수",
  "email": "chul@test.com"
}
  • 메서드 + URL : 무엇을 어떻게 할지
  • 헤더(Header) : 요청에 대한 부가 정보 (데이터 형식, 인증 토큰 등)
  • 바디(Body) : 전달할 실제 데이터. GET, DELETE는 보통 바디가 없다

응답(Response) 구조

서버가 돌려주는 응답도 비슷한 구조다.

HTTP/1.1 201 Created              ← 상태 코드
Content-Type: application/json

{                                  ← 바디: 결과 데이터
  "id": 1,
  "name": "김철수",
  "email": "chul@test.com"
}

HTTP 상태 코드

숫자로 요청 결과를 나타낸다.

코드의미상황
200 OK성공GET, PUT 성공
201 Created생성 성공POST 성공
204 No Content성공 (반환값 없음)DELETE 성공
400 Bad Request잘못된 요청필수 파라미터 누락
401 Unauthorized인증 필요로그인 안 한 상태
403 Forbidden권한 없음접근 불가 리소스
404 Not Found없는 리소스없는 유저 조회
500 Internal Server Error서버 오류서버 코드 버그

2xx는 성공, 4xx는 클라이언트 잘못, 5xx는 서버 잘못이라고 기억하면 된다.

JSON

REST API는 데이터를 주고받을 때 대부분 JSON(JavaScript Object Notation) 형식을 쓴다.

{
  "id": 1,
  "name": "김철수",
  "email": "chul@test.com",
  "scores": [90, 85, 92],
  "address": {
    "city": "서울",
    "district": "강남구"
  }
}
  • { } : 객체 (키-값 쌍)
  • [ ] : 배열 (여러 값)
  • 키는 항상 문자열(""), 값은 문자열, 숫자, boolean, 배열, 객체 가능

Spring Boot는 Kotlin의 data class를 JSON으로 자동 변환해준다. 별도 설정이 필요 없다.


유저 API 만들기

이 글에서는 유저를 만들고 조회하는 API를 단계별로 만든다.

GET  /users      → 전체 유저 목록
GET  /users/{id} → 특정 유저 조회
POST /users      → 유저 생성

1단계: 데이터 구조 정의

유저 데이터를 담을 클래스를 만든다.

// User.kt
data class User(
    val id: Long,
    val name: String,
    val email: String
)

요청 받을 때 쓸 클래스도 따로 만든다. 요청에는 id가 없어도 되기 때문이다.

// CreateUserRequest.kt
data class CreateUserRequest(
    val name: String,
    val email: String
)

2단계: Repository - 데이터 저장소

Repository는 데이터를 저장하고 꺼내는 역할이다. 지금은 데이터베이스 대신 메모리(리스트)에 저장한다.

// UserRepository.kt
@Repository
class UserRepository {

    // 임시 저장소 (서버 재시작하면 사라짐)
    private val users = mutableListOf<User>()
    private var nextId = 1L

    fun findAll(): List<User> {
        return users.toList()
    }

    fun findById(id: Long): User? {
        return users.find { it.id == id }
    }

    fun save(name: String, email: String): User {
        val user = User(id = nextId++, name = name, email = email)
        users.add(user)
        return user
    }
}
  • findAll() : 전체 유저 반환
  • findById() : id로 유저 찾기. 없으면 null
  • save() : 유저 저장하고 반환

3단계: Service - 비즈니스 로직

Service는 실제 작업을 처리하는 곳이다. Repository를 불러서 데이터를 다루고, 필요하면 검증도 한다.

// UserService.kt
@Service
class UserService(
    private val userRepository: UserRepository  // Spring이 자동으로 넣어줌
) {

    fun getAllUsers(): List<User> {
        return userRepository.findAll()
    }

    fun getUser(id: Long): User {
        return userRepository.findById(id)
            ?: throw IllegalArgumentException("유저를 찾을 수 없습니다. id: $id")
    }

    fun createUser(request: CreateUserRequest): User {
        // 이메일 형식 간단 검증
        if (!request.email.contains("@")) {
            throw IllegalArgumentException("이메일 형식이 올바르지 않습니다.")
        }
        return userRepository.save(request.name, request.email)
    }
}

?: 는 앞이 null일 때 뒤를 실행한다. 여기서는 유저가 없으면 예외를 던진다.


4단계: Controller - 요청 받기

Controller는 HTTP 요청을 받아서 Service를 호출하고 응답을 돌려준다.

// UserController.kt
@RestController
@RequestMapping("/users")
class UserController(
    private val userService: UserService
) {

    // GET /users
    @GetMapping
    fun getAllUsers(): List<User> {
        return userService.getAllUsers()
    }

    // GET /users/1
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): User {
        return userService.getUser(id)
    }

    // POST /users
    @PostMapping
    fun createUser(@RequestBody request: CreateUserRequest): User {
        return userService.createUser(request)
    }
}

어노테이션 정리:

어노테이션역할
@RestController이 클래스가 API Controller
@RequestMapping("/users")이 Controller는 /users로 시작하는 경로 담당
@GetMappingGET 요청 처리
@PostMappingPOST 요청 처리
@PathVariableURL 경로의 값 (/users/1에서 1)
@RequestBody요청 본문(JSON)을 객체로 변환

실행해서 테스트하기

서버를 실행하고 API를 테스트해보자. Postman 또는 IntelliJ의 HTTP Client를 쓸 수 있다.

유저 생성

POST http://localhost:8080/users
Content-Type: application/json

{
  "name": "김철수",
  "email": "chul@test.com"
}

응답:

{
  "id": 1,
  "name": "김철수",
  "email": "chul@test.com"
}

전체 유저 조회

GET http://localhost:8080/users

응답:

[
  { "id": 1, "name": "김철수", "email": "chul@test.com" }
]

특정 유저 조회

GET http://localhost:8080/users/1

응답:

{ "id": 1, "name": "김철수", "email": "chul@test.com" }

예외 처리

존재하지 않는 유저를 요청하면 어떻게 되나?

GET http://localhost:8080/users/999

지금은 Spring이 자동으로 500 오류를 내보낸다. 클라이언트에게 "없다"는 걸 알려주려면 404를 보내야 한다.

예외 핸들러 추가

// GlobalExceptionHandler.kt
@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException::class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    fun handleNotFound(e: IllegalArgumentException): Map<String, String> {
        return mapOf("error" to (e.message ?: "요청한 리소스를 찾을 수 없습니다"))
    }
}

이제 없는 유저를 요청하면:

// 응답 상태: 404 Not Found
{
  "error": "유저를 찾을 수 없습니다. id: 999"
}

@RestControllerAdvice는 "이 클래스가 예외를 처리한다"는 표시다. 프로젝트 전체에서 발생하는 예외를 한 곳에서 관리할 수 있다.


전체 흐름 정리

POST /users 요청
    │
    ▼
UserController.createUser()
  - @RequestBody로 JSON을 CreateUserRequest 객체로 변환
    │
    ▼
UserService.createUser()
  - 이메일 유효성 검사
  - 문제 없으면 Repository 호출
    │
    ▼
UserRepository.save()
  - 리스트에 저장하고 User 객체 반환
    │
    ▼
UserController
  - User 객체를 JSON으로 자동 변환해서 응답

JSON 변환은 Spring이 자동으로 해준다. Kotlin의 data class를 쓰면 별도 설정 없이 바로 된다.


시리즈: Kotlin + Spring Boot 입문

  1. 개발 환경 설치 - IntelliJ, JDK, 첫 Spring Boot 실행
  2. Kotlin 기초 - 변수, 함수, 클래스, 인터페이스
  3. Spring Boot 구조 - MVC, 3계층 아키텍처
  4. IoC, DI, 인터페이스 추상화
  5. REST API 만들기 ← 현재 글
  6. JPA와 데이터베이스 연동