kotlin(1)

최준병·2026년 5월 27일

이 프로젝트에서 쓰인 Kotlin 문법 정리

각 항목마다 이 프로젝트의 실제 코드를 예시로 사용합니다.


목차

  1. 변수 선언 — val vs var
  2. 타입 추론
  3. 함수 선언 — fun
  4. 표현식 함수 (단일 표현식)
  5. 기본 파라미터값 & 이름 있는 인수
  6. 클래스와 주 생성자
  7. data class
  8. companion object
  9. Null 안전성 — ?, ?:, !!
  10. 문자열 템플릿 — $
  11. 람다와 고차 함수 — map, it
  12. 스코프 함수 — also
  13. require() — 전제 조건 검사
  14. mapOf() & to 중위 함수
  15. 범위 연산자 — .. & in
  16. 멀티라인 문자열 — """ & trimIndent()
  17. 숫자 리터럴의 밑줄 — 1_000_000
  18. 인터페이스 — interface
  19. 와일드카드 import — *
  20. 코틀린에서 자바 클래스 참조 — ::class.java
  21. Unit vs Void
  22. 후행 쉼표 (Trailing Comma)

1. 변수 선언 — val vs var

키워드의미Java 대응
val한 번만 할당 가능 (불변)final
var재할당 가능 (가변)일반 변수
// PostService.kt
val offset = (page - 1) * size   // 이후 바꿀 일 없음 → val
val posts  = postMapper.findAll(offset, size)
// Post.kt (domain)
// MyBatis 가 리플렉션으로 값을 채워넣어야 하므로 var 사용
var id: Long = 0
var title: String = ""
// PostService.kt — updatePost()
// 조회 후 값을 바꿔야 하므로 var 이어야 가능
post.title   = request.title    // Post.title 이 var 이기 때문에 가능
post.content = request.content

원칙: 바꿀 필요가 없으면 val, 바꿔야 한다면 var.
DTO(요청/응답 객체)는 한 번 만들고 끝이니 val, 도메인 객체는 DB에서 값을 주입받아야 하니 var.


2. 타입 추론

Kotlin 컴파일러가 오른쪽 값을 보고 타입을 자동으로 알아냅니다.

val offset = (page - 1) * size   // Int 로 추론
val start  = System.currentTimeMillis()  // Long 으로 추론
val posts  = postMapper.findAll(offset, size)  // List<Post> 로 추론

타입을 명시할 수도 있습니다 (선택 사항).

val offset: Int  = (page - 1) * size   // 명시적으로 써도 됨
val start: Long  = System.currentTimeMillis()

3. 함수 선언 — fun

// 기본 형태
fun 함수이름(파라미터: 타입): 반환타입 {
    // 본문
}
// PostService.kt
fun getPosts(page: Int, size: Int): PostListResponse {
    val offset = (page - 1) * size
    val posts  = postMapper.findAll(offset, size)
    // ...
    return PostListResponse(...)
}

반환값이 없으면 반환 타입을 생략하거나 Unit 을 씁니다.

fun deletePost(id: Long) {           // 반환 타입 생략 = Unit
    postMapper.delete(id)
}

4. 표현식 함수 (단일 표현식)

함수 본문이 return 표현식 딱 한 줄이면, = 으로 줄여 쓸 수 있습니다.

// PostController.kt — 일반 형태
fun getPost(@PathVariable id: Long): ResponseEntity<PostResponse> {
    return ResponseEntity.ok(postService.getPost(id))
}

// ↓ 표현식 함수로 줄이면
fun getPost(@PathVariable id: Long): ResponseEntity<PostResponse> =
    ResponseEntity.ok(postService.getPost(id))

이 프로젝트의 Controller 메서드 대부분이 이 형태입니다.


5. 기본 파라미터값 & 이름 있는 인수

기본 파라미터값 (Default Parameter)

Java 의 메서드 오버로딩 없이, 파라미터에 기본값을 지정할 수 있습니다.

// PostController.kt
fun getPosts(
    @RequestParam(defaultValue = "1")  page: Int = 1,
    @RequestParam(defaultValue = "20") size: Int = 20,
): ResponseEntity<PostListResponse>
// Post.kt (domain) — 모든 파라미터에 기본값 → "인수 없는 생성자" 효과
data class Post(
    var id: Long = 0,
    var title: String = "",
    var content: String = "",
    // ...
)

이름 있는 인수 (Named Argument)

함수를 호출할 때 파라미터 이름을 명시해서 가독성을 높입니다.

// PostService.kt — createPost()
val post = Post(
    title   = request.title,    // 이름을 붙여서 호출
    content = request.content,
    author  = request.author,
)

이름을 붙이면 순서를 바꿔도 되고, 어떤 값이 어떤 파라미터인지 한눈에 보입니다.


6. 클래스와 주 생성자

Kotlin 클래스는 선언과 동시에 생성자를 정의합니다.

// 클래스 이름 뒤 괄호가 "주 생성자(Primary Constructor)"
class PostController(private val postService: PostService) {
    // postService 는 클래스 전체에서 사용 가능한 필드가 됨
}

private val postService 처럼 생성자 파라미터 앞에 접근 제어자를 붙이면,
생성자 파라미터이자 클래스 필드 가 됩니다.

Java 로 표현하면:

// Java
public class PostController {
    private final PostService postService;  // 필드 선언

    public PostController(PostService postService) {  // 생성자
        this.postService = postService;
    }
}

Spring 은 이 생성자를 보고 PostService 빈을 자동 주입(DI)합니다.


7. data class

data 키워드를 붙이면 컴파일러가 아래 메서드를 자동 생성합니다.

자동 생성 메서드역할
toString()Post(id=1, title=안녕, ...) 형태의 문자열 반환
equals()모든 필드값이 같으면 true
hashCode()equals() 와 일관된 해시값
copy()일부 필드만 바꾼 새 객체 생성
componentN()구조 분해 선언 지원
// Post.kt
data class Post(
    var id: Long = 0,
    var title: String = "",
    var content: String = "",
    var author: String = "",
    var viewCount: Int = 0,
    var createdAt: LocalDateTime = LocalDateTime.now(),
    var updatedAt: LocalDateTime = LocalDateTime.now(),
)

copy() 활용 예시

val original = Post(title = "제목", content = "내용", author = "홍길동")

// title 만 바꾼 새 Post 객체 생성 (original 은 그대로)
val updated = original.copy(title = "새 제목")

data class 는 언제 쓰나?
데이터를 담는 것이 주 목적인 클래스 (DTO, 도메인 객체 등).
비즈니스 로직이 많은 클래스는 일반 class 가 낫습니다.


8. companion object

Java 의 static 에 해당하는 개념입니다.
Kotlin 에는 static 키워드가 없고, 대신 클래스 안에 companion object 블록을 만듭니다.

// PostDto.kt
data class PostResponse(
    val id: Long,
    val title: String,
    // ...
) {
    companion object {                         // ← 여기
        fun from(post: Post) = PostResponse(   // 정적 팩토리 메서드
            id    = post.id,
            title = post.title,
            // ...
        )
    }
}

호출 방법

// Java 의 PostResponse.from(post) 와 동일하게 호출
val response = PostResponse.from(post)

Java 와 비교

// Java
public class PostResponse {
    // ...
    public static PostResponse from(Post post) {   // static 메서드
        return new PostResponse(post.getId(), post.getTitle(), ...);
    }
}

왜 팩토리 메서드 패턴을 쓰나?
도메인 객체(Post)를 응답 DTO(PostResponse)로 변환하는 로직을 한 곳에 모아두면,
변환 방식이 바뀔 때 from() 만 수정하면 됩니다.


9. Null 안전성 — ?, ?:, !!

Kotlin 은 null 이 될 수 있는 타입과 없는 타입을 컴파일 시점에 구분합니다.

표기의미
String절대 null 이 될 수 없음
String?null 이 될 수도 있음 (nullable)

? — nullable 타입

// PostMapper.kt
fun findById(@Param("id") id: Long): Post?
//                                       ↑ DB에 없으면 null 을 반환할 수 있음

?: — 엘비스 연산자 (Elvis Operator)

"null 이면 오른쪽을 실행해라" 라는 뜻입니다.

// PostService.kt
val post = postMapper.findById(id)
    ?: throw NoSuchElementException("게시글을 찾을 수 없습니다. id=$id")
//  ↑ findById 가 null 을 반환하면 예외를 던짐
// DataSeederController.kt
val totalCount = jdbc.queryForObject("SELECT COUNT(*) FROM posts", Long::class.java) ?: 0
//                                                                                    ↑ null 이면 0 으로 대체

!! — non-null 단언 (강제 언박싱)

"나는 이 값이 null 이 아님을 확신한다" 는 선언입니다.
null 이면 NullPointerException 이 발생하므로, 사용에 주의해야 합니다.

val name: String? = "홍길동"
val length = name!!.length   // null 이 아님을 개발자가 보장

이 프로젝트에서는 !! 대신 ?: 로 안전하게 처리하고 있습니다.


10. 문자열 템플릿 — $

문자열 안에 변수나 표현식을 직접 삽입할 수 있습니다.

// PostService.kt
throw NoSuchElementException("게시글을 찾을 수 없습니다. id=$id")
//                                                            ↑ $변수명

중괄호로 감싸면 더 복잡한 표현식도 넣을 수 있습니다.

// DataSeederController.kt
"rate" to "${count * 1000 / elapsed.coerceAtLeast(1)} 건/초"
//          ↑ ${ 표현식 }

Java 와 비교하면:

// Java
throw new NoSuchElementException("게시글을 찾을 수 없습니다. id=" + id);

// Kotlin
throw NoSuchElementException("게시글을 찾을 수 없습니다. id=$id")

11. 람다와 고차 함수 — map, it

람다 (Lambda)

중괄호 { } 로 감싼 코드 블록입니다. 함수의 인수로 전달할 수 있습니다.

// PostService.kt
posts.map { PostResponse.from(it) }
//    ↑ map 에 람다를 전달

it — 암묵적 파라미터

람다 파라미터가 하나뿐일 때, 이름을 직접 지어주는 대신 it 으로 참조합니다.

posts.map { it -> PostResponse.from(it) }  // it 명시적으로 쓴 것
posts.map { PostResponse.from(it) }        // it 생략한 것 (같은 의미)

map

컬렉션의 각 요소를 변환해서 새 리스트를 만드는 함수입니다.

// [Post, Post, Post] → [PostResponse, PostResponse, PostResponse]
val responses: List<PostResponse> = posts.map { PostResponse.from(it) }

Java 의 Stream 으로 표현하면:

// Java
List<PostResponse> responses = posts.stream()
    .map(PostResponse::from)
    .collect(Collectors.toList());

함수 참조 (::)

람다 대신 메서드 참조를 쓸 수도 있습니다.

posts.map { PostResponse.from(it) }  // 람다
posts.map(PostResponse::from)        // 함수 참조 (동일한 의미)

12. 스코프 함수 — also

스코프 함수는 객체에 대해 코드 블록을 실행하는 함수들입니다.
이 프로젝트에서는 also 를 사용합니다.

also

"이것도 해라" 라는 의미입니다.
객체 자신을 it 으로 받아 부수 작업을 한 뒤, 원래 객체를 그대로 반환합니다.

// PostService.kt
return PostResponse.from(post.also { it.viewCount++ })
//                            ↑ post.viewCount 를 증가시키고, post 자체를 반환
//                       ↑ 증가된 post 를 from() 에 전달

흐름을 풀어 쓰면:

// also 없이 쓴다면
post.viewCount++
return PostResponse.from(post)

// also 를 쓰면 한 줄로
return PostResponse.from(post.also { it.viewCount++ })

스코프 함수 한눈에 비교

함수참조 방식반환값주 용도
letit람다 결과null 체크 후 변환
runthis람다 결과초기화 + 계산
applythis객체 자신객체 설정(빌더 패턴)
alsoit객체 자신부수 작업 (로깅, 증가 등)
withthis람다 결과특정 객체에 여러 작업

13. require() — 전제 조건 검사

파라미터의 유효성을 검사할 때 씁니다.
조건이 false 이면 IllegalArgumentException 을 자동으로 던집니다.

// DataSeederController.kt
require(count in 1..10_000_000) { "count 는 1 ~ 10,000,000 사이여야 합니다." }
//      ↑ 조건                    ↑ 실패 시 메시지를 반환하는 람다

Java 로 표현하면:

// Java
if (!(count >= 1 && count <= 10_000_000)) {
    throw new IllegalArgumentException("count 는 1 ~ 10,000,000 사이여야 합니다.");
}

비슷한 함수: check() 는 상태 검사 (IllegalStateException), error() 는 무조건 예외.


14. mapOf() & to 중위 함수

mapOf()

불변 Map 을 만드는 표준 함수입니다.

// DataSeederController.kt
return mapOf(
    "inserted"   to count,
    "totalPosts" to totalCount,
    "elapsedMs"  to elapsed,
)

to — 중위 함수 (Infix Function)

A to BPair(A, B) 와 같습니다.
mapOf()Pair 들을 받아 Map 을 만듭니다.

"inserted" to count        // Pair<String, Int>
// = Pair("inserted", count)  // 동일한 의미

Java 로 표현하면:

// Java
Map<String, Object> result = new HashMap<>();
result.put("inserted", count);
result.put("totalPosts", totalCount);

15. 범위 연산자 — .. & in

.. — 범위 생성

1..10          // 1 이상 10 이하 (IntRange)
1..10_000_000  // 1 이상 천만 이하

in — 범위 포함 여부 검사

// DataSeederController.kt
require(count in 1..10_000_000) { "..." }
//             ↑ count 가 1~10,000,000 범위 안에 있는지 확인

Java 로 표현하면:

// Java
count >= 1 && count <= 10_000_000

컬렉션에도 사용할 수 있습니다.

val list = listOf("a", "b", "c")
"b" in list    // true

16. 멀티라인 문자열 — """ & trimIndent()

""" — 삼중 따옴표 문자열

줄바꿈과 들여쓰기를 그대로 포함한 문자열입니다.

// DataSeederController.kt
jdbc.execute("""
    INSERT INTO posts (title, content, author, ...)
    SELECT
        CASE (i % 10)
            WHEN 0 THEN '공지사항: ...' || i
            ...
        END
    FROM generate_series(1, $count) AS t(i)
""".trimIndent())

trimIndent()

삼중 따옴표 문자열에서 공통 들여쓰기를 제거합니다.
붙이지 않으면 SQL 앞에 공백이 그대로 포함됩니다.

val sql = """
    SELECT *
    FROM posts
""".trimIndent()

// trimIndent() 적용 결과:
// "SELECT *\nFROM posts\n"
// (앞의 공백 4칸이 사라짐)

17. 숫자 리터럴의 밑줄 — 1_000_000

긴 숫자를 읽기 쉽게 구분하는 문법입니다. 값 자체에는 영향이 없습니다.

// DataSeederController.kt
require(count in 1..10_000_000) { "..." }
//                  ↑ 10,000,000 과 완전히 같은 값
val million = 1_000_000   // 1000000 과 동일
val billion = 1_000_000_000

18. 인터페이스 — interface

Java 와 거의 같습니다. 구현 없이 메서드 시그니처만 선언합니다.

// PostMapper.kt
@Mapper
interface PostMapper {

    fun findAll(
        @Param("offset") offset: Int,
        @Param("limit")  limit: Int,
    ): List<Post>

    fun findById(@Param("id") id: Long): Post?   // null 반환 가능

    fun insert(post: Post)   // 반환값 없음 (Unit)
}

MyBatis 의 @Mapper 어노테이션을 붙이면,
Spring 이 시작할 때 이 인터페이스의 구현체를 자동으로 만들어줍니다.


19. 와일드카드 import — *

패키지 안의 모든 것을 한 번에 가져옵니다.

// PostController.kt
import me.study.index.dto.*   // dto 패키지의 모든 클래스를 가져옴

// 덕분에 아래를 따로 import 하지 않아도 됨:
// import me.study.index.dto.PostCreateRequest
// import me.study.index.dto.PostUpdateRequest
// import me.study.index.dto.PostResponse
// import me.study.index.dto.PostListResponse

20. 코틀린에서 자바 클래스 참조 — ::class.java

Kotlin 의 타입 시스템과 Java 의 타입 시스템을 연결할 때 씁니다.

// DataSeederController.kt
jdbc.queryForObject("SELECT COUNT(*) FROM posts", Long::class.java)
//                                                ↑ Java 의 Long.class 에 해당
// javaClass 프로퍼티: 현재 인스턴스의 Java 클래스를 반환
private val log = LoggerFactory.getLogger(javaClass)
//                                        ↑ this.getClass() 와 동일
Kotlin 표현Java 표현
String::class— (KClass)
String::class.javaString.class
javaClassthis.getClass()

21. Unit vs Void

KotlinJava
반환값 없는 함수Unit (생략 가능)void
제네릭에서 "없음"UnitVoid
// 함수 반환값이 없으면 Unit (보통 생략)
fun deletePost(id: Long): Unit { ... }
fun deletePost(id: Long)       { ... }  // 위와 동일
// ResponseEntity 에서 반환 바디가 없을 때 Java 의 Void 를 써야 하는 경우
// PostController.kt
fun deletePost(@PathVariable id: Long): ResponseEntity<Void> {
    postService.deletePost(id)
    return ResponseEntity.noContent().build()
}

Spring 의 ResponseEntity 는 Java 클래스라 Void 를 사용합니다.
순수 Kotlin 코드에서 "반환값 없음"은 Unit 입니다.


22. 후행 쉼표 (Trailing Comma)

마지막 파라미터/요소 뒤에 쉼표를 붙여도 됩니다.

// PostDto.kt
data class PostCreateRequest(
    val title: String,
    val content: String,
    val author: String,   // ← 마지막에도 쉼표 OK
)

Java 에서는 문법 오류지만 Kotlin 에서는 허용됩니다.
나중에 줄을 추가하거나 순서를 바꿀 때 git diff 가 깔끔해지는 장점이 있습니다.


전체 흐름으로 보는 문법 총정리

HTTP 요청
   ↓
[Controller]  — 표현식 함수(=), @PathVariable, @RequestParam, 기본 파라미터값
   ↓
[Service]     — val/var, 엘비스 연산자(?:), also, map, require
   ↓
[Mapper]      — interface, nullable 반환(Post?), @Param
   ↓
[Domain/DTO]  — data class, companion object, val/var, 기본값
   ↓
DB (PostgreSQL + MyBatis)
profile
나의 기록

0개의 댓글