| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
- 카카오
- Kotlin
- Sort
- 클린아키텍처
- 스프링 핵심원리
- 이차전지관련주
- 스프링부트
- 스프링핵심원리
- Spring
- ElasticSearch
- 카카오 면접
- 김영한
- JavaScript
- effectivejava
- k8s
- Effective Java
- kubernetes
- 알고리즘정렬
- java
- 자바
- 이펙티브자바
- 자바스크립트
- 스프링
- 티스토리챌린지
- 오블완
- 예제로 배우는 스프링 입문
- 이펙티브 자바
- 엘라스틱서치
- 알고리즘
- Effective Java 3
- Today
- Total
Kim-Baek 개발자 이야기
Kotlin 제어 구조와 타입 시스템 완벽 가이드 | when, if expression, Enum, Sealed Class 본문
Kotlin 제어 구조와 타입 시스템 완벽 가이드 | when, if expression, Enum, Sealed Class
김백개발자 2025. 12. 4. 09:58Kotlin의 강력한 제어 구조와 타입 시스템을 마스터하여 더 안전하고 간결한 코드를 작성하는 방법
목차
- 제어 구조의 진화: Expression vs Statement
- when - 강력한 패턴 매칭
- if expression - 더 이상 Statement가 아닙니다
- Enum Class - 타입 안전한 상수
- Sealed Class - 제한된 클래스 계층
- Companion Object - 정적 멤버의 Kotlin 방식
- 실전 예제: 네트워크 응답 처리
들어가며

Java에서 Kotlin으로 넘어오면서 가장 놀라웠던 것은 제어 구조가 Expression이라는 점이었습니다. if문에서 값을 반환받고, when문으로 복잡한 분기를 우아하게 처리하는 것을 보고 "이게 가능하다고?"라는 생각이 들었습니다.
특히 프로젝트에서 네트워크 응답이나 상태 관리를 할 때, Sealed Class와 when의 조합은 컴파일 타임에 모든 케이스를 처리했는지 보장해주어 런타임 오류를 크게 줄일 수 있었습니다.
오늘은 Kotlin의 제어 구조와 타입 시스템을 실전 예제와 함께 깊이 있게 다뤄보겠습니다. 각 개념의 동작 원리와 함께 언제, 왜 사용해야 하는지도 함께 알아보겠습니다.
1. 제어 구조의 진화: Expression vs Statement
Statement vs Expression의 차이
Java의 Statement 방식
// Java: if는 statement (값을 반환하지 않음)
int max;
if (a > b) {
max = a;
} else {
max = b;
}
Kotlin의 Expression 방식
// Kotlin: if는 expression (값을 반환)
val max = if (a > b) a else b
Expression의 장점
- 간결성: 불필요한 변수 선언 제거
- 불변성: val 사용 가능 (더 안전)
- 함수형 프로그래밍: 체이닝과 조합 가능
- 가독성: 의도가 더 명확
핵심 포인트
// ✅ 좋은 예: expression 활용
val result = if (score >= 60) "Pass" else "Fail"
// ❌ 나쁜 예: statement 방식
var result: String
if (score >= 60) {
result = "Pass"
} else {
result = "Fail"
}
2. when - 강력한 패턴 매칭
when은 Java의 switch를 훨씬 강력하게 만든 Kotlin의 핵심 기능입니다.
2.1 기본 사용법
단순 값 매칭
fun getGrade(score: Int): String {
return when (score) {
100 -> "Perfect!"
in 90..99 -> "A"
in 80..89 -> "B"
in 70..79 -> "C"
in 60..69 -> "D"
else -> "F"
}
}
여러 값 한 번에 처리
fun isWeekend(day: String): Boolean {
return when (day) {
"토요일", "일요일" -> true
else -> false
}
}
2.2 타입 체크와 스마트 캐스트
fun processData(data: Any): String {
return when (data) {
is String -> "문자열 길이: ${data.length}" // 자동 캐스팅
is Int -> "정수값: ${data * 2}"
is List<*> -> "리스트 크기: ${data.size}"
is Boolean -> if (data) "참" else "거짓"
else -> "알 수 없는 타입"
}
}
// 사용 예시
println(processData("Hello")) // 문자열 길이: 5
println(processData(42)) // 정수값: 84
println(processData(listOf(1,2))) // 리스트 크기: 2
2.3 조건식 활용
fun checkTemperature(temp: Int): String {
return when {
temp < 0 -> "영하입니다"
temp < 10 -> "춥습니다"
temp < 20 -> "선선합니다"
temp < 30 -> "따뜻합니다"
else -> "덥습니다"
}
}
2.4 범위와 컬렉션 체크
fun checkNumber(num: Int): String {
return when (num) {
in 1..10 -> "1부터 10 사이"
in listOf(11, 13, 17, 19) -> "소수"
!in 0..100 -> "범위 밖"
else -> "기타"
}
}
2.5 when의 고급 활용
결과를 변수에 저장
val result = when (val code = getResponseCode()) {
200 -> "성공"
404 -> "찾을 수 없음"
500 -> "서버 오류"
else -> "알 수 없는 오류: $code"
}
여러 줄 처리
when (event) {
is ClickEvent -> {
logEvent("Click", event.position)
handleClick(event)
updateUI()
}
is ScrollEvent -> {
logEvent("Scroll", event.offset)
handleScroll(event)
}
}
실전 팁: when 활용 시나리오
// 1. Enum과 함께 사용 (완전성 보장)
enum class Status { IDLE, LOADING, SUCCESS, ERROR }
fun handleStatus(status: Status) = when (status) {
Status.IDLE -> showIdle()
Status.LOADING -> showLoading()
Status.SUCCESS -> showSuccess()
Status.ERROR -> showError()
// else 불필요 (모든 케이스 처리)
}
// 2. Sealed Class와 함께 (완벽한 타입 안전성)
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val exception: Exception) : Result()
object Loading : Result()
}
fun handle(result: Result) = when (result) {
is Result.Success -> println("데이터: ${result.data}")
is Result.Error -> println("오류: ${result.exception.message}")
is Result.Loading -> println("로딩 중...")
// else 불필요, 컴파일러가 완전성 체크
}
3. if expression - 더 이상 Statement가 아닙니다
3.1 기본 expression 사용
// Java 스타일 (비권장)
var status: String
if (isConnected) {
status = "온라인"
} else {
status = "오프라인"
}
// Kotlin 스타일 (권장)
val status = if (isConnected) "온라인" else "오프라인"
3.2 복잡한 로직도 expression으로
val price = if (isMember) {
val discount = originalPrice * 0.1
originalPrice - discount
} else {
originalPrice
}
3.3 Elvis 연산자와 조합
val name = user?.name ?: "Guest"
// if expression과 조합
val displayName = if (user != null) {
user.name ?: "Unknown"
} else {
"Guest"
}
3.4 중첩 if expression
val result = if (score >= 90) {
"A"
} else if (score >= 80) {
"B"
} else if (score >= 70) {
"C"
} else {
"F"
}
// when으로 더 깔끔하게
val result = when {
score >= 90 -> "A"
score >= 80 -> "B"
score >= 70 -> "C"
else -> "F"
}
실전 활용: 뷰 상태 관리
// Android 예시
fun updateUI(isLoading: Boolean, data: List<Item>?) {
binding.progressBar.visibility = if (isLoading) {
View.VISIBLE
} else {
View.GONE
}
binding.emptyView.visibility = if (!isLoading && data.isNullOrEmpty()) {
View.VISIBLE
} else {
View.GONE
}
}
4. Enum Class - 타입 안전한 상수
4.1 기본 Enum
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
// 사용
val direction = Direction.NORTH
4.2 프로퍼티와 메서드를 가진 Enum
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF); // 세미콜론 필수!
fun containsRed(): Boolean {
return (rgb and 0xFF0000) != 0
}
}
// 사용
println(Color.RED.rgb) // 16711680
println(Color.RED.containsRed()) // true
4.3 인터페이스 구현
interface Clickable {
fun click()
}
enum class Button : Clickable {
OK {
override fun click() = println("OK 클릭")
},
CANCEL {
override fun click() = println("Cancel 클릭")
}
}
// 사용
Button.OK.click() // OK 클릭
4.4 Enum의 유용한 메서드
enum class Priority {
LOW, MEDIUM, HIGH, URGENT
}
fun main() {
// 모든 값 가져오기
Priority.values().forEach { println(it) }
// 문자열로 변환
val priority = Priority.valueOf("HIGH")
println(priority) // HIGH
// 이름과 순서
println(Priority.HIGH.name) // HIGH
println(Priority.HIGH.ordinal) // 2
// 안전한 변환
val maybePriority = Priority.values()
.find { it.name == "MEDIUM" }
}
4.5 when과 함께 사용
enum class HttpStatus(val code: Int) {
OK(200),
NOT_FOUND(404),
INTERNAL_ERROR(500)
}
fun handleResponse(status: HttpStatus) = when (status) {
HttpStatus.OK -> "성공"
HttpStatus.NOT_FOUND -> "찾을 수 없음"
HttpStatus.INTERNAL_ERROR -> "서버 오류"
// else 불필요 (모든 케이스 처리)
}
실전 예제: 네트워크 상태 관리
enum class NetworkState(val message: String) {
CONNECTED("인터넷 연결됨"),
DISCONNECTED("인터넷 연결 끊김"),
CONNECTING("연결 중..."),
ERROR("연결 오류");
fun isAvailable(): Boolean {
return this == CONNECTED
}
companion object {
fun fromBoolean(isConnected: Boolean): NetworkState {
return if (isConnected) CONNECTED else DISCONNECTED
}
}
}
// 사용
val state = NetworkState.CONNECTED
println(state.message) // 인터넷 연결됨
println(state.isAvailable()) // true
5. Sealed Class - 제한된 클래스 계층
Sealed Class는 Enum의 확장판이라고 생각할 수 있습니다. 각 하위 클래스가 서로 다른 데이터를 가질 수 있다는 점이 핵심입니다.
5.1 기본 구조
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String, val code: Int) : Result()
object Loading : Result()
}
5.2 when과의 완벽한 조합
fun handleResult(result: Result) = when (result) {
is Result.Success -> println("성공: ${result.data}")
is Result.Error -> println("오류 ${result.code}: ${result.message}")
is Result.Loading -> println("로딩 중...")
// else 불필요! 컴파일러가 완전성 체크
}
5.3 Enum vs Sealed Class
Enum의 한계
// ❌ Enum은 모든 인스턴스가 같은 구조
enum class Result {
SUCCESS, // 추가 데이터를 가질 수 없음
ERROR // 각기 다른 데이터 불가
}
Sealed Class의 유연성
// ✅ Sealed Class는 각 하위 타입이 다른 데이터 보유
sealed class Result {
data class Success(val data: String, val timestamp: Long) : Result()
data class Error(val exception: Exception, val retryable: Boolean) : Result()
object Empty : Result()
}
5.4 실전 예제: API 응답 처리
sealed class ApiResponse<out T> {
data class Success<T>(val data: T) : ApiResponse<T>()
data class Error(
val message: String,
val code: Int,
val exception: Throwable? = null
) : ApiResponse<Nothing>()
object Loading : ApiResponse<Nothing>()
}
// 사용
fun <T> handleApiResponse(response: ApiResponse<T>) = when (response) {
is ApiResponse.Success -> {
println("데이터 수신: ${response.data}")
// 데이터 처리
}
is ApiResponse.Error -> {
println("오류 발생: ${response.message} (코드: ${response.code})")
response.exception?.printStackTrace()
}
is ApiResponse.Loading -> {
println("데이터 로딩 중...")
}
}
// 실제 사용 예시
data class User(val name: String, val age: Int)
val response: ApiResponse<User> = ApiResponse.Success(
User("홍길동", 30)
)
handleApiResponse(response)
5.5 계층 구조 활용
sealed class UIEvent {
// 클릭 이벤트 그룹
sealed class Click : UIEvent() {
object ButtonClick : Click()
data class ItemClick(val position: Int) : Click()
}
// 스크롤 이벤트 그룹
sealed class Scroll : UIEvent() {
data class VerticalScroll(val offset: Int) : Scroll()
data class HorizontalScroll(val offset: Int) : Scroll()
}
// 입력 이벤트
data class TextInput(val text: String) : UIEvent()
}
fun handleEvent(event: UIEvent) = when (event) {
is UIEvent.Click.ButtonClick -> println("버튼 클릭")
is UIEvent.Click.ItemClick -> println("아이템 ${event.position} 클릭")
is UIEvent.Scroll.VerticalScroll -> println("세로 스크롤: ${event.offset}")
is UIEvent.Scroll.HorizontalScroll -> println("가로 스크롤: ${event.offset}")
is UIEvent.TextInput -> println("입력: ${event.text}")
}
5.6 상태 관리 패턴
sealed class ScreenState<out T> {
object Initial : ScreenState<Nothing>()
object Loading : ScreenState<Nothing>()
data class Success<T>(val data: T) : ScreenState<T>()
data class Error(val message: String) : ScreenState<Nothing>()
object Empty : ScreenState<Nothing>()
}
// ViewModel에서 사용
class UserViewModel : ViewModel() {
private val _state = MutableStateFlow<ScreenState<List<User>>>(
ScreenState.Initial
)
val state: StateFlow<ScreenState<List<User>>> = _state
fun loadUsers() {
viewModelScope.launch {
_state.value = ScreenState.Loading
try {
val users = userRepository.getUsers()
_state.value = if (users.isEmpty()) {
ScreenState.Empty
} else {
ScreenState.Success(users)
}
} catch (e: Exception) {
_state.value = ScreenState.Error(e.message ?: "알 수 없는 오류")
}
}
}
}
// UI에서 상태 처리
fun renderState(state: ScreenState<List<User>>) = when (state) {
is ScreenState.Initial -> showInitial()
is ScreenState.Loading -> showLoading()
is ScreenState.Success -> showUsers(state.data)
is ScreenState.Error -> showError(state.message)
is ScreenState.Empty -> showEmpty()
}
6. Companion Object - 정적 멤버의 Kotlin 방식
Java의 static을 대체하는 Kotlin의 우아한 방법입니다.
6.1 기본 사용법
class MyClass {
companion object {
const val TAG = "MyClass"
fun create(): MyClass {
return MyClass()
}
}
}
// 사용
val tag = MyClass.TAG
val instance = MyClass.create()
6.2 팩토리 패턴
class User private constructor(
val name: String,
val email: String
) {
companion object {
fun fromJson(json: String): User {
// JSON 파싱 로직
return User("John", "john@example.com")
}
fun create(name: String, email: String): User {
require(email.contains("@")) { "올바른 이메일이 아닙니다" }
return User(name, email)
}
}
}
// 사용
val user1 = User.create("홍길동", "hong@example.com")
val user2 = User.fromJson("""{"name":"김철수","email":"kim@example.com"}""")
6.3 이름이 있는 Companion Object
class Database {
companion object Factory {
fun getInstance(): Database {
return Database()
}
}
}
// 두 가지 방법으로 접근 가능
val db1 = Database.getInstance()
val db2 = Database.Factory.getInstance()
6.4 인터페이스 구현
interface Factory<T> {
fun create(): T
}
class User(val name: String) {
companion object : Factory<User> {
override fun create(): User {
return User("Default User")
}
}
}
// 사용
val user = User.create()
6.5 확장 함수
class Person(val name: String) {
companion object
}
// Companion Object에 확장 함수 추가
fun Person.Companion.create(name: String): Person {
return Person(name)
}
// 사용
val person = Person.create("홍길동")
6.6 실전 예제: 싱글톤 패턴
class DatabaseManager private constructor() {
companion object {
@Volatile
private var INSTANCE: DatabaseManager? = null
fun getInstance(): DatabaseManager {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: DatabaseManager().also {
INSTANCE = it
}
}
}
}
fun query(sql: String) {
println("쿼리 실행: $sql")
}
}
// 더 간단한 방법: object 사용
object SimpleDatabaseManager {
fun query(sql: String) {
println("쿼리 실행: $sql")
}
}
6.7 상수와 정적 메서드
class Constants {
companion object {
// const: 컴파일 타임 상수
const val MAX_COUNT = 100
const val API_KEY = "your_api_key"
// 일반 프로퍼티: 런타임에 계산
val TAG = this::class.java.simpleName
@JvmStatic // Java에서 정적 메서드처럼 접근 가능
fun log(message: String) {
println("[$TAG] $message")
}
}
}
7. 실전 예제: 네트워크 응답 처리
모든 개념을 종합한 실전 예제입니다.
7.1 응답 타입 정의
// Sealed Class로 모든 가능한 상태 표현
sealed class NetworkResult<out T> {
data class Success<T>(
val data: T,
val code: Int = 200
) : NetworkResult<T>()
data class Error(
val message: String,
val code: Int,
val exception: Throwable? = null
) : NetworkResult<Nothing>()
object Loading : NetworkResult<Nothing>()
// Companion Object로 팩토리 메서드 제공
companion object {
fun <T> success(data: T, code: Int = 200): NetworkResult<T> {
return Success(data, code)
}
fun error(message: String, code: Int, exception: Throwable? = null): NetworkResult<Nothing> {
return Error(message, code, exception)
}
fun loading(): NetworkResult<Nothing> {
return Loading
}
}
}
// HTTP 상태 코드를 Enum으로 관리
enum class HttpStatus(val code: Int, val message: String) {
OK(200, "성공"),
CREATED(201, "생성됨"),
BAD_REQUEST(400, "잘못된 요청"),
UNAUTHORIZED(401, "인증 필요"),
FORBIDDEN(403, "접근 거부"),
NOT_FOUND(404, "찾을 수 없음"),
INTERNAL_ERROR(500, "서버 오류");
companion object {
fun fromCode(code: Int): HttpStatus? {
return values().find { it.code == code }
}
}
}
7.2 Repository 구현
class UserRepository {
suspend fun getUser(userId: String): NetworkResult<User> {
return try {
// 로딩 상태 반환
NetworkResult.loading()
// API 호출 시뮬레이션
delay(1000)
// when expression으로 응답 코드 처리
val responseCode = 200 // API에서 받은 코드
when (val status = HttpStatus.fromCode(responseCode)) {
HttpStatus.OK -> {
val user = User("홍길동", 30)
NetworkResult.success(user)
}
HttpStatus.NOT_FOUND -> {
NetworkResult.error(
message = "사용자를 찾을 수 없습니다",
code = status.code
)
}
HttpStatus.UNAUTHORIZED -> {
NetworkResult.error(
message = "인증이 필요합니다",
code = status.code
)
}
else -> {
NetworkResult.error(
message = status?.message ?: "알 수 없는 오류",
code = responseCode
)
}
}
} catch (e: Exception) {
NetworkResult.error(
message = "네트워크 오류가 발생했습니다",
code = 0,
exception = e
)
}
}
}
7.3 ViewModel에서 사용
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _userState = MutableStateFlow<NetworkResult<User>>(
NetworkResult.Loading
)
val userState: StateFlow<NetworkResult<User>> = _userState
fun loadUser(userId: String) {
viewModelScope.launch {
_userState.value = NetworkResult.Loading
val result = repository.getUser(userId)
// when으로 결과 처리
_userState.value = when (result) {
is NetworkResult.Success -> {
logSuccess(result.data)
result
}
is NetworkResult.Error -> {
logError(result.message, result.code)
result
}
is NetworkResult.Loading -> result
}
}
}
private fun logSuccess(user: User) {
println("사용자 로드 성공: ${user.name}")
}
private fun logError(message: String, code: Int) {
println("사용자 로드 실패: $message (코드: $code)")
}
}
7.4 UI에서 상태 처리
@Composable
fun UserScreen(viewModel: UserViewModel) {
val userState by viewModel.userState.collectAsState()
// when expression으로 UI 상태 처리
when (val state = userState) {
is NetworkResult.Loading -> {
LoadingIndicator()
}
is NetworkResult.Success -> {
UserContent(user = state.data)
}
is NetworkResult.Error -> {
ErrorView(
message = state.message,
onRetry = { viewModel.loadUser("userId") }
)
}
}
}
@Composable
fun UserContent(user: User) {
Column {
Text("이름: ${user.name}")
Text("나이: ${user.age}")
}
}
@Composable
fun ErrorView(message: String, onRetry: () -> Unit) {
Column {
Text(text = message, color = Color.Red)
Button(onClick = onRetry) {
Text("다시 시도")
}
}
}
@Composable
fun LoadingIndicator() {
CircularProgressIndicator()
}
7.5 확장 함수로 편의성 추가
// NetworkResult 확장 함수
fun <T> NetworkResult<T>.getOrNull(): T? = when (this) {
is NetworkResult.Success -> data
else -> null
}
fun <T> NetworkResult<T>.isSuccess(): Boolean = this is NetworkResult.Success
fun <T> NetworkResult<T>.isError(): Boolean = this is NetworkResult.Error
fun <T> NetworkResult<T>.isLoading(): Boolean = this is NetworkResult.Loading
// 사용 예시
val result: NetworkResult<User> = repository.getUser("123")
if (result.isSuccess()) {
val user = result.getOrNull()
println("사용자: ${user?.name}")
}
마치며
핵심 요약
- when: Java의 switch보다 훨씬 강력한 패턴 매칭
- 범위, 타입 체크, 조건식 모두 가능
- Sealed Class와 함께 사용하면 완전성 보장
- if expression: 값을 반환하는 if문
- 간결하고 불변성 유지
- 삼항 연산자 불필요
- Enum: 타입 안전한 상수 집합
- 프로퍼티와 메서드 포함 가능
- when과 함께 사용하면 else 불필요
- Sealed Class: 제한된 클래스 계층
- 각 하위 클래스가 다른 데이터 보유 가능
- 상태 관리에 완벽
- Companion Object: static의 Kotlin 방식
- 팩토리 패턴에 이상적
- 인터페이스 구현 가능
다음 단계
이제 여러분은 Kotlin의 제어 구조와 타입 시스템을 자유자유롭게 다룰 수 있습니다. 이 개념들을 조합하면:
- 더 안전한 코드: 컴파일 타임에 오류 잡기
- 더 간결한 코드: 불필요한 보일러플레이트 제거
- 더 읽기 쉬운 코드: 의도가 명확한 표현
다음 포스팅에서는 Kotlin의 컬렉션 API와 함수형 프로그래밍 기법을 다룰 예정입니다. 더욱 강력한 Kotlin 코드를 작성하는 방법을 배워보세요!
함께 보면 좋은 글
▶ Kotlin 기초 완벽 가이드 | 변수, 함수, 클래스
▶ Kotlin Null Safety 마스터하기 | ?, !!, let, run
▶ Kotlin Coroutine 입문 | 비동기 프로그래밍의 새로운 패러다임
참고 자료
- Kotlin 공식 문서: kotlinlang.org
- Effective Kotlin by Marcin Moskała
- Kotlin in Action by Dmitry Jemerov & Svetlana Isakova
작성일: 2025년 1월
카테고리: Kotlin, 프로그래밍, 안드로이드 개발
태그: #Kotlin #when #SealedClass #Enum #CompanionObject #제어구조 #타입시스템
