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 }// 고차 함수
함수가 일급 시민 대우를 받는다
= 다른 요소와 동등한 권리를 가진다
= 함수를 마치 숫자나 문자열처럼 사용
일급 시민의 조건:
- 변수에 할당 가능
- 함수의 인자로 전달 가능
- 함수의 반환값으로 사용 가능
- 런타임에 생성 가능
- 동등성 비교 가능 (숫자나 문자열처럼 쉽게 비교 가능) → ? : 추후에 더 알아보자
// 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 |