본문 바로가기
IT/Java, Kotlin

[Kotlin] 코틀린 기본

by nohumb 2026. 10. 3.

과거에 안드로이드 프로그래밍을 하며 정리해둔 코틀린 문서를 업로드합니다. 틀린 부분이 있을 수 있습니다.

언어 특성

  • 세미콜론 없어도 됨
  • 강타입(정적 타입)언어로 형변환 필수
  • Java의 라이브러리를 사용할 수 있으나 코틀린 라이브러리 쓰는게 좀 더 낫다함

Java와 차이점

  • 고차 함수 지원(Java는 직접적으로 지원하지 않는다)
  • 함수형 프로그래밍에 더 능함
  • Java는 SAM 변환 지원, Kotlin은 지원하지 않음(람다, 고차함수 적극 사용)
  • SAM(Simple Abstract Method) 변환 : 매개변수로 전달하거나 반환하는 인터페이스 객체가 하나의 추상 메서드만을 가지고 있을 때 이를 해당 메서드(익명함수) 객체로 자동 변환

main 함수

  • Java와 달리 빌드 시 클래스 필수적이지 않다
  • mian 함수는 필수적이다
fun main(){
	print("Hello World!!")
	println("Hello World!!") //개행
}

변수

  • Kotlin은 자동으로 문맥 상 변수타입 추론이 지원된다
  • var i = 10 //타입 추론 var j: Int = 20 //타입 지정 //자바의 Wrapper 클래스와 같이 대문자로 시작 val max = 100 //상수(final), 런타임 할당
  • 상수
  • const val max = 100 //컴파일 타임에 할당되어 성능상 유리, 변경 불가 fun main(){ //... }
  • 기본 자료형 형변환
  • var i = 10 //정수 var l = 20L //Long 타입 var name = "" l = i.toLong() name = i.toString() i = name.toInt()
  • 문자열 처리
  • var name = "Taehyeon Kim" print(name.uppercase()) //대문자 print("the Name is" + name + '\n') print("my Name is $name My Men.") print("my Name is ${name.uppercase()}")
  • lateinit : 초기 값을 정확히 알 수 없는 변수를 미리 정의할 때 (null 사용 지양)
  • class MyClass{ lateinit var str: String fun myFunc(){ str = "Hello World!" } }

입력 받기

//Java Style
import java.util.Scanner;

var sc = Scanner(System.'in');
var num = sc.nextInt();

//Kotlin Style
var input = readln()
//var input: Int = readln().toInt()

기타 메서드, 함수

  • 최대 최소
  • import kotlin.math.max var i = 10 var l = 20 //println("MAX = ${kotlin.math.max(i, l)}") println(max(i, l))
  • 난수
  • val ranInt = Random.nextInt(0, 100) //0 ~ 99
  • 소수점 자리수
  • var num = 3.1415 println(String.format("%.3f".num))
  • measureTimeMillis() : 시간 측정, ms단위
  • var measuredTime = measureTimeMillis{ //측정하고자 하는 동작 }

조건문

  • alt+enter를 통해 when, if문 상호치환
  • Java Style 가능
  • var i=5 //Java Style if(i < 5) { ... } else if(i > 5) { ... } else{ ... } //Kotlin Style when { i < 5 -> { ... } i > 5 -> { ... } else -> { ... } }
  • if문의 return 값
  • var i = 5 var result = when { i < 5 -> { "1" } i > 5 -> { "2" } else -> { "3" } } //삼항연산자 var result = if(i > 10) true else false

반복문

  • for문
  • var items = listOf(1, 2, 3, 4, 5) for(item in items){ print(item) } items.forEach { item -> print(item) } for(i in 0..items.size){ print(items[i]) } for(i in 0..10 step 2){ print(i) } //0246810
  • while문, break/continue는 Java와 완전 동일

자료구조

  • List
    • List, MutableList는 서로 다른 타입
    • List : immutable, 수정 불가
    • //var items: List<Int> = listOf<Int>(1, 2, 3, 4, 5) var items = listOf(1, 2, 3, 4, 5) //타입추론으로 인한 생략 //[1, 2, 3, 4, 5]
    • MutableList : mutable, 수정 가능
    • var items = mutableListOf<Int>() //빈 리스트 items.add(1) items.add(2) //[1, 2] var items = mutableListOf<MutableList<Int>>() //빈 2차원 정수 리스트 //[]
    • 메서드
    • items.add(mutableListOf(1, 2, 3, 4, 5) items.add(mutableListOf(5, 6, 7, 8 ,9) //[[1, 2, 3, 4, 5], [5, 6, 7, 8, 9]] items.remove(5) //값이 5인 원소 하나 삭제 items.removeAt(5) //index가 5인 원소 하나 삭제 items.size //길이
  • Array (List를 사용하는게 일반적)
  • var items = arrayOf(1, 2, 3, 4, 5) print(items[3]) //4 //size, set, get 등 메서드 존재

예외 처리

  • try-catch
  • var items = listOf(1, 2, 3) try { var item = items[3] } catch(e: Exception){ //Java는 catch(Exception e){ ... } println(e.message) } finally{ println("Error Handling Completed") } //var e: Exception = Exception("This is an Error") //임의의 에러 //throw e throw Exception("This is an Error") //임의로 에러 발생
  • null Safety
    • 선언 시 ‘?’가 없으면 null 대입 불가
    • var name: String? = null //var name: String = null 은 에러
    • Null Checking : ‘?’가 붙은 것과 아닌 것은 서로 다른 타입, Null이 아닐 경우 상호작용 가능
    • var name: String? = "" var name2: String = "" if(name != null){ //if문 name2 = name } name?.let{ name2 = it //또는 name2 = name } name2 = name!! //개발자 임의로 null이 아님을 보증

함수

  • main 함수 밖에 있는 함수는 Top-Level 함수로 어느 파일에서도 사용 가능
  • 기본형
  • fun sum(a: Int, b: Int): Int{ //Return값 추론 가능할 경우 리턴 타입 생략 가능 return a+b } fun main(){ print(sum(10, 20)) //30 }
  • 축약
  • fun sum(a: Int, b: Int): Int = a + b
  • 오버로딩 : Java Style로 오버로딩 가능, 매개변수에 기본값 지정하여 축약 가능
  • //Java Style fun sum(a: Int, b: Int): Int { ... } fun sum(a: Int, b: Int, c: Int): Int { ... } //Kotlin Style fun sum(a: Int, b: Int, c: Int = 0): Int { ... }
  • 함수 사용 시 매개변수 지정 가능 (명시적으로 지정 시 순서 상관x)
  • fun sum(a: Int = 0, b: Int = 0, c: Int = 0) = a + b - c fun main(){ print(sum(10, c=20)) //-10 print(sum(c=20, a=10, b=20)) //10 }

Class

  • 기본적으로 멤버에 대한 getter, setter가 존재
  • 자바와 달리 Getter, Setter 메서드 지양
  • 기본형
  • class Person( //기본생성자(필수) var name: String, val race: String, //한번만 할당 가능 private var age: Int ){ init { //... 생성자 } //... Methods, Property }
  • Data 클래스 : 객체의 값이 같으면 같은 객체로 인식
    • 프로퍼티는 상관 없다
    • Java에선 Equals, HashCode 이용
    //Kotlin
    data class Person(
    	//...
    )
    
  • 그 외 프로퍼티 조정
    • field는 해당 프로퍼티의 값
    class Person( 
    	//... 
    ){
    	var name = "KTH"
    		private set //setter 막을 때
    		get() = "Name:${field}"
    		set(str: String){
    			field = str
    		}
    }
    
  • 상속
    • open 키워드가 있어야 overriding 가능
    • interface 외에는 모두 단일 상속만 가능
    • 일반 상속
    • open class Person(var name: String, var age: Int){ } class Male(menName: String, menAge:Int) : Person(menName, menAge){ //Java의 super(menName, menAge); 와 같은 개념 }
    • Abstract Class
    • abstract class Person{ abstract fun walk() //추상 메서드, 상속 시 반드시 오버라이딩하여 구현해야함 open fun greet(){ println("Hello") } } class Men : Person(){ override fun walk(){ println("I'm walking") } override fun greet(){ println("What's up") } }
    • Interface : 다중 상속 가능
    • interface Moveable{ fun walk() } interface Talkable{ fun saySom() } open class Animal : Moveable{ override fun walk(){ //... } } class Human : Animal, Talkable{ override fun saySom(){ //... } }
  • Generic
  • class Container<T>(var value: T){ //... } fun main(){ var container = Container(10) //var container: Container<Int> = Container<Int>() }
  • Type Check (is)
  • var men: Animal = Human() //Animal Type의 Human Instance 생성 men.saySome() //에러 //men 객체는 Animal Type이므로 Human의 속성을 이용할 수 없다 if(men is Human){ //men의 Type을 검사하여 Human인게 확인될 경우 men.saySome() //Human의 속성을 사용할 수 있다 }
  • 강제 Type Casting (as) : 같은 타입이 아니라면 에러난다
  • var men: Animal = Human() men as Human men.saySome() //가능
  • object
    • 싱글톤 객체
    • 생성자가 없음, 단일 객체를 가짐
    • 명시적 선언
    • object obj { val name = "KTH" fun talk(){ println("Hello") } } class MyClass(){ object DataSource{ var baseUrl = "/etc/" } } fun main(){ obj.age = 20 obj.talk() myClass.DataSource.baseUrl //값 : /etc/ }
  • Companion object
    • 클래스 수준의 싱글톤 객체
    • 자바의 정적 메서드, 정적 프로퍼티와 같은 개념(static)
  • class MyClass{ companion object{ var baseUrl = "/etc/" } } fun main(){ myClass.baseUrl //값 : /etc/ }

고급

  • 함수형 인터페이스 : 추상 메서드를 하나만 가진 인터페이스
    • 매개변수로 함수를 넘겨줄 때 사용
    • fun 키워드는 메서드가 여럿일 경우 에러 띄움
      //안드로이드의 Event Listener(예: OnClickListener 같은 것)와 비슷한 구조의 사용 예시
      interface Listener{
      	fun run()
      }
      
      fun setListener(listener: Listener){
      	listener.run()
      }
      
      setListener( object : Listener{
      	override fun run(){
      		...
      	}
      }) //Java Legacy Style
      
    • → Java의 @FunctionalInterface 어노테이션과 같다
    • Runnable 인터페이스 : Java와 Kotlin에서 지원하는 함수형 인터페이스
    • fun runMyFunc(runnable: Runnable){ runnable.run() } runMyFunc(object : Runnable{ override run(){ ... } }) /* Java runMyFunc(new Runnable(){ @Override public void run(){ ... } }) */
  • Lambda
    • Kotlin의 람다식은 return 키워드는 쓰지 않음(마지막 표현식이 반환됨)
    • 반환값을 고차함수에 전달할 경우 return@[고차함수] [반환값] 형식으로 명시 가능 (필수x)
    • func: () -> Unit = { … }
      • func : 람다식이 할당된 변수명
      • () -> Unit : 람다식의 타입
      • { ... } : 람다식의 내용
    //Unit은 void와 비슷한 타입(아무것도 반환하지 않음)
    val func: () -> Unit = { 
    	...
    }
    
    //람다식의 타입 부분을 생략(타입 추론)
    //매개변수 x, y를 명시적으로 초기화
    val func = { x: Int, y: Int ->
    	...
    }
    
    //Int형 매개변수 2개와 Int형 리턴 타입을 가진 예제
    //아래 둘은 같은 동작을 한다
    val sum: (x: Int, y: Int) -> Int = { x, y -> //시그니처 타입 모두 명시
    	x + y
    }
    
    val sum = { x: Int, y: Int ->
    	x + y
    }
    
  • 고차 함수 (Higher-Order Function) : 콜백 함수를 인자로 받거나 리턴 값으로 사용 가능한 함수
    • 콜백 함수 : 다른 함수에 의해 호출되는 함수
    • 기본 형태
    • fun myFunc(callBack: () -> Unit){ callBack() } //원형 myFunc({ ... }) //고차 함수의 마지막 인자가 콜백함수이므로 //인자로 넘겨주는 람다식의 중괄호를 밖으로 뺄 수 있다 myFunc(){ ... } //고차 함수가 받는 인자가 콜백 함수 뿐인 경우 //아래와 같이 소괄호도 생략할 수 있다 myFunc{ ... }
    • 매개변수, 리턴이 있는 콜백 함수를 인자로 받을 때
    • fun myFunc(callBack: (x: Int) -> Int){ var ret = callBack(5) } myFunc(){ x -> //매개변수의 타입은 추론이 되므로 생략 가능 ... } //콜백 함수의 매개변수가 하나 이하일 경우 //해당 매개변수를 명시적으로 초기화하지 않아도 ‘it’로 접근할 수 있다 myFunc(){ println(it) //결과 : 5 }
    • 콜백 함수 오버로딩
    • //콜백 함수의 기본값을 빈 람다로 정의 fun myFunc(callBack: () -> Unit = {}){ callBack() } myFunc() //콜백 함수 전달하지 않아도 호출 가능
    • ‘::’ : 일반 함수를 인자로 넘길 때 또는 클래스 참조를 전달할 때
      • Kotlin에서 MyClass::class, 클래스 참조는 klass 타입
      • Java에서 MyClass.class, Class타입
      MyClass::class //klass 타입 반환
      MyClass::class.java //Java의 Class 타입 반환
      
    • fun printNum(num: Int){ println(num) } fun myFunc(callBack: (x: Int) -> Unit){ callBack(5) } myFunc(::printNum) //결과 : 5
    • 람다와 고차함수로 인한 Event Listener의 간결화
      fun interface Listener{
      	fun run()
      } //Legacy
      
      fun setListener(listener: Listener){
      	listener.run()
      } //Legacy
      
      fun setListener(run: () -> Unit){
      	run()
      } //고차함수를 통한 간결화 가능
      
      setListener{ ... }
      
    • #Java에선 Event Listener가 함수형 인터페이스 구조로 구현되어있다.
      Listener를 Set하는 함수에 익명 내부 클래스와 람다식을 둘 다 전달할 수 있는 건 SAM(Single Abstract Method) 변환이 지원되기 때문에 EventListner의 구현을 Legacy로 두더라도 람다로도 전달할 수 있는 것이다.
      하지만 Kotlin은 SAM 변환을 지원하지 않으므로 함수형 인터페이스 구조와 고차 함수 구조의 오버로딩을 통해 구현하여 Java의 Event Listener와 같은 동작을 하게끔 구현했을 것

Thread-Coroutine

Thread

  • start() 메서드로 쓰레드 생성 및 시작
  • Thread 클래스 상속 받아 생성
  • class SimpleThread(val num: Int, var latency: Long) : Thread(){ override fun run(){ Thread.sleep(latency) //latency만큼 해당 Thread 대기(ms) println("${num} Thread is Running") } } val simpleThread = SimpleThread(1, 3000) simpleThread.start() SimpleThread(2, 2000).start() //익명객체로 인스턴스 생성하여 실행 SimpleThread(3, 1000).start() /* 결과 3 Thread is Running 2 Thread is Running 1 Thread is Running */
  • Runnable 인터페이스 상속
    • Runnable을 implement
    • 생성된 객체를 Thread에 인자로 넘겨 Thread 객체 생성
    • 이렇게 하는 이유
      • Thread 클래스는 쓰레드의 실행의 역할만 있고 Runnable 인터페이스로 작업을 구현하도록 역할이 나뉘어 있음
      • Thread 클래스를 상속 받을 경우 단일 상속 원칙으로 다른 클래스 상속 불가
    class SimpleRunnable(val latency: Long) : Runnable {
    	override fun run(){
    		Thread.sleep(latency)
    		println("${num} Thread is Running")
    	}
    }
    
    val simpleRunnable = SimpleRunnable(4, 3000)
    val thread = Thread(simpleRunnable)
    
    thread.start()
    Thread(SimpleRunnable(5, 2000)) //익명객체로 인스턴스 생성하여 실행
    Thread(SimpleRunnable(6, 1000))
    
  • Thread 익명 객체
  • var thread = object : Thread(){ override fun run(){ println("Thread is Running") } } thread.start() /* Java Thread thread = new Thread(){ @Override public void run(){ System.out.println("Thread is Running"); } }; thread.start(); */
  • 람다식 : Thread 클래스는 람다식을 매개변수로 생성자에 전달할 수 있다
  • //원형 var thread = Thread({ println("Thread is Running") }) thread.start() //kotlin식 축약, 람다 매개변수로 넣을 시 소괄호 빼기 var thread = Thread{ println("Thread is Running") } thread.start() //최대한 축약하여 쓰레드 실행 Thread{ println("Thread is Running") }.start() /* Java new Thread(()->{ System.out.println("Thread is Running"); }).start(); */
  • 헬퍼 함수 만들기 : 쓰레드를 생성, 실행하는 함수 직접 만들기
  • fun makeThread( start: Boolean = true, name: String? = null, callBack: () -> Unit ): Thread{ val thread = object : Thread(){ override run(){ callBack() } } name?.let{ thread.name = it } if(start==true) thread.start() } makeThread(){ ... }

Coroutine

  • 병행성(Concurrency)
    • Single Core 사용으로 병렬이 아닌 순차적 동작, Logical Level
    • Time-Sharing으로 CPU를 나눠 사용함으로 동시에 실행하는 것처럼 보임
    • Thread는 병렬성(Parallelism)을 지원하므로 실제로 동시에 작업이 처리되나(Physical Level), Thread간 작업 교환에 System call, Blocking call 등 비용 발생, 즉 Thread에 비해 비용이 적다
  • 스코프 : 코루틴의 실행 범위
    • GlobalScope : 전역 스코프, 앱의 생명주기와 독립적, 안드로이드에서 사용 지양
    • GlobalScope.launch{ ... }
    • CoroutineScope : 해당 코루틴이 관리하는 범위 내에서만 코루틴을 실행하고 관리
    • CoroutineScope(Dispatchers.Default).launch{ ... }
    • lifecycleScope : Jetpack의 수명 주기 라이브러리에 포함, 안드로이드 액티비티나 프래그먼트의 수명주기와 연결되어있어 Active 상태일 때만 실행하여 메모리 누수 및 생명 주기 이슈x
    • lifecycleScope.launch{ ... }
  • 코루틴 빌더 : 코루틴 생성 및 실행을 위한 구문, 코루틴 내부에서 사용된다
    • launch : 반환 값이 없는 비동기 작업 수행 시 사용
    • launch{ ... }
    • async : 반환 값이 있는 비동기 작업 수행 시 사용, Deferred 객체로 결과 반환됨
    • var deferred = async{ return@async "This is Data" } var result = deferred.await() //
    • runblocking : 내부의 모든 코루틴이 수행되기 전까지 부모 스레드를 차단시킴, 테스트 목적
    • runBlocking{ ... } fun main() = runBlocking{ ... }
  • 디스패쳐(Dispatcher) : 코루틴이 실행될 스레드나 스레드 풀을 지정
    • Dispatchers.Default : CPU 바운드 작업에 적합한 디스패쳐, 고정 크기의 스레드 풀
    • Dispatchers.IO : I/O 바운드 작업에 적합한 디스패쳐, 백그라운드 스레드 풀
    • Dispatchers.Main : 안드로이드에서 UI 작업을 수행하는 메인 스레드에 대한 디스패쳐
    • Dispatchers.Unconfined : 현재 스레드에서 즉시 실행
  • Suspend 함수
    • 일시 정지(async-await, delay 등) 가능한 함수
    • → 해당 함수가 일시정지 상태일 경우 쓰레드는 다른 작업을 진행
    • 스레드의 중단 없이 부모 루틴의 상태를 저장하고 불러오는 방식으로 성능저하를 최소화함
    • suspend 함수의 사용은 코루틴 또는 suspend 함수 내에서만 가능
    #async-await와 같은 동작은 일시정지가 필요하므로 suspend 함수에서만 동작
  • suspend fun readData() : Int{ var job = CoroutineScoupe(Dispatchers.IO).async{ delay(2000) //Reading Data return@async 433 } return job.await() } fun main(){ CoroutineScope(Dispatchers.Default).launch{ readData() } }
  • withContext : 부모 루틴의 Dispatcher와 다른 Dispatcher를 호출하여 사용
    • suspend 함수이므로 동작이 끝낼 때까지 부모 루틴 정지됨
    CoroutineScope(Dispatchers.Default){
    	withContext(Dispatchers.IO){
    		...
    	}
    }
    
  • Methods
    • join() : 동작이 완료될 때까지 쓰레드 일시정지
    • var job1 = launch{ for(i in 0..5){ delay(100) println("Running ${i}") } } delay(200) println("Main Coroutine") job1.join() println("Main Done") /* Running 0 Running 1 Main Coroutine Running 2 Running 3 Running 4 Running 5 Main Done */
    • delay() : 시간만큼 일시정지, 정지된 시간동안 다른 루틴에 양보
    • yield() : 한번 양보
    • launch{ for(i in 0..10 step 2){ print("${i} ") yield() } } launch{ for(i in 1..10 step 2){ print("${i} ") yield() } } /* 결과(스케줄링 결과에 따라 랜덤할 수 있음) 0 1 2 3 4 5 6 7 8 9 10 */
    • cancel() : 작업 중지
    • launch{ for(i in 0..10){ println("Running ${i}") if(i == 5) this@launch.cancel() } } /* Running 0 Running 1 Running 2 Running 3 Running 4 Running 5 */
  • 비동기 작업의 효율성(작업시간 차이 비교)
  • fun main()=runBlocking{ println("Main's On") launch{ var time = measureTimeMillis { var name = async{ readName() } var age = async{ readAge() } println("Name : ${name.await()}, Age : ${age.await()}") } println("Running time : ${time}ms") } launch{ var time2 = measureTimeMillis { var name = readName() var age = readAge() println("Name : ${name}, Age : ${age}") } println("Running time : ${time2}ms") } println("Main's Down") } suspend fun readName() : String{ var job = CoroutineScope(Dispatchers.IO).async{ delay(3000) return@async "James" } return job.await() } suspend fun readAge() : Int{ var job = CoroutineScope(Dispatchers.IO).async{ delay(1000) return@async 25 } return job.await() }