다음 편: [Kotlin] 2. 고급 기능
안드로이드에서 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 class는 equals(), hashCode(), toString(), copy()를 자동 생성한다. DTO 만들 때 매우 유용하다.
JPA는 프록시를 만들어야 하는데 data class는 final이라 프록시가 안 된다. 엔티티는 일반 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)"
}
정리
| 함수 | 참조 | 반환값 | 용도 |
|---|---|---|---|
let | it | 람다 결과 | null 체크, 변환 |
apply | this | 객체 자신 | 객체 초기화 |
also | it | 객체 자신 | 부수 효과 (로깅) |
run | this | 람다 결과 | 객체 함수 실행 |
with | this | 람다 결과 | run과 유사 |
let, apply, also 세 개만 익히면 충분하다.Java와의 상호운용
Kotlin은 Java 코드를 그대로 호출할 수 있다. 같은 프로젝트에 Java와 Kotlin 파일이 공존해도 문제없다.
// Java 라이브러리를 그냥 쓰면 된다
val list = ArrayList<String>() // java.util.ArrayList
Spring Boot 라이브러리는 전부 Java로 되어 있지만 Kotlin에서 아무 문제 없이 사용 가능하다. 이게 코프링이 가능한 이유다.