Today I Learned_230711

12 분 소요

실무 프로젝트로 배우는 Kotlin & Spring : 리팩토링부터 서비스 구현까지

코틀린 개발 환경 구성하기

OpenJDK 설치 : https://adoptium.net/temurin/releases/

이클립스 재단에서 제공하는 temurin (Azul zulu와 비슷한듯)

intel 칩 사용하고 있으므로 x64로 설치

코틀린 언어만 학습할 때에는 플레이그라운드 사용

https://play.kotlinlang.org

변수

// 변수 선언 시 top level에 위치시킬 수 있다.
var x = 5

fun main() {
    
    x += 1		// 함수 밖에서 선언한 변수에 접근이 가능
    
    val a : Int = 1		//키워드 변수병 : 타입 = 할당할 값
    
    // 타입 생략도 가능하다. 타입 자동 추론이 가능하다.
    val b = 1
    
    // 지연 할당
    val c : Int
    c = 3		// 먼저 선언하고 이후에 할당하는 방식
    
    // 변수 선언 시 타입을 정해주지 않으면 컴파일 에러 발생. 타입을 c와 같이 반드시 넣어줘야 한다.
    val d
    d = 123
    
    // 변수 선언 시 두 가지 키워드 존재
    // val(value) : java의 final처럼 한 번 값이 초기화 된 이후에는 재할당 불가. readonly
    // var(variable) : 가변 변수를 나타내는 키워드. 이후에 재할당이 가능하다.
    
    val e : String = "Hello"
    e = "World" // 컴파일 오류 발생
    
    var f = 123
    f = "hi"	// 컴파일 오류 발생. 최초에 고정된 타입과 다른 타입이 들어오면 안 된다.
}

함수

// 기본적인 함수 선언 스타일
// fun 함수명(함수의 인자) : 함수가 반환할 타입
// { 함수의 몸통 }
fun sum(a: Int, b: Int) : Int {
    return a + b
}

// 표현식 스타일
fun sum2(a: Int, b: Int) : Int = a + b  //몸통 없이 로직만 들어간다

// 표현식 & 반환타입 생략
fun sum3(a: Int, b: Int) = a + b    // 변수의 타입 추론이 되기에 반환타입 생략. 함수도 기본적으로 추론해주므로 생략이 가능하다.

// 몸통이 있는 함수는 반환 타입을 제거하면 컴파일 오류
// 표현식은 반환타입 생략이 가능
fun sum4(a: Int, b: Int) {
    return a + b
}

// 반환타입이 없는 함수는 Unit을 반환한다 (java의 void와 유사)
// Unit의 기능을 사용하기 보다는 반환타입이 없으면 Unit이 된다.
fun prinfSum(a: Int, b: Int) {
    println("$a + $b = ${a + b}")
}

// 디폴트 파라미터
// java에는 디폴트 파라미터가 없다.
fun greeting(message: String = "안녕하세요!") {
    println(message)
}

fun main() {
    greeting()
    greeting("HI!!!")
}

// named argument
fun log(level: String = "INFO", message: String) {
    println("[${level}]$message")
}

// named argument : 함수에 변수 값을 넣을 때 이름과 값을 매핑하는 기법
// 버그를 줄일 수 있는데 유용한 문법. 순서 바꿔 넣거나 헷갈리게 넣을 수 있는 오류를 줄여준다.
fun main() {
    log(message = "인포 로그")
    log(level = "DEBUG", "디버그 로그") // named argument를 직접 지정하지 않아도 level 다음으로 오기에 message에 들어온다
    log("WARN", "워닝 로그") // 순서대로 넣어주기
    log(level = "ERROR", message = "에러 로그")
}

흐름제어

if

fun main() {
    // if..else 사용
    val job = "Software Developer"

    if(job == "Software Developer"){
        println("개발자")
    } else {
        println("개발자 아님")
    }

    // 코틀린의 if...else는 표현식이다
    val age : Int = 10

    val str = if(age> 10) {
        "성인"
    } else {
        "아이"
    }

    // 코틀린은 삼항 연산자가 없다. if...else가 표현식이므로 불필요하다.
    val a = 1
    val b = 2
    val c = if (b > a) b else a // 삼항 연산자 대신 if else 사용
}

when

fun main() {
    // 자바의 switch 코드를 코틀린의 when식으로 변환한 코드
    // 자바의 switch는 표현식이 아니기에 그 자체로 값을 반환하지는 않았다 (최근 자바에서는 지원)
    val day = 2

    val result = when (day) {
        1 -> "월요일"
        2 -> "화요일"
        3 -> "수요일"
        4 -> "목요일"
        5 -> "금요일"
        else -> "기타"
    }

    println(result)

    // else를 생략할 수 있다
    when(getColor()) {
        Color.RED -> print("red")
        Color.GREEN -> print("green")
    }

    // 여러개의 조건을 콤마로 구분해 한줄에서 정의할 수 있다.
    when (getNumber()) {
        0, 1 -> print("0 또는 1")
        else -> print("0 또는 1이 아님")
    }
}

enum class Color {
    RED, GREEN
}

fun getColor() = Color.RED

fun getNumber() = 2

for

fun main() {
    // 범위 연산자 .. 를 사용해 for loop 돌리기
    // .. : 코틀린에서 제공하는 범위 연산자. 뒤에 온 값들을 포함한다.
    for (i in 0 .. 3) {
        println(i) // 0, 1, 2, 3
    }
    println()

    // until을 사용해 반복한다(맨 마지막 값 포함하지 않음)
    for (i in 0 until 3) {
        println(i) // 0, 1, 2
    }
    println()

    // step에 들어온 값 만큼 증가시킨다
    // 특정 값만큼 증가시키면서 반복할 때 사용
    for (i in 0 .. 6 step 2){
        println(i) // 0, 2, 4, 6
    }
    println()

    // downTo를 사용해 반복하면서 값을 감소시킨다
    for (i in 3 downTo 1) {
        println(i)  // 3, 2, 1
    }
    println()

    // 전달받은 배열을 반복
    val numbers = arrayOf(1, 2, 3)

    for(i in numbers) {
        println(i) // 1, 2, 3
    }
}

while

fun main() {
    // 자바의 while문과 동일
    // 조건을 확인하고 참이면 코드 블록을 실행한 후 다시 조건을 확인
    var x = 5

    while (x > 0) {
        println(x) // 5, 4, 3, 2, 1
        x--
    }
}

널 안정성

많은 프로그래밍 언어에서 제일 많이 발생하는 예외 유형이 NPE

JDK 8에서 NPE를 해결하기 위해 Optional 지원

→ 값을 래핑. 객체 생성에 따른 오버헤드 발생. 컴파일 단계에서 검사하지 않는다.

코틀린에서 널을 받을 수 없는 타입 존재.

100% 널을 제거할 수가 없다. DB값이나 자바 호환 코드가 있기 때문

→ nullable type 제공

val a : String? = null;

val a : String? = null;
a.length // 컴파일러 단에서 잡는다
a?.length  // 안 잡는다

안전 연산자(?) 를 통해 접근하면 NPE 없이 접근할 수 있다.

// 널에 대한 조건이 들어왔을 때 값을 반환하는 방법
    val b : Int = if (a != null) a.length else 0
val c = a?.length ?: 0

엘비스 연산자 (?: )를 사용하면 좀 더 간결하게 할 수 있다.

좌변이 null이면 0을 리턴한다.

안전 연산자(?)와 엘비스 연산자(?:)를 사용하면 널 처리를 쉽게 할 수 있다.

→ 자바에서 코틀린으로 넘어와야 하는 이유!

코틀린에서는 널이 아예 안 나오는게 아니라 가능성이 현저히 줄어드는 것이다.

코틀린에서 널이 나오게 하는 방법

// 코틀린에서 NPE가 발생하려면?
    throw NullPointerException()        // NPE 직접 발생시키기

    val c: String? = null
    val d = c!!.length          // 단언 연산자. null이 발생하지 않음을 단언하는 연산자

단언 연산자를 실제로 많이 사용하지는 않는다.

이 외에 자바와 상호 운영하다보면 NPE가 발생하기도 한다.

예외처리

코틀린의 모든 예외 클래스는 Throwable 상속

  • Error : 시스템에 발생하는 비정상적인 상황. 예측이 어렵고 기본적으로 복구가 불가능
  • Exception : 시스템에서 포착 가능하며(try-catch) 복구 가능하고 예외 처리가 강제됨
    • IOException, FileNotFounrException
  • RunTimeException : 런타임 시 발생하는 에러로 예외 처리를 강제하지 않음.

자바에서 체크드 익셉션은 컴파일 에러가 발생하므로 무조건 try-catch로 감싸거나 throws로 예외 전파 필요

코틀린에서는 예외 처리를 강제하지 않는다.

fun main () {
    Thread.sleep(1)  // java에서는 try-catch로 감싸야 함
}

개발자에 의해 try catch가 필요하다면 try catch 사용 가능

자바에서는 try/catch가 구문이였기에 값 반환이 안되었다면, 코틀린에서는 표현식이기 때문에 값을 전달할 수 있다.

val a = try {
        "1234".toInt()
    } catch (e: Exception) {
        println("예외 발생!")
    }

throw도 표현식이기에 그 자체로 값이 될 수 있다.

fun failTest(message: String) {
    throw IllegalArgumentException(message)
}

여기서 함수는 Nothing 타입을 반환한다.

코드가 정상적으로 수행되는 것을 보장하지 않을 때는 Nothing 타입을 반환한다.

Nothing 타입 뒤에 오는 코드를 분석해서 실행되지 않음을 판단하기도 한다.

Nothing 타입이 들어가면 null 안정 코드를 작성하지 않아도 된다. (엘비스 연산자와 조합)

val b: String? = "null"
    val c = b ?: failTest("a is null")

    println(c?.length)
}

fun failTest(message: String): Nothing {
    throw IllegalArgumentException(message)
}

이 경우 b에 절대 null이 들어갈 수 없다. 컴파일러는 이를 분석하여 널 안정 코드를 작성하지 않아도 된다고 한다.

클래스와 프로퍼티

코틀린에서는 클래스 본문을 생략할 수도 있다.

class EmptyClass

클래스 생성자 만들 때 constructor 사용 가능

기본 생성자에 대해 contructor 생략 가능 → 가독성이 좋다.

특정 어노테이션을 같이 쓰거나 할때 contructor 사용

class Coffee constructor(
    val name: String,
    val price: Int,){   //뒤에 필드가 없음에도 후행 쉼표(,)를 넣을 수 있음. 후에 프로퍼티 추가 시 쉽게 작성할 수 있다.
fun main() {
    val coffee = Coffee()
    coffee.name = "아이스 아메리카노"   // setter를 사용해서 값을 할당
    coffee.price = 2000

    println("${coffee.name} 가격은 ${coffee.price}")
}

코틀린의 컴파일러가 getter/setter를 자동으로 생성해주므로 내부적으로는 getter/setter로 동작

값 할당시에는 setter, 사용 시에는 getter 사용

var로 선언한다면 setter가 없고 getter만 존재한다.

커스텀 getter/setter를 생성할 수 있다.

class Coffee constructor(
    var name: String = "",
    var price: Int = 0,) {
    val brand: String
        get() = "스타벅스"  // 커스텀 getter

    // custom setter
    var quantity : Int = 0
        set(value) {
            if (value > 0) {    // 수량이 0 초과인 경우만 할당
                quantity = value
            }
        }
}

class EmptyClass

fun main() {
    val coffee = Coffee()
    coffee.name = "아이스 아메리카노"   // setter를 사용해서 값을 할당
    coffee.price = 2000
    coffee.quantity = 1

    println("${coffee.brand} ${coffee.name} ${coffee.quantity}잔 가격은 ${coffee.price}")
}
  • field 식별자
    • getter/setter에 대해 식별자 사용
    • 실제 필드의 참조에 접근
    • 이를 통해 backing field에 접근 가능
  • backing field가 필요한 이유
    • 코틀린에서 프로퍼티를 사용할 때 setter를 사용
    • 아래와 같이 직접 할당하게 된다면?
      // custom setter
          var quantity : Int = 0
              set(value) {
                  if (value > 0) {    // 수량이 0 초과인 경우만 할당
                      quantity = value
                  }
              }
      }
        
      // StackOverFlow 발생!
      Exception in thread "main" java.lang.StackOverflowError
      	at Coffee.setQuantity(7_ClassProperty.kt:14)
      	at Coffee.setQuantity(7_ClassProperty.kt:14)
      	at Coffee.setQuantity(7_ClassProperty.kt:14)
      	at Coffee.setQuantity(7_ClassProperty.kt:14)
      	at Coffee.setQuantity(7_ClassProperty.kt:14)
    
    • quantity가 내부 quantity를 호출하고.. 그 quantity가 내부를 호출하고.. 무한 재귀 상태에 빠지다가 stacoverflow가 난다.

코틀린의 프로퍼티는 객체지향적이다.

  • 객체의 상태는 프로퍼티로, 행위는 메소드로 표현
  • 자바의 경우는 상태를 메소드로 나타나게 된다
    • ex) 보통 상태 판단을 위해 isChecked()라는 메소드를 통해서 나타낸다.
  • 코틀린에서는 object.checked 값 자체를 판단하면 된다!
  • 내부적으로는 getter 함수를 사용하긴 하지만, 눈으로 봤을 때는 프로퍼티이므로 자바에 비해 객체지향적이다

상속

상속을 통해 기존 코드 재사용 혹은 확장

자바는 final 키워드로 확장을 막을 수 있다 (ex. System 클래스는 확장하지 못하도록 final class로 작성되어 있음)

상속에 대한 여러가지 부작용이 있다. 상속을 목적으로 하는 클래스가 아니면 final로 작성하자.

Java에서 모든 클래스의 조상은 Object

코틀린에서 모든 클래스의 조상은 Any

public open class Any

우리가 작성하는 모든 클래스에서는 equals, hashcode, tostring 제공

코틀린의 클래스는 기본값이 final → 상속이 불가함

꼭 필요한 클래스에 대해서는 open이라는 키워드를 통해 상속을 오픈할 수 있음

// 이 상태로는 상속이 불가능
class Dog

// 이 상태로는 상속이 가능
open class Dog

open으로 class를 열었음에도 프로퍼티를 open하지 않으면 오버라이딩 할 수 없다

override 키워드를 써 줘야 상위 클래스에서 오픈한 프로퍼티 혹은 함수를 재정의 가능

//상속이 가능한 상태로 open
open class Dog {
    open var age: Int = 0
    open fun bark() {
        println("멍멍")
    }
}

// 프로퍼티 및 메소드 재정의
class Bulldog : Dog() {
    override var age: Int = 0
    override fun bark() {
        println("컹컹")
    }
}

자바에서는 override가 필수가 아니다

https://stackoverflow.com/questions/4822954/do-we-really-need-override-and-so-on-when-code-java

자바에서는 가독성을 위해 작성 (오버라이드 해야하는데 다른 함수명을 작성해서 오버라이드 한 것으로 착각한다던가..)

코틀린에서 override는 필수이다 (안하면 이렇게 에러가 난다)

C# 사용자들은 상속이 헷갈릴 수가 있다..

// 이렇게 하면 안됨!
class Bulldog : Dog {
}

// 클래스 상속은 아래와 같이 해야 한다.
// 생성자 호출까지 해야 한다.
class Bulldog : Dog() {
}

기본 생성자를 통해서 프로퍼티 오버라이딩이 가능

class Bulldog(override var age: Int = 0) : Dog() {
    override fun bark() {
        println("컹컹")
    }
}

오버라이딩한 하위 클래스에서 오버라이딩한 프로퍼티와 함수는 자동으로 Open상태가 된다.

open class Bulldog(override var age: Int = 0) : Dog() {
    override fun bark() {
        println("컹컹")
    }
}

class ChildBulldog : Bulldog() {
    override var age: Int = 0
    override fun bark() {
        super.bark()
    }
}
  • Bulldog에서 프로퍼티와 함수에 별도로 open을 붙여주지 않았는데도 ChildBulldog에서 상속이 가능하다.
  • 그런데 상속을 막고 싶다면? final 키워드를 사용하자.
open class Bulldog(final override var age: Int = 0) : Dog() {
    final override fun bark() {
        println("컹컹")
    }
}
  • 이러면 age와 bark의 상속이 불가능해진다.

하위 클래스에서 상위 클래스의 프로퍼티나 메소드를 접근할 때는 super 사용

open class Dog {
    open var age: Int = 0
    open fun bark() {
        println("멍멍")
    }
}

open class Bulldog(override var age: Int = 0) : Dog() {
    override fun bark() {
        super.bark()    // 멍멍
    }
}

코틀린은 자바와 마찬가지로 추상 클래스 제공 → abstract로 구현

하위 클래스에서 구현되어야 하는 프로퍼티나 함수들도 abtract 사용

// 추상 클래스
abstract class Developer {
    abstract var age: Int
    abstract fun code(language: String)
    var concrete: String = ""
}

// 하위 클래스. 추상 클래스의 abstract 내용을 구현해야 함
// abstract 붙지 않은 것은 구현하지 않아도 된다
class BackendDeveloper(override var age: Int = 0) : Developer() {
    override fun code(language: String) {
        println("I code with $language")
    }
    // concrete는 하위 클래스에서 구현하지 않아도 됨
}

인터페이스

코틀린에서 인터페이스 정의 → interface 키워드 사용

class Product(val name: String, val price: Int)

interface Cart {
    fun add(product: Product)
    fun rent() {
        // Cart 인터페이스 내부에서 구현
    }
}

주의! 클래스와는 다르게 인터페이스 구현 시에는 생성자로 받는게 아니다!

class MyCart : Cart() // 이거 아님!
class MyCart() : Cart // 이렇게 해야 함
  • java의 인터페이스는 추상 메소드와 상수(public static final)만 가질 수 있었음
    • java 8부터는 추상 프로퍼티를 가질 수도 있었음
  • 코틀린의 인터페이스는 추상 메소드, 상수, 구현된 프로퍼티, 추상 프로퍼티 등을 가질 수 있다.
class Product(val name: String, val price: Int)

interface Cart {
    // 상수
    companion object {
        const val cartName: String = "GMKT"
    }

    // getter 접근자를 통한 프로퍼티 구현
    val weight : String
        get() = "20KG"  // 인터페이스에서는 특정 값을 만들어줌으로써 프로퍼티 사용이 가능하다.
    
    // 추상 프로퍼티
    val coin : Int
    
    // 추상 함수
    fun add(product: Product)
    
    // 구현된 함수
    fun rent() {
        println("빌려줬습니다")
    }
}

(잠시 딴길로) companion object란?

  • 코틀린에서 클래스 내부에 정적인 멤버를 정의하기 위해 사용되는 특별한 객체
  • 클래스 내부에 하나만 존재
  • 클래스 내부에 선언하면 해당 객체의 멤버는 정적으로 접근할 수 있다.
class MyClass {
    companion object {
        fun myStaticMethod() {
            // 구현
        }

        const val CONSTANT: Int = 10
    }
}

getter 접근자를 통해 프로퍼티에 값을 직접 넣을 수 있다.

인터페이스의 프로퍼티에 값을 넣을 때는 backing field 사용이 불가하므로 값을 직접 넣자.

// getter 접근자를 통한 프로퍼티 구현
    val weight : String
        get() {
            field   // 인터페이스에서는 backing field를 사용하면 컴파일 에러가 발생한다.
        }
        get() = "20KG"  // 인터페이스에서는 특정 값을 만들어줌으로써 프로퍼티 사용이 가능하다.

인터페이스 바탕으로 클래스 구현하기

// 인터페이스 받아 구현할 때는 생성자를 붙이는게 아니라 인터페이스명만 붙인다.
// class MyCart : Cart() 가 아님!
class MyCart(override val coin: Int) : Cart {
    override fun add(product: Product) {
        if(coin <= 0) println("코인을 넣어주세요")
        else println("${product.name}이(가) 카트에 추가됐습니다.")
    }
    // rent가 구현되어 있으므로 굳이 구현하지 않아도 된다.
    // 값을 넣어 준 프로퍼티, 상수 또한 구현할 필요가 없다
}

인터페이스의 상속

코틀린의 인터페이스는 상위 인터페이스를 가질 수 있다. (자바도 가능)

interface Wheel {
    fun roll()
}

interface Cart: Wheel {
    override fun roll() {
        println("카트가 굴러갑니다.")  // 여기서 구현해도 되고
    }
}

class MyCart(override val coin: Int) : Cart {
    override fun roll() {
        println("카트가 또 굴러갑니다.") // 여기서 구현해도 된다
    }
}

java와 마찬가지로 여러 인터페이스를 동시에 구현할 수 있다.

,로 인터페이스를 구분한다.

interface Cart
interface Order
class MyCart : Cart, Order

복수 인터페이스를 구현했을 때의 문제점

동일한 시그니처를 가진 함수를 구현할 때 문제점 발생

→ 동일한 시그니처의 시그니처란?

  • 컴파일러가 함수를 구분하기 위해 함수를 구분하기 위한 구성요소를 살펴보는데, 이를 시그니처라 한다.
  • 함수의 이름, 매개변수의 개수, 매개변수 자료형이 같다면 동일한 시그니처라고 할 수 있다.
interface Cart: Wheel {
    fun add(product: Product) {}
}

interface Order {
    fun add(product: Product) {
        println("${product.name} 주문이 완료되었습니다")
    }
}

class MyCart(override val coin: Int) : Cart, Order {
    override fun add(product: Product) {
        if(coin <= 0) println("코인을 넣어주세요")
        else println("${product.name}이(가) 카트에 추가됐습니다.")
    }
}

이 상태로라면 인터페이스의 add가 아니라 MyCart의 add가 호출된다.

인터페이스인 Order의 add 호출을 위해서는 super를 통해 호출해야 한다.

그런데 super를 통해서 Order의 add를 호출해야 하는데, 코드 입장에서는 어느 쪽의 add를 호출해야할지 모른다 → 구현 시 super<클래스>를 통해 호출할 인터페이스 지정이 가능하다.

class MyCart(override val coin: Int) : Cart, Order {
    override fun add(product: Product) {
        if(coin <= 0) println("코인을 넣어주세요")
        else println("${product.name}이(가) 카트에 추가됐습니다.")

        super<Order>.add(product)
    }
}

디폴트로 구현된 함수는 기본적으로 구현 클래스에서 구현하지 않아도 되지만, 인터페이스에서 동일한 시그니처 함수에 대해 구현하고 있을 경우 하위 클래스에서 구현을 강제한다.

interface Cart{
    // 디폴트 함수 1
    fun printId() = println("1234")
}

interface Order {
    // 디폴트 함수 2
    fun printId() = println("5678")
}

class MyCart(override val coin: Int) : Cart, Order {

    override fun printId() {
        println("여기서 반드시 구현해야 한다. 구현하지 않으면 컴파일 에러 발생")
    }
}

카테고리:

업데이트: