Java 개발자를 위한 Kotlin (1/4)

다음 편: [Kotlin] 2. 고급 기능

Java 개발자를 위한 Kotlin 시리즈

안드로이드에서 Kotlin을 맛봤고, 이제 서버(Spring Boot)에서 쓰려는 Java 개발자를 위한 가이드다.

이 문서에서는 Java와 대비해서 Kotlin 문법을 빠르게 훑는다.

변수 선언

Java에서 타입을 앞에 썼다면, Kotlin은 뒤에 쓴다. 그리고 대부분 추론해준다.

// 불변 (Java의 final)
val name: String = "블로그"
val name = "블로그"           // 타입 추론

// 가변
var count: Int = 0
var count = 0                 // 타입 추론

Kotlin에서는 val을 기본으로 쓰고, 꼭 필요할 때만 var을 쓴다. Java에서 final을 잘 안 붙이는 습관이 있었다면 여기서 고치면 된다.

Null Safety

Java 개발자가 Kotlin에서 가장 먼저 체감하는 차이다. Kotlin은 타입 시스템 레벨에서 null을 구분한다.

var name: String = "블로그"     // null 불가
var name: String? = null       // null 가능 (? 붙이면)

Safe Call (?.)

// Java
if (user != null) {
    String name = user.getName();
}

// Kotlin
val name = user?.name          // user가 null이면 name도 null

Elvis 연산자 (?:)

// Java
String name = user != null ? user.getName() : "익명";

// Kotlin
val name = user?.name ?: "익명"

Non-null Assertion (!!)

val name = user!!.name         // user가 null이면 NPE 던짐
!! 남용 금지

!!을 쓰는 순간 Kotlin의 null safety를 포기하는 거다. 정말 null이 아닌 게 확실할 때만 쓰고, 가능하면 ?.이나 ?:로 처리한다.

함수

// 기본
fun greet(name: String): String {
    return "안녕, $name"
}

// 단일 표현식 (한 줄이면 중괄호 생략 가능)
fun greet(name: String) = "안녕, $name"

// 기본값
fun greet(name: String = "방문자") = "안녕, $name"

// Named argument
greet(name = "관리자")

Java에서 메서드 오버로딩으로 처리하던 걸 기본값 파라미터 하나로 끝낼 수 있다. 오버로딩 지옥에서 벗어나는 느낌.

문자열 템플릿

// Java
String msg = "안녕, " + name + "! 나이: " + age;

// Kotlin
val msg = "안녕, $name! 나이: $age"
val msg = "글 수: ${posts.size}"   // 표현식은 {}로 감싸기

클래스

기본 클래스

// Java
public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
}

// Kotlin — 이게 끝이다
class User(val name: String, val age: Int)

주 생성자(primary constructor)에 val/var를 붙이면 프로퍼티 선언 + 생성자 파라미터 + getter(/setter)가 한 번에 된다.

Data Class

// Java에서 equals, hashCode, toString, copy 직접 만들던 걸...
data class PostResponse(
    val id: Long,
    val title: String,
    val viewCount: Int
)

data classequals(), hashCode(), toString(), copy()를 자동 생성한다. DTO 만들 때 매우 유용하다.

JPA 엔티티에는 data class 쓰지 말 것

JPA는 프록시를 만들어야 하는데 data classfinal이라 프록시가 안 된다. 엔티티는 일반 class로 만든다. 이건 다음 문서에서 자세히 다룬다.

상속

Kotlin의 클래스는 기본이 final이다. Java와 반대다.

// 상속 허용하려면 open 키워드 필요
open class Animal(val name: String)

class Dog(name: String) : Animal(name)

Object (싱글톤)

// Java의 싱글톤 패턴을 한 줄로
object BlogConfig {
    val maxPostSize = 10000
}

// 사용
BlogConfig.maxPostSize

Companion Object (static 대체)

class Post {
    companion object {
        const val MAX_TITLE_LENGTH = 200

        fun of(title: String, content: String): Post {
            // 팩토리 메서드
        }
    }
}

// 사용
Post.MAX_TITLE_LENGTH
Post.of("제목", "내용")

제어문

when (switch 대체)

// Java의 switch보다 훨씬 강력하다
val message = when (status) {
    200 -> "성공"
    401 -> "인증 실패"
    404 -> "찾을 수 없음"
    in 500..599 -> "서버 에러"
    else -> "알 수 없는 상태"
}

when표현식이다. 값을 반환한다. Java의 switch 같은 fall-through도 없다.

if도 표현식

// Java
String status = isPublished ? "발행" : "임시저장";

// Kotlin — 삼항 연산자가 없다. if가 표현식이니까 필요 없다.
val status = if (isPublished) "발행" else "임시저장"

for

for (i in 1..10) { }           // 1부터 10까지 (10 포함)
for (i in 1 until 10) { }     // 1부터 9까지 (10 미포함)
for (i in 10 downTo 1) { }    // 10부터 1까지
for (i in 1..10 step 2) { }   // 1, 3, 5, 7, 9

for (post in posts) { }        // 향상된 for문
for ((index, post) in posts.withIndex()) { }  // 인덱스 포함

컬렉션

// 불변 (기본)
val tags = listOf("Spring", "Kotlin")
val map = mapOf("key" to "value")

// 가변
val tags = mutableListOf("Spring", "Kotlin")
tags.add("JPA")

Kotlin은 불변 컬렉션이 기본이다. 수정이 필요하면 mutableListOf를 명시적으로 쓴다.

컬렉션 가공

Java의 Stream API와 비슷하지만 더 간결하다.

// Java
List<String> titles = posts.stream()
    .filter(p -> p.isPublished())
    .map(Post::getTitle)
    .collect(Collectors.toList());

// Kotlin
val titles = posts
    .filter { it.isPublished }
    .map { it.title }
  • stream(), collect() 불필요
  • it은 람다의 단일 파라미터를 가리키는 키워드
  • 체이닝이 자연스럽다

확장 함수

기존 클래스를 수정하지 않고 함수를 추가할 수 있다. Java에서는 StringUtils.isEmpty(str) 같은 유틸 클래스를 만들었지만, Kotlin에서는:

// 정의
fun String.isValidEmail(): Boolean {
    return this.contains("@") && this.contains(".")
}

// 사용
"test@blog.com".isValidEmail()   // true

Spring에서 유틸 메서드를 깔끔하게 정리할 때 유용하다.

스코프 함수

Kotlin 특유의 기능이다. 처음에 헷갈리지만 익숙해지면 코드가 매우 깔끔해진다.

let

// null 체크 + 변환
val name = user?.let {
    "${it.firstName} ${it.lastName}"
} ?: "익명"

apply

// 객체 초기화에 유용
val post = Post().apply {
    title = "새 글"
    content = "<p>내용</p>"
    isPublished = true
}

also

// 부수 효과 (로깅 등)
val post = postRepository.save(newPost).also {
    log.info("글 저장 완료: ${it.id}")
}

run

// 객체의 함수 실행 + 결과 반환
val length = "Hello".run {
    println(this)
    length       // 마지막 줄이 반환값
}

with

// run과 비슷한데 객체를 인자로 받음
val info = with(post) {
    "$title (조회수: $viewCount)"
}

정리

함수참조반환값용도
letit람다 결과null 체크, 변환
applythis객체 자신객체 초기화
alsoit객체 자신부수 효과 (로깅)
runthis람다 결과객체 함수 실행
withthis람다 결과run과 유사
처음엔 let, apply, also 세 개만 익히면 충분하다.

Java와의 상호운용

Kotlin은 Java 코드를 그대로 호출할 수 있다. 같은 프로젝트에 Java와 Kotlin 파일이 공존해도 문제없다.

// Java 라이브러리를 그냥 쓰면 된다
val list = ArrayList<String>()   // java.util.ArrayList

Spring Boot 라이브러리는 전부 Java로 되어 있지만 Kotlin에서 아무 문제 없이 사용 가능하다. 이게 코프링이 가능한 이유다.