고딩왕 코범석

OOP 본문

Language & Framework/Kotlin

OOP

고딩왕 코범석 2022. 8. 2. 16:49
반응형

Index

  1. 코틀린에서 클래스를 다루는 방법
    1. 클래스와 프로퍼티
    2. 생성자와 init
    3. 커스텀 getter, setter
    4. backing field
  2. 코틀린에서 상속을 다루는 방법
    1. 추상 클래스
    2. 인터페이스
    3. 클래스를 상속할 때 주의할 점
    4. 상속 관련 지시어 정리
  3. 코틀린에서 접근 제어를 다루는 방법
    1. 자바와 코틀린의 가시성 제어
    2. 코틀린 파일의 접근 제어
    3. 다양한 구성요소의 접근 제어
    4. 자바와 코틀린을 함께 사용할 경우 주의할 점
  4. 코틀린에서 object 키워드를 다루는 방법
    1. static 함수와 변수
    2. 싱글톤
    3. 익명 클래스
  5. 코틀린에서 중첩 클래스를 다루는 방법
    1. 중첩 클래스의 종류
    2. 코틀린의 중첩 클래스와 내부 클래스
  6. 코틀린에서 다양한 클래스를 다루는 방법
    1. Data class
    2. Enum class
    3. Sealed Class, Sealed Interface

코틀린에서 클래스를 다루는 방법

클래스와 프로퍼티

코틀린에서 클래스는 아래와 같이 선언한다.

class Person(
        val name: String,
        var age: Int,
    )

클래스 옆 괄호()는 자바 생성자 역할을 한다. 코틀린에서는 val은 getter, var는 getter, setter를 모두 자동으로 생성하기 때문에 필드라 부르지 않고 프로퍼티라고 부른다.

fun main() {
        val person = Person("고범석", 28)
        println(person.name) // getter
        person.age = 20 // setter
    }

생성자와 init

코틀린에서의 생성자의 로직은 아래와 같다.

class Person (
        val name: String,
        var age: Int,
    ) {
        init { // 초기화 블록, 생성자가 호출될 때 초기화되는 블록
            if (age <= 0) {
                throw IllegalArgumentException("...")
            }
        }
    }

추가적인 생성자가 필요하다면 다음과 같이 작성한다.

fun main() {
        val person = Person()
    }
    
    class Person (
        val name: String,
        var age: Int,
    ) {
        init {
            if (age <= 0) {
                throw IllegalArgumentException("1살 이상이어야 합니다.")
            }
            println("초기화 블록")
        }
    
            // 부생성자, 이 this는 밑의 파라미터 한개짜리 부생성자를 호출
        constructor(): this("홍길동") {
            println("파라미터 없는 부생성자")
        }
    
        constructor(name: String): this(name, 1) {
            println("파라미터 1개 부생성자")
        }
    }

그렇다면 init과 부생성자가 여러 개면 어떤 순서대로 호출될까?

초기화 블록
    파라미터 1개 부생성자
    파라미터 없는 부생성자

코틀린에서는 먼저 초기화 블록을 먼저 실행한 다음 알맞은 부생성자들을 호출한다.

코틀린은 부생성자를 여러개 만드는 것보다 디폴트 파라미터 사용을 권장한다. 그리고 다른 객체를 받아서 객체를 생성하는 경우는 정적 팩토리 메서드 패턴을 권장한다.

커스텀 getter, setter

코틀린에서 클래스에 프로퍼티가 있는 것처럼 작성할 수 있다. 이를 custom getter라 한다.

// 기본적인 함수
    fun isAdult(): Boolean = this.age >= 20
    
    // custom getter, 프로퍼티처럼 보이게 한다.
    val isAdult: Boolean
        get() = this.age >= 20  // Expression이라 바로 반환 가능

그렇다면 함수, custom getter를 언제 사용해야할까?

  • 객체의 속성이라면 custom getter
  • 그렇지 않고 기능처럼 접근해야하면 함수를 사용하자

backing field

만약, 이름을 get할 때 항상 대문자로만 가져와야하는 요구사항이 있다고 가정하면 다음과 같이 custom getter를 활용할 수 있다.

class Person (
        name: String,
        var age: Int = 1,
    ) {
    
        val name: String = name
                    // field?
            get() = field.toUpperCase()
    }

field 예약어?

  • 코틀린은 클래스.프로퍼티명을 통해 getter를 호출한다.
  • 위 코드에서 get() = name.toUpperCase()를 호출할 때 name부분도 결국 getter를 호출한다.
  • 무한루프가 일어나게 되는데 이를 field라는 예약어를 통해 필드를 호출한다.
  • 이를 코틀린에서 자기 자신을 가리키는 보이지 않는 필드다. 라고 하여 backing field라고 부른다.

위로

코틀린에서 상속을 다루는 방법

추상 클래스

자바, 코틀린 모두 추상클래스를 인스턴스화 할 수 없다.

abstract class Animal(
        protected val species: String,
        protected open val legCount: Int,
    ) {
        abstract fun move()
    }
    
    class Cat(
        species: String
    ): Animal(species, 4) // 추상 클래스 생성자 호출  {
    
            // override 지시어 사용
        override fun move() {
            println("고양이가 움직임")
        }
    }
    
    class Penguin(
        species: String
    ): Animal(species, 2) {
    
        private val wingCount: Int = 2
    
        override fun move() {
            println("펭귄이 움직인다.")
        }
    
        override val legCount: Int
            get() = super.legCount + this.wingCount
    }

추상 클래스 프로퍼티의 getter를 override 한다면 해당 추상 클래스 프로퍼티에는 open 키워드를 붙인다. 기본적으로 코틀린에서는 상속을 막는다.

그리고 custom getter를 통해 추상 클래스 프로퍼티의 getter를 구현한다.

인터페이스

자바, 코틀린 모두 인터페이스를 인스턴스화 할 수 없다.

구현시에도 콜론 옆에 인터페이스를 기입한다.

특정 인터페이스의 default 메서드를 호출할 때는 super<인터페이스타입>.메서드 로 호출한다.

그리고 코틀린에서는 backing field가 없는 프로퍼티를 인터페이스에서 만들 수 있다.

interface Flyable {
    
        fun act() {
            println("파닥파닥")
        }
    }
    
    interface Swimable {
    
            // backing field가 없는 프로퍼티
            val swimAbility: Int
            get() = 3
    
        fun act() {
            println("어푸어푸")
        }
    }
    
    class Penguin(
        species: String
    ): Animal(species, 2), Flyable, Swimable {
    
        private val wingCount: Int = 2
    
        override fun move() {
            println("펭귄이 움직인다.")
        }
    
        override val legCount: Int
            get() = super.legCount + this.wingCount
    
        override fun act() {
            super<Swimable>.act()
            super<Flyable>.act()
        }
    
            // Swimable 인터페이스에서 프로퍼티를 선언했다면
            // 구현체에서는 getter 혹은 setter를 구현해주길 기대할 것이다.
            override val swimAbility: Int
            get() = 4
    }

클래스를 상속할 때 주의할 점

아래와 같은 상황에서 Derived 클래스를 인스턴스화 했을 때 출력값은 어떻게 될까?

fun main() {
        val instance = Derived(10)
      println(instance.number)
    }
    
    open class Base(
        open val number: Int = 100
    ) {
        init {
            println("Base class")
            println(number)
        }
    }
    
    class Derived (
        override val number: Int,
    ): Base(number) {
        init {
            println("Derived Class")
        }
    }
    
    // console
    Base class
    0
    Derived Class
    10

생성자에 10을 넣어주었지만 init 에서는 0으로 출력되었다. 왜 이런 일이 벌어졌을까?

  • 하위 클래스인 Derived를 인스턴스화 할 때 상위 클래스 init이 호출된다.
  • 이 때, 상위 클래스에서 하위 클래스의 프로퍼티를 가져와야하는데, 아직 하위 클래스가 초기화되지 않았다.
  • 따라서 기본값인 0이 호출되었다.

따라서, 상위 클래스를 설계할 때 생성자 또는 초기화 블록에 사용되는 프로퍼티에는 open을 피해야 한다.

상속 관련 지시어 정리

  • final : override를 할 수 없게 한다. default로 보이지 않게 존재한다.
  • open : override를 열어준다.
  • abstract : 반드시 override 해야한다.
  • override : 상위 타입을 오버라이드 하고 있다.

위로

코틀린에서 접근 제어를 다루는 방법

자바와 코틀린의 가시성 제어

자바는 public, protected, default, private 이라는 가시성 제어가 있다. 코틀린에서는 다음과 같이 바뀌었다.

가시성 제어 자바 코틀린
public 모든 곳에서 접근 가능 동일 (기본 지시어)
protected 같은 패키지 또는 하위 클래스에서만 접근 가능 선언된 클래스 또는 하위 클래스에서만 접근 가능
default 같은 패키지에서만 접근 가능 (기본 지시어) 같은 모듈에서만 접근 가능 (internal 로 변경)
private 선언된 클래스 내에서만 접근 가능 동일
  • 코틀린의 패키지는 namespace를 관리하기 위한 용도로만 사용하며 가시성 제어에는 사용되지 않는다. 그래서 코틀린의 protected는 같은 패키지가 아닌 선언된 클래스에서 접근을 제어한다.
  • 모듈은 한 번에 컴파일되는 코틀린 코드를 의미한다. 만약 internal 키워드를 하위 모듈에 작성했다면 상위 모듈에는 internal에 접근할 수 없다.

코틀린 파일의 접근 제어

코틀린은 하나의 kt 파일에 클래스, 변수, 함수를 선언할 수 있다. 그리고 이 파일에 대한 접근을 제어할 수 있다.

접근 제어자 설명
public 기본값으로 어디서든 접근 가능
protected 파일(최상단) 에서는 사용 불가능
internal 같은 모듈에서만 접근 가능
private 같은 파일 내에서만 접근 가능

다양한 구성요소의 접근 제어

제어자 키워드를 클래스에 붙이면 앞에서 설명한 것과 동일하다.

생성자도 동일하다. 단, constructor 키워드를 붙여야한다.

class Person private constructor(...)
    
    // protected의 경우 자기 자신과 상속받은 하위 클래스에서 사용할 수 있다.
    // 클래스 앞에 open 키워드가 없을 경우 자기 자신만 쓸 수 있어서 인텔리제이에서는 노란줄로 경고가 뜬다.
    open class Person protected constructor(...)

자바에서 유틸성 코드를 만들때 abstract class + private constructor를 통해 인스턴스화를 막았다. 코틀린에서는 파일 최상단에 유틸 함수를 작성하면 매우 편하다.

프로퍼티도 가시성 범위는 동일하다. 가시성을 설정하는 방법은 다음과 같다.

class Car(
            // 바로 선언, 모듈에서만 접근 가능한 getter만 존재
        internal val name: String,
            // 바로 선언, 클래스 내부에서만 접근 가능한 getter, setter 존재
        private var owner: String,
    
        _price: Int,
    ) {
    
            // setter는 private, getter는 public
            // 애초에 필드가 아닌 프로퍼티가 생성되기 때문에 getter, setter의
            // 접근 제어자를 분리할 경우 사용
        var price = _price
            private set
    }

자바와 코틀린을 함께 사용할 경우 주의할 점

  • internal을 바이트 코드로 변경하면 자바에서는 public으로 변경된다.
    • 만약 상위 모듈이 자바, 하위 모듈이 코틀린이라 가정했을 때 하위 모듈에 internal로 작성된 접근 제어자가 있다면 이를 바이트 코드로 변경할 때 public으로 변경된다. 즉, 상위 모듈 자바에서 접근이 가능하게 된다.
  • 코틀린의 protected와 자바의 protected는 다르다.
    • 자바는 같은 패키지의 코틀린 protected 멤버에 접근이 가능하다.
    • 자바는 동일한 패키지에서 접근이 가능하기 때문이다.

위로

코틀린에서 object 키워드를 다루는 방법

static 함수와 변수

코틀린에서 static 키워드가 없어졌다. static은 동반 객체와 함께 다룬다.

class Person private constructor(
        var name: String,
        val age: Int,
    ) {
    
            // 자바에서의 static 역할
        companion object Factory {
            private const val MIN_AGE = 1
            fun newBaby(name: String) = Person(name, MIN_AGE)
        }
    }

잠깐 용어 정리

  • static : 클래스가 인스턴스 화 될 때 새로은 값이 복제되는게 아닌 인스턴스끼리 해당 값을 정적으로 공유한다.
  • companion object : 클래스와 동행하는 유일한 오브젝트
  • const : val MIN_AGE로 선언하면 해당 값이 런타임에 선언된다. const 키워드를 통해 컴파일 시에 변수에 값이 할당된다. 즉, 진짜 상수에 붙이기 위한 용도로 쓰인다. 기본 타입과 String에 붙일 수 있다.

주의 사항

  • companion object도 하나의 객체이다. 이 객체에게 이름을 붙일 수 있다.
  • 객체라는건? 인터페이스를 구현할 수 있다.
  • companion object에 유틸성 함수를 넣어도 되지만, 최상단 파일을 활용하는 것을 권장한다.
  • 자바에서 companion object를 사용하려면 @JvmStatic을 사용해야한다.

싱글톤

코틀린에서 싱글톤은 object 키워드를 통해 만들 수 있다.

익명 클래스

익명 클래스는 특정 인터페이스나 클래스를 상속받은 구현체를 일회성으로 사용할 때 쓰는 클래스다. 코틀린에서는 익명 클래스를 다음과 같이 표현한다.

fun main() {
        moveSomething(object: Movable {
            override fun fly() {
                println("fly")
            }
    
            override fun move() {
                println("move")
            }
        })
    }
    
    private fun moveSomething(movable: Movable) {
        movable.fly()
        movable.move()
    }
    
    interface Movable {
        fun fly()
        fun move()
    }

위로

코틀린에서 중첩 클래스를 다루는 방법

중첩 클래스의 종류

자바에서는 어딘가에 소속되어 있는 클래스가 여러 종류 있었다.

  1. static이 붙은 중첩 클래스
    • 바깥의 클래스를 직접 참조할 수 없다.
  2. static이 붙지 않은 중첩 클래스
    1. 내부 클래스
      • 밖의 클래스를 직접 참조할 수 있다.
    2. 지역 클래스
    3. 익명 클래스
      • 일회성 클래스

static 키워드가 붙고 안붙고의 차이를 그림으로 표현하면 아래와 같다.

https://user-images.githubusercontent.com/37062337/182299124-1ad3e27f-baa5-4a9b-8e23-818c1ef5406c.png

Effective Java에서는 아래와 같이 내부 클래스에 대해 언급한다.

  1. 내부 클래스는 숨겨진 외부 클래스 정보를 갖고 있어 참조를 해제하지 못하는 경우 메모리 누수가 생길 수 있고, 이를 디버깅하기 어렵다.

  2. 내부 클래스의 직렬화 형태가 명확하게 정의되지 않아 직렬화에 대한 제한이 있다.

따라서, 클래스 내에 클래스를 만들 때는 static을 사용하라는 가이드가 있다. 그리고 코틀린은 이 가이드를 따르고 있다.

코틀린의 중첩 클래스와 내부 클래스

코틀린에서는 기본적으로 바깥 클래스에 대한 연결이 없는 중첩 클래스가 만들어진다. (권장 방법)

class House(
        val address: String,
        val livingRoom: LivingRoom,
    ) {
        class LivingRoom (
            var area: Double,
        )
    }

바깥 클래스를 참조해야하는 경우는 아래와 같이 작성한다. (권장하지 않는 방법)

class House(
        private val address: String,
        private val livingRoom: LivingRoom,
    ) {
            // inner 키워드 명시
        inner class LivingRoom (
            private var area: Double,
        ) {
                    // 바깥 클래스를 참조하려면 {this@바깥클래스.프로퍼티명}을 통해 접근
            val address: String
                get() = this@House.address
        }
    }

위로

코틀린에서 다양한 클래스를 다루는 방법

Data class

자바에서 DTO는 주로 필드와 생성자, getter, equals, hashCode, toString과 같은 부가적인 메서드를 가지고 있다. IDE를 활용하거나 lombok을 활용할 수 있지만 클래스 생성 이후 추가적인 처리를 해줘야하는 단점이 있다.

코틀린에서는 이를 Data class로 정의하여 간단하게 만들 수 있다.

data class PersonDto(
        val name: String,
        val age: Int,
    )

Enum class

자바에서 Enum은 추가적인 클래스를 상속받을 수 없다. 인터페이스는 구현할 수 있으며 각 코드가 싱글톤이다. 코틀린의 Enum도 자바와 비슷하게 작성한다.

enum class Country(
        private val code: String,
    ) {
        KOREA("KO"),
        AMERICA("US"),
        ;
    }

when과 같이 사용해보기

아래는 자바에서 Enum의 코드에 따라 분기처리할 때의 로직이다.

private static void handleCountry(Country country) {
        if (country == Country.KOREA) {
            // logic
        }
    
        if (country == Country.AMERICA) {
            // logic
        }
    }

위 코드에서 만약 코드들이 많이지게 된다면 else 로직에 대한 처리가 애매하며 가독성이 떨어진다.

코틀린에서 when으로 받는 변수가 Enum이라면 else를 작성하지 않아도 된다. 그리고 Enum에 코드가 추가될 때도 IDE에서 warning이나 error가 뜬다. 이유는 컴파일러가 해당 타입의 코드들을 모두 알고 있기 때문이다.

fun handleCountry(country: Country) {
        return when (country) {
            Country.KOREA -> TODO()
            Country.AMERICA -> TODO()
        }
    }

Sealed Class, Sealed Interface

Sealed는 ‘봉인된’ 이라는 뜻이다. 추상 클래스를 만들고 싶지만 외부에서는 이 클래스를 상속받지 않았으면 하는 경우가 있다. Sealed class는 외부에서 상속하지 못하도록 막고, 상속한 하위 클래스를 봉인하는 의미에서 사용된다.

특징으로는 컴파일 타임 때 하위 클래스의 타입을 모두 기억하며 런타임 때 클래스 타입이 추가될 수 없다. 또한, 하위 클래스는 같은 패키지에 있어야 한다.

Enum과 차이점?

  • Sealed Class는 클래스를 상속받을 수 있다.
  • 하위 클래스는 멀티 인스턴스가 가능하다.

when과 같이 사용해보기

fun handleHyundaiCar(hyundaiCar: HyundaiCar) {
        when (hyundaiCar) {
            is Avante -> TODO()
            is Grandeur -> TODO()
            is Sonata -> TODO()
        }
    }

활용 방안?

  • 추상화가 필요한 Entity
  • 추상화가 필요한 DTO

abstract class vs sealed class

이 둘의 가장 큰 차이점은 ‘구현체가 같은 패키지에 있어야 하는지? 아니면 어디에 있어도 상관 없는지?’ 이다.

멀티 모듈 사용 시 A 모듈에서 만들어둔 class를 B 모듈과 C 모듈에서 구현하게 하고 싶다면 abstract class를 사용한다. 즉, 다른 패키지에서 접근해야할 경우 abstract class를 사용한다.

위로

반응형

'Language & Framework > Kotlin' 카테고리의 다른 글

특성  (0) 2022.08.05
FP  (0) 2022.08.03
코드 제어  (0) 2022.07.30
변수, 타입, 연산자  (0) 2022.07.30