| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- effectivejava
- 이펙티브 자바
- 스프링 핵심원리
- Effective Java 3
- 스프링
- 이펙티브자바
- 알고리즘
- k8s
- Sort
- java
- 스프링부트
- 오블완
- Effective Java
- 자바스크립트
- 엘라스틱서치
- kubernetes
- ElasticSearch
- Spring
- 알고리즘정렬
- JavaScript
- 티스토리챌린지
- 클린아키텍처
- 카카오 면접
- Kotlin
- 자바
- 이차전지관련주
- 예제로 배우는 스프링 입문
- 카카오
- 스프링핵심원리
- 김영한
- Today
- Total
Kim-Baek 개발자 이야기
Kotlin Extension Functions & String 처리 완벽 가이드 | 기존 클래스 확장하고 문자열 마스터하기 🎨 본문
Kotlin Extension Functions & String 처리 완벽 가이드 | 기존 클래스 확장하고 문자열 마스터하기 🎨
김백개발자 2025. 12. 3. 12:41이 글을 읽으면: Kotlin의 강력한 Extension Functions로 기존 클래스에 새로운 기능을 추가하는 방법과, String Templates, Multi-line String, 정규식 등 실무에서 바로 쓸 수 있는 문자열 처리 기법을 완벽하게 배울 수 있습니다.
📌 목차
- Extension Functions란? - 마법의 확장 기능
- Custom Extension Functions 만들기
- String Templates - 변수 삽입의 정석
- Multi-line String - 복잡한 텍스트 다루기
- 실전 String 처리 기법
- Extension Functions 고급 활용
1. Extension Functions란? - 마법의 확장 기능
😫 Java의 불편함 - 유틸리티 클래스 지옥
// Java - 유틸리티 클래스 필요
public class StringUtils {
public static boolean isValidEmail(String email) {
return email != null && email.contains("@");
}
public static String toTitleCase(String text) {
return text.substring(0, 1).toUpperCase() +
text.substring(1).toLowerCase();
}
}
// 사용
String email = "user@example.com";
boolean valid = StringUtils.isValidEmail(email); // 불편함
String title = StringUtils.toTitleCase("hello"); // 가독성 낮음
✨ Kotlin의 해결책 - Extension Functions
// Kotlin - Extension Functions
fun String.isValidEmail(): Boolean {
return this.contains("@")
}
fun String.toTitleCase(): String {
return this.replaceFirstChar { it.uppercase() }
}
// 사용
val email = "user@example.com"
val valid = email.isValidEmail() // 직관적!
val title = "hello".toTitleCase() // 읽기 쉬움!
장점:
- ✅ 마치 원래 있던 메서드처럼 사용
- ✅ 가독성 향상 (메서드 체이닝 가능)
- ✅ IDE 자동완성 지원
- ✅ 기존 클래스 수정 불필요
2. Custom Extension Functions 만들기
2.1 기본 문법
// 기본 형태
fun 타입.함수명(파라미터): 반환타입 {
// this는 확장 대상 객체
return 결과
}
// 예제
fun Int.isEven(): Boolean {
return this % 2 == 0
}
// 사용
println(4.isEven()) // true
println(7.isEven()) // false
2.2 실전 예제 1 - Collection 확장
// filterIf: 조건부 필터링
fun <T> Iterable<T>.filterIf(
condition: Boolean,
predicate: (T) -> Boolean
): Iterable<T> {
return if (condition) this.filter(predicate) else this
}
// 사용 예시
data class ServiceCode(val code: String, val value: String)
val serviceCodes = listOf(
ServiceCode("planner", "기획자"),
ServiceCode("dev", "개발자"),
ServiceCode("designer", "디자이너")
)
val catalogType = CatalogType.TECH
// TECH 타입일 때만 planner 제외
val filtered = serviceCodes.filterIf(catalogType == CatalogType.TECH) {
!it.code.startsWith("planner")
}
왜 유용한가?
- 조건에 따라 필터링을 적용하거나 건너뛸 수 있음
- if-else 분기 없이 체이닝 가능
- 코드 가독성 향상
2.3 실전 예제 2 - Map 확장
// filterValueNotNull: Map의 null value 제거
fun <T, U> Map<T, U?>.filterValueNotNull(): Map<T, U> {
return this.mapNotNull { (key, value) ->
value?.let { key to it }
}.toMap()
}
// 사용 예시
val serviceMap = mapOf(
"SERVICE-001" to "Active",
"SERVICE-002" to null,
"SERVICE-003" to "Inactive",
"SERVICE-004" to null
)
val cleanedMap = serviceMap.filterValueNotNull()
println(cleanedMap)
// {SERVICE-001=Active, SERVICE-003=Inactive}
2.4 Nullable 타입 확장
// orEmpty: Nullable String을 빈 문자열로
fun String?.orEmpty(): String {
return this ?: ""
}
// isNullOrBlank 개선 버전
fun String?.isNotNullAndNotBlank(): Boolean {
return !this.isNullOrBlank()
}
// 사용
val name: String? = null
println(name.orEmpty()) // ""
val text: String? = " "
println(text.isNotNullAndNotBlank()) // false
2.5 실무 예제 - List 확장
// 빈 리스트를 null로 변환
fun <T> List<T>.nullIfEmpty(): List<T>? {
return if (this.isEmpty()) null else this
}
// 중복 제거하면서 순서 유지
fun <T> List<T>.distinctPreserveOrder(): List<T> {
return this.toSet().toList()
}
// 안전한 첫 번째 요소 가져오기
fun <T> List<T>.firstOrDefault(default: T): T {
return this.firstOrNull() ?: default
}
// 사용
val users = emptyList<User>()
val result = users.nullIfEmpty() // null
val numbers = listOf(1, 2, 2, 3, 3, 3, 4)
val distinct = numbers.distinctPreserveOrder() // [1, 2, 3, 4]
val names = listOf<String>()
val first = names.firstOrDefault("Unknown") // "Unknown"
3. String Templates - 변수 삽입의 정석
3.1 기본 사용법
// $변수명
val name = "김철수"
val message = "안녕하세요, $name님!"
println(message) // "안녕하세요, 김철수님!"
// ${표현식}
val age = 30
val intro = "저는 ${age}세이고, 내년에는 ${age + 1}세가 됩니다."
println(intro) // "저는 30세이고, 내년에는 31세가 됩니다."
3.2 Java vs Kotlin 비교
Java:
// Java - 문자열 연결 지옥
String name = "김철수";
int age = 30;
String message = "이름: " + name + ", 나이: " + age + "세";
// 또는 String.format
String message = String.format("이름: %s, 나이: %d세", name, age);
Kotlin:
// Kotlin - 간결하고 명확
val name = "김철수"
val age = 30
val message = "이름: $name, 나이: ${age}세"
3.3 실전 예제 - 에러 메시지
// 실무 코드: 동적 에러 메시지
data class ItgService(
val itgCatalogId: String,
val name: String,
val status: String
)
fun validateService(service: ItgService) {
if (service.status != "CREATED") {
throw IllegalStateException(
"서비스가 아직 생성되지 않은 상태에서 삭제 요청됨: ${service.itgCatalogId}"
)
}
}
// 사용자 친화적 메시지
fun getUserMessage(userName: String, count: Int): String {
return "$userName님, 총 ${count}개의 알림이 있습니다."
}
3.4 복잡한 표현식
data class User(val name: String, val age: Int)
val user = User("김철수", 30)
// 메서드 호출
val message1 = "이름 길이: ${user.name.length}자"
// 조건식
val message2 = "${user.name}님은 ${if (user.age >= 18) "성인" else "미성년자"}입니다."
// 함수 호출
fun getGreeting(name: String) = "안녕하세요, $name님!"
val message3 = "${getGreeting(user.name)} 오늘 날씨가 좋네요."
println(message1) // "이름 길이: 3자"
println(message2) // "김철수님은 성인입니다."
println(message3) // "안녕하세요, 김철수님! 오늘 날씨가 좋네요."
3.5 특수 문자 이스케이프
// $ 문자 자체를 출력하고 싶을 때
val price = 1000
val message = "가격: \$${price}원" // "가격: $1000원"
// 백슬래시
val path = "C:\\Users\\Documents"
// 따옴표
val quote = "그는 \"안녕하세요\"라고 말했다."
4. Multi-line String - 복잡한 텍스트 다루기
4.1 Triple Quotes (""")
// 기본 사용
val text = """
첫 번째 줄
두 번째 줄
세 번째 줄
"""
println(text)
// 결과:
// 첫 번째 줄
// 두 번째 줄
// 세 번째 줄
4.2 trimIndent() - 들여쓰기 제거
// 들여쓰기 자동 제거
val html = """
<html>
<body>
<h1>제목</h1>
</body>
</html>
""".trimIndent()
println(html)
// 결과:
// <html>
// <body>
// <h1>제목</h1>
// </body>
// </html>
4.3 실전 예제 - Slack 메시지
// 실무 코드: Slack 메시지 포맷팅
data class ItgServiceCreateRequest(
val name: String,
val admin: String,
val member: List<String>,
val description: String
)
fun createSlackMessage(request: ItgServiceCreateRequest): String {
val memberMentions = request.member.joinToString(" ") { "@$it" }
return """
> *❏ 서비스 이름(필수)*
> `특수문자 제외, 50자 이내`
> : ${request.name}
> *❏ 서비스 설명(필수)*
> `100자 이내`
> : ${request.description}
> *❏ 담당자(필수)*
> : @${request.admin}
> *❏ 멤버*
> : $memberMentions
""".trimIndent()
}
// 사용
val request = ItgServiceCreateRequest(
name = "새로운 서비스",
admin = "admin01",
member = listOf("user01", "user02", "user03"),
description = "서비스 설명입니다"
)
println(createSlackMessage(request))
출력 결과:
> *❏ 서비스 이름(필수)*
> `특수문자 제외, 50자 이내`
> : 새로운 서비스
> *❏ 서비스 설명(필수)*
> `100자 이내`
> : 서비스 설명입니다
> *❏ 담당자(필수)*
> : @admin01
> *❏ 멤버*
> : @user01 @user02 @user03
4.4 trimMargin() - 커스텀 마진
// | 기준으로 들여쓰기 제거
val sql = """
|SELECT *
|FROM users
|WHERE age >= 18
|ORDER BY name
""".trimMargin()
println(sql)
// SELECT *
// FROM users
// WHERE age >= 18
// ORDER BY name
// 다른 마진 문자 사용
val text = """
#첫 줄
#두 번째 줄
#세 번째 줄
""".trimMargin("#")
4.5 실전 예제 - SQL 쿼리
// 실무 코드: 동적 SQL 생성
fun buildUserQuery(
minAge: Int?,
city: String?,
isActive: Boolean?
): String {
val conditions = mutableListOf<String>()
minAge?.let { conditions.add("age >= $it") }
city?.let { conditions.add("city = '$it'") }
isActive?.let { conditions.add("is_active = $it") }
val whereClause = if (conditions.isNotEmpty()) {
"WHERE " + conditions.joinToString(" AND ")
} else {
""
}
return """
SELECT id, name, email, age, city
FROM users
$whereClause
ORDER BY created_at DESC
LIMIT 100
""".trimIndent()
}
// 사용
println(buildUserQuery(minAge = 18, city = "서울", isActive = true))
// SELECT id, name, email, age, city
// FROM users
// WHERE age >= 18 AND city = '서울' AND is_active = true
// ORDER BY created_at DESC
// LIMIT 100
5. 실전 String 처리 기법
5.1 문자열 분리 및 결합
// split - 문자열 분리
val csv = "apple,banana,cherry"
val fruits = csv.split(",")
println(fruits) // [apple, banana, cherry]
// joinToString - 리스트를 문자열로
val numbers = listOf(1, 2, 3, 4, 5)
val result1 = numbers.joinToString()
println(result1) // "1, 2, 3, 4, 5"
val result2 = numbers.joinToString(separator = " | ")
println(result2) // "1 | 2 | 3 | 4 | 5"
val result3 = numbers.joinToString(
separator = ", ",
prefix = "[",
postfix = "]"
)
println(result3) // "[1, 2, 3, 4, 5]"
// transform과 함께
data class User(val name: String, val age: Int)
val users = listOf(
User("김철수", 30),
User("이영희", 25)
)
val userList = users.joinToString(", ") { "${it.name}(${it.age}세)" }
println(userList) // "김철수(30세), 이영희(25세)"
5.2 실전 예제 - 멘션 생성
// 실무 코드: Slack/Agit 멘션 문자열 생성
fun createMentions(userIds: List<String>?, prefix: String = "@"): String {
return userIds
?.takeIf { it.isNotEmpty() }
?.joinToString(" ") { "$prefix$it" }
?: ""
}
// 사용
val planners = listOf("planner01", "planner02", "planner03")
val plannerMentions = createMentions(planners)
println(plannerMentions) // "@planner01 @planner02 @planner03"
val emptyList = emptyList<String>()
val emptyMentions = createMentions(emptyList)
println(emptyMentions) // ""
val nullList: List<String>? = null
val nullMentions = createMentions(nullList)
println(nullMentions) // ""
5.3 문자열 검색 및 치환
// contains - 포함 여부
val text = "Hello, World!"
println(text.contains("World")) // true
println(text.contains("world")) // false
println(text.contains("world", ignoreCase = true)) // true
// startsWith, endsWith
println(text.startsWith("Hello")) // true
println(text.endsWith("!")) // true
// replace - 치환
val original = "Hello, World!"
val replaced = original.replace("World", "Kotlin")
println(replaced) // "Hello, Kotlin!"
// 정규식으로 치환
val phoneNumber = "010-1234-5678"
val cleaned = phoneNumber.replace(Regex("[^0-9]"), "")
println(cleaned) // "01012345678"
5.4 실전 예제 - 입력값 정제
// 실무 코드: 사용자 입력 정제
fun sanitizeUserInput(input: String?): String? {
return input
?.trim() // 앞뒤 공백 제거
?.takeIf { it.isNotBlank() } // 빈 문자열 제외
?.replace(Regex("\\s+"), " ") // 연속 공백을 하나로
?.take(100) // 최대 100자
}
// 사용
println(sanitizeUserInput(" Hello World ")) // "Hello World"
println(sanitizeUserInput(" ")) // null
println(sanitizeUserInput(null)) // null
5.5 문자열 포맷팅
// padStart, padEnd - 패딩
val number = "123"
println(number.padStart(5, '0')) // "00123"
println(number.padEnd(5, '0')) // "12300"
// take, takeLast - 부분 문자열
val text = "Hello, World!"
println(text.take(5)) // "Hello"
println(text.takeLast(6)) // "World!"
// drop, dropLast - 제거
println(text.drop(7)) // "World!"
println(text.dropLast(1)) // "Hello, World"
5.6 실전 예제 - 파일명 처리
// 실무 코드: 안전한 파일명 생성
fun createSafeFileName(input: String, maxLength: Int = 50): String {
return input
.replace(Regex("[^a-zA-Z0-9가-힣._-]"), "_") // 특수문자 제거
.take(maxLength) // 길이 제한
.trim('_') // 앞뒤 언더스코어 제거
}
// 사용
println(createSafeFileName("My File@2024!.txt")) // "My_File_2024_.txt"
println(createSafeFileName("프로젝트 문서 (최종).docx")) // "프로젝트_문서__최종_.docx"
6. Extension Functions 고급 활용
6.1 Generic Extension Functions
// 제네릭 타입 확장
fun <T> T.print(): T {
println(this)
return this
}
// 사용 - 체이닝 가능
val result = listOf(1, 2, 3)
.map { it * 2 }
.print() // [2, 4, 6] 출력
.filter { it > 3 }
.print() // [4, 6] 출력
// Nullable 제네릭 확장
fun <T> T?.orThrow(message: String): T {
return this ?: throw IllegalArgumentException(message)
}
// 사용
val user: User? = findUser(123)
val validUser = user.orThrow("사용자를 찾을 수 없습니다")
6.2 Receiver가 있는 Lambda
// buildString - StringBuilder를 쉽게
val html = buildString {
append("<html>")
append("<body>")
append("<h1>제목</h1>")
append("</body>")
append("</html>")
}
// 커스텀 버전
fun buildCsv(builder: StringBuilder.() -> Unit): String {
return StringBuilder().apply(builder).toString()
}
// 사용
val csv = buildCsv {
append("Name,Age,City\n")
append("김철수,30,서울\n")
append("이영희,25,부산\n")
}
println(csv)
6.3 실전 예제 - DSL 스타일
// 실무 코드: HTML 빌더
class HtmlBuilder {
private val content = StringBuilder()
fun tag(name: String, body: HtmlBuilder.() -> Unit) {
content.append("<$name>")
this.body()
content.append("</$name>")
}
fun text(value: String) {
content.append(value)
}
override fun toString() = content.toString()
}
fun html(builder: HtmlBuilder.() -> Unit): String {
return HtmlBuilder().apply(builder).toString()
}
// 사용
val page = html {
tag("html") {
tag("body") {
tag("h1") {
text("환영합니다")
}
tag("p") {
text("Kotlin DSL 예제입니다")
}
}
}
}
println(page)
// <html><body><h1>환영합니다</h1><p>Kotlin DSL 예제입니다</p></body></html>
6.4 Infix Functions
// infix - 중위 표기법
infix fun String.shouldBe(expected: String) {
if (this != expected) {
throw AssertionError("Expected: $expected, but was: $this")
}
}
// 사용
"hello" shouldBe "hello" // OK
// "hello" shouldBe "world" // AssertionError
// 실전 예제 - 테스트 코드
infix fun <T> T.shouldEqual(expected: T) {
if (this != expected) {
throw AssertionError("Expected $expected but got $this")
}
}
// 사용
val result = calculateSum(2, 3)
result shouldEqual 5
6.5 Extension Properties
// Extension Property
val String.lastChar: Char
get() = this[this.length - 1]
val List<Int>.sumValue: Int
get() = this.sum()
// 사용
println("Hello".lastChar) // 'o'
println(listOf(1, 2, 3).sumValue) // 6
// 실전 예제
val String.isValidEmail: Boolean
get() = this.contains("@") && this.contains(".")
val String?.isNullOrEmpty: Boolean
get() = this == null || this.isEmpty()
// 사용
println("user@example.com".isValidEmail) // true
println("invalid".isValidEmail) // false
7. 정규식 활용
7.1 기본 정규식
// Regex 객체 생성
val emailPattern = Regex("[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z]+")
// matches - 전체 매칭
println(emailPattern.matches("user@example.com")) // true
println(emailPattern.matches("invalid")) // false
// containsMatchIn - 부분 매칭
val text = "연락처: user@example.com으로 보내주세요"
println(emailPattern.containsMatchIn(text)) // true
// find - 첫 번째 매칭 찾기
val match = emailPattern.find(text)
println(match?.value) // "user@example.com"
7.2 실전 예제 - 이메일 추출
// 실무 코드: 텍스트에서 모든 이메일 추출
fun extractEmails(text: String): List<String> {
val emailPattern = Regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
return emailPattern.findAll(text)
.map { it.value }
.toList()
}
// 사용
val message = """
담당자: john@company.com
참조: jane@company.com, bob@partner.com
""".trimIndent()
val emails = extractEmails(message)
println(emails) // [john@company.com, jane@company.com, bob@partner.com]
7.3 정규식으로 치환
// 전화번호 포맷 변경
val phone = "010-1234-5678"
val formatted = phone.replace(Regex("(\\d{3})-(\\d{4})-(\\d{4})"), "($1) $2-$3")
println(formatted) // "(010) 1234-5678"
// 민감 정보 마스킹
fun maskEmail(email: String): String {
return email.replace(Regex("(?<=.{2}).(?=.*@)"), "*")
}
println(maskEmail("user@example.com")) // "us**@example.com"
8. 자주 하는 실수와 해결법
❌ 실수 1: Extension Function 범위
// 나쁜 예 - 너무 광범위한 확장
fun Any.toJson(): String {
// 모든 객체에 toJson 추가 (충돌 위험)
}
// 좋은 예 - 구체적인 타입에만 확장
fun User.toJson(): String {
return """{"name":"${this.name}","age":${this.age}}"""
}
❌ 실수 2: String Template의 중괄호 생략
// 나쁜 예 - 모호한 경우
val name = "Kim"
val message = "$name님" // OK
val user = User("Kim", 30)
// val message = "$user.name님" // 컴파일 에러!
// 좋은 예 - 항상 중괄호 사용
val message = "${user.name}님" // OK
❌ 실수 3: Multi-line String 들여쓰기
// 나쁜 예 - 들여쓰기가 결과에 포함됨
fun getHtml(): String {
return """
<html>
<body>
</body>
</html>
"""
}
// 결과에 공백 포함됨!
// 좋은 예 - trimIndent() 사용
fun getHtml(): String {
return """
<html>
<body>
</body>
</html>
""".trimIndent()
}
💡 핵심 요약 (10초 복습)
// 1. Extension Functions
fun String.isEmail() = this.contains("@")
"user@example.com".isEmail() // true
// 2. String Templates
val name = "Kim"
val message = "Hello, $name!" // "Hello, Kim!"
val calc = "1 + 1 = ${1 + 1}" // "1 + 1 = 2"
// 3. Multi-line String
val text = """
Line 1
Line 2
""".trimIndent()
// 4. Collection Extension
fun <T> List<T>.secondOrNull() = this.getOrNull(1)
listOf(1, 2, 3).secondOrNull() // 2
📊 Extension Functions 사용 가이드
상황 사용 여부 이유
| 자주 사용하는 유틸리티 | ✅ 권장 | 가독성 향상 |
| 도메인 특화 로직 | ✅ 권장 | 명확한 의도 |
| 복잡한 비즈니스 로직 | ❌ 비권장 | Service 계층 사용 |
| 외부 라이브러리 확장 | ✅ 권장 | 기존 클래스 수정 불가 |
| 너무 범용적인 확장 | ❌ 비권장 | 충돌 위험 |
❓ FAQ (자주 묻는 질문)
Q1. Extension Functions는 성능에 영향을 주나요?
아니요. 컴파일 시 static 메서드로 변환되므로 성능 차이가 없습니다.
Q2. Extension Functions를 어디에 선언해야 하나요?
전역 함수로 선언하거나, object나 companion object 안에 선언하세요. 파일명은 보통 Extensions.kt를 사용합니다.
Q3. String Template에서 $를 출력하려면?
\$로 이스케이프하세요. 예: "가격: \$${price}"
Q4. Multi-line String에서 들여쓰기를 제거하려면?
trimIndent() 또는 trimMargin()을 사용하세요.
Q5. Extension Functions vs Utility Class?
Extension Functions가 더 직관적이고 IDE 지원이 좋습니다. Kotlin에서는 Extension Functions를 권장합니다.
🎯 실전 과제
과제 1: Email 유효성 검사
// TODO: Email Extension Function 작성
// - @와 .이 모두 포함되어야 함
// - @는 .보다 앞에 있어야 함
// - 최소 5자 이상
fun String.isValidEmail(): Boolean {
// 여기에 코드 작성
}
// 테스트
println("user@example.com".isValidEmail()) // true
println("invalid".isValidEmail()) // false
println("@example.com".isValidEmail()) // false
과제 2: 문자열 포맷팅
// TODO: 전화번호 포맷팅 Extension Function
// "01012345678" → "010-1234-5678"
fun String.formatPhoneNumber(): String {
// 여기에 코드 작성
}
// 테스트
println("01012345678".formatPhoneNumber()) // "010-1234-5678"
과제 3: Slack 메시지 빌더
// TODO: Multi-line String으로 Slack 메시지 생성
// 파라미터: title, description, mentions (List<String>)
fun buildSlackMessage(
title: String,
description: String,
mentions: List<String>
): String {
// 여기에 코드 작성
// 힌트: """ """ 사용, trimIndent() 사용
}
정답과 해설은 댓글로! 😊
📢 마치며
Kotlin의 Extension Functions와 String 처리를 완벽하게 마스터하셨습니다!
이제 여러분은:
- ✅ 기존 클래스에 새로운 기능을 추가할 수 있습니다
- ✅ String Templates로 가독성 높은 코드를 작성할 수 있습니다
- ✅ Multi-line String으로 복잡한 텍스트를 다룰 수 있습니다
- ✅ 실무에서 바로 사용 가능한 Extension Functions를 만들 수 있습니다
다음 글 예고: 다음에는 Kotlin의 제어 구조(when, if expression)와 타입 시스템(Enum, Sealed Class, Companion Object)을 다룹니다. 더욱 강력하고 안전한 코드를 작성하는 방법을 배워보세요!
궁금한 점은 댓글로 남겨주세요! 💬
