소요 시간: 약 15분
TL;DR
data class로 간결한 데이터 모델 정의filter,map,sortedBy등 컬렉션 처리 함수 활용- 확장 함수로 기존 타입에 도메인 특화 기능 추가
when,?:,?.등 Kotlin 표현식 적극 활용
대상 독자: Kotlin 환경 설정을 마친 개발자 선수 지식: 환경 설정
설치된 환경에서 Kotlin의 핵심 기능을 직접 실행하며 익힙니다. 각 예제는 src/main/kotlin/ 아래에 파일을 만들어 실행합니다.
Step 1 — Hello Kotlin#
가장 간단한 Kotlin 프로그램부터 시작합니다.
// src/main/kotlin/hello/Hello.kt
package hello
fun main() {
println("안녕하세요, Kotlin!")
// 변수
val name = "Kotlin" // val: 읽기 전용
var version = "2.0" // var: 변경 가능
// 문자열 템플릿
println("언어: $name $version")
println("길이: ${name.length}자")
// 조건식 — if는 표현식
val message = if (version >= "2.0") "최신 버전" else "구버전"
println(message)
// when — switch의 강화 버전
val score = 85
val grade = when {
score >= 90 -> "A"
score >= 80 -> "B"
score >= 70 -> "C"
else -> "F"
}
println("점수: $score, 등급: $grade")
}./gradlew run --args="hello"
# 또는 IntelliJ에서 main() 왼쪽 ▶ 클릭Step 2 — 데이터 클래스로 사용자 모델링#
data class는 equals, hashCode, toString, copy를 자동으로 생성합니다.
// src/main/kotlin/model/User.kt
package model
data class User(
val id: Long,
val name: String,
val email: String,
val age: Int,
val role: Role = Role.USER // 기본값
)
enum class Role { USER, ADMIN, MODERATOR }
// 사용 예
fun main() {
val user1 = User(1, "홍길동", "hong@example.com", 30)
val user2 = User(2, "김영희", "kim@example.com", 25, Role.ADMIN)
println(user1)
// User(id=1, name=홍길동, email=hong@example.com, age=30, role=USER)
// copy — 일부만 변경한 복사본
val promoted = user1.copy(role = Role.MODERATOR)
println(promoted.role) // MODERATOR
// 구조 분해
val (id, name, email) = user1
println("$id: $name <$email>")
// 동등성 비교 — 값 기반
val same = user1.copy()
println(user1 == same) // true
}Step 3 — 컬렉션 처리#
Kotlin 컬렉션은 함수형 스타일로 데이터를 처리합니다.
// src/main/kotlin/collection/CollectionExample.kt
package collection
import model.Role
import model.User
fun main() {
val users = listOf(
User(1, "홍길동", "hong@example.com", 30),
User(2, "김영희", "kim@example.com", 25, Role.ADMIN),
User(3, "이철수", "lee@example.com", 35),
User(4, "박지수", "park@example.com", 28, Role.ADMIN),
User(5, "최민준", "choi@example.com", 22)
)
// filter — 조건에 맞는 항목만
val admins = users.filter { it.role == Role.ADMIN }
println("관리자: ${admins.map { it.name }}")
// 관리자: [김영희, 박지수]
// map — 변환
val names = users.map { it.name }
println("이름 목록: $names")
// sortedBy — 정렬
val byAge = users.sortedBy { it.age }
println("나이순: ${byAge.map { "${it.name}(${it.age})" }}")
// find — 조건에 맞는 첫 항목 (없으면 null)
val youngest = users.minByOrNull { it.age }
println("최연소: ${youngest?.name}")
// groupBy — 그룹화
val byRole = users.groupBy { it.role }
byRole.forEach { (role, list) ->
println("$role: ${list.map { it.name }}")
}
// any, all, none — 조건 확인
println("30대 있음: ${users.any { it.age >= 30 }}")
println("전원 성인: ${users.all { it.age >= 18 }}")
// count — 개수
println("관리자 수: ${users.count { it.role == Role.ADMIN }}")
// 체이닝 — 여러 연산 연결
val result = users
.filter { it.age >= 25 }
.sortedByDescending { it.age }
.take(3)
.map { "${it.name}(${it.age}세)" }
println("25세 이상 상위 3명: $result")
// 가변 컬렉션
val mutableList = mutableListOf<User>()
mutableList.add(users[0])
mutableList.addAll(users.filter { it.role == Role.ADMIN })
// Map 활용
val userMap: Map<Long, User> = users.associateBy { it.id }
val found = userMap[2L]
println("ID 2: ${found?.name}")
}Step 4 — 확장 함수 활용#
도메인에 특화된 확장 함수를 작성하여 가독성을 높입니다.
// src/main/kotlin/extension/Extensions.kt
package extension
import model.Role
import model.User
// User에 비즈니스 로직 확장
fun User.isAdult() = age >= 18
fun User.hasAdminAccess() = role == Role.ADMIN || role == Role.MODERATOR
fun User.displayName() = "[$role] $name"
fun User.maskEmail(): String {
val parts = email.split("@")
return "${parts[0].take(3)}***@${parts[1]}"
}
// List<User>에 도메인 특화 확장
fun List<User>.admins() = filter { it.role == Role.ADMIN }
fun List<User>.averageAge() = map { it.age }.average()
fun List<User>.findByEmail(email: String) = find { it.email == email }
// String 확장
fun String.toSlug() = lowercase().replace(" ", "-").replace("[^a-z0-9-]".toRegex(), "")
fun main() {
val users = listOf(
User(1, "홍길동", "hong@example.com", 30),
User(2, "김영희", "kim@example.com", 25, Role.ADMIN),
User(3, "이철수", "lee@example.com", 17)
)
users.forEach { user ->
println("${user.displayName()} - 성인: ${user.isAdult()}, 관리자 권한: ${user.hasAdminAccess()}")
println(" 이메일 마스킹: ${user.maskEmail()}")
}
println("\n관리자 목록: ${users.admins().map { it.name }}")
println("평균 나이: ${users.averageAge()}")
println("이메일 검색: ${users.findByEmail("kim@example.com")?.name}")
val title = "Hello Kotlin World"
println("슬러그: ${title.toSlug()}") // hello-kotlin-world
}Step 5 — Null Safety 실습#
Kotlin의 null 안전 처리를 실습합니다.
// src/main/kotlin/nullsafety/NullExample.kt
package nullsafety
data class Address(val street: String, val city: String, val zipCode: String?)
data class Person(val name: String, val address: Address?)
fun findPerson(id: Int): Person? {
return when (id) {
1 -> Person("홍길동", Address("종로구", "서울", "03000"))
2 -> Person("김영희", null) // 주소 없음
else -> null
}
}
fun main() {
// 안전 호출 연산자 ?.
val person1 = findPerson(1)
println(person1?.address?.city) // 서울
val person2 = findPerson(2)
println(person2?.address?.city) // null (예외 없음)
val person3 = findPerson(999)
println(person3?.name) // null
// 엘비스 연산자 ?:
val city = findPerson(2)?.address?.city ?: "주소 미등록"
println(city) // 주소 미등록
// let으로 null 안전 처리
findPerson(1)?.let { person ->
println("${person.name}의 우편번호: ${person.address?.zipCode ?: "없음"}")
}
// requireNotNull, checkNotNull
val id = System.getenv("USER_ID")?.toIntOrNull() ?: 1
val user = requireNotNull(findPerson(id)) { "ID $id 사용자를 찾을 수 없습니다" }
println("사용자: ${user.name}")
// 스마트 캐스트
val address = person1?.address
if (address != null) {
println(address.street) // Address 타입으로 스마트 캐스트
}
}Step 6 — 작은 CLI 프로그램#
앞서 배운 개념들을 활용한 간단한 사용자 관리 CLI입니다.
// src/main/kotlin/cli/UserCli.kt
package cli
import model.Role
import model.User
class UserManager {
private val users = mutableListOf<User>()
private var nextId = 1L
fun addUser(name: String, email: String, age: Int, role: Role = Role.USER): User {
val user = User(nextId++, name, email, age, role)
users.add(user)
println("사용자 추가: ${user.name} (ID: ${user.id})")
return user
}
fun listUsers() {
if (users.isEmpty()) {
println("등록된 사용자가 없습니다.")
return
}
println("\n=== 사용자 목록 (${users.size}명) ===")
users.sortedBy { it.id }.forEach { user ->
println(" ${user.id}. [${user.role}] ${user.name} <${user.email}> (${user.age}세)")
}
}
fun search(keyword: String): List<User> {
return users.filter { it.name.contains(keyword) || it.email.contains(keyword) }
}
fun stats() {
println("\n=== 통계 ===")
println(" 전체: ${users.size}명")
println(" 평균 나이: ${"%.1f".format(users.averageOf { it.age.toDouble() })}세")
val roleGroups = users.groupBy { it.role }
roleGroups.forEach { (role, list) ->
println(" $role: ${list.size}명")
}
}
private fun List<User>.averageOf(selector: (User) -> Double): Double {
return if (isEmpty()) 0.0 else sumOf(selector) / size
}
}
fun main() {
val manager = UserManager()
// 사용자 추가
manager.addUser("홍길동", "hong@example.com", 30, Role.ADMIN)
manager.addUser("김영희", "kim@example.com", 25)
manager.addUser("이철수", "lee@example.com", 35)
manager.addUser("박지수", "park@example.com", 28, Role.MODERATOR)
// 목록 출력
manager.listUsers()
// 검색
val searchResult = manager.search("김")
println("\n'김' 검색 결과: ${searchResult.map { it.name }}")
// 통계
manager.stats()
}실행:
./gradlew run예상 출력:
사용자 추가: 홍길동 (ID: 1)
사용자 추가: 김영희 (ID: 2)
사용자 추가: 이철수 (ID: 3)
사용자 추가: 박지수 (ID: 4)
=== 사용자 목록 (4명) ===
1. [ADMIN] 홍길동 <hong@example.com> (30세)
2. [USER] 김영희 <kim@example.com> (25세)
3. [USER] 이철수 <lee@example.com> (35세)
4. [MODERATOR] 박지수 <park@example.com> (28세)
'김' 검색 결과: [김영희]
=== 통계 ===
전체: 4명
평균 나이: 29.5세
ADMIN: 1명
USER: 2명
MODERATOR: 1명핵심 정리
data class—toString,equals,copy를 자동 생성하는 간결한 모델 정의- 컬렉션 함수
filter,map,groupBy,sortedBy— 선언적 데이터 처리- 확장 함수 — 기존 타입에 도메인 특화 메서드 추가
?.,?:,?.let { }— null 안전 처리의 기본 패턴
테스트 작성#
JUnit 5와 AssertJ를 사용하여 앞서 작성한 data class와 확장 함수를 검증합니다. spring-boot-starter-test에 두 라이브러리가 모두 포함되어 있으므로 별도 의존성 추가 없이 사용할 수 있습니다.
// src/test/kotlin/model/UserTest.kt
package model
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
@DisplayName("User 데이터 클래스 테스트")
class UserTest {
private val user = User(1L, "홍길동", "hong@example.com", 30, Role.USER)
@Test
fun `data class는 값 기반 동등성을 제공한다`() {
val copy = user.copy()
assertThat(copy).isEqualTo(user)
assertThat(copy).isNotSameAs(user) // 참조는 다름
}
@Test
fun `copy로 일부 필드만 변경한 새 인스턴스를 만든다`() {
val promoted = user.copy(role = Role.ADMIN)
assertAll(
{ assertThat(promoted.id).isEqualTo(user.id) },
{ assertThat(promoted.name).isEqualTo(user.name) },
{ assertThat(promoted.role).isEqualTo(Role.ADMIN) }
)
}
@Test
fun `toString은 모든 필드를 포함한다`() {
assertThat(user.toString()).contains("홍길동", "hong@example.com", "30")
}
}확장 함수도 단위 테스트로 검증합니다. 순수 함수는 테스트 작성이 간단합니다.
// src/test/kotlin/extension/ExtensionsTest.kt
package extension
import model.Role
import model.User
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class ExtensionsTest {
@Test
fun `isAdult는 18세 이상만 성인으로 판단한다`() {
val adult = User(1L, "성인", "a@test.com", 18)
val minor = User(2L, "미성년", "b@test.com", 17)
assertThat(adult.isAdult()).isTrue()
assertThat(minor.isAdult()).isFalse()
}
@Test
fun `hasAdminAccess는 ADMIN과 MODERATOR만 허용한다`() {
val admin = User(1L, "관리자", "a@test.com", 30, Role.ADMIN)
val mod = User(2L, "중재자", "b@test.com", 30, Role.MODERATOR)
val user = User(3L, "일반", "c@test.com", 30, Role.USER)
assertThat(admin.hasAdminAccess()).isTrue()
assertThat(mod.hasAdminAccess()).isTrue()
assertThat(user.hasAdminAccess()).isFalse()
}
@Test
fun `maskEmail은 아이디 앞 세 글자만 남긴다`() {
val user = User(1L, "테스트", "hong@example.com", 25)
assertThat(user.maskEmail()).isEqualTo("hon***@example.com")
}
@Test
fun `toSlug는 소문자 하이픈 형식으로 변환한다`() {
assertThat("Hello Kotlin World".toSlug()).isEqualTo("hello-kotlin-world")
}
}테스트 실행./gradlew test # 또는 특정 클래스만 ./gradlew test --tests "model.UserTest"
다음 단계#
- Spring Boot 연동 — Kotlin으로 REST API 서버 구축
- 확장 함수 — 확장 함수 원리 심화