dev-course day59

2rlokr·2025년 5월 29일

dev-course

목록 보기
40/43
post-thumbnail

오늘 배운 것

실습

case 1 : 구현하는 인터페이스가 같은 이름의 메서드를 가질 때

interface DiInterfaceA {
    fun print() {
        println("A: 기본적으로 구현되어있는 내용입니다!")
    }
}

interface DiInterfaceB {
    fun print() {
        println("B: 기본적으로 구현되어있는 내용입니다!")
    }
}

class DiImpl2 : DiInterfaceA, DiInterfaceB {
    override fun print() {
        println("D: 직접 구현했습니다!")
    }
}

class DiImpl3 : DiInterfaceA, DiInterfaceB {
    override fun print() {

        super<DiInterfaceA>.print()
        super<DiInterfaceB>.print()

        println("D: 직접 구현했습니다!")
    }
}
  • 구현하는 클래스에서 print() 메서드를 재정의해준다.
  • super 키워드를 사용할 때 어떤 인터페이스의 메서드를 사용하는 것인지 <>로 명시해준다.

case 2: Companion object

companion

  • 클래스 내부에 정적인 멤버를 정의할 때 사용한다.
  • companion object는 한 쌍처럼 작동하며, 클래스 내부에 단 하나만 선언할 수 있습니다.

object

  • 싱글톤 객체를 만들 때 사용하는 키워드이다.

companion object

class HelloRobot {

    companion object {
        fun hello() {
            println("Hello! Hello! Hello!")
        }
    }

}

fun main() {
	HelloRobot.hello()
}
  • 정적으로 접근할 수 있는 메서드가 생긴 것이다.
  • 자바 코드에서도 사용할 수 있게 된다.
public static void main(String[] args) {
        HelloRobot.Companion.hello();
  • 다만, Companion을 붙여줘야 가능하다.
class ByeRobot {

    companion object {
        @JvmStatic
        fun bye() {
            println("Bye! Bye! Bye!")
        }
    }
}
  • 위와 같이 @JvmStatic을 붙여주면, Kotlin에서 정의된 정적(static) 멤버를 Java에서 자연스럽게 사용할 수 있다.
public static void main(String[] args) {
	ByeRobot.bye();
}

case 3: data class

  • 데이터를 담기 위한 용도로 설계된 클래스로, 컴파일러가 equals(), hashCode(), toString(), copy(), componentN() 등을 자동으로 생성해준다.

⚠️ data class 제약사항

  1. primary constructor(주 생성자)에는 최소 하나 이상의 val 또는 var 프로퍼티가 있어야 함
  2. abstract, open, sealed, inner는 사용할 수 없음
  3. 상속은 안 됨 (final class)
data class MemberLabel(var name: String, val code : String)

구조 분해

val (member1Name, member1Code) = member1

println("member1Name = ${member1Name}")
println("member1Name = ${member1Code}")

val (member2Name, _) = member2
val (_, member2Code) = member2

println("member2Name = ${member2Name}")
println("member2Code = ${member2Code}")

val member3Name = member3.component1()

println("member3Name = ${member3Name}")
  • data class의 값들을 얻기 위해서는 위와 같이 접근해야 한다.
  • 일반적인, var member1Name = member1.name은 사용할 수 없다.

case 4 : object

object 는 단 하나의 인스턴스만 존재하는 싱글통 객체가 만들어진다.

  • 생성자를 만들 수 없다.
object Logger {
    fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
}

object Config {
    val appName: String
    val version : String

    init {
        println("Config initialized")
        appName = "MyApp"
        version = "1.0"
    }
}

fun main() {
	Logger.log("Hello")

    Config.appName
}
  • Kotlin에서 object 키워드를 사용하면 기본적으로 lazy(지연) 초기화된다.

case 5 : 예외

runCatching

  • 예외 발생 여부에 따라 Result 객체로 감싸준다.
    → 함수형 스타일 예외 처리 가능
  • 후행 람다식이며 변수에 값을 담아줄 수 있다.
runCatching {
    10/0
}.onSuccess {
    println("나눗셈한 결과 $it")
}.onFailure {
    println("오류가 발생했습니다!")
    println("${it.message}")
}
  • 예외 발생 여부에 따라 Result.success / Result.failure로 나눠 받는다.
  • 체이닝 (onSuccess, onFailure) 사용 가능
  • 코드 간결함, 함수형 스타일

제네릭

클래스나 함수, 인터페이스를 정의할 때 데이터 타입을 일반화(Generalization)하는 것이다. 컴파일 타임에 타입을 지정하게 하기 때문에 타입 안정성을 확보할 수 있다.

제네릭 클래스

클래스 정의 시 타입 매개변수를 사용하여 다양한 데이터 타입을 처리할 수 있는 클래스를 의미한다.

fun <문자> 함수이름(매개변수:문자,[..]) {

}

class 클래스명<문자> {

}

타입 추론

  • Kotlin 컴파일러는 제네릭 객체를 생성하는 시점에 매개변수를 통하여 해당 변수의 자료형을 추론할 수 있다.
fun <T> func(param:T) {

}

fun main() {
	func<String>("100")
    func(100)
}
  • 자료형을 특정하지 않더라도 컴파일 시점에 컴파일러가 타입을 추론할 수 있을 때는 <>을 생략할 수 있다.

제네릭 함수

제네릭 함수 (Generic Function)는 함수 정의 시 데이터 타입을 일반화하여 여러 타입에 대해 동일한 로직을 적용할 수 있도록 하는 기능이다. 제네릭 함수는 함수나 메서드 앞쪽에 형식 매개변수를 지정하는 방식으로 사용한다.

fun <타입문자열[, ...]> 함수 이름(매개변수 : [<타입문자열[,...]>]): [<타입문자열>]

fun <T> printList(target: List<T>) : Unit {
    for (i: T in target) {
        print("$i ")
    }
}

제네릭 타입제한

Kotlin의 제네릭 타입 제한 (Type Bounds)은 제네릭 타입에 사용할 수 있는 타입을 제한하는 기능이다. 이 개념은 타입의 안전성(Type Safety)을 유지하면서, 특정 메서드나 클래스가 사용할 수 있는 기능(메서드)을 보장하기 위해 사용된다.

자료형 제한

class Container<T: Number>

fun main() {
	val intContainer: Container<Int> = Container()
    val doubleContainer: Container<Double> = Container()
    
    val stringContainer: Container<String> = Container()
    // Number로 타입이 제한되어있기 때문에 String을 매개변수로 가질 수 없다.
}
  • 형식 매개변수를 : 를 이용해서 제네릭 타입의 상한을 지정할 수 있다.
class GeDog: GeAnimal("Dog")
class GeCat: GeAnimal("Cat")

class GeCar

fun <T : GeAnimal> feedToAnimal(animals: List<T>, sth: String): Unit {
    for (animal in animals) {
        animal.eat(sth)
    }
}

fun main() {
    val animals = listOf(GeDog(), GeCat())
    val cars = listOf(GeCar(), GeCar())

    feedToAnimal(animals, "간식")
//    feedToAnimal(cars, "휘발유") // 타입 제한으로 사용할 수 없음
}
  • 함수를 선언할 때 형식 매개변수를 제한하는 경우에는 위와 동일하게 :를 사용하면 된다.

다중 타입 제한

interface A
interface B
interface C

class AbImpl : A, B
class AbcImpl : A, B, C

class AImpl : A
class BImpl : B

class Container<T> where T : A, T : B

fun main() {

    Container<AbImpl>() // 생성 가능
    Container<AbcImpl>() // 생성 가능
    
//    Container<AImpl>() // 생성 불가능
//    Container<BImpl>() // 생성 불가능
    
}
  • 여러 타입으로 제한하고 싶다면 where 키워드를 통해 제한할 수 있다.

가변성 (Variance)

가변성은 형식 매개변수가 클래스 계층에 영향을 주는 것을 의미한다. 가변성은 제네릭 타입 A와 B가 있을 때, A가 B의 하위 타입이라면 Class<A>Class<B>의 하위타입이어야 할지 아닐지를 결정하기 위한 규칙이라고 볼 수 있다.

open class Animal

class Cat : Animal()
class Dog : Animal()

val cats : List<Cat> = listOf(Cat(), Cat())
val animals: List<Animal> = cats // 불가능
  • CatAnimal의 하위 타입이지만, List<Cat>List<Animal>의 하위 타입이 아니기 때문에 animalscats에 다시 할당하는 것이 불가능하다.
    이러한 내용을 불변성(Invariant)라고 한다.
  • Kotlin에서 제네릭 타입은 기본적으로 불변성을 가진다.

공변성

제네릭 타입 A, B가 있는데 A가 B의 하위 타입일 때, 제네릭 타입이나 함수 등의 특정 형식에서 A를 B처럼 안전하게 사용할 수 있는 상황을 공변성이라고 한다. 즉, 공변성은 타입간의 상속관계가 어떤 컨텍스트에서도 그대로 보존되는 성질을 의미한다.

하지만, 공변성을 아무 조건없이 허용하게 되면 오류가 발생할 수 있다.

open class Animal
class Cat : Animal()
class Dog : Animal()

class Box<T>(private var item: T) {
    fun get(): T = item
    fun set(value: T) { item = value }
}

fun feedAnimal(box: Box<Animal>) {
    // ...
}

val catBox: Box<Cat> = Box(Cat())
// feedAnimal(catBox) // ❌ 오류! 타입 불일치
  • Box<Cat>Box<Animal>의 하위 타입이 아니다.
  • Box는 invariant (불변) → 타입 정확히 일치해야 한다.

그렇기 때문에, 명시적으로 읽기 전용으로만 쓸 것이라고 표기하고 값을 받을 수 있도록 한다면 A를 B처럼 안전하게 사용할 수 있는 상황이 된다.

class ReadOnlyBox<out T>(private val item: T) {
    fun get(): T = item
    // fun set(value: T) { item = value } // ❌ 컴파일 오류: out 사용 시 set 불가
}

fun readAnimal(box: ReadOnlyBox<Animal>) {
    println(box.get())
}

val catBox = ReadOnlyBox(Cat()) // 생성자에서 값 넣는 건 OK
val animalBox: ReadOnlyBox<Animal> = catBox // 공변성 덕분에 업캐스팅 OK
readAnimal(catBox) // ✅ OK! ReadOnlyBox<Cat> is a subtype of 
val animal: Animal = animalBox.get() // 값 꺼내는 건 OK
  • 공변성을 위해 out을 사용할 수 있다.
  • 생성 시 값을 세팅하는 건 안전하다고 간주되기 때문에 생성자로는 값을 전달할 수 있다.

반공변성

반공변성은 공변성과 반대의 개념으로, 타입 상속관계가 특정 컨텍스트에서 반대방향으로 적용되는 성질을 의미한다. 반공변성은 쓰기만 하는 객체를 다룰 때 안전하게 타입을 일반화할 수 있다.

open class Animal {
    fun feed() = println("Feeding animal")
}
class Cat : Animal()
class Dog : Animal()

class AnimalFeeder<in T> {
    fun feed(animal: T) {
        println("Feeding one ${animal::class.simpleName}")
    }
}

val catFeeder: AnimalFeeder<Cat> = AnimalFeeder<Animal>() // ✅ 가능!
catFeeder.feed(Cat()) // ✅ Cat을 받아서 먹일 수 있음

val animalFeeder: AnimalFeeder<Animal> = AnimalFeeder<Cat>() // ❌ 불가!
  • 반공변성을 위해 in를 사용한다.
  • 목적은 "넣을 수는 있지만 꺼낼 수는 없게" 해서 타입 안전성 확보하는 것이다.
  • T의 하위 타입에서도 안전하게 사용할 수 있도록 해준다.

자료형 프로젝션

특정 자료형에 in 또는 out을 지정하여 제한하는 것을 자료형 프로젝션(Type Projection)이라고 한다.

선언 지점 변성

Kotlin은 가변성을 설정할 때 클래스를 정의하는 시점, 즉, 타입 파라미터를 선언하는 그 순간에 변성 방향성을 명시할 수 있는데 이를 선언 지점 변성(Declaration-Site Variance)이라 한다.

// T를 out으로 선언하여 공변성 부여
class ReadOnlyBox<out T>(private val value: T) {
    fun get(): T = value
    // fun set(newValue: T) { value = newValue } // out은 값을 넣을 수 없음
}

open class Animal
class Dog : Animal()
class Cat : Animal()

fun main() {
    val dogBox: ReadOnlyBox<Dog> = ReadOnlyBox(Dog())

    // 공변성 덕분에 Dog는 Animal의 하위 타입이므로 대입 가능
    val animalBox: ReadOnlyBox<Animal> = dogBox

    val animal = animalBox.get() // 리턴은 Animal로 추론
    println(animal is Dog) // true
}
  • 클래스 선언 시 클래스 자체에 가변성을 지정하는 방식으로 in 또는 out을 지정하는 것이다.

사용자 지점 변성(Use-Site Variance)

이미 정의된 제네릭 타입을 사용하는 입장에서, 즉, 함수나 변수에 전달할 때 가변성을 지정할 때 사용하는 방식을 말한다. 사용자 지점 변성은 제네릭 타입을 사용하는 그 순간에 해당 타입의 방향성(입력만 가능한지, 출력만 가능한지)를 지정해주는 방식을 의미한다.

open class Animal {
    fun eat() = println("밥을 먹습니다!")
}
class Dog : Animal() {
    fun bark() = println("멍!")
}

fun copyAnimals(from: List<out Animal>) {
    for (animal in from) {
        animal.eat()
        // animal.bark() // Animal로 인식되므로 bark 사용 불가
    }
}

fun main() {
    val dogs: List<Dog> = listOf(Dog(), Dog())
    copyAnimals(dogs) // Dog 리스트를 Animal 리스트처럼 사용

    val animals: List<Animal> = listOf(Animal(), Dog())
    copyAnimals(animals) // Animal 리스트도 가능
}
  • dogs 리스트가 copyAnimals 함수의 인자로 전달된다 하더라도 Animal 타입을 받고 있기 때문에 animal.bark() 함수를 사용할 수 없다.
open class Animal {
    fun eat() = println("밥을 먹습니다!")
}

class Dog : Animal() {
    fun bark() = println("멍멍!")
}

class Feeder<T> {
    fun feed(animal: T) {
        println("먹이를 줍니다.")
        animal.eat()
    }
}

fun feedDogs(feeder: Feeder<in Dog>) {
    feeder.feed(Dog()) // Dog 넣기 가능
}

fun main() {
    val animalFeeder = Feeder<Animal>()
    feedDogs(animalFeeder) // Feeder<Animal>을 Feeder<Dog> 대신 사용 가능 (in 덕분)
}

스타 프로젝션

스타 프로젝션 *제네릭 타입 매개변수의 구체적인 타입을 알 수 없거나, 여러 가지 타입 중 하나를 허용할 필요가 있을 때 사용된다.

fun printEl(el: List<*>) {
    for ( item: Any? in el ) {
        print("$el ")
    }
}
  • *는 타입 매개변수에 대한 제한이 없는 경우에 사용된다. 이 경우, 어떤 타입이든 상관없이 사용할 수 있다.
  • 타입을 모를 때, 하지만 타입 안정성은 유지하면서 쓰고 싶을 때 사용한다.
fun printBox(box: Box<*>) {
    println(box.value)
}
  • box.value는 타입이 뭐든 알 수 없으니까 Any?로 간주된다.

1. 컴파일 시점의 *

Box<>를 쓰면 컴파일러는 "타입 파라미터가 뭔지 모른다"라고 인식한다. 그래서 Box<> 내부 값은 Any? 타입으로 처리되고, 쓰기(값 넣기)는 제한된다.
즉, 컴파일러 입장에서는 "이건 어떤 타입인지 알 수 없지만, 타입 안전을 위해 제한적으로 다룰게"라는 뜻입니다.

2. 런타임에는?

JVM에서 제네릭은 타입 소거(Type Erasure)되어서 런타임에는 제네릭 타입 정보가 사라진다.
따라서 Box<String>이나 Box<Int> 모두 그냥 Box로 실행됩니다.
즉, 런타임에 *인지 아닌지, 구체적인 타입인지 알 수 없어요.

정리하자면, (스타 프로젝션)은 컴파일러에게 "타입 정보를 알 수 없어요"라고 표시하는 것이고,
런타임에서는 제네릭 타입 정보가 아예 없기 때문에, 결국
는 완전히 타입 정보를 모르는 상태이다.


느낀점

아... 코틀린 어렵다.. 근데 어려운 게 당연하다고 생각해.. 지금까지 안 어려웠던 것도 대단하다고 생각해 .. 그래 어려워야지.. 가변성부터 진~짜 너무 헷갈린다. 공변성, 불공변성, 스타 프로젝션, 자료형 프로젝션 진짜 너무 헷갈린다. 실습 해보면 또 다를 것 같긴 한데 오늘 복습을 잘 해놔야 내일 잘 이해한 건지 확인하면서 실습 들을 수 있을 것 같다.

어렵군..! 정말 !!

그리고, 오늘 강사님이 자습 시간에 들어오셔서 면접 질문같이 CS 질문을 하셨는데,, 대답하지 못한 나.. ㅠㅠ 참.. 제대로 알지도 못하는 게 1차 문제지만, 이걸 말로 설명하는 것도 문제다 ! 안주하지 말고, 더 더 열심히 하려고 노력해야겠다.. 파이팅하자 !

0개의 댓글