본문으로 건너뛰기 Kotlin Spring Boot 입문 | 30분 만에 REST API 만들기 [초보자용]

Kotlin Spring Boot 입문 | 30분 만에 REST API 만들기 [초보자용]

Kotlin Spring Boot 입문 | 30분 만에 REST API 만들기 [초보자용]

이 글의 핵심

Kotlin Spring Boot 완전 초보자를 위한 입문 튜토리얼. Java보다 간결한 문법으로 REST API 만들기. 설치부터 배포까지 30분이면 충분합니다.

들어가며: Kotlin으로 더 쉽게 시작하기

“요즘 채용 공고 보므로 Kotlin + Spring Boot를 많이 쓰던데요?”

맞습니다. Kotlin은 최근 백엔드 개발에서 빠르게 성장하고 있습니다:

  • Coupang, Kakao, Toss, 당근마켓 등 주요 기업 사용
  • Java 대비 30-40% 적은 코드량
  • Null 안전성으로 NullPointerException 방지
  • Google이 Android 공식 언어로 지정

이 튜토리얼은 완전 초보자를 위한 가이드입니다. 설치 → 프로젝트 생성 → REST API → 실행까지 30분이면 충분합니다.

Java vs Kotlin 코드 비교

Java (장황함):

public class User {
    private Long id;
    private String name;
    
    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

Kotlin (간결함):

data class User(
    val id: Long,
    var name: String
)

똑같은 기능이지만 Kotlin은 2줄!

이 튜토리얼에서 만들 것

간단한 북마크 관리 REST API:

  • GET /api/bookmarks - 북마크 목록 조회
  • POST /api/bookmarks - 북마크 추가
  • PUT /api/bookmarks/{id} - 북마크 수정
  • DELETE /api/bookmarks/{id} - 북마크 삭제

사전 요구사항

  • Java 17 이상 설치 (Kotlin은 JVM에서 실행)
  • IDE: IntelliJ IDEA Community (무료, Kotlin 기본 지원)

1단계: 환경 설정 (3분)

Java 설치 확인

# 터미널에서 확인
java -version

# 출력 예시:
# openjdk version "17.0.2"

Java가 없다면:

  • Windows: https://adoptium.net/ → JDK 17 다운로드
  • Mac: brew install openjdk@17
  • Linux: sudo apt install openjdk-17-jdk

IntelliJ IDEA 설치

https://www.jetbrains.com/idea/download/

  • Community Edition (무료) 다운로드
  • Kotlin 플러그인 자동 포함

2단계: 프로젝트 생성 (5분)

방법 1: Spring Initializr (웹)

  1. https://start.spring.io/ 접속
  2. 다음과 같이 설정:
    • Project: Gradle - Kotlin
    • Language: Kotlin
    • Spring Boot: 3.2.x (최신 안정 버전)
    • Packaging: Jar
    • Java: 17
  3. Project Metadata:
    • Group: com.example
    • Artifact: demo
    • Name: demo
    • Package name: com.example.demo
  4. Dependencies 추가:
    • Spring Web
    • Spring Data JPA
    • H2 Database
  5. GENERATE 클릭 → ZIP 다운로드
  6. 압축 해제 후 IntelliJ로 열기

방법 2: IntelliJ IDEA (권장)

  1. New Project → Spring Initializr
  2. 위와 동일하게 설정
  3. Create

3단계: 첫 REST API 만들기 (10분)

프로젝트 구조

src/main/kotlin/com/example/demo/
├── DemoApplication.kt          # 메인
├── controller/
│   └── BookmarkController.kt   # REST API
├── model/
│   └── Bookmark.kt             # 데이터 모델
├── repository/
│   └── BookmarkRepository.kt   # DB 접근
└── service/
    └── BookmarkService.kt      # 비즈니스 로직

1) 데이터 모델 (Bookmark.kt)

src/main/kotlin/com/example/demo/model/Bookmark.kt:

package com.example.demo.model

import jakarta.persistence.*

@Entity
@Table(name = "bookmarks")
data class Bookmark(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    
    var title: String,
    var url: String,
    var description: String? = null
)

Java와 비교:

  • data class: getter/setter/equals/hashCode 자동 생성
  • val: 불변 (Java의 final)
  • var: 가변
  • String?: Null 허용 타입

2) Repository (BookmarkRepository.kt)

src/main/kotlin/com/example/demo/repository/BookmarkRepository.kt:

package com.example.demo.repository

import com.example.demo.model.Bookmark
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface BookmarkRepository : JpaRepository<Bookmark, Long>

한 줄로 끝! Spring Data JPA가 자동으로 구현합니다.

3) Service (BookmarkService.kt)

src/main/kotlin/com/example/demo/service/BookmarkService.kt:

package com.example.demo.service

import com.example.demo.model.Bookmark
import com.example.demo.repository.BookmarkRepository
import org.springframework.stereotype.Service

@Service
class BookmarkService(
    private val repository: BookmarkRepository
) {
    fun getAllBookmarks(): List<Bookmark> = repository.findAll()
    
    fun getBookmarkById(id: Long): Bookmark? = 
        repository.findById(id).orElse(null)
    
    fun createBookmark(bookmark: Bookmark): Bookmark = 
        repository.save(bookmark)
    
    fun updateBookmark(id: Long, bookmarkDetails: Bookmark): Bookmark {
        val bookmark = repository.findById(id)
            .orElseThrow { RuntimeException("Bookmark not found") }
        
        bookmark.title = bookmarkDetails.title
        bookmark.url = bookmarkDetails.url
        bookmark.description = bookmarkDetails.description
        
        return repository.save(bookmark)
    }
    
    fun deleteBookmark(id: Long) = repository.deleteById(id)
}

Kotlin의 장점:

  • 생성자 주입: 파라미터에 val만 붙이면 자동으로 필드 선언 + 초기화
  • 표현식 함수: fun get() = repository.findAll() (중괄호 생략)
  • Null 안전: Bookmark? 타입으로 명시적 처리

4) Controller (BookmarkController.kt)

src/main/kotlin/com/example/demo/controller/BookmarkController.kt:

package com.example.demo.controller

import com.example.demo.model.Bookmark
import com.example.demo.service.BookmarkService
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/bookmarks")
class BookmarkController(
    private val service: BookmarkService
) {
    // GET /api/bookmarks - 전체 조회
    @GetMapping
    fun getAllBookmarks(): List<Bookmark> = service.getAllBookmarks()
    
    // GET /api/bookmarks/{id} - 단건 조회
    @GetMapping("/{id}")
    fun getBookmarkById(@PathVariable id: Long): ResponseEntity<Bookmark> {
        val bookmark = service.getBookmarkById(id)
        return if (bookmark != null) {
            ResponseEntity.ok(bookmark)
        } else {
            ResponseEntity.notFound().build()
        }
    }
    
    // POST /api/bookmarks - 생성
    @PostMapping
    fun createBookmark(@RequestBody bookmark: Bookmark): Bookmark =
        service.createBookmark(bookmark)
    
    // PUT /api/bookmarks/{id} - 수정
    @PutMapping("/{id}")
    fun updateBookmark(
        @PathVariable id: Long,
        @RequestBody bookmarkDetails: Bookmark
    ): ResponseEntity<Bookmark> {
        return try {
            val updated = service.updateBookmark(id, bookmarkDetails)
            ResponseEntity.ok(updated)
        } catch (e: RuntimeException) {
            ResponseEntity.notFound().build()
        }
    }
    
    // DELETE /api/bookmarks/{id} - 삭제
    @DeleteMapping("/{id}")
    fun deleteBookmark(@PathVariable id: Long): ResponseEntity<Void> {
        service.deleteBookmark(id)
        return ResponseEntity.ok().build()
    }
}

Java 대비 코드량 절반!

5) application.properties 설정

src/main/resources/application.properties:

# 서버 포트
server.port=8080

# H2 데이터베이스 (개발용)
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA 설정
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

# H2 콘솔 활성화
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

4단계: 실행 및 테스트 (7분)

서버 실행

# Gradle
./gradlew bootRun

# 또는 IntelliJ에서 DemoApplication.kt 실행 (▶️ 버튼)

출력:

Tomcat started on port 8080
Started DemoApplicationKt in 2.123 seconds

API 테스트 (curl)

# 1. 북마크 추가
curl -X POST http://localhost:8080/api/bookmarks \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Kotlin 공식 문서",
    "url": "https://kotlinlang.org",
    "description": "Kotlin 배우기"
  }'

# 응답:
# {"id":1,"title":"Kotlin 공식 문서","url":"https://kotlinlang.org","description":"Kotlin 배우기"}

# 2. 전체 조회
curl http://localhost:8080/api/bookmarks

# 3. 단건 조회
curl http://localhost:8080/api/bookmarks/1

# 4. 수정
curl -X PUT http://localhost:8080/api/bookmarks/1 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Kotlin 공식 문서 (수정됨)",
    "url": "https://kotlinlang.org/docs",
    "description": "깊이 배우기"
  }'

# 5. 삭제
curl -X DELETE http://localhost:8080/api/bookmarks/1

브라우저에서 테스트

  1. H2 콘솔 접속: http://localhost:8080/h2-console

    • JDBC URL: jdbc:h2:mem:testdb
    • Username: sa
    • Password: (빈칸)
  2. SQL 쿼리:

SELECT * FROM bookmarks;
INSERT INTO bookmarks (title, url, description) 
VALUES ('Google', 'https://google.com', 'Search engine');

5단계: Kotlin의 핵심 문법 (5분)

1) data class (자동 생성)

// Kotlin: 이것만으로 getter/setter/equals/hashCode/toString 자동 생성
data class User(val id: Long, var name: String)

// Java 동등 코드: 50줄+

2) Null 안전성

// Null 불가 타입
val name: String = "홍길동"
// name = null  // 컴파일 에러!

// Null 가능 타입
val description: String? = null
println(description?.length)  // Safe call: null이면 null 반환

// Elvis 연산자
val length = description?.length ?: 0  // null이면 0

Java에서는 항상 NullPointerException 위험!

3) 표현식 함수

// 한 줄 함수
fun add(a: Int, b: Int) = a + b

// 조건식도 표현식
fun max(a: Int, b: Int) = if (a > b) a else b

4) 생성자 주입 (간결함)

// Kotlin: 한 줄
class TodoController(private val service: TodoService)

// Java: 여러 줄
public class TodoController {
    private final TodoService service;
    
    public TodoController(TodoService service) {
        this.service = service;
    }
}

5) 스마트 캐스팅

fun process(value: Any) {
    if (value is String) {
        println(value.length)  // 자동으로 String으로 캐스팅!
    }
}

실무 팁

자주 하는 실수

1) data class에서 var 남용

// ❌ 나쁜 예: 모두 가변
data class User(var id: Long, var name: String)

// ✅ 좋은 예: ID는 불변
data class User(val id: Long, var name: String)

2) !! 연산자 남용 (Null 강제 언래핑)

// ❌ 위험: NullPointerException 가능
val length = description!!.length

// ✅ 안전: Safe call 또는 Elvis
val length = description?.length ?: 0

3) lateinit 잘못 사용

// ❌ nullable한 변수에는 사용 불가
lateinit var name: String?  // 컴파일 에러

// ✅ Non-null 타입에만 사용
lateinit var name: String

디버깅 팁

# application.properties
logging.level.org.springframework=DEBUG
logging.level.com.example=DEBUG

# SQL 로그 상세히
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

IntelliJ 단축키

단축키기능
Ctrl + Shift + KKotlin REPL 실행
Ctrl + Alt + L코드 포맷팅
Alt + EnterQuick fix
Ctrl + Space자동 완성

다음 단계: 실무로 가는 길

이 튜토리얼을 마쳤다면 다음을 학습하세요:

필수 학습 (순서대로)

1) Exception 처리

@RestControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(RuntimeException::class)
    fun handleNotFound(ex: RuntimeException) =
        ResponseEntity.status(404).body(ex.message)
}

2) Validation

data class Bookmark(
    @field:NotBlank(message = "제목은 필수입니다")
    var title: String,
    
    @field:URL(message = "올바른 URL을 입력하세요")
    var url: String
)

@PostMapping
fun create(@Valid @RequestBody bookmark: Bookmark) = ...

3) Kotlin Coroutines (비동기)

@Service
class BookmarkService(private val repository: BookmarkRepository) {
    suspend fun getAllBookmarks() = withContext(Dispatchers.IO) {
        repository.findAll()
    }
}

4) 테스트

@SpringBootTest
class BookmarkControllerTest {
    @Test
    fun `북마크 생성 테스트`() {
        // given, when, then
    }
}

5) 프로덕션 DB (PostgreSQL)

// build.gradle.kts
dependencies {
    runtimeOnly("org.postgresql:postgresql")
}
# application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=postgres
spring.datasource.password=password
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

학습 로드맵

Kotlin 입문 튜토리얼 (30분)

Kotlin 시리즈 #1-10 (5시간)

Kotlin Coroutines 학습 (2시간)

실전 프로젝트 (1-2주)

포트폴리오 제작

흔한 에러 및 해결

에러 1: unresolved reference: springframework

# build.gradle.kts 확인
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
}

# 의존성 다시 다운로드
./gradlew clean build --refresh-dependencies

에러 2: lateinit property has not been initialized

// ❌ 초기화 전 사용
lateinit var service: BookmarkService
println(service.getAllBookmarks())

// ✅ 생성자 주입 사용
class Controller(private val service: BookmarkService)

에러 3: Type mismatch: inferred type is Long? but Long was expected

// ❌ Nullable을 Non-null에 할당
val id: Long = bookmark.id  // bookmark.id는 Long?

// ✅ Null 체크 또는 Elvis
val id: Long = bookmark.id ?: 0L

Java vs Kotlin 실전 비교

엔티티 클래스

Java (34줄):

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    
    public User() {}
    
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    @Override
    public boolean equals(Object o) { ... }
    
    @Override
    public int hashCode() { ... }
    
    @Override
    public String toString() { ... }
}

Kotlin (7줄):

@Entity
@Table(name = "users")
data class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    var name: String,
    var email: String
)

80% 코드량 감소!


실전 프로젝트 아이디어

이제 배운 내용으로 실전 프로젝트를 만들어 보세요:

초급

  • 간단한 북마크 관리 앱
  • 메모장 API
  • URL 단축 서비스

중급

  • JWT 인증이 있는 TODO 앱
  • 파일 업로드 서비스
  • 실시간 채팅 (Kotlin Coroutines)

고급

  • 마이크로서비스 (Spring Cloud)
  • Kafka 이벤트 스트리밍
  • Redis 캐싱 + PostgreSQL

빠른 레퍼런스

주요 애노테이션

애노테이션용도Java와 동일
@RestControllerREST API 컨트롤러
@GetMappingGET 요청
@PostMappingPOST 요청
@RequestBodyJSON → 객체
@PathVariableURL 경로 변수
@Service비즈니스 로직
@RepositoryDB 접근
@EntityJPA 엔티티

Kotlin 고유 문법

// Null 안전
val name: String? = null
println(name?.length ?: 0)

// data class
data class User(val id: Long, var name: String)

// 표현식 함수
fun add(a: Int, b: Int) = a + b

// 생성자 주입
class Service(private val repo: Repository)

// when 표현식 (switch 대체)
when (status) {
    200 -> "OK"
    404 -> "Not Found"
    else -> "Error"
}

Spring Boot 내부와 프로덕션에 가까운 메모

서블릿 스택과 임베디드 Tomcat

Spring Boot Web은 기본적으로 서블릿 컨테이너(임베디드 Tomcat 등) 위에서 동작합니다. HTTP 요청은 커넥터 스레드 풀에서 받으며, 디스패처 서블릿이 컨트롤러 매핑으로 넘깁니다. 블로킹 I/O 모델에서는 동시 처리량이 스레드 풀 크기와 직결되므로, 장시간 점유하는 요청이 많으면 스레드 고갈이 납니다. 이때는 WebFlux·비동기 서블릿·외부 워커 큐 같은 아키텍처 변경을 검토합니다.

설정 계층: application.yml과 프로파일

실제 서비스에서는 application-dev.yml, application-prod.yml처럼 프로파일로 DB URL·로그 레벨을 나눕니다. 시크릿은 저장소에 평문으로 두지 않고 환경 변수·시크릿 매니저로 주입합니다.

관측: Actuator와 헬스

운영 환경에서는 spring-boot-starter-actuator/health, /metrics를 열으며, Kubernetes라면 liveness/readiness 프로브와 연결합니다. 데이터베이스가 끊긴 상태에서도 프로세스는 살아 있을 수 있으므로 readiness에 DB 검사를 넣는지 팀 규약을 정합니다.

트러블슈팅 빠른 표

증상점검
Whitelabel Error Page 500서버 로그 스택 트레이스, @ControllerAdvice 없을 때 기본 페이지
포트 충돌server.port, 다른 로컬 프로세스 점유
JPA가 테이블을 안 만듦ddl-auto, 엔티티 스캔 패키지, DB 권한
CORS 에러@CrossOrigin 또는 API 게이트웨이에서 헤더 — 브라우저만의 증상임을 기억

마무리

핵심 정리

  1. Kotlin은 Java보다 30-40% 코드 감소
  2. data class로 엔티티 한 줄 정의
  3. Null 안전성으로 NullPointerException 방지
  4. Spring Boot의 모든 기능 100% 호환

30분 동안 배운 것

  • Kotlin + Spring Boot 프로젝트 생성
  • REST API 4가지 (GET, POST, PUT, DELETE)
  • data class로 엔티티 정의
  • JPA로 데이터베이스 연동
  • H2 인메모리 DB 사용

Kotlin을 선택해야 하는 이유

Java 대비 장점:

  • ✅ 코드량 30-40% 감소
  • ✅ Null 안전성 (컴파일 타임 체크)
  • ✅ 간결한 문법 (data class, 표현식)
  • ✅ Coroutines (비동기 처리)
  • ✅ 100% Java 호환

채용 시장

  • Coupang, Kakao, Toss, 당근마켓, 배달의민족
  • 스타트업에서 빠르게 도입 중
  • Android + 백엔드 동시 개발 가능

축하합니다! 첫 Kotlin Spring Boot API를 만들었습니다. 이제 Kotlin 시리즈 #9 Spring Boot로 넘어가 실무 수준을 익히세요.


관련 글 (내부 링크)

Kotlin과 함께 보면 좋은 백엔드 개발 가이드입니다:


한 줄 요약: Kotlin은 Java보다 30-40% 적은 코드로 Spring Boot API를 만들 수 있습니다. data class와 Null 안전성으로 생산성이 높아 최근 백엔드 채용에서 주목받고 있습니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Kotlin Spring Boot 완전 초보자를 위한 입문 튜토리얼. Java보다 간결한 문법으로 REST API 만들기. 설치부터 배포까지 30분이면 충분합니다. 코드 복사만으로 따라할 수 있는 단계별 가이드. S… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Kotlin, Spring Boot, 입문, 튜토리얼, REST API, 초보자 등으로 검색하시면 이 글이 도움이 됩니다.