Web/Kotlin & Spring

[Kotlin & Spring] - 코틀린의 기본 문법

Hyunseo😊 2023. 6. 23. 17:34

코틀린의 기본문법은 제가 느끼기에 학부 모바일 앱 프로그래밍2에서 배운 Swift(스위프트)문법과 매우 비슷하게 느껴졌습니다. 그래서 제가 생소하다고 느낄만한것과 중요하다고 생각되는 것만 정리하겠습니다.

 

코틀린에서 NPE를 해결하는 다양한 방법

  • 코틀린은 언어적 차원에서 NPE가 발생할 가능성을 제거합니다.
  • 코틀린의 타입은 기본적으로 Non-Null타입으므로 null을 허용하지 않습니다.
fun getNullStr(): String? = null

fun getLengthIfNotNull(str: String?) = str?.length ?: 0

fun main() {
    val nullableStr = getNullStr()
    val nullableStrLength = nullableStr?.length ?: "null인 경우 반환".length
    println(nullableStrLength)

    val length = getLengthIfNotNull(null)
    println(length)
}

 

코틀린에서의 예외처리

  • 코틀린의 모든 예외 클래스는 Throwable을 상속합니다.
  • Error: 시스템에 비정상적인 상황이 발생. 예측이 어렵고 기본적으로 복구가 불가능한 것들입니다.
    • e.g. OutOfMemoryError, StackOverflowError, etc
  • Exception: 시스템에서 포착 가능하여(try-catch) 복구 가능. 예외처리 강제
    • IOException, FileNotFoundException, etc
    • @Transactional 에서 해당 예외가 발생하면 기본적으로 롤백이 동작하지 않음
      • rollbackFor를 사용해야 함
  • RuntimeException: 런타임시에 발생하는 예외. 예외처리를 강제하지 않음
    • e.g. NullPointerException, ArrayIndexOutOfBoundsException, etc
  • 코틀린에서는 자바의 Exception계층을 코틀린 패키지로 래핑하는 것입니다!

 

  • 자바에서는 체크드 익셉션은 컴파일 에러가 발생하기 때문에 무조건 try-catch로 감싸거나 throws로 예외를 전파해야 합니다. 하지만 코틀린의 체크드 익셉션을 강제하지 않습니다. 원하는 경우에 try-catch를 쓸 수 있죠
  • 그리고 코틀린에서 try-catch는 표현식입니다. 
val a = try { "1234".toInt() } catch (e: NumberFormatException) { println("catch    ") }

 

  • 그리고 코틀린에서 Exception을 발생시키려면 throw를 사용해야 합니다.
  • throw또한 표현식이기 때문에 throw를 리턴할 수 있습니다.
fun failFast(message: String): Nothing {
    throw IllegalArgumentException(message)
}
  • 그리고 Nothing타입을 사용하면 컴파일러는 해당 코드 이후는 실행되지 않는다는 경고를 보여줍니다. 이는 엘비스 연산자와 사용하면 null 안전 코드를 작성하지 않아도 되게 됩니다.
fun main() {
    val a: String? = null
    val b: String = a ?: failFast("a is null ")
    println(b.length)
}

 

클래스와 프로퍼티

/**
 * @author ihyeonseo
 */

class Coffee (
    var name: String = "",
    var price: Int = 0, // 후행 쉼표
    var iced: Boolean = false,
) {
    // 커스텀 getter
    val brand: String
        get() {
            return "스타벅스"
        }

    var quantity: Int = 0
        set(value) {
            if(value > 0) { // 수량이 0 이상인 경우에만 할당
                field = value // field는 식별자 -> 실제 field의 참조에 접근(quantity) = backingfield
            }
        }
}

fun main() {
    var coffee = Coffee()
    coffee.name = "아이스 아메리카노"
    coffee.price = 2000
    coffee.quantity = 1
    coffee.iced = true

    if (coffee.iced) {
        println("아이스 커피")
    }

    println("${coffee.name} 가격은 ${coffee.price} 수량은 ${coffee.quantity}")
}
  • 코틀린의 기본 생성자는 class Coffee constructor()이렇게 작성하지 않고 constructor를 생략할 수 있습니다.
  • 또한 코틀린에서는 클래스에 프로퍼티를 선언할 때 후행 쉼표(trailing comma)를 사용할 수 있습니다.
  • 후행 쉼표를 사용하면 이전의 마지막 줄을 수정하지 않고 프로퍼티를 쉽게 추가할 수 있고 git에서 diff등으로 코드를 비교했을 때 변경사항을 명확히 알 수 있게 됩니다.

 

  • 코틀린에서 var로 선언된 프로퍼티는 getter, setter를 자동으로 생성합니다.
  • val로 선언된 프로퍼티는 getter만 존재하는 것이죠
  • 또한 코틀린은 위처럼 커스텀 getter, setter도 만들 수 있습니다.
  • 코틀린은 getter, setter에서 field라는 식별자를 사용해 필드의 참조에 접근하는 데 이를 Backing Field에 접근하다고 합니다. 
    • 이가 중요한 이유는, 코틀린에서 프로퍼티에 값을 할당할 때 실제론 setter를 사용하는데 이때 무한 재귀 즉 StackOverflow가 발생할 수 있기 때문입니다!

 

  • 기본적으로 코틀린의 프로퍼티는 객체지향적입니다. 객체지향에서 객체의 상태는 프로퍼티로 표현하고 행위는 메소드로 표현하는데 자바는 상태를 메소드로 나타냅니다. 근데 코틀린은 프로퍼티를 사용해 상태를 나타낼 수 있기 때문에 자바보다 객체지향적으로 코드를 작성할 수 있게 됩니다.

 

코틀린에서의 상속

  • 객체지향 핵심 원칙 중 하나인 상속은 상속을 통해 기존 코드를 재사용하거나 확장할 수 있습니다.
  • 자바는 또한 기본적으로는 모든 클래스가 상속이 가능하나 상속에 따른 부작용이 발생할 경우를 대비해 final키워드로 막을 수 있습니다. 
  • 이펙티브 자바의 아이템 중 상속을 위한 설계와 문서를 작성하고 그렇지 않으면 상속을 금지하라 라는 주제가 있는데 여기에는 여러가지 상속에 대한 문제점에 대해 나와있으며 결과적으로 상속을 목적으로 만든 클래스가 아니라면 모두 final로 작성하는 것이 좋다고 되어 있습니다.

 

  • 자바의 모든 클래스의 조상은 Object이고 코틀린은 Any입니다.
  • 코틀린의 클래스는 기본적으로 final클래스와 같이 상속을 막고 꼭 필요한 경우 open키워드로 상속을 허용할 수 있습니다.
  • 또한 프로퍼티는 기본 생성자를 사용해 오버라이드 할 수 있습니다.
open class Dog(open var age: Int = 0) {
    open fun bark() {
        println("   ")
} }
class Bulldog(override var age: Int = 0) : Dog() {
    override fun bark() {
        println("   ")
} }
fun main() {
    val dog = Bulldog(age = 2)
    println(dog.age)
    dog.bark()
}
  • override된 함수나 프로퍼티는 기본적으로 open되어 있으므로 하위 클래스에서 오버라이드를 막기 위해선 final을 앞에 붙입니다.
open class Dog {
    open var age: Int = 0

    open fun bark() {
        println("멍멍")
    }
}

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

class ChildBulldog: Bulldog() {
    
    override var age: Int = 0 // 컴파일 오류 
    override fun bark() {} // 컴파일 오류
}

fun main() {
    val dog = Bulldog(age = 2)
    println(dog.age)
    dog.bark()
}
  • 그리고 하위 클래스에서 상위 클래의 함수나 프로퍼티를 접근할 땐 super 키워드를 사용합니다.
  • 실제로 코틀린에서 final 클래스인 점으로 인해 실무에선 몇가지 문제점이 있는데 이후에 이를 해결하는 법을 다뤄보도록 하겠습니다.
abstract class Developer {

    abstract var age: Int
    abstract fun code(language: String)
}

class BackendDeveloper(override var age: Int) : Developer() {
    override fun code(language: String) {
        println("I code with ${language}")
    }
}


fun main() {
    val backendDeveloper = BackendDeveloper(age = 20)
    println(backendDeveloper.age)
    backendDeveloper.code("Kotlin")
}
  • 그리고 코틀린에서는 위와같이 abstract 키워드를 사용해 추상클래스도 제공합니다.
  • 이때 하위 클래스에서 구현해야하는 프로퍼티나 함수 또한 abstract키워드를 사용합니다.

 

코틀린에서의 인터페이스

  • 코틀린의 인터페이스 내부에서는 추상 함수와 자바 8의 디폴트 메소드 처럼 구현을 가진 함수 모두를 정의할 수 있습니다.
  • 클래스에서 인터페이스를 구현할 때는 : 을 붙이고 인터페이스의 이름을 그냥 적어주면 됩니다.
  • 그리고 코틀린의 인터페이스는 프로퍼티가 존재할 수 있습니다.
  • 그리고 코틀린의 인터페이스는 상위 인터페이스를 가질 수 있게 됩니다. 
  • 마지막으로 클래스는 하나 이상의 인터페이스를 구현할 수 있는데, 아래에 Order엔 add가 구현이 있는 디폴트 함수이고 Cart는 abstract함수입니다.
    • 이떄 동일한 시그니처를 가진 함수가 있는 경우 super<인터페이스>를 사용해 호출할 수 있습니다.
  • 그리고 두 인터페이스에 구현을 가진 동일한 디폴트 함수를 사용한다면 하위클래스에서 직접 구현하도록 컴파일 오류가 무조건 발생합니다.
class Product(val name: String, val price: Int)

interface Wheel {
    fun roll()
}

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

    fun printId() {
        println("1234")
    }
}

interface Cart: Wheel {

    var coin: Int

    val weight: String
        get() = "20KG"

    fun add(product: Product)

    fun rent() {
        if (coin > 0) {
            println("카트를 대여합니다.")
        }
    }

    override fun roll() {
        println("카트가 굴러갑니다.")
    }

    fun printId() {
        println("5678")
    }
}

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

        // 주문하기
        super<Order>.add(product)
    }

    override fun printId() {
        super<Order>.printId()
        super<Cart>.printId()
    }
}


fun main() {
    val cart = MyCart(coin = 100)
    cart.rent()
    cart.roll()
    cart.add(Product(name = "장난감", price = 1000))
    cart.printId()
//    val cart2 = MyCart(coin = 0)
//    cart2.rent()
//    cart2.add(Product(name = "장난감", price = 1000))
}

 

코틀린에서의 열거형

  • 코틀린은 서로 연관된 상수의 집합을 enum class를 사용해서 정의할 수 있습니다.
  • enum클래스도 어떻게 보면 클래스이므로 생성자와 프로퍼티를 정의할 수 있습니다.
  • 그리고 enum도 클래스이므로 여기도 인터페이스를 구현할 수 있습니다.
  • valueOf(value: String): String을 사용해서 enum클래스를 생성할 수 있게 됩니다. 
  • 마지막으로 enum클래스의 동등성 비교는 ==를 사용합니다.
enum class PaymentStatus(val label: String): Payable {
    UNPAID("미지급") {
        override fun isPayable(): Boolean = true
                  },
    PAID("지급완료") {
        override fun isPayable(): Boolean = false
                 },
    FAILED("지급실패") {
        override fun isPayable(): Boolean = false
                   },
    REFUNDED("환불") {
        override fun isPayable(): Boolean = false
    };
}

interface Payable {
    fun isPayable(): Boolean
}


fun main() {
//    if (PaymentStatus.UNPAID.isPayable()) {
//        println("결제 가능 상태")
//    }

    val paymentStatus = PaymentStatus.valueOf("PAID")
    println(paymentStatus.label)

    if (paymentStatus == PaymentStatus.PAID) {
        println("결제 완료 상태")
    }
}