본문 바로가기
language/Kotilin In Action

1주차 (1~3장)

by abstract.jiin 2025. 9. 1.

https://play.kotlinlang.org/

1장

다중 패러다임 언어, 정적 타입지정 언어(statically typed)

→ 실행 시점이 아니라 컴파일 시점에 많은 오류를 잡아낼 수 있다

객체 지향 언어와 함수형 언어 아이디어의 조합


특성 1. 정적 타입 지정

  • 성능, 신뢰성, 유지보수성 향상
  • 타입 추론: 컴파일러가 문맥을 고려해서 자동으로 변수 타입 지정
  • 널이 될 수 있는 타입: 신뢰성 향상 (NullPointerException 방지)
val name = "홍길동"// String 타입 자동 추론
val age: Int = 25// 명시적 타입 지정
val email: String? = null// 널 허용 타

특성 2. 객체 지향과 함수형 프로그래밍의 조합

  • 객체 지향: 클래스, 상속, 캡슐화 지원
  • 함수형: 일급 시민 함수(First-class functions: 함수를 값처럼 다루기), 불변성(값이 변하지 않음), 고차 함수(함수를 받거나 반환하는 함수) 지원
// 객체 지향
class Person(val name: String) {
    fun greet() = "안녕하세요, $name입니다"
}

// 함수형
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }// 고차 함수

함수가 일급 시민 대우를 받는다

= 다른 요소와 동등한 권리를 가진다

= 함수를 마치 숫자나 문자열처럼 사용

일급 시민의 조건:

  1. 변수에 할당 가능
  2. 함수의 인자로 전달 가능
  3. 함수의 반환값으로 사용 가능
  4. 런타임에 생성 가능
  5. 동등성 비교 가능 (숫자나 문자열처럼 쉽게 비교 가능) → ? : 추후에 더 알아보자
// First-class functions 예시
val myFunction = { x: Int -> x * 2 }  // 변수에 할당
fun processData(data: List<Int>, fn: (Int) -> Int) = data.map(fn)  // 인자로 전달
fun createMultiplier(n: Int) = { x: Int -> x * n }  // 반환값으로 사용

---

// 컴파일 타임에 이미 정해진 함수
fun add(x: Int) = x + 10

// 런타임에 생성되는 함수
val userInput = "곱하기"  // 사용자가 실행 중에 선택
val dynamicFunc = when (userInput) {
    "더하기" -> { x: Int -> x + 10 }  // 이 순간 함수가 만들어짐!
    "곱하기" -> { x: Int -> x * 2 }   // 조건에 따라 다른 함수 생성
    else -> { x: Int -> x }
}

특성 3. 코루틴

  • 경량 스레드: 스레드보다 메모리 효율적
  • 비동기 프로그래밍: 동기 코드처럼 작성하면서 비동기 처리
  • 구조화된 동시성: 자식-부모 관계로 동시성 코드 구조화
suspend fun fetchData(): String {
    delay(1000)// 1초 대기 (비블로킹)
    return "데이터 로드 완료"
}

용례

  • 서버 개발: 스프링 부트, Ktor 등
  • 안드로이드 앱 개발: 구글 공식 지원
  • 데스크톱 애플리케이션: Compose Desktop
  • 웹 프론트엔드: Kotlin/JS
  • 멀티플랫폼: Kotlin Multiplatform으로 여러 플랫폼 동시 개발

2장

코틀린에서 문(Statement)과 식(Expression)의 차이

식(Expression)

  • 값을 반환하는 코드
  • 다른 코드의 일부로 사용될 수 있음
val a = 5 + 3// 5 + 3은 8이라는 값을 반환
val b = if (a > 5) "큰수" else "작은수"// if도 값을 반환하는 식
val c = when (a) {// when도 값을 반환
    8 -> "정답"
    else -> "오답"
}

문(Statement)

  • 동작을 수행하지만 값을 반환하지 않음
  • 그 자체로 완결된 명령
println("안녕하세요")// 출력만 하고 값은 반환하지 않음
var x = 10// 변수 선언
x = 20// 할당

핵심 차이점

식은 값으로 치환 가능:

val result = 5 + 3// 5 + 3을 8로 치환할 수 있음

문은 값으로 치환 불가능:

val result = println("hello")// println은 Unit을 반환하므로 의미 없음

Unit = '의미 있는 값을 반환하지 않음'을 나타내는 타입

Java의 void와 비슷하지만, 코틀린에서는 실제 객체

코틀린은 많은 것들이 식으로 작동함

예를 들어:

  • if-else 값을 반환하는 식
  • when 값을 반환하는 식
  • try-catch 값을 반환할 수 있는 식

if 블록에서:

val result = if (score >= 90) {
    println("축하합니다!")// 이건 Unit을 반환 (값이 아님)
    println("A학점입니다")// 이것도 Unit을 반환
    "우수"// 마지막 줄 이 값이 블록의 결과가 됨
} else {
    println("더 노력하세요")
    "보통"// 이 값이 else 블록의 결과
}

// result에는 "우수" 또는 "보통"이 저장됨

when 블록에서:

val grade = when (score) {
    in 90..100 -> {
        println("완벽해요!")
        updateRecord()// 어떤 함수 호출
        calculateBonus()// 다른 함수 호출
        "A+"// 마지막 줄이 when의 결과값
    }
    in 80..89 -> {
        println("잘했어요")
        "A"// 이 값이 결과
    }
    else -> "F"
}

함수에서도 동일:

fun processData(): String {
    val data = loadData()
    validateData(data)
    transformData(data)
    "처리완료"// 함수의 반환값 (return 키워드 없이도)
}

if나 when 모두 분기에 블록을 사용할수있다. 그런 경우 블록의 마지막 문장이 블록 전체의 결과가 된다

  • 블록 내 여러 줄이 있어도 마지막 줄만이 결과값
  • 중간 줄들은 부수효과를 위한 것 (로깅, 상태변경 등)
  • 마지막 줄은 반드시 값이어야 함

블록의 마지막 식이 블록의 결과라는 규칙은 블록이 값을 만들어내야 하는 경우 항상 성립함. 하지만 이 규칙은 일반적인 함수에 대해서는 성립하지 않는다

성립하는 경우 (값을 만들어내야 하는 블록 ⇒ 다른 식의 일부분)

val result = when (x) {
    1 -> {
        doSomething()
        "one"// 블록의 결과
    }
    else -> "other"
}

성립하지 않는 경우 (일반 함수 ⇒ 독립적인 선언)

fun doWork(): String {
    performTask()
    cleanUp()
    "done"// 에러! return 키워드 필요
}

// 올바른 방법:
fun doWork(): String {
    performTask()
    cleanUp()
    return "done"// 명시적 return
}

// 함수에서는 return 타입이 Unit이 아닌 경우 반드시 명시적으로 return을 써야 하지만, if/when/try 같은 식(expression)에서는 마지막 줄이 자동으로 결과가 됨
  • 값을 만드는 블록: 컴파일러가 "여기서 값이 필요하다"는 걸 명확히 알고 있음
  • 일반 함수: 개발자가 명시적으로 무엇을 반환할지 return으로 지정해야 함

코틀린 for문

for (변수 in 컬렉션)

컬렉션: for (item in list)

범위: for (i in 1..10)

인덱스와 함께: for ((index, item) in list.withIndex())

: for ((key, value) in map)

범위 표현식

1..10: 1부터 10까지 (10 포함)

1 until 10: 1부터 9까지 (10 제외)

10 downTo 1: 10부터 1까지 역순

1..10 step 2: 1, 3, 5, 7, 9 (2씩 증가)

3장

최상위 함수와 프로퍼티

사실 코틀린에서는 함수를 클래스 안에 선언할 필요가 전혀 없다.

그 결과 특별한 상태나 인스턴스 메서드가 없는 클래스가 생겨난다. 이런 클래스는 다양한 정적 메서드를 모아두는 역할만 담당한다. [유틸리티 클래스헬퍼 클래스] → 코틀린에서는 필요 없다

// Utils.kt 파일// 클래스 없이 바로 함수와 프로퍼티 선언!
const val APP_VERSION = "1.0.0"// 최상위 프로퍼티
val currentTime get() = System.currentTimeMillis()

fun sayHello(name: String) = "안녕하세요, $name!"// 최상위 함수
fun calculateTax(price: Int) = price * 0.1

// 다른 파일에서 바로 사용
fun main() {
    println(APP_VERSION)// 클래스명 없이 바로 사용
    println(sayHello("김철수"))
    println(calculateTax(1000))
}

확장 함수와 프로퍼티

fun String.isPhoneNumber(): Boolean = this.matches(Regex("\\\\d{3}-\\\\d{4}-\\\\d{4}"))
fun String.removeSpaces(): String = this.replace(" ", "")

// Int에도 확장 함수 추가
fun Int.isEven(): Boolean = this % 2 == 0

// 확장 프로퍼티
val String.lastChar: Char
    get() = this[this.length - 1]

// 사용 예시
fun main() {
    val phone = "010-1234-5678"
    println(phone.isPhoneNumber())// true

    val text = "hello world"
    println(text.removeSpaces())// "helloworld"
    println(text.lastChar)// 'd'

    println(42.isEven())// true
}

  • 확장 함수는 오버라이드할 수 없다 → 컴파일 타임에 결정 되기 때문
// 기본 클래스
open class Animal {
    open fun makeSound() = "동물 소리"
}

class Dog : Animal() {
    override fun makeSound() = "멍멍"  // 멤버 함수는 오버라이드 가능
}

// 확장 함수 정의
fun Animal.speak() = "동물이 말합니다"
fun Dog.speak() = "개가 말합니다"     // 이건 오버라이드가 아님

fun main() {
    val animal: Animal = Dog()  // Dog 객체를 Animal 타입으로 참조
    
    // 멤버 함수 - 실제 객체 타입에 따라 호출됨
    println(animal.makeSound())  // "멍멍" (Dog의 오버라이드된 함수)
    
    // 확장 함수 - 변수의 선언 타입에 따라 호출됨
    println(animal.speak())      // "동물이 말합니다" (Animal의 확장 함수)
    
    val dog: Dog = Dog()
    println(dog.speak())         // "개가 말합니다" (Dog의 확장 함수)
}
  • 확장한 함수와 그 클래스의 멤버 함수의 이름과 같다면 멤버 함수가 호출(우선순위 높음)
class Person {
    fun greet() = "안녕하세요, 저는 클래스 멤버입니다"
}

// 같은 이름의 확장 함수 정의
fun Person.greet() = "안녕하세요, 저는 확장 함수입니다"

fun main() {
    val person = Person()
    println(person.greet())  // "안녕하세요, 저는 클래스 멤버입니다"
    // 확장 함수는 무시됨!
}

vararg (가변 인자)

함수에 몇 개의 인자를 넘길지 미리 정하지 않고, 호출할 때마다 다르게 넘길 수 있음.

fun main() {
    // vararg 함수 정의
    fun printNumbers(vararg numbers: Int) {
        for (number in numbers) {
            println(number)
        }
    }
    
    // 사용 예시
    println("3개 숫자 출력:")
    printNumbers(1, 2, 3)
    
    println("\\n2개 숫자 출력:")
    printNumbers(10, 20)
    
    println("\\n1개 숫자 출력:")
    printNumbers(100)
    
    println("\\n0개도 가능:")
    printNumbers()
    
    // 배열을 vararg에 전달할 때는 스프레드 연산자(*) 사용
    val numberArray = intArrayOf(7, 8, 9)
    println("\\n배열 전달:")
    printNumbers(*numberArray)
}

중위 호출 (infix)

infix 함수란?

함수 이름이 두 값 사이에 들어가는 함수 (+, - 같은 연산자처럼)

// 일반적인 함수 호출 방식
val result1 = 3.plus(5)// 8
val result2 = "Hello".plus(" World")// "Hello World"

infix 함수 호출

보통 객체.함수(인자) 형태로 호출하는데, infix를 사용하면 객체 함수 인자 형태로 더 자연스럽게 씀

// infix로 호출 - 더 자연스러움
val result1 = 3 plus 5// 8 (함수 이름이 가운데)
val result2 = "Hello" plus " World"// "Hello World
// infix 함수 정의
infix fun Int.times(str: String): String = str.repeat(this)

// 사용 
fun main() {
// 일반 호출 vs 중위 호출
    println(3.times("Hello "))// 일반: 3.times("Hello ")
    println(3 times "Hello ")// 중위: 3 times "Hello " (더 읽기 쉬움)

// 코틀린 내장 중위 함수들
    val map = mapOf(1 to "one", 2 to "two")// to는 중위 함수
    println(5 in listOf(1, 2, 3, 4, 5))// in도 중위 함수
}

구조 분해 선언

to는 함수: 두 값을 Pair로 묶어주는 확장 함수

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

중위 호출: 1 to "one"처럼 자연스러운 문법으로 함수 호출

구조 분해: val (a, b) = pair처럼 묶인 값들을 개별 변수로 분리

mapOf: 내부적으로 Pair 객체들을 받아서 맵을 만드는 함수

fun main() {
    // 1. to 함수로 Pair 만들기
    println("=== to 함수 사용 ===")
    val pair1 = 1.to("one")        // 일반 호출
    val pair2 = 1 to "one"         // 중위 호출 (같은 결과)
    
    println("pair1: $pair1")  // (1, one)
    println("pair2: $pair2")  // (1, one)
    
    // 2. 구조 분해 선언
    println("\\n=== 구조 분해 선언 ===")
    val (number, name) = 1 to "one"
    println("number: $number")  // 1
    println("name: $name")      // one
    
    // 3. 맵 만들기
    println("\\n=== 맵 만들기 ===")
    val map = mapOf(
        1 to "one", 
        7 to "seven", 
        53 to "fifty-three"
    )
    println("map: $map")
    
    // 4. 맵 순회에서 구조 분해 사용
    println("\\n=== 맵 순회 ===")
    for ((key, value) in map) {
        println("$key -> $value")
    }
    
    // 5. 리스트 인덱스와 함께 순회
    println("\\n=== withIndex() 사용 ===")
    val fruits = listOf("apple", "banana", "cherry")
    for ((index, element) in fruits.withIndex()) {
        println("$index: $element")
    }
    
    // 6. 다양한 타입으로 to 사용
    println("\\n=== 다양한 타입 ===")
    val pairs = listOf(
        1 to "one",
        "hello" to 42,
        listOf(1, 2, 3) to listOf(1, 2, 3).size
    )
    
    for (pair in pairs) {
        println(pair)
    }
}
=== to 함수 사용 ===
pair1: (1, one)
pair2: (1, one)

=== 구조 분해 선언 ===
number: 1
name: one

=== 맵 만들기 ===
map: {1=one, 7=seven, 53=fifty-three}

=== 맵 순회 ===
1 -> one
7 -> seven
53 -> fifty-three

=== withIndex() 사용 ===
0: apple
1: banana
2: cherry

=== 다양한 타입 ===
(1, one)
(hello, 42)
([1, 2, 3], 3)

인자가 하나뿐인 일반 메서드나 인자가 하나뿐인 확장 함수에만 중위 호출을 사용 할 수 있다. 함수(메서드)를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다.

infix fun Any. to (other: Any) = Pair (this, other)

Any.to: Any 타입의 확장 함수, Any = 모든 타입의 부모 클래스 모든 타입에 to 함수를 추가한다는 뜻 other: Any: 인자가 정확히 1개 infix: infix 호출 허용

문자열 처리 함수들

fun main() {
    val text = "Hello, World!"

// 다양한 문자열 처리 함수들
    println(text.split(","))// ["Hello", " World!"]
    println(text.substring(0, 5))// "Hello"
    println(text.replace("World", "Kotlin"))// "Hello, Kotlin!"
    println(text.startsWith("Hello"))// true
    println(text.endsWith("!"))// true

// 정규식 처리
    val email = "test@example.com"
    val emailPattern = Regex("[a-zA-Z0-9]+@[a-zA-Z0-9]+\\\\.[a-zA-Z]+")
    println(email.matches(emailPattern))// true

// 숫자만 추출
    val mixed = "abc123def456"
    val numbers = mixed.replace(Regex("[^0-9]"), "")
    println(numbers)// "123456"
}

삼중 따옴표 문자열

fun main() {
// 자바 스타일 - 이스케이프
    val javaStyle = "{\\n    \\"name\\": \\"김철수\\",\\n    \\"age\\": 25,\\n    \\"address\\": \\"서울시 강남구\\"\\n}"

// 코틀린 스타일 - 삼중 따옴표로 깔끔
    val kotlinStyle = """
        {
            "name": "김철수",
            "age": 25,
            "address": "서울시 강남구"
        }
    """.trimIndent()

// 정규식도 깔끔하게
    val regex1 = "\\\\d{4}-\\\\d{2}-\\\\d{2}"// 이스케이프 필요
    val regex2 = """\\d{4}-\\d{2}-\\d{2}"""// 이스케이프 불필요

// SQL 쿼리도 깔끔
    val sql = """
        SELECT name, age
        FROM users
        WHERE age > 20
        ORDER BY name
    """.trimIndent()

    println(kotlinStyle)
}

로컬 함수 (중복 제거)

// 중복이 많은 코드
fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException("이름이 비어있습니다")
    }
    if (user.email.isEmpty()) {
        throw IllegalArgumentException("이메일이 비어있습니다")
    }
    if (user.age < 0) {
        throw IllegalArgumentException("나이가 올바르지 않습니다")
    }
// 실제 저장 로직...
}

// 로컬 함수로 깔끔하게
fun saveUser(user: User) {
// 로컬 함수 - 이 함수 안에서만 사용
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("${fieldName}이 비어있습니다")
        }
    }

    fun validateAge(age: Int) {
        if (age < 0) {
            throw IllegalArgumentException("나이가 올바르지 않습니다")
        }
    }

// 중복 없이 깔끔
    validate(user.name, "이름")
    validate(user.email, "이메일")
    validateAge(user.age)

// 실제 저장 로직...
}

그냥함수를 쓰는것과는 무슨 차이가 있을까?

'language > Kotilin In Action' 카테고리의 다른 글

6주차 (13장)  (2) 2025.09.01
5주차(11~12장)  (1) 2025.09.01
4주차(9~10장)  (1) 2025.09.01
3주차 (6~8장)  (1) 2025.09.01
2주차 (4~5장)  (1) 2025.09.01