본문 바로가기
프로그래밍/코틀린

코틀린 스터디 -2장. 코틀린 기초

by 직장인 투자자 2021. 12. 5.

2.1 기본 요소: 함수와 변수

  • 타입 선언 생략 가능 (타입 추론)
  • 불변 데이터 사용 장려

2.1.1 Hello World

fun main (args: Array<String>) {
  println("Hello, world!")
}
  • fun 키워드, : type 형식의 타입지정
  • 최상위에 함수 정의 (클래스 내부에 정의되어야 하는 Java와 달리)
  • println: 코틀린 표준 라이브러리 함수
  • ; 생략 가능

2.1.2 함수

fun max (a: Int, b: Int): Int {
  return if (a > b) a else b
}
println(max(1, 2))
  • if가 실행 제어 문(statement)이 아니라 결과를 만드는 식(expression) 임 - 삼항 연산자와 유사

문(staement)과 식(expression)

  • 식은 값을 만들어내며 다른 식의 하위 요소로 계산에 참여 가능
  • 문은 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소, 값을 만들어내지 않음
  • 자바: 모든 제어 구조가 문
  • 코틀린: 루프를 제외한 대부분의 제어 구조가 식
  • 단, 대입은 자바에서 식이었으나 코틀린에서 문 (대입과 비교 혼동에 의한 버그 방지)

식이 본문인 함수

fun max(a: Int, b: Int): Int = if (a > b) a else b
  • 중괄호 및 return을 제거 후 등호(=)를 식 앞에 붙여 간결하게 함수 표현
  • 중괄호로 둘러싸인 함수: 블록이 본문인 함수
  • 등호와 식으로 이뤄진 함수: 식이 본문인 함수
  • 타입 추론: 반환 타입 생략하더라도 식의 결과 타입을 함수 반환 타입으로 지정
    • 식이 본문인 함수만 반환 타입 생략 가능

2.1.3 변수

val question = "삶, 우주, 그리고 모든 것에 대한 궁극적인 질문"
val answer: Int = 42
val yearsToCompute = 7.5e6
val answer2: Int
answer2 = 42
  • 타입 생략/명시 가능
  • 부동소수점 상수 => Double
  • 초기화 식이 없는 경우 -> 타입 명시 필수

가변 및 불변 변수

  • val (value) - 불변(immutable) 참조 변수 (자바의 final)
  • var (variable) - 가변(mutable) 참조 변수
  • 함수형 프로그래밍: side effect 제거를 위해 가능 한 val사용 권장
  • val이 정확히 한 번 초기화 실행됨을 컴파일러가 확인 가능 -> 여러 값으로 초기화 가능
val message: String
if (canPerformOperation()) {
  message = "Success"
}
else {
  message = "Failed"
}
  • 참조가 불변일 뿐, 객체 내부 값은 변경될 수 있음
val languages = arrayListOf("Java")
languages.add("Kotlin")
  • var 변수 값 변경 가능하나 타입은 불가
var answer = 42
answer = "no answer" // "Error: type mismatch" 컴파일 오류 발생

2.1.4 더 쉽게 문자열 형식 지정: 문자열 템플릿

fun main(args: Array<String>) {
  val name = if (args.size > 0) args [0] else "Kotlin"
  println("Hello, $name!") // 문자열 템플릿 사용 (변수)
}
  • 컴파일된 코드는 StringBuilder를 사용하여 문자열 상수와 변수 값을 append
  • 존재하지 않는 변수를 템플릿에서 사용 시 컴파일 오류
  • $를 넣고 싶은 경우 \로 escape
  • 복잡한 식도 중괄호로 넣을 수 있음
fun main(args: Array<String>) {
  if (args.size > 0) {
    println("Hello, ${args[0]}!")
  }
}

한글을 문자열 템플릿에서 사용할 경우 주의할 점

  • 한글도 변수명에 사용할 수 있음
  • $name님 반가워요! 는 unresolved reference를 발생시킴
  • ${name}님 반가워요!처럼 중괄호 처리
  • 변수명만 사용하더라도 중괄호 사용 권장
    • 검색 / 일괄 변환 편리
    • 템플릿 내 변수 식별 편리
  • 중괄호로 둘러싼 식 안에서 큰 따옴표 사용 및 문자열 템플릿 사용 가능
fun main(args: Array<String>) {
  println("Hello, ${if (args.size > 0) args[0] else "someone"}!")
}
fun main(args: Array<String>) {
  if (args.size > 0) {
    val s = args[0]
    println("${if(s.length > 2) "too short" else "normal string ${s}"}")
  }
}

2.2 클래스와 프로퍼티

/* Java */
public class Person {
  private final String name;

  public Person(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }
}
class Person(val name: String)
  • 코드 없이 데이터만 저장하는 클래스: 값 객체(value object)
  • 코틀린 기본 가시성은 public - 변경자 생략

2.2.1 프로퍼티

  • 클래스 개념의 목적은 데이터의 캡슐화와 이를 다루는 코드를 한 주체 아래에 가두는 것
  • Java
    • 필드에 저장, 필드는 보통 private
    • Accessor Method: Getter / Setter 메서드 제공
    • 필드 + 접근자 -> 프로퍼티
  • Kotlin: 프로퍼티로 자바의 필드와 접근자 메서드를 대체
class Person(
  val name: String, // 읽기 전용 프로퍼티: private field & getter
  var isMarried: Boolean // 쓸 수 있는 프로퍼티: private field & getter, setter
)
  • val: private field & getter 생성
  • var: private field & getter, setter 생성
  • 자바에서 활용 가능
/* Java */
Person person = new Person("Bob", true);
System.out.println(person.getName());
// Bob 출력
System.out.println(person.isMarried()); // 변수명이 is~로 시작하는 경우 원래 이름 그대로 사용
// true 출력
/* Kotlin */
val person = Person("Bob", true) // new 키워드 사용하지 않음
println(person.name) // 프로퍼티 이름 직접 사용해도 Getter 호출
println(person.isMarried) // 프로퍼티 이름 직접 사용해도 Getter 호출

2.2.2 커스텀 접근자

  • 뒷받침하는 필드(Backing Field): 대부분의 프로퍼티에는 그 프로퍼티의 값을 저장하기 위한 필드 존재 -
  • 원하는 경우 그때그때 계산 가능
class Rectangle(val height: Int, val width: Int) {
  val isSquare: Boolean
    get() { // 프로퍼티 Getter 선언
      return height == width
    }
}
  • isSquare 프로퍼티는 접근할 때마다 Getter가 프로퍼티 값을 다시 계산
  • { .. } 문으로 함수가 작성되었으나 마찬가지로 =로 식으로 정의 가능
val rectangle = Rectangle(41, 43)
println(rectangle.isSquare)
// false 출력

4장에서 더 자세한 내용:(생성자를 명시적으로 선언하는 문법 등)을 다룰 예정

2.2.3 코틀린 소스코드 구조: 디렉터리와 패키지

  • Java: 모든 클래스를 패키지 단위로 관리
  • 코틀린에도 비슷한 개념의 패키지 존재
    • pacakage 정의 시 파일 내 모든 선언(클래스, 함수, 프로퍼티 등)이 해당 패키지에 포함
    • 같은 패키지의 경우 Import 없이 직접 사용 가능, 다른 패키지인 경우 Import 필요
package geometry.shsapes // 패키지 선언

import java.util.Random // 표준 자바 라이브러리 클래스 Import

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

fun createRandomRectangle(): Rectangle {
  val random = Random()
  return Rectangle(random.nextInt(), random.nextInt())
}
package geometry.example

import geometry.shapes.createRandomRectangle // 이름으로 Import

fun main(args: Array<String>) {
  println(createRandomRectangle().isSquare)
}
  • Star Import: 패키지 이름 뒤에.*를 추가하면 패키지 내 모든 선언 Import
    • 패키지 내 모든 클래스 및 최상위에 정의된 함수와 프로퍼티 포함
  • Java
    • 패키지 구조와 일치하는 디렉터리 계층 구조 생성
    • 패키지 내 클래스 각각을 클래스 이름과 같은 자바 파일로 저장하여 패키지 디렉터리에 위치
  • Kotlin
    • 여러 클래스를 한 파일에 넣을 수 있으며 파일 이름도 자유
    • 디스크상 어느 디렉터리에 소스코드 파일을 위치하여도 무방
    • 그러나 Java와 같이 패키지별 디렉터리를 구성하는 편이 나음 (특히 Java & Kotlin 혼용 시)
      • 여러 클래스를 한 파일에 넣는 것은 무방

2.3 선택 표현과 처리: enum과 when

  • when: Java의 switch를 대체하되 훨씬 강력

2.3.1 enum 클래스 정의

enum class Color {
  RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
  • Java: enum
  • Kotlin: enum class
    • enum은 Soft Keyword로 다른 곳에서 이름에 사용 가능
    • class는 Keyword로 clazz 등의 이름 사용 필요
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 // enum 클래스 안에서 메서드 정의
}
  • enum도 일반적인 클래스와 마찬가지로 생성자와 프로퍼티 선언
  • 상수 목록과 메서드 정의 사이: 세미콜론 사용 필수

2.3.2 when으로 enum 클래스 다루기

각 enum을 별도의 String으로 매핑하는 예제

fun getMnemonic(color: Color) =
  when (color) {
    Color.RED -> "Richard"
    Color.ORANGE -> "Of"
    Color.YELLOW -> "York"
    Color.GREEN -> "Gave"
    Color.BLUE -> "Battle"
    Color.INDIGO -> "In"
    Color.VIOLET -> "Vain"
  }
println(getMnemonic(color.BLUE))
// Battle 출력
  • 분기별 break 불필요
  • 한 분기 안에서 여러 값을 매치 패턴으로 사용 가능 (, 로 분리)
fun getWarmth(color: Color) = when(color) {
  Color.RED, COLOR.ORANGE, Color.YELLOW -> "warm"
  COLOR.GREEN -> "neutral"
  Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}

2.3.3 when과 임의의 객체를 함께 사용

  • 분기 조건에 상수만 사용할 수 있는 자바 switch와 달리 임의의 객체 허용
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")
  }
  • 객체 사이 동등성(Equility) 비교 수행
  • 위에서 아래 순으로 각 분기 검사

2.3.4 인자 없는 when 사용

  • 인자 없어도 사용 가능
fun mixOptimized(c1: Color, c2: Color) =
  when {
    (c1 == RED && c2 == YELLOW) ||
    (c1 == YELLOW && c2 == RED) ->
      ORANGE
    ....
    else -> throw.Exception("Dirty color")
  }

2.3.5 스마트 캐스트: 타입 검사와 타입 캐스트를 조합

  • 산술 트리 예제
interface Expr
class Num(val value: Int) : Expr // value 프로퍼티만 가지는 클래스, Expr 인터페이스 구현
class Sum(val left: Expr, val right: Expr) : Expr

(1 +2) + 4의 경우 --> `Sum(Sum(Num(1), Num(2)), Num(4)))

  • 산술 트리를 계산하기 위한 eval 함수 구현
fun eval(e: Expr): Int {
  if (e is Num) {
    val n = e as Num // 불필요한 과정 (Smart Cast에 의해 자동으로 타입 구분)
    return n.value
  }
  if (e is Sum) {
    return eval(e.right) + eval(e.left) // Smart Cast에 의해 e는 Sum 타입으로 식별됨
  }
  throw IllegalArgumentException("Unknown expression")
}
  • is를 사용해 변수 타입 검사 가능
  • is 검사는 instanceof와 유사하나, Java에서는 확인 후 명시적으로 캐스팅 필요
  • Smart Casting: Kotlin에서는 자동으로 is를 통해 확인된 타입으로 자동 캐스팅됨
    • is로 검사 이후 변하지 않는 경우에만 동작
      • val 이어야 하고
      • 커스텀 접근자를 사용하면 안 됨 (가변 값)
  • TS의 Discriminated Union 개념과 역할이 비슷하네요..

2.3.6 리팩터링: if를 when으로 변경

우선 위 eval을 if 식을 본문으로 간략하게 표현

fun eval(e: Expr): Int =
  if (e is Num) {
    e.value
  } else if (e is Sum) {
    eval(e.right) + eval(e.left)
  } else {
    throw IllegalArgumentException("Unknown expression")
  }
  • if 분기에 식이 하나밖에 없다면 중괄호 생략 가능
  • if 분기에 블록을 사용하는 경우 블록의 마지막 식이 분기의 결과 값

다시 한번when을 사용해 다듬기

fun eval(e: Expr): Int =
  when (e) {
    is Num -> // Smart Cast 적용
      e.value
    is Sum -> // Smart Cast 적용
      eval(e.right) + eval(e.left)
    else ->
      throw IllegalArgumentException("Unknown expression")
  }

2.3.7 if와 when의 분기에서 블록 사용

각 분기에서 수행해야 하는 로직이 복잡한 경우 분기 본문에 블록 사용 가능

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")
 
  }

2.4 대상을 이터레이션: while과 for 루프

  • while: 자바와 동일
  • for: for-each 루프에 해당하는 형태만 존재 (for.. in.. 형식)

2.4.1 while 루프

while (condition) {
   /* do something */
}

do {
  /* do something */
} while (condition)
  • 자바와 동일

2.4.2 수에 대한 이터레이션: 범위와 수열

  • for (int i = 0; i < 10; ++i) {.. } 같은 형식의 for 없음
  • range 사용하여 표현:..연산자로 시작 값과 끝 값을 연결해서 범위 표현 (폐구간)
val oneToTen = 1..10
  • 수열(progression): 정수 범위로 수행할 수 있는 가장 단순한 작업 - < 범위에 속한 모든 값에 대한 이터레이션
for (i in 1..100) {
  println(i)
}
// 1 ~ 100 출력
  • 역순: downTo
  • 증분 설정:step (역순인 경우 기본값 -1)
for (i in 100 downTo 1 step 2) {
  println(i)
}
// 100 98 96 94 ... 출력
  • until:.. 는 닫힌 구간인데, 끝 값을 열린 구간으로 하고 싶은 경우
for (x in 0..size-1) {
  /* do something */
}
for (x until size) {
  /* do something */
}

2.4.3 맵에 대한 이터레이션

  • 문자열 범위 이터레이션 가능
  • (key, value)에 대해 Map 이터레이션 가능
val binaryReps = TreeMap<Char, String>()
for (c in 'A'..'F') { // 문자열 범위 이터레이션
  val binary = Integer.toBinaryString(c.toInt())
  binaryReps[c] = binary // put(c, binary) 사용하지 않고 이와 같이 entry 삽입 가능
}
for ((letter, binary) in binaryReps) { // 맵에 대해 이터레이션 (key, value)
  println("$letter = $binary")
}
  • 구조 분해(7.4.1에서 자세히 다룸)를 이용해 다른 컬렉션에도 활용 가능
val list = arrayListOf("10", "11", "1001")
for ((index, element) in list.withIndex()) { // 인덱스와 함께 컬렉션 이터레이션
  println("$index: $element")
}
  • withIndex는 3장에서 제대로 다룸

2.4.4 in으로 컬렉션이나 범위의 원소 검사

  • in,! in연산자를 사용해 어떤 값이 범위에 속하는지 검사 가능
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
  • when에서도 사용 가능
fun recognize(c: Char) = when (c) {
  in '0'..'9' -> "It's a digit!"
  in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
  else -> "I don't know..."
}
  • 비교가 가능한 클래스(java.lang.Comparable 인터페이스 구현)라면 범위를 만들 수 있음
  • 그러나, 그 범위 내의 모든 객체를 항상 이터레이션 할 수 있는 것은 아님
  • 다만 in 연산자를 사용해 범위 안에 속하는지는 결정 가능
println("Kotlin" in "Java".."Scala") // "Java" <= "Kotlin" && "Kotlin" <= "Scala"
// true 출력
  • 컬렉션에도 in 적용 가능
println("Kotlin" in setOf("Java", "Scala"))
// false 출력
  • 7.3.2에서 in 검사를 적용할 수 있는 객체에 대한 일반 규칙을 살펴볼 예정

2.5 코틀린의 예외 처리

  • Java와 비슷
if (percentage !in 0..100) {
  throw IllegalArgumentException("A percentage value must be between 0 and 100: $percentage") // new 불필요
}
  • throw는 식(expression) 임
val percentage =
  if (number in 0..100)
    number
  else
  throw IllegalArgumentException("A percentage value must be between 0 and 100: $percentage")
  • throw를 식에 활용할 때의 기술 사항에 대해서는 6.2.6에서 자세히 다룸

2.5.1 try, catch, finally

  • Java와 달리 함수 정의에 throws 없음
  • Kotlin은 Checked / Unchecked 예외 구분하지 않음
fun readNumber(reader: BufferedReader): Int? { // 던지는 예외 명시 불필요
  try {
    val line = reader.readLine()
    return Integer.parseInt(line)
  }
  catch (e: NumberFormatException) {
    return null
  }
  finally { // 자바와 동일하게 동작
    reader.close()
  }
}
  • try-with-resource 문법은 없지만 라이브러리 함수로 같은 기능 구현 (8.2.5에서 다룸)

2.5.2 try를 식으로 사용

  • try 키워드는 if나 when과 마찬가지로 식
  • if와 달리 try의 본문은 { .. } 중괄호 필수
  • 블록의 마지막 식의 값이 결과
fun readNumber(reader: BufferedReader): Int? {
  val number = try {
    val line = reader.readLine()
    Integer.parseInt(line)
  } catch (e: NumberFormatException) {
    null // 예외시 null 반환
  }
  println(number)
}

2.6 요약

  • 함수:fun 키워드로 함수 정의
  • 변수: val readonly reference, var mutable reference
  • 문자열 템플릿: "${variable}"
  • 클래스: Java에 비해 간결
  • if: 식(expression)으로, 값을 만들어냄
  • when: Java switch와 유사하나 더 강력
  • Smart Cast: 타입 검사 이후 캐스팅 불필요
  • for, while, do-while: 일반적인 문법, for은 이터레이션만 제공
  • 범위(range): 1.. 5 등 for에 대한 추상화 제공 및 in, !in을 통한 검사 수행
  • 예외처리: Java와 비슷하나 함수에 발생 예외 명시 불필요
반응형

댓글