REST API 만들기 - Controller, Service, Repository
실제 유저 API를 만들면서 Controller, Service, Repository 역할을 이해한다.
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로 유저 찾기. 없으면 nullsave(): 유저 저장하고 반환
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로 시작하는 경로 담당 |
@GetMapping | GET 요청 처리 |
@PostMapping | POST 요청 처리 |
@PathVariable | URL 경로의 값 (/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를 쓰면 별도 설정 없이 바로 된다.