[Kotlin in Action 2/e] 2장 코틀린 기초

왕왕조현·2026년 1월 31일

Kotlin in Action 2/e

목록 보기
2/18
post-thumbnail

안녕하세요!

학습 내용 정리와 함께 다시 돌아온 개발자 꿈나무 김조현입니다.

2장에서는 코틀린 프로그램을 동작시키기 위한 기초적인 문법에 대한 내용을 볼 수 있습니다.


코틀린스러운 코드란?

코틀린 뿐만 아니라 다른 프로그래밍 언어를 배운다면 '언어스러운 코드'라는 말을 많이 들어봤을겁니다.

어찌보면 자연스럽게 나오게 되는 말인 것 같습니다. 자바를 쓰면 자바스럽게 써야하고, 코틀린을 쓰면 코틀린스럽게 써야 그 언어를 잘 이해하고 잘 사용한다고 말할 수 있지 않을까요?

그 언어스러운 코드란 언어 커뮤니티에서 일반적으로 받아들여지는 프로그래밍 스타일과 잘 들어맞고, 언어 설계자들이 권장하는 방식을 따르는 코드라는 의미입니다.

즉, 개발자들이 권장하는 코드 스타일과 기능을 적절하게 사용하는 것이 코틀린스러운 코드 작성이라고 할 수 있습니다.


코틀린의 기본 요소는?

어느 언어나 마찬가지로 기본 요소를 소개하라고 한다면 변수, 함수, 클래스 등을 찾아볼 것 같습니다.

이 책에서도 마찬가지로 기본 요소로서 변수, 함수, 클래스, 이넘, 프로퍼티를 선언하는 방법에 대해 설명합니다.


함수를 선언하는 방법은?

"Hello, World!"를 출력하는 프로그램을 작성해보겠습니다. 간단해보이는 이 예제에서도 코틀린의 문법이나 특성을 많이 찾아볼 수 있습니다.

fun main() {
	println("Hello, World!")
}

코틀린은 함수를 선언할 때 다음과 같은 순서를 가집니다.

  • fun 키워드로 시작합니다.
  • 키워드 뒤에 함수 이름을 적습니다.
  • 함수 이름 뒤에는 괄호로 묶어 파라미터 목록을 적습니다. 파라미터를 작성할 때는 파라미터 이름, 타입을 순서대로 작성하며, 이름과 타입은 콜론(:)을 사용하여 구분합니다.
  • 괄호 뒤에는 반환 타입을 선언합니다. 이 때 반환타입과 괄호는 콜론(:)을 사용하여 구분합니다.
  • 이후에 중괄호로 포괄된 함수 본문을 적습니다.

이 코드의 경우는 파라미터를 받지 않고, 값을 반환도 하지 않습니다. 그리고 println 메소드를 통해 "Hello, World!"라는 문장을 출력하는 함수 본문도 가지고 있습니다.

이 코드를 보면 main 함수의 특징이 반영된 것을 볼 수 있습니다.

main 함수는 파라미터를 선언할 수도 있고, 선언하지 않을 수도 있습니다. 어떤 경우라도 main함수는 아무 값도 반환하지 않습니다.


반환 값이 있는 함수는 어떻게 선언하나요?

반환 값이 있는 함수는 위에서 설명한 것처럼 콜론(:)으로 구분하여 반환할 타입을 명시해주면 됩니다.

fun max(a: Int, b: Int): Int {
	return if (a > b) a else b
}

fun main() {
	println(max(1, 2))
	// 2
}

위의 코드는 정수 a, b를 받고 if 식을 사용해 더 큰 정수 값을 반환하는 함수입니다.

함수를 호출할 때에는 함수 이름을 적고 그 뒤의 괄호 사이에 인자를 제공하면 값을 반환할 수 있습니다.

다른 언어에서 if를 부를 때는 if 식이 아니라 if 문이라고 불렀던 것 같았는데, 이 책에서는 왜 코틀린의 if를 if식이라고 부르는지 설명해줍니다.

'식'이라는 것은 값을 만들어내며 다른 식의 하위 요소로 계산에 참여할 수 있고, '문'은 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어내지 않는다는 차이가 있다고 정의합니다.

즉, 코틀린의 if는 값을 만들어낼 수 있다는 것입니다.

이런 특징은 다른 언어의 삼항 연산자와 비슷한 역할을 가집니다.

다른 언어에서 '(a > b) ? a : b'와 같이 사용하는 식을 코틀린은 'if (a > b) a else b' 처럼 사용할 수 있는 것입니다.


함수를 줄일 수 있는 방법은?

max함수처럼 함수의 본문이 하나의 식으로 이루어져 있으면 중괄호와 return을 생략하고 반환 타입 뒤에 등호(=)와 함수 본문을 적어주는 방식으로 줄일 수 있습니다.

fun max(a: Int, b: Int): Int = if(a > b) a else b
// 위의 max와 같은 함수다.

첫 번째 max 함수처럼 중괄호로 둘러싸인 함수를 블록 본문 함수라고 부르고, 두 번째 max 함수처럼 등호와 식으로 이뤄진 함수를 식 본문 함수라고 부릅니다.

또한 식 본문 함수를 사용할 경우에는 함수의 반환 타입을 생략할 수 있습니다.

어떻게 생략이 가능한가? 이는 1장에서 설명한 것처럼 코틀린은 타입 추론 기능을 제공하기 때문입니다.

타입 추론 기능에 대해 간단히 다시 설명해보면 컴파일러가 함수 본문 식을 분석해서 식의 결과 타입을 함수 반환 타입으로 정해주는 것입니다.

반면, 블록 본문 함수는 반드시 타입을 지정하고 return을 통해 값을 반환해야 합니다.

이렇게 설계한 이유는 실전 프로그램에서는 긴 함수에 여러 return을 반환해야하는 경우가 자주 있는데, 이런 긴 코드를 읽을 때 함수의 반환 타입과 어디서 반환하는지를 더 쉽게 하기 위해 이런 구조로 설계한 것입니다.

InteliJ에서는 위의 사진처럼 return을 클릭한 후에 나오는 전구를 누르고 'Converet to experssion body'를 클릭하게 되면 블록 본문 함수를 식 본문 함수로 변경할 수 있습니다.


변수는 어떻게 선언하는가?

변수를 선언하는 방법은 val 또는 var 키워드를 사용하고 뒤에 변수 이름을 명시해주면 끝입니다.
변수명 뒤에는 변수 타입을 명시할 수도 있지만, 생략을 해도 상관없습니다. 생략이 가능한 이유는 식 본문 함수와 마찬가지로 컴파일러가 자동으로 타입을 지정해줄 수 있기 때문입니다.

fun main() {
	val question: String = "코틀린 재미있어요오"
	val answer: Int = 1111
	
	val question2 = "타입 생략도 가능해요"
	val answer2 = 1234
}

그치만 변수를 당장 초기화하지 않고 나중에 초기화하기 위해서는 변수 타입을 반드시 명시해야합니다. 왜냐하면 변수값이 존재하지 않는다면 컴파일러가 변수의 타입을 알아낼 수 없기 때문입니다.

fun main() {
	val answer: Int // 즉시 초기화하는 것이 아니기 때문에 명시적으로 타입을 지정해줌.
	answer = 1230
}

val은?

val 은 읽기 전용 참조로, val로 선언된 변수는 단 한번만 대입될 수 있습니다.

초기화한 후에는 다른 값을 대입할 수 없습니다.

⭐️자바에서 final로 표현한 변수와 같은 의미입니다.

기본적으로 코틀린에서 모든 변수를 val 키워드를 사용해 선언하는 방식을 지켜야합니다. 그치만 반드시 필요한 경우에만 var를 사용하는 것을 허용합니다.

이런 방식을 지켜야하는 이유는 읽기 전용 참조와 변경 불가능한 객체를 부수 효과가 없는 함수와 조합해 사용하면 함수형 프로그래밍 스타일이 제공하는 이점을 살릴 수 있기 때문이다.

val 은 정확히 한 번 초기화를 해야하지만 조건에 따라 val 값을 다른 여러 값으로 초기화할 수도 있습니다.

fun canPerformOperation(): Boolean {
	return true
}

fun main() {
	val result: String
	if (canPerformOperation()) {
		result = "Success"
	}
	else {
		result = "Can't perform operation"
	}
}

result가 String타입인 것만 명시해놓고 조건에 따라 result를 다른 값으로 초기화할 수 있는 코드입니다.

val 키워드를 사용했기 때문에 한 번 초기화하면 다시 초기화를 할 수 없습니다.

val의 값을 바꿀 수 없는 것이지만 val의 값이 참조하는 값은 변경할 수 있습니다. 대표적으로 이를 보여줄 수 있는 값이 mutableList입니다.

fun main() {
	val languages = mutableListOf("Java")
	languages.add("Kotlin")
}
// languages리스트에 값을 추가할 수 있다.

var은?

var 은 재대입 가능한 참조 변수를 의미하며, 다시 대입할 값의 타입이 변수의 타입과 같다면 여러번 다른 값을 대입할 수 있습니다.

만약 대입할 값과 변수의 타입이 다르다면 컴파일 오류가 발생합니다.

fun main() {
	var answer = 42
	answer = "no answer"
}
// answer가 기대하는 타입과 변경하려는 타입이 일치하지 않아 오류가 나타난다.

문자열 템플릿이란?

1장에서 설명했듯이 문자열 리터럴 안에서 변수를 사용할 수 있는 것입니다. 문자열 템플릿을 사용하는 방법은 변수 이름 앞에 $를 붙이는 것입니다.

${식}의 형태로 사용하여 문자열 템플릿 안에서 복잡한 식도 사용할 수 있습니다.

fun main() {
	val input = readln()
	val name = if (input.isNotBlank()) input else "Kotlin"
	println("Hello $name!")
	// input이 비어있는 값이라면 name은 Kotlin을 가지게 되고, Hello Kotlin!을 출력한다.
	// input이 asdf라면 name은 asdf를 가지게 되고, Hello asdf!를 출력한다.
	println("Hello ${name.length}!")
}

문자열 템플릿을 사용할 때 주의할 점은 변수 이름 뒤에 글자가 붙어있다면 오류가 발생합니다.

예를 들면 $name로 name을 받아오려고 했지만 println(”Hello 제 이름은 $name입니다.) 등으로 사용할 경우에는 변수명을 name으로 받는 것이 아닌 ‘name입니다’라는 변수명을 찾게되고 존재하지 않으면 오류를 반환하게 됩니다.

이를 해결하는 방법은 변수명과 문자열이 만나는 부분을 띄어 표현하거나, 중괄호를 사용해 변수를 묶어주는 것입니다.

위의 세 사진처럼 변수명의 하이라이트가 바뀌는 것을 볼 수 있습니다.


클래스와 프로퍼티란?

이 글에 담고싶은 내용은 코틀린에서 어떻게 클래스를 선언하는가 이기 때문에 클래스에 대한 내용을 자세하게 짚고 가지 않겠습니다. 찾아보면 잘 정리된 글이 이미 많습니다 ㅎㅎ...

코틀린도 다른 객체지향 언어와 마찬가지로 클래스라는 추상화를 제공합니다.

⭐️자바와 코틀린의 차이점

  • 자바는 생성자를 반복적으로 생상해야하고, 관습에 따라 게터, 세터 함수와 같은 접근자 메소드를 생성해줘야한다.
  • 코틀린은 public이 기본 지정자이기 때문에 생략할 수 있다. 코틀린에서는 프로퍼티를 선언할 때 변수와 마찬가지로 val, var 을 사용한다.
  • 코틀린에서 프로퍼티를 선언하는 방식은 프로퍼티와 관련 있는 접근자를 선언하는 것이다.

아래의 두 코드는 자바와 코틀린으로 똑같은 Person 클래스 생성한 것입니다.

코틀린은 자바처럼 생성자를 반복적으로 생성할 필요가 없고, 접근자 메소드를 기본으로 지원하기 때문에 클래스 코드가 길어질 일이 없는 것입니다.

// 자바코드
public class Person {
	private final String name;
	
	public Person(String name) {
		this.name = name
	}
	
	public String getName() {
		return name;
	}
}
// 코틀린 코드
class Person(val name: String) 

또한 아래의 코드처럼 코틀린에서 선언한 클래스는 자바에서 그대로 사용할 수 있습니다. 다만 자바에서 클래스의 프로퍼티를 초기화하거나 불러올 때는 게터와 세터로 선언해야 합니다.

위에서 설명했듯이 코틀린 클래스의 프로퍼티는 자동으로 게터와 세터를 만들기 때문에 자바에서도 선언을 통해 코틀린의 클래스를 사용할 수 있습니다.

class Person(
	val name: String, // 읽기 전용 프로퍼티로, 게터도 만들어낸다.
	var isStudent: Boolean // 쓸 수 있는 프로퍼티로 게터, 세터를 만들어낸다.
}
// 이 클래스는 자바와 코틀린 어느 코드를 사용해도 상관없이 동작한다.

// 자바에서 사용
public class Demo {
	public static void main(String[] args) {
		Person person = new Person("Bob", true);
		System.out.println(person.getName());
		// Bob
		System.out.pritnln(person.isStudent());
		// true
		person.setStudent(false);
		System.out.println(person.isStudent());
		// false
	}
}

// 코틀린에서 사용
fun main() {
	val person = Person("Bob", true)
	println(person.name)
	// Bob
	println(person.isStudent)
	// true
	person.isStudent = false
	println(person.isStudent)
	// false
}
// 프로퍼티 이름을 직접 사용해도 코틀린은 자동으로 세터를 호출한다.

접근자는 게터와 세터밖에 없는건가?

게터와 세터 외에 정보를 별도에 필드에 계산하지 않는 커스텀 접근자를 생성할 수 있습니다.

아래의 코드는 직사각형 클래스에서 가로와 세로의 길이가 같을 때 true를 반환해주는 프로퍼티를 만든 것입니다.

이처럼 어떤 프로퍼티가 같은 객체 안의 다른 프로퍼티에서 계산된 직접적인 결과를 필요로 할 때 커스텀 접근자를 생성할 수 있습니다.

class Rectangle(val height: Int, val width: Int) {
	val isSquare: Boolean
		get() {
			return height == width
		}
		// get() = height == width
}

이 때 isSquare에는 정보가 저장되지 않습니다. 왜냐하면 get()을 사용한 것은 호출할 때마다 값을 가져오겠다는 의미이기 때문에, 값을 반환하지만 저장하지 않습니다.

그렇기 때문에 val isSquare: Boolean = height == width와는 다른 코드가 되는 것입니다.
이 코드는 등호를 통해 val 키워드 변수인 isSquare에 값을 초기화하는 것이기 때문에 재설정을 할 수 없게 되는 것입니다. 즉, 이 방식은 커스텀 게터를 생성하는 것이 아니라 함수를 정의하는 방식인 것입니다.

파라미터가 없는 함수를 정의하는 방식과 커스텀 접근자를 생성하는 방식의 결과가 같다면 어느 쪽이 더 나을까? 라는 질문에 이 책은 두 방식 모두 비슷하다고 정의했습니다.

그치만 클래스의 특성을 기술하고 싶다면 프로퍼티로, 클래스의 행동을 기술하고 싶다면 프로퍼티가 아니라 멤버 함수로 정의해야한다고 합니다.


소스코드의 구조는?

프로그램이 더 커지고 그 안에 클래스, 함수 등 많은 구성물이 들어가게 될 때 프로젝트가 정리가 안되어 있다면 후에 유지 보수나 기능 추가 등에서 어려움을 겪게 될 것입니다.

코틀린은 이러한 문제를 안 생기도록 하기 위해 패키지라는 개념을 사용합니다.

패키지를 사용하는 방법은 코틀린 파일 맨 앞에 package 문을 사용하면 됩니다. package문이 있는 파일의 모든 선언은 해당 패키지 안에 들어가게 됩니다.

코틀린 프로젝트를 생성한 후에 main.kt 파일을 보면 패키지 문이 자동으로 생성됩니다.

같은 패키지에 속해 있다면 다른 파일에서 정의한 선언일지라도 직접 사용할 수 있습니다.

반면 다른 패키지에 정의한 선언을 사용하려면 package문 아래에 import 키워드를 사용해서 다른 패키지를 불러와야 합니다.

위의 코드처럼 example이라는 패키지의 코드에서 shapes라는 패키지명을 가진 코드에 존재하는 선언을 불러오고 싶다면 import 키워드를 사용해야 합니다.

패키지 이름 뒤에 .*을 붙이면 패키지 안의 모든 선언을 임포트할 수 있다. 이를 스타 임포트, 와일드카드 임포트 등으로 부릅니다.

⭐️자바와 코틀린의 차이점

  • 자바에서는 패키지의 구조와 일치하는 디렉터리 계층 구조를 만들고 클래스의 소스코드를 그 클래스가 속한 패키지와 같은 디렉터리에 위치시켜야 한다.
  • 클래스를 자신의 이름과 같은 파일로 저장해야한다.
  • 코틀린에서는 여러 클래스를 같은 파일에 넣을 수 있고, 파일의 이름도 마음대로 정할 수 있다.

코틀린은 자바보다 자유로운 형식을 가지고 있지만, 자바와 같은 패키지별 디렉터리를 구성하는 것을 권장합니다.

그 이유는 자바와 코틀린을 함께 사용하는데 자바의 방식을 따르지 않는다면 자바 클래스를 코틀린 클래스로 마이그레이션 할 때 문제가 발생할 수도 있기 때문입니다.


enum은?

enum은 비슷한 성격의 상수들을 모아 이름을 붙인 집합 자료형입니다.

enum class Color {
	RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

위의 코드는 빨주노초파남보의 색이라는 비슷한 성격을 담고있는 enum클래스를 생성한 것입니다.

enum을 선언하는 것은 enum class 키워드를 사용하면 됩니다.

⭐️자바와 코틀린의 차이점
코틀린은 enum class 키워드를 사용하지만, 자바는 enum을 사용한다.

enum은 소프트 키워드입니다.

소프트 키워드인 enum은 class 앞에서만 특별한 기능을 하기 때문에 변수의 이름으로 사용할 수도 있습니다. 반대로 하드 키워드는 이름을 변수명과 같은 식별자로 사용할 수 없습니다.

예를 들면 class, int 등은 변수의 이름으로 사용하지 못하는 하드 키워드다.

enum을 확장하기 위해서 프로퍼티나 메소드를 추가할 수 있습니다. enum 클래스 안에서 메소드를 정의하기 위해서는 이넘 상수 목록과 메서드 정의 사이에 세미콜론을 넣어 구분해줘야 합니다.

enum class Color(
	val r: Int,
	val g: Int,
	val b: Int
) {
	RED(255, 0, 0),
	ORANGE(255, 165, 0),
	YELLOW(255, 255, 0),
	GREEN(0, 255, 0),
	BLUE(0, 0, 255),
	INDIGO(75, 0, 130),
	VIOLET(238, 130, 238); // -> 세미콜론을 사용해 영역 구분
	
	fun rgb() = (r * 256 + g) * 256 + b
	fun printColor() = println("$this is $rgb")
}

fun main() {
	println(Color.BLUE.rgb)
	// 255
	Color.GREEN.printColor()
	// GREEN is 65280
}

when은?

when은 자바의 switch문과 비슷한 개념으로 사용되는 코틀린 문법입니다.

when은 식입니다. if와 같은 식이기 때문에 마찬가지로 식 본문 함수에 when을 사용해서 식을 바로 반환할 수 있습니다.

	when (color) {
		Color.RED -> "Richard"
		Color.ORANGE -> "Of"
		Color.YELLOW -> "York"
		Color.GREEN -> "Gave"
		Color.BLUE -> "Battle"
		Color.INDIGO -> "In"
		Color.VIOLET -> "Vain"
	}
	
fun main() {
	println(getMnemonic(Color.BLUE))
	// Battle
}

when은 받은 값과 같은 분기를 찾아 값을 반환하는 역할을 합니다. 위의 코드는 color의 값에 따라 각각 다른 값을 반환할 수 있습니다.

⭐️자바와 코틀린의 차이점
자바의 switch는 각 분기마다 break를 선언해야한다. 상황에 따라 필수는 아니지만 분기마다 다른 값을 반환하도록 하려면 break를 선언해야한다. 선언하지 않는다면 같은 분기를 찾은 후 아래의 분기도 모두 접근하게 된다.
반면 코틀린은 break를 사용하지 않아도 특정 분기만 접근할 수 있는 것이다.

그렇다면 여러 조건에 대해서 같은 분기에 접근하도록 할 수는 없는 것일까? 코틀린은 콤마를 사용해 같은 분기에 여러 조건을 넣을 수 있습니다.

fun measureColor() = Color.ORANGE

fun getWarmthFromSensor(): String {
	val color = measureColor()
	return when(color) {
		Color.RED, Color.ORANGE, Color.YELLOW -> "warm (red = ${color.r})"
		Color.GREEN -> "neutral (green = ${color.g})"
		Color.BLUE, Color.INDIGO, Color.VIOLET -> "coold (blue = ${color.b})"
	}
}

fun main() {
	println(getWarmthFromSensor())
	// warm (red = 255)
}

빨간색, 주황색, 노란색은 따뜻한 색, 초록은 중립적인 색, 파란색, 남색, 보라색은 차가운 색
이처럼 여러 조건에 대해 같은 분기로 나눌 수도 있습니다.

위의 enum 객체들은 앞에 클래스명을 붙여주는 번거로움을 가집니다. enum 객체들은 import로 enum 클래스를 스타 임포트로 선언한다면 객체 앞의 클래스명을 생략할 수 있습니다.

import ch02.colors.Color.*

fun measureColor() = ORANGE

fun getWarmthFromSensor(): String {
	val color = measureColor()
	return when(color) {
		RED, ORANGE, YELLOW -> "warm (red = ${color.r})"
		GREEN -> "neutral (green = ${color.g})"
		BLUE, INDIGO, VIOLET -> "coold (blue = ${color.b})"
	}
}
// Color. 을 생략해도 똑같이 동작한다.

fun main() {
	println(getWarmthFromSensor())
	// warm (red = 255)
}

위의 두 코드는 같은 코드입니다. 또한 변수를 when안에 선언하면서 함수의 길이를 더 줄일 수도 있습니다.

fun getWarmthFromSensor(): String {
	return when(val color = measureColor()) { // 변수의 영역이 when 본문으로 제한되지만, 코드를 깔끔하게 바꿀 수 있다.
		RED, ORANGE, YELLOW -> "warm (red = ${color.r})"
		GREEN -> "neutral (green = ${color.g})"
		BLUE, INDIGO, VIOLET -> "coold (blue = ${color.b})"
	}
}

enum에서 when을 사용할 때마다 컴파일러는 when이 철저한지를 살펴봅니다. 위의 코드들은 Color의 모든 객체에 대해 분기를 선언하였고, 이는 모든 이넘상수에 대해 when이 분기를 가지고 있는지 확인하는 것입니다.

그치만 String이나 Int 등 같이 다양한 값이 나와 철저하지 않은 경우 컴파일러를 만족시키기 위해 else 키워드를 사용해 디폴트 케이스를 제공해야만 합니다.

when의 분기 조건에는 임의의 객체를 사용할 수 있습니다. 아래의 예시는 색을 조합하여 조합된 값을 반환하는 것입니다. 이 때 각 분기마다 set을 구현하고 집합을 각 분기마다 비교합니다.

이후에 설명하겠지만 setOf는 코틀린의 집합 객체인 Set을 생성하는 함수입니다. 집합은 원소가 하나씩 모여 있는 컬렉션으로, 각 원소의 순소는 중요하지 않으며, 중복되는 값을 가지지 않습니다.

따라서 c1이 BLUE, c2가 YELLOW인 것과 c1이 YELLOW, c2가 BLUE인 것은 같은 분기점에서 반환하게 됩니다.

fun mix(c1: Color, c2: Color) = 
	when (setOf(c1, c2)) {
		setOf(RED, YELLOW) -> ORANGE
		setOf(YELLOW, BLUE) -> GREEN
		setOf(BLUE, VIOLET) -> INDIGO
		else -> throw Exception("dirty color")
	}

fun main() {
	println(mix(BLUE, YELLOW))
	// GREEN
}

하지만 이렇게 객체를 만드는 방식은 when이 실행될 떄마다 불필요한 객체를 생성하기 때문에 비효율적입니다.

when에서 인자를 사용하지 않으면 불필요한 값을 생성하지 않고 조건식을 넣을 수 있습니다. 하지만 이런 방식은 가독성이 조금 떨어지게 된다는 단점이 있다.

fun mixOptimized(c1: Color, c2: Color) = 
	when {
		(c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> ORANGE
		(c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) -> GREEN
		(c1 == BLUE && c2 == VIOLEY) || (c1 == VIOLET && c2 == BLUE) -> INDIGO
		else -> throw Exception("Dirty color")
	}
	
fun main() {
	println(mixOptimized(BLUE, YELLOW))
	// GREEN
}

when과 if의 리펙토링

if와 when은 모두 분기에 블록을 사용할 수 있습니다. 이런 경우에는 블록의 마지막 문장이 블록 전체의 결과가 됩니다.

그렇기에 코드의 과정을 알고싶다면 블록 중간에 println문을 추가하여 로그를 찍는 방법으로 연산 과정을 파악할 수 있습니다.

fun evalWithLogging(e: Expr): Int = 
	when (e) {
    	is Num -> {
        	println("num: ${e.value}")
            e.value
        }
        is Sum -> {
        	val left = evalWithLogging(e.left)
            val right = evalWithLogging(e.right)
            println("sum: $left + $right")
            left + right
        }
        else -> throw IllegalArgumentException("Unknown expression")
    }
    
fun main() {
	println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))))
    // num: 1
    // num: 2
    // sum: 1 + 2
    // num: 4
    // sum: 3 + 4
    // 7
}

스마트 캐스트 활용

스마트 캐스트를 사용하면 어려 타입의 객체를 사용할 수 있게 됩니다.

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Int, val right: Expr) : Expr

fun eval(e: Expr): Int {
	if (e is Num) {
    	val n = e as Num
        return n.value
    }
    if (e is Sum) {
    	return eval(e.right) + eval(e.left)
    }
    throw IllegalArgumentException("Unknown expression")
}

fun main() {
	println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
}

인터페이스 Expr은 아무런 메소드도 수행하지 않고 Num과 Sum의 공통 타입 역할만을 수행합니다. 이러한 인터페이스를 마커 인터페이스라고 부릅니다.

Expr 객체를 받지만 is라는 스마트 캐스트를 사용해 Num과 Sum을 검사하고, 그 타입의 객체로 사용할 수 있도록 도와줍니다.

그렇기에 Expr 객체지만 Sum일 경우에 right, left와 같은 파라미터에 접근할 수 있는 것입니다.

프로퍼티에 대해 스마트 캐스트를 사용한다면 그 프로퍼티는 반드시 val이어야 하며, 커스텀 접근자를 사용한 것도 안됩니다. 왜냐하면 위와 같은 사용을 하게 된다면 해당 프로퍼티에 대한 접근이 항상 같은 값을 내놓는다고 확신할 수 없기 때문입니다.

is로 스마트 캐스팅을 하면 검사된 객체로 사용할 수 있지만, as를 사용해서 객체를 명시적으로 타입 캐스팅해도 됩니다.


대상 이터레이션이란?

이터레이션이란 컬랙션이나 범위에 존재하는 값을 하나씩 순차적으로 꺼내서 처리하는 과정을 의미합니다.

이터레이션 방법에는 while과 for 루프가 있습니다. while은 while과 do-while 루프로 나뉘게 됩니다.


while 루프에 대하여

while은 조건이 참일 경우에 본문을 실행하고, do-while은 무조건 본문을 한 번 실행한 후에 조건을 검증하여 while과 같은 동작을 합니다.

두 루프의 차이점은 while의 조건이 맞지 않는다면 while문은 한 번도 실행하지 못할 수도 있지만 do-while은 반드시 한 번은 실행된다는 점입니다.

while (조건) {
	/* ... */
	if (shouldExit) break
}

do {
	if (shouldSkip) continue
	/* ... */
} while (조건)

반복문에서 사용되는 키워드에는 break와 continue가 있습니다. break는 자신을 둘러싼 루프를 중단시키는 역할을 하고, continue는 루프의 다음 단계를 시작하는 역할을 합니다.

코틀린에는 레이블이라는 개념을 사용하여 내포된 루프의 경우 지정할 수 있으며, break나 continue를 사용할 때 레이블을 참조할 수 있습니다.

레이블을 선언하는 방법은 @ 기호 다음에 식별자를 붙이면 됩니다.

outer@ while (outerCondition) {
	while (innerCondition) {
		if (shouldExitInner) break
		if (shouldSkipInner) continue
		if (shouldExit) break@outer
		if (shouldSkip) continue@outer
	}
}

이 코드로 설명을 하면 만약 shouldExitInner가 참이라면 안쪽 while문에서 탈출하는 것이고, shouldSkipInner가 참이라면 shouldSkipInner가 참이 된 조건을 넘어가 다음 조건을 검사하는 것입니다.

shouldExit가 참일 경우에는 @outer가 뒤에 붙은 break가 실행되는데 이는 안쪽이 아닌 바깥쪽 즉, outer 레이블이 적용된 반복문에서 탈출하는 것입니다. continue@outer도 마찬가지로 바깥쪽 while문의 조건을 넘어가 다음 조건에 대해 검사하게 됩니다.


수에 대한 이터레이션이란?

코틀린에서 범위를 쓸 때는 .. 연산자를 사용합니다. 1..10은 1부터 10까지를 포함하는 것을 의미합니다.(1 ~ 10)

코틀린의 범위처럼 양끝을 포함하는 구간을 폐구간이라고 합니다.

위처럼 어떤 범위에 속한 값을 일정한 순서로 이터레이션하는 경우를 순열이라고 한다.

downTo를 사용하면 역방향 순열을 만들 수 있습니다. 100 downTo 1는 100부터 1까지 반대 방향으로 범위를 가집니다.

순열 또는 역방향 순열 뒤에 step을 붙여서 증가 값을 변경할 수 있습니다. 1..10 step 2는 1, 3, 5, 7, 9의 범위를 가지고, 10 downTo 1 step 2는 10, 8, 6, 4, 2를 가집니다.

만약 범위를 지정할 때 마지막 값을 포함하고 싶지 않다면 1..<10와 같이 ..< 연산자를 사용하면 됩니다. (1 ~ 9)


컬랙션에 대한 이터레이션이란?

for (x in y)루프를 사용해 컬랙션에 대한 이터레이션을 사용할 수도 있다. 컬랙션에 대한 이터레이션은 말 그대로 컬랙션의 원소를 순서대로 접근하는 것입니다.

  • 리스트에 대한 이터레이션
fun main() {
	val collection = listOf("red", "green", "blue")
	for (color in collection) {
		print("$color ")
	}
	// red green blue
}
  • 맵에 대한 이터레이션
fun main() {
	val binaryReps = mutableMapOf<Char, String>()
	for (char in 'A'..'F') {
		val binary = char.code.toString(radix = 2)
		binaryReps[char] = binary
	}
	
	for ((letter, binary) in binaryReps) {
		println("$letter = $binary")
	}
	
	// A = 1000001 D = 1000100
	// B = 1000010 E = 1000101
	// C = 1000011 F = 1000110
}

⭐️자바와 코틀린의 차이점
맵은 key와 value가 한 쌍을 가지는 컬렉션입니다.
코틀린에서는 get과 put 같은 함수 사용 대신 map[key] = value를 사용해 값을 설정하고 map[key]를 사용해 값을 불러올 수 있다.
즉, 인덱스를 활용해 map에 접근할 수 있는 것입니다.
반면 자바는 get메소드를 사용해 map에 접근해야하는 차이점이 있습니다.

컬랙션 안에 원소가 포함되는지 확인하는 방법에는 in키워드를 사용하는 검사하는 방법이 있습니다. in 키워드를 사용하면 원소가 범위 안에 포함되는지를 true/false로 반환해줍니다.

문자에서도 숫자와 마찬가지로 ..연산자를 사용하여 범위를 찾아낼 수 있습니다. c in ‘a’..’z’는 a에서 z까지라는 의미이고, a ≤ c && c ≤ z로 변환되어 사용됩니다.

"java".."kotlin"처럼 문자열에서도 사용할 수 있으며, 알파벳 순서로 비교합니다.

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'

fun main() {
	println(isLetter('q'))
	// true
	println(isNotDigit('x'))
	// true
    println("Kotlin" in "Java".."Scale")
    // true
    // Kotlin이 Java와 Scale 사이에 있기에 true가 반환된다.
}

예외를 던지고 잡아내는 방법은?

throw 키워드를 사용해 예외를 던질 수 있습니다. 아래의 예시는 퍼센트 정수가 0~100 범위에 포함되지 않는다면 예외를 던지는 코드입니다.

if (percentage !in 0..100) {
	throw IllegalArgumentException(
		"A percentage value must be between 0 and 100: $percentage")
}

⭐️자바와 코틀린의 차이점
자바와 달리 코틀린에서 throw는 식이기 때문에 다른 식에 포함될 수 있다.
자바의 throw는 문이기 때문에 값을 반환하지 않아 코틀린과 같이 적절한 곳에 throw를 던질 수 없습니다.
if 조건이 참이라면 percentage 값을 number로 초기화하지만, 조건이 거짓이라면 변수가 초기화되지 않고 예외를 던진다.

아래의 코드처럼 자바의 throw문은 독립된 문장으로만 존재해야합니다.

val percentage = 
	if (number in 0..100)
		number
	else
		throw IllegalArgumentException(
			"A percentage value must be between 0 and 100: $percentage")
int percentage;

if (number >= 0 && number <= 100) {
    percentage = number;
} else {
    throw new IllegalArgumentException();
}

예외를 던진다면 처리까지 해야 예외를 관리할 수 있는 것이겠죠. 예외를 처리하기 위해서는 자바와 마찬가지로 try, catch, finally 절과 함께 사용해야합니다.

자바 코드와 가장 큰 차이점은 자바는 함수를 작성할 때 함수 선언 뒤에 thorws IOException을 붙여야하는 것입니다. IOException을 붙이는 이유는 readLine이나 close가 발생할지도 모르는 IOException이 체크 예외이기 때문이다.

자바에서는 명시적으로 처리해야만 하는 유형의 예외를 체크 예외라고 합니다.

즉, 자바에서는 함수에 선언된 체크 예외를 모두 잡아 처리하거나 함수에서도 다시 그 예외를 던질 수 있다고 선언해야만 하는 것입니다.

그치만 코틀린에서는 컴파일러가 예외 처리를 강제하지 않기 때문에 예외를 잡아내고 싶은 경우에만 try-catch를 사용하면 되는 것입니다.

try 또한 마찬가지로 식이며, 반드시 본문을 중괄호로 둘러싸야 한다. 또한 본문 내부에 여러 문장이 있다면 마지막 식이 전체 결과값입니다.

⭐️자바와 코틀린의 차이점
코틀린에서는 함수가 던질 수 있는 예외를 선언하지 않아도 된다.

import java.io.BufferedReader
import java.io.StringReader

fun readNumber(reader: BufferReader): Int? {
	try {
		val line = reader.readLine()
		return Integer.parseInt(line)
	}
	catch (e: NumberFormatException) {
		return null
	}
	finally {
		reader.close()
	}
}

fun main() {
	val reader = BufferedReader(StringReader("239"))
	println(readNumber(reader))
	// 239
}
Integer readNumber(BufferedReader reader) throws IOException

마무리입니다!

이렇게 오늘은 코틀린 프로그램을 동작시키기 위한 기초 문법과 개념에 대해 정리해보았습니다.

1장에서는 여러 개념들을 간단하게 찍먹한 느낌이면 2장부터는 이제 제대로 깊이를 가지고 개념에 대해 알아보는 시간을 가졌습니다.

이번 장에서 알게된 가장 큰 부분은 문과 식의 개념입니다. if와 when을 변수에 대입하는 방법으로 사용하는 것은 코틀린의 문법이라고만 생각하고 깊게 생각해본 적이 없었습니다. 이런 생각치도 못한 곳에서도 코틀린 개발자들이 편의성에 신경을 썼다고 생각하니 코틀린이라는 언어가 더 대단하다고 느껴지게 됐습니다.

다음에는 함수에 대한 글로 찾아뵙겠습니다.

읽어주셔서 감사합니다!🙂‍↕️

profile
천천히, 꾸준히, 한 걸음씩

0개의 댓글