kotlin class
-
[Kotlin] 08. 클래스 계층 이해하기2022.11.01
[Kotlin] 08. 클래스 계층 이해하기
클래스 상속 개념을 소개하고 하위 클래스를 정의하는 방법을 설명하며, 추상 클래스, 인터페이스, 클래스 위임, 봉인된 클래스를 통해 클래스 계층 구조를 알아보자.
- 상속과 오버라이딩
- 타입 검사와 캐스트
- 추상 클래스
- 인터페이스
- 봉인된 클래스
- 위임
8.1 상속
자바와 마찬가지로 코틀린 클래스는 단일 상속만을 지원한다.
8.1.1 하위 클래스 선언
클래스를 정의하면서 주생성자 뒤에 콜론(
:
)으로 표기한다.
- 자바의 클래스와 메서드는 default로 상속에 열려있고, 상속을 금지하려면 final을 명시
- 코틀린은 default로 final 이고, 상속을 허용하려면 클래스 앞에 open 지시어 선언
open class Vehicle {
var currentSpeed = 0
fun start() {
println("I'm moving")
}
fun stop() {
println("Stopped")
}
}
open class FlyingVehicle : Vehicle() { // Vehicle는 open 지시어를 통해 상속에 열려 있음
fun takeOff() {
println("Take off")
}
fun land() {
println("Landed")
}
}
class Aircraft(val seats: Int) : FlyingVehicle()
// error: this type is final, so it cannot be inherited from
class Airbus(seats: Int) : Aircraft(seats) // Aircraft는 default final 이기에 상속에 대해 닫혀 있음
하위 클래스 인스턴스는 상위 클래스의 멤버를 모두 상속한다.
val aircraft = Aircraft(100)
val vehicle: Vehicle = aircraft
vehicle.start()
vehicle.stop()
aircraft.start()
aircraft.takeOff()
aircraft.land()
aircraft.stop()
println(aircraft.seats)
data 클래스는 항상 final 이기에 open 선언이 불가하다.
// error: modifier 'open' is incompatible with 'data'
open data class Person(val name: String, val age: Int)
인라인 클래스는 상속 및 상위 클래스 역할 불가하다.
class MyBase
// error: inline classes can be only final
open value class MyString(val value: String)
// error: inline class cannot extend classes
value class MyStringInherited(val value: String): MyBase()
객체(동반 객체 포함)는 클래스 상속이 가능하다.
open class Person(val name: String, val age: Int) {
companion object : Person("Unknown", 0)
}
object JohnDoe : Person("John Doe", 30)
상위 클래스의 open으로 지정된 멤버를 하위 클래스에서 오버라이드 가능하고,
자바와 차이점은 구현하는 메서드 앞에 override 지시어를 붙여야 함
open class Vehicle2 {
open fun start() {
println("I’m moving")
}
fun stop() { // final 이기에 상속만 가능하고 하위 메서드에서 오버라이드 불가
println("Stopped")
}
}
class Car : Vehicle2() {
override fun start() {
println("I'm riding")
}
}
class Boat : Vehicle2() {
override fun start() {
println("I'm sailing")
}
}
오버라이드하는 멤버를 final로 선언하면 더 이상 하위 클래스에서 오버라이드 불가하다.
open class Vehicle6 {
open fun start() {
println("I’m moving")
}
}
open class Car5 : Vehicle6() {
final override fun start() {
println("I'm riding a car")
}
}
// Car5 클래스에서 start 멤버 함수를 final로 선언했기 때문에 Car5클래스를 오버라이드할 수 없음
class Bus : Car5() {
// error: 'start' in 'Car' is final and cannot be overridden
override fun start() {
println("I'm riding a bus")
}
}
프로퍼티 오버라이드는 본문 구현 외에 주생성자 파라미터 선언으로 오버라이드 가능하다.
open class Entity {
open val name: String get() = ""
}
class Person2(override val name: String) : Entity()
불변 프로퍼티를 가변 프로퍼티로 오바라이드할 수 있다.
(단, 꼭 필요한 경우가 아니라면 상위 클래스의 선언한 값을 그대로 사용)
open class Entity2 {
open val name: String get() = ""
}
class Person3() : Entity2() {
override var name: String = "" // val -> var
}
자바와 마찬가지로 코틀린도 멤버의 영역을 하위 클래스의 영역으로만 제한하는 특별한 접근 변경자(protected)를 제공한다.
- Java: 동일한 패키지 안에서 속한 코드에서 멤버 접근을 허용
- Kotlin: 동일 클래스 및 그 클래스를 상속한 하위 클래스에서만 멤버 접근이 허용
open class Vehicle7 {
protected open fun onStart() { }
fun start() {
println("Starting up...")
onStart()
}
}
class Car6 : Vehicle7() {
override fun onStart() {
println("It's a car")
}
}
fun main3() {
val car = Car6()
car.start() // Ok
// error: cannot access 'onStart': it is protected in 'Car'
// car.onStart() 상속한 하위 클래스에서만 접근 가능
}
함수나 프로퍼티를 오버라이드한 상위 클래스를 재사용해야 하는 경우 super 키워드를 사용한다.
open class Vehicle {
open fun start(): String? = "I'm moving"
}
open class Car : Vehicle() {
override fun start() = super.start() + " in a car"
}
fun main() {
println(Car().start())
}
// I'm moving in a car
8.1.2 하위 클래스 초기화
특정 클래스의 인스턴스 생성 시 자신의 상위 클래스 생성자를 호출하며 최상위 클래스에 이를 때까지 연쇄적으로 일어나고, 최상위클래스로 부터 하위 클래스 순서로 초기화가 진행된다.
open class Vehicle {
init {
println("Initializing Vehicle")
}
}
open class Car : Vehicle() {
init {
println("Initializing Car")
}
}
class Truck : Car() {
init {
println("Initializing Truck")
}
}
fun main() {
Truck()
}
// result
Initializing Vehicle
Initializing Car
Initializing Truck
위임 호출(delegating call)을 사용하면 인자를 상위 클래스의 일반생성자 및 주,부생성자에게 전달할 수 있다.
// 일반생성자에 전달
open class Person(val name: String, val age: Int)
class Student(name: String, age: Int, val university: String) :
Person(name, age)
fun main() {
Student("Euan Reynolds", 25, "MIT")
}
// 주, 부생성자에 전달
open class Person {
val name: String
val age: Int
constructor(name: String, age: Int) {
this.name = name
this.age = age
}
}
class Student(name: String, age: Int, val university: String) :
Person(name, age)
Student 클래스에서 부생성자를 사용할 경우 위임 호출할 클래스를 생성자 뒤에
( )
괄호없이 작성한다.
open class Person(val name: String, val age: Int)
class Student : Person {
val university: String
constructor(name: String, age: Int, university: String) :
super(name, age) { // super 키워드는 부생성자가 상위 클래스의 생성자를 위임 호출하는것을 컴파일러에게 전달
this.university = university
}
}
상위 클래스의 초기화 코드가 호출되고 하위 클래스에서 오버라이드한 함수가 아직 초기화되지 않은 상태일 경우 this 누출(leaking this) 문제가 발생할 수 있다.
open class Person10(val name: String, val age: Int) {
open fun showInfo() {
println("$name, $age")
}
init {
showInfo() // 잠재적인 위험 요소
}
}
class Student7(
name: String,
age: Int,
val university: String
) : Person10(name, age) {
override fun showInfo() {
println("$name, $age (student at $university)")
}
}
fun main() {
// Euan Reynolds, 25 (student at null)
Student7("Euan Reynolds", 25, "MIT")
}
8.1.3 타입 검사와 캐스팅
코틀린의 타입 검사와 캐스팅 연산에는 is를 사용한다.
val objects = arrayOf("1", 2, "3", 4)
// objects 는 Any 로 이뤄진 배열에 원소에 String or Int 연산을 바로 할 수 없음
for (obj in objects) {
println(obj*2) // error: unresolved reference
}
for (obj in objects) {
println(obj is Int) // is 연산자를 통해 타입 검사가 가능하다
}
// result
false
true
false
true
null값에 대한 is 연산 처리
- Java instanceof 와 유사하고 차이점은 null 에 대해 항상 false
- 연산자 오른쪽에 널이 될 수 있는지 여부에 따라 다르다(스마트 캐스트 지원)
//null이 될 수 없는 타입
println(null is Int) // false
//null이 될 수 있는 타입
println(null is String?) // true
!is 연산자는 is와 반대인 연산을 제공한다.
val o: Any = ""
println(o !is Int) // true
println(o !is String) // false
값의 타입을 자동으로 세분화해서 Null이 될 수 없는 타입으로 바꿔준다.
val objects2 = arrayOf("1", 2, "3", 4)
var sum = 0
for (obj in objects2) {
if (obj is Int) {
sum += obj // 여기서는 obj의 타입을 `Int`로 세분화한다
}
}
println(sum) // 6
is/!is 검사를 in/!in처럼 특별한 조건으로 사용할 수 있는 식 내부에서도 스마트 캐스트가 지원된다.
- 컴파일러는 검사 시점과 사용 시점 사이에 변수가 변경되지 않는다고 확신할 수 있을 때만 스마트 캐스트를 허용한다.
val objects3 = arrayOf("1", 2, "3", 4)
var sum2 = 0
for (obj in objects3) {
when (obj) {
is Int -> sum2 += obj // 여기서 obj는 Int 타입이다
is String -> sum2 += obj.toInt() // 여기서 obj는 String 타입이다
}
}
println(sum2) // 10
- 프로퍼티나 커스텀 게터가 정의된 변수에는 스마트 캐스트 사용이 불가하다. (값이 변경될 수 있다고 예측될 때)
class Holder {
val o: Any get() = "" // 게터 통해서 값이 변경될 수 있음
}
fun main9() {
val o: Any by lazy { 123 }
if (o is Int) {
println(o*2) // error: smart cast to 'Int' is impossible
}
val holder = Holder()
if (holder.o is String) {
println(holder.o.length) // error: smart cast to 'String' is impossible
}
}
- 가변 지역 변수의 경우 런타임 시점에 변수가 변경될지 예측할 수 없을 경우 스마트 캐스트 지원이 불가하다.
fun main() {
var o: Any = 123 // 위임이 없는 불변 지역변수는 가능
if (o is Int) {
println(o + 1) // Ok: Int로 스마트 캐스트
o = ""
println(o.length) // Ok: String으로 스마트 캐스트
}
if (o is String) {
val f = { o = 123 } // 람다 안에서 변수를 변경할 경우 지원 불가
// println(o.length) // error: smart cast to 'String' is impossible
}
}
- 스마트 캐스트를 쓸 수 없는 경우 (as와 as?)명시적인 연산자를 사용해 값의 타입을 강제로 변환할 수 있다.
val o: Any = 123
println((o as Int) + 1) // 124
println((o as? Int)!! + 1) // 124
println((o as? String ?: "").length) // 0 변환하려는 타입과 일치하지 않치만 안전한 as?
//println((o as String).length) // java.lang.ClassCastException 실제 타입과 일치하지 않아 예외
8.1.4 공통 메서드
Kotlin.Any 클래스는 코틀린 클래스 계층 구조의 루트로 모든 클래스는 직간적접으로 상속하고 기본 연산자를 제공한다.
open class Any {
public open operator fun equals(other: Any?): Boolean // 구조적 동등성 (== 나 !=)
public open fun hashCode(): Int // HashSet, HashMap
public open fun toString(): String // String 변환
}
디폴트로 정의된 모든 클래스에는 Any에서 상속받은 참조 동등성만 구현하기에 두 인스턴스가 같은 객체로 간주되지 않는다.
fun main() {
val addresses = arrayOf(
Address("London", "Ivy Lane", "8A"),
Address("New York", "Kingsway West", "11/B"),
Address("Sydney", "North Road", "129")
)
// -1
println(addresses.indexOf(Address("Sydney", "North Road", "129")))
}
equal() 메서드를 오버라이드하여 값 비교 (동등성) 문제를 해결할 수 있다.
class Address(
val city: String,
val street: String,
val house: String
) {
// Address에 정의한 equals 함수
override fun equals(other: Any?): Boolean {
if (other !is Address) return false
return city == other.city &&
street == other.street &&
house == other.house
}
}
fun main() {
// 2
println(addresses.indexOf(Address("Sydney", "North Road", "129")))
}
일반 값에 대한 동등성 비교는 ==와 !=, 참조 동등성은 === 사용하여 비교한다.
- 코틀린에서 ==는 내부적으로 equals 호출 (=자바 equals)
- 자바 주소 값 비교인 == 와 코틀린의 === 동일한 역할
val addr1 = Address2("London", "Ivy Lane", "8A")
val addr2 = addr1 // 같은 인스턴스
val addr3 = Address2("London", "Ivy Lane", "8A") // 다른 인스턴스지만, 동등함
println(addr1 === addr2) // true
println(addr1 == addr2) // true
println(addr1 === addr3) // false
프로퍼티는 각자의 equals()와 hashCode() 오버라이드하여 해시와 동등성을 계산 할 수 있다.(배열은 타입은 예외)
class Address(
val city: String,
val street: String,
val house: String
) {
// Address에 정의한 equals 함수
override fun equals(other: Any?): Boolean {
if (other !is Address2) return false
return city == other.city &&
street == other.street &&
house == other.house
}
// 위 equals와 호환이 되는 해시코드 정의
override fun hashCode(): Int {
var result = city.hashCode()
result = 31 * result + street.hashCode()
result = 31 * result + house.hashCode()
return result
}
}
fun main() {
val addr1 = Address("London", "Ivy Lane", "8A")
val addr3 = Address("London", "Ivy Lane", "8A")
// true
println(addr1 == addr3)
}
8.2 추상 클래스와 인터페이스
8.2.1 추상 클래스와 추상 멤버
자바와 동일하게 추상 클래스를 지원하고, abstract라는 키워드를 붙여야 한다.
abstract class Entity(val name: String)
// Ok: 하위 클래스에서 위임 호출
class Person(name: String, val age: Int) : Entity(name)
// 인스턴스 생성 불가하고 부모 클래스로만 사용
// error: cannot create an instance of an abstract class
val entity = Entity("Unknown")
추상 클래스는 추상 멤버를 정의할 수 있다.
import kotlin.math.PI
abstract class Shape {
abstract val width: Double
abstract val height: Double
abstract fun area(): Double
}
class Circle(val radius: Double) : Shape() {
val diameter get() = 2*radius
override val width get() = diameter // 추상 클래스 멤버를 오버라이드해서 구현
override val height get() = diameter
override fun area() = PI*radius*radius
}
class Rectangle(
override val width: Double,
override val height: Double
) : Shape() {
override fun area() = width*height
}
fun Shape.print() {
println("Bounds: $width*$height, area: ${area()}")
}
fun main() {
// Bounds: 20.0*20.0, area: 314.1592653589793
Circle(10.0).print()
// Bounds: 3.0*5.0, area: 15.0
Rectangle(3.0, 5.0).print()
}
추상 클래스 제약사항은 다음과 같다.
- 추상 프로퍼티를 초기화할 수 없고 명시적인 접근자나 by 절을 추가할 수 없다.
- 추상 함수에는 본문이 없어야 한다.
- 추상 프로퍼티와 함수 모두 명시적으로 반환 타입을 적어야 한다.
- 추상 멤버는 암시적으로 열려 있어 open 생략 가능하다.
8.2.2 인터페이스
코틀린 인터페이스 개념은 자바의 인터페이스와 비슷하고,자바 8에 디폴트 메서드가 도입된 이후로 더 많이 비슷해 졌다.
- default 키워드 없이 메서드 구현
- 프로퍼티, 함수 구현 추가 가능
interface Vehicle9 {
val currentSpeed: Int // 디펄트가 추상 멤버이고, abstract 지시어는 생략 가능
fun move()
fun stop()
}
인터페이스는 클래스나 다른 인터페이스의 상위 타입이 될 수 있다.
- 코틀린에서는 모든 상속(클래스, 인터페이스)을 똑같은 기호 ( : )를 사용해 표시한다.
interface FlyingVehicle2 : Vehicle9 {
val currentHeight: Int
fun takeOff()
fun land()
}
class Car8 : Vehicle9 {
override var currentSpeed = 0 // Vehicle9에 추상 멤버에 대한 구현을 제공해야 한다
private set
override fun move() {
println("Riding...")
currentSpeed = 50
}
override fun stop() {
println("Stopped")
currentSpeed = 0
}
}
class Aircraft2 : FlyingVehicle2 {
override var currentSpeed = 0 // FlyingVehicle2, Vehicle9에 추상 멤버에 대한 구현을 제공해야 한다
private set
override var currentHeight = 0
private set
override fun move() {
println("Taxiing...")
currentSpeed = 50
}
override fun stop() {
println("Stopped")
currentSpeed = 0
}
override fun takeOff() {
println("Taking off...")
currentSpeed = 500
currentHeight = 5000
}
override fun land() {
println("Landed")
currentSpeed = 50
currentHeight = 0
}
}
인터페이스 안의 함수와 프로퍼티에 구현을 추가할 수도 있다.
interface Vehicle10 {
val currentSpeed: Int
val isMoving get() = currentSpeed != 0
fun move() // 암시적으로 열려 있고, final로 정의하면 컴파일 오류 발생
// error: modifier 'final' is not applicable inside 'interface'
final fun move() {}
fun stop()
fun report() {
println(if (isMoving) "Moving at $currentSpeed" else "Still")
}
}
인터페이스 내부에는 초기화 코드나 위임이 붙은 프로퍼티는 금지된다.
interface Vehicle13 {
// error: property initializers are not allowed in interfaces
val currentSpeed = 0
// error: delegated properties are not allowed in interfaces
val maxSpeed by lazy { 100 }
}
추상 클래스와 달리 인터페이스에 대한 생성자는 금지된다.
// error: property initializers are not allowed in interface
interface Person(val name: String)
interface Vehicle {
// error: delegated properties are not allowed in interface
constructor(name: String)
}
한 타입이 동일한 시그니처를 가지는 멤버가 들어있는 다른 인터페이스를 둘 이상 상속할 때는 super<상위타입>.메서드 호출을 사용한다.
interface Car11 {
fun move()
}
interface Ship2 {
fun move()
}
class Amphibia : Car11, Ship2 {
override fun move() {
println("I'm moving")
}
}
interface Car12 {
fun move(){
println("I'm riding")
}
}
interface Ship3 {
fun move()
}
class Amphibia2 : Car12, Ship3 {
override fun move() {
super.move() // Car에서 상속받은 메서드를 호출하는 문제가 생김
super<Car11>.move() // Car11에서 상속받은 메서드를 호출
super<Ship2>.move()
}
}
fun main() {
Amphibia2().move() // I'm riding
}
8.2.3 봉인된 클래스와 인터페이스
Enum 클래스는 미리 정의된 상수 집합을 표현하여 이외의 값이 들어오는걸 막을 수 있다.
enum class Result {
SUCCESS, ERROR
}
fun runComputation(): Result {
try {
println("Input a int:")
val a = readLine()?.toInt() ?: return Result.ERROR
println("Input a int:")
val b = readLine()?.toInt() ?: return Result.ERROR
println("Sum: ${a + b}")
return Result.SUCCESS
} catch (e: NumberFormatException) {
return Result.ERROR
}
}
fun main() {
val message = when (runComputation()) { // when식을 통해 미리 정의된 상수 처리 및 이외의 값이 들어올 수 없는 것을 컴파일 단에서 감지
Result.SUCCESS -> "Completed successfully"
Result.ERROR -> "Error!"
}
println(message)
}
하지만 특정 케이스의 경우 각 종류별로 애트리뷰트가 다를 수 있어, 추상 클래스를 사용하여 성공 및 하위 클래스의 특정 케이스를 처리할 수 있지만, Enum 처럼 type의 제한을 할 수 없다.
abstract class Result2 {
class Success(val value: Any) : Result2() {
fun showResult() {
println(value)
}
}
class Error(val message: String) : Result2() {
fun throwException() {
throw Exception(message) // type 을 다양하게 처리 가능함
}
}
}
fun runComputation2(): Result2 {
try {
println("Input a int:")
val a = readLine()?.toInt()
?: return Result2.Error("Missing first argument")
println("Input a int:")
val b = readLine()?.toInt()
?: return Result2.Error("Missing second argument")
return Result2.Success(a + b)
} catch (e: NumberFormatException) {
return Result2.Error(e.message ?: "Invalid input")
}
}
fun main() {
val message = when (val result = runComputation2()) {
is Result2.Success -> "Completed successfully: ${result.value}"
is Result2.Error -> "Error: ${result.message}"
else -> return // result 결과를 제한할 수 없어 else 절이 필요함
}
println(message)
}
코틀린은 (sealed)봉인된 클래스나 인터페이스(코틀린 1.5 이상)를 통해 이런 문제를 극복할 수 있다.
- 상속하는 클래스는 내포된 클래스 or 객체로 정의, 같은 파일 내에 최상위 클래스로 정의돼야만 한다.
- 이 밖에 영역에서는 봉인된 클래스 상속 불가하다. (final 클래스와 동일 효과)
- 직접 인스턴스를 만들 수 없어 기본적으로 추상 클래스 이다.
sealed class Result { // Result 클래스는 Success 와 Error로 제한했다
class Success(val value: Any) : Result() {...}
class Error(val message: String) : Result() {...}
}
Enum과 마찬가지로 불필요한 else를 사용 안해도 되는 빠진 부분이 없는 when 형태를 지원한다.
// compile 시점에 Result의 하위 클래스는 Success와 Error만 존재한다고 인식
val message = when (val result = runComputation()) {
is Result.Success -> "Completed successfully: ${result.value}"
is Result.Error -> "Error: ${result.message}"
}
// 봉인된 클래스 확장시 when 구문에 실수로 코드를 누락하는일이 발생하지 않는다
class Confirm(val message: String) : Result() {...}
상속 제한은 봉인된 클래스를 직접 상속한 클래스에 대해서만 성립한다.
// Result.kt
sealed class Result { // Result 클래스는 Success 와 Error로 제한했다
class Success(val value: Any) : Result()
open class Error(val message: String) : Result()
}
// Util.kt
class FatalError(message: String): Result.Error(message)
데이터 클래스가 봉인된 클래스 계층에 속할 수도 있다.
sealed class Expr
data class Const(val num: Int): Expr() // 봉인된 클래스의 하위 계층으로 사용
data class Neg(val operand: Expr): Expr()
봉인된 클래스를 객체로 구현할 수도 있다.
sealed class Result {
object Completed: Result()
class ValueProduced(val value: Any) : Result()
class Error(val message: String) : Result()
}
8.2.4 위임
기존 클래스의 확장이나 변경이 어려울 경우 위임 패턴을 사용할 수 있다.
interface PersonData {
val name: String
val age: Int
}
open class Person17(
override val name: String,
override val age: Int
): PersonData
data class Book(val title: String, val author: PersonData) {
override fun toString() = "'$title' by ${author.name}"
}
fun main() {
val valWatts = Person17("Val Watts", 30)
val introKotlin = Book("Introduction to Kotlin", valWatts)
println(introKotlin) // 'Introduction to Kotlin' by Val Watts
}
// 메서드나 프로퍼티를 다른 객체에 전달하기 위해 작성해야하는 준비 코드가 너무 많은 것이 단점
class Alias(
private val realIdentity: PersonData,
private val newIdentity: PersonData
) :PersonData {
override val name: String // 인터페이스의 프로퍼티를 오버라이드한 커스텀 게터 작성
get() = newIdentity.name
override val age: Int
get() = newIdentity.age
}
fun main21() {
val valWatts = Person17("Val Watts", 30)
val johnDoe = Alias(valWatts, Person17("John Doe", 25))
val introJava = Book("Introduction to Java", johnDoe)
println(introJava) // 'Introduction to Java' by John Doe
}
코틀린은 위와 같은 위임을 처리하는 기능을 내장하고 있다.
- 상위 인터페이스 이름 바로 뒤 (by) 키워드 작성 후 위임할 인스턴스를 작성한다
class Alias(
private val realIdentity: PersonData,
private val newIdentity: PersonData
): PersonData by newIdentity // newIdentity 인스턴스에 있는 이름으로 메서드가 구현된다
구현을 바꾸고 싶은 경우 직접 멤버를 오버라이드할 수 있다.
class Alias(
private val realIdentity: PersonData,
private val newIdentity: PersonData
): PersonData by newIdentity {
override val age: Int get() = realIdentity.age
}
클래스는 인터페이스 멤버를 구현할 때만 위임을 쓸 수 있다.
open class Person(
override val name: String,
override val age: Int
)
class Alias(
private val realIdentity: PersonData,
private val newIdentity: PersonData
): Person by newIdentity // error: only interface can be delegated to // 클래스는 위임 불가
클래스 위임은 번거로운 준비 코드없이 객체 합성과 상속의 이점을 살릴 수 있다.
'Programming > Kotlin' 카테고리의 다른 글
[Kotlin] Spring Boot와 Armeria를 이용한 gRPC Server 톺아 보기 (0) | 2023.12.12 |
---|---|
[Kotlin] ERROR: Query failed with error code 2 and error message ‘Field ‘locale’ is invalid. (3) | 2023.03.27 |
[Kotlin] Kotlin + Spring Webflux + gRPC 환경 구성 (0) | 2023.03.24 |
[Kotlin] ERROR: Only the Kotlin standard library is allowed to use the 'kotlin' package (0) | 2023.03.22 |
[Kotlin] 13. Kotlin에서 Coroutine이란? (0) | 2022.12.10 |