ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 3. 코틀린에서의 OOP
    Kotlin/자바 개발자를 위한 코틀린 입문 2023. 7. 10. 16:51

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

    1. 클래스와 프로퍼티

    class Person constructor(name: String, age: Int){
    
        val name = name
        var age = age
    
    }

    - 프로퍼티 = 필드 + getter + setter

        ㆍ코틀린에서는 필드만 만들면 getter, setter를 자동으로 만들어준다.

     

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

    - constructor 생략 가능

    - 클래드의 필드 선언과 생성자를 동시에 선언할 수 있다.

     

    fun main() {
        val person = Person("박현민", 27)
        println(person.name)
        person.age = 10
        print(person.age)
    }

    - .필드를 통해 getter와 setter를 바로 호출한다.

    - Java 클래스에 대해서도 .필드로 getter, setter를 사용한다.

     

    2. 생성자와 init

    class Person(val name: String, var age: Int){
        init {  // 클래스가 초기화되는 시점에 실행
            if(age <= 0) {
                throw IllegalArgumentException("나이는 ${age}일 수 없습니다.")
            }
        }
    }

    - init(초기화) 블록은 생성자가 호출되는 시점에 호출된다.

     

    class Person(val name: String, var age: Int){
    
        // 3.
        constructor(name: String): this(name, 1)
    }

    - constructor(파라미터)로 생성자를 추가!

     

    - 주생성자(primary constructor) 반드시 존재해야 한다!

        ㆍ단, 주생성자에 파라미터가 하나도 없다면 생략 가능!

    - 부생성자(secondary constructor)

        ㆍ있을 수도 있고 없을 수도 있다.

        ㆍ최종적으로 주생성자를 this로 호출해야 한다.

        ㆍbody를 가질 수 있다.

    - 본문은 역순으로 실행된다.

     

    * 코틀린에서 부생성자보다는 default parameter를 권장한다.

    - Converting과 같은 경우 부생성자를 사용할 수 있지만, 그보다는 정적 팩토리 메소드를 추천!

     

    3. 커스텀 getter, setter

    fun isAdult(): Boolean {
        return this.age >= 20
    }
    val isAdult: Boolean
        get() = this.age >= 20

    - 모두 동일한 기능이고 표현 방법만 다르다! ( 가독성 )

     

    * 객체의 속성이라면, custom getter 그렇지 않다면 함수

     

    4. backing field

    - custom getter를 사용하면 자기 자신을 변형해 줄 수도 있다!

    class Person(
        name: String,
        var age: Int,
    ) {
        val name = name
            get() = field.uppercase()
    }

    - 주생성자에서 받은 name을 불변 프로퍼티 name에 바로 대입

    - name에 대한 Custom getter를 만들때 field를 사용

    - 왜 field 를 사용하는 것일까?! 이게 무엇이지?!

        ㆍ무한루프를 막기 위한 예약어, 자기 자신을 가리킨다. ( backing field )

     

    * custom getter 에서 backing field를 쓰는 경우는 드물었다.

     

    fun getUppercaseName(): String {
        return this.name.uppercase()
    }
    
    val uppercaseName: String
        get() = this.name.uppercase()

     

    - name을 set할 때 무조건 대문자로 바꿔보자.

    var name = name
        set(value) {
            field = value.uppercase()
        }

    - 사실은 Setter 자체를 지양하기 떄문에 custom setter도 잘안쓴다.

     

    정리

    - 코틀린에서는 필드를 만들면 getter와 (필요에 따라) setter가 자동으로 생긴다

        ㆍ때문에 이를 프로퍼티라고 부른다

    - 코틀린에서는 주생성자가 필수이다.

    - 코틀린에서는 constructor 키워드를 사용해 부생성자를 추가로 만들 수 있다.

        ㆍ단, default parameter나 정적 팩토리 메소드를 추천한다.

    - 실제 메모리에 존재하는 것과 무관하게 custom gettercustom setter 를 만들 수 있다.

    - custom getter, custom setter에서 무한 루프를 막기 위해 field 라는 키워드를 사용한다

        ㆍ이를 backing field 라고 부른다

     

     

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

    1. 추상 클래스

    class Cat(
        species: String
    ) : Animal(species, 4) {
    
         override fun move() {
            println("고양이가 사뿐 사뿐 걸어가~")
         }
    
    }

    - extends 키워드를 사용하지 않고 : 을 사용한다.

    - 상위 클래스의 생성자를 바로 호출한다.

    - override를 필수적으로 붙여줘야 한다.

     

    abstract class Animal(
        protected val species: String,
        protected open val legCount: Int,
    ) {
    
        abstract fun move()
    }

    - 추상 프로퍼티가 아니라면, 상속받을때 open을 꼭 붙여야 한다.

     

    - Java와 마찬가지로 상위 클래스에 접근하는 키워드는 super이다.

    - Java, Kotlin 모두 추상 클래스는 인스턴스화 할 수 없다!

     

    2. 인터페이스

    interface Flyable {
        
        fun act() {
            println("파닥 파닥")
        }
    }

    - default 키워드 없이 메소드 구현이 가능하다.

    - Kotlin 에서도 추상 메소드를 만들 수 있다.

     

    - 인터페이스 구현도 : 을 사용한다.

    override fun act() {
        super<Swimable>.act()
        super<Flyable>.act()
    }

    - 중복되는 인터페이스를 특정할 때 super<타입>.함수 사용 

     

    - Java, Kotlin 모두 인터페이스를 인스턴스화 할 수 없다.

    - Kotlin에서는 backing field가 없는 프로퍼티를 Interface에 만들 수 있다.

     

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

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

     

    4. 상속 관련 지시어 정리

    - 1. final : override를 할 수 없게 한다. default로 보이지 않게 존재한다.

    - 2. open : override를 열어준다.

    - 3. abstract : 반드시 override 해야 한다.

    - 4. override : 상위 타입을 오버라이드 하고 있다.

     

    정리

    - 상속 또는 구현을 할 때에 : 을 사용해야 한다.

    - 상위 클래스 상속을 구현할 때 생성자를 반드시 호출해야 한다.

    - override를 필수로 붙여야 한다.

    - 추상 멤버가 아니면 기본적으로 오버라이드가 불가능하다

        ㆍopen을 사용해주어야 한다.

    - 상위 클래스의 생성자 또는 초기화 블록에서 open 프로퍼티를 사용하면 얘기치 못한 버그가 생길 수 있다.

     

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

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

    Java Kotlin
    public 모든 곳에서 접근 가능 public 모든 곳에서 접근 가능
    protected 같은 패키지 또는 하위 클래스에서만 접근 가능 protected 선언된 클래스 또는 하위 클래스에서만 접근 가능
    default 같은 패키지에서만 접근 가능 internal 같은 모듈에서만 접근 가능
    private  선언된 클래스 내에서만 접근 가능 private 선언된 클래스 내에서만 접근 가능

    - Kotlin에서는 패키지를 namespace를 관리하기 위한 용도로만 사용!

        ㆍ가시성 제어에는 사용되지 않는다.

    - 모듈 : 한번에 컴파일 되는 Kotlin 코드

        ㆍIDEA Module, Maven Project, Gradle Source Set, Ant Task <kotlinc>의 호출로 컴파일 파일의 집합

     

    - Java의 기본 접근 지시어는 default / Kotlin의 기본 접근 지시어는 public

     

    2. 코틀린 파일의 접근 제어

    - 코틀린은 .kt 파일에 변수, 함수, 클래스 여러개를 바로 만들 수 있다.

    public 기본값, 어디서든 접근할 수 있다
    protected 파일(최상단)에는 사용 불가능
    internal 같은 모듈에서만 접근 가능
    private 같은 파일 내에서만 접근 가능

     

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

    - 클래스 안의 멤버

    public 모든 곳에서 접근 가능
    protected 선언된 클래스 또는 하위 클래스에서만 접근 가능
    internal 같은 모듈에서만 접근 가능
    private 선언된 클래스 내에서만 접근 가능

     

    - 생성자

        ㆍ생성자도 가시성 범위는 동일하다.

        ㆍ단, 생성자에 접근 지시어를 붙일려면, constructor를 써줘야 한다.

    class Bus internal constructor(
        val price: Int
    )

     

    - Java에서 유틸성  코드를 만들때 abstract class + private constructor를 사용해서 인스턴스화를 막았음

    public abstract class StringUtils {
        private StringUtils() {}
        
        public boolean isDirectoryPath(String path) {
            return path.endsWith("/");
        }
    }

    - Kotlin은 파일 최상단에 바로 유틸 함수를 작성하면 편하다

    fun isDirectoryPath(path: String): Boolean {
        return path.endsWith("/")
    }

     

    - 프로퍼티도 가시성 범위는 동일하다.

    - 단! 프로퍼티의 가시성을 제어하는 방법으로는

        ㆍgetter, setter 한 번에 접근 지시어를 정하거나

        ㆍSetter에만 추가로 가시성을 부여할 수 있다.

    class Car(
       internal val name: String,
       _price: Int
    ) {
        var price = _price
            private set
    }

     

    4. Java와 Kotlin을 함께 사용할 경우 주의할 점

    - Internal은 바이트 코드 상 public이 된다.

        ㆍ때문에 Java 코드에서는 Kotlin 모듈의 internal 코드를 가져올 수 있다.

     

    - Kotlin 의 protected와 Java의 protected는 다르다.

        ㆍJava는 같은 패키지의 Kotlin protected 멤버에 접근할 수 있다.

     

    정리

    - Kotlin에서 패키지는 namespace 관리용이기 때문에 protected는 의미가 달라졌다.

    - Kotlin에서는 default가 사라지고, 모듈간의 접근을 통제하는 internal이 새로 생겼다.

    - 생성자에 접근 지시어를 붙일 때는 constructor 를 명시적으로 써주어야 한다.

    - 유틸성 함수를 만들 때는 파일 최상단을 이용하면 편리하다.

     - 프로퍼티의 custom setter에 접근 지시어를 붙일 수 있다.

    - Java에서 Kotlin 코드를 사용할 때 internal과 protected는 주의해야 한다.

     

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

    1. static 함수와 변수

    class Person private constructor(
        var name: String,
        var age: Int
    ) {
    
        companion object {
            private const val MIN_AGE = 1
            fun newBaby(name: String): Person {
                return Person(name, MIN_AGE)
            }
        }
    }

    - static 대신 companion object를 사용

    - static : 클래스가 인스턴스화 될 때 새로운 값이 복제되는게 아니라 정적으로 인스턴스끼리의 값을 공유

    - companion object : 클래스와 동행하는 유일한 오브젝트

     

    - 그냥 val MIN_AGE 라고 사용시 런타임 시에 변수가 할당된다.

    - const val MIN_AGE 라고 할 경우 컴파일 시에 변수가 할당된다.

        ㆍ진짜 상수에 붙이기 위한 용도. 기본 타입과 String에 붙일 수 있음

        ㆍ사용법은 Java와 동일하다!

     

    - 자바와 다른 점

        ㆍcompanion object, 즉 동반객체도 하나의 객체로 간주된다

        ㆍ때문에 이름을 붙일 수도 있고, interface를 구현할 수도 있다.

     

    - companion object에 유틸성 함수들을 넣어도 되지만, 최상단 파일을 활용하는 것을 추천

     

    - Java에서 Kotlin companion object를 사용하려면 @JvmStatic 을 붙여야 한다.

    companion object {
        private const val MIN_AGE = 0
        
        @JvmStatic
        fun newBaby(name: String): Person {
            return Person(name, MIN_AGE)
        }
    }
    Person person = Person.newBaby("A")

     

    2. 싱글톤

    public class JavaSingleton {
    
        private static final JavaSingleton INSTANCE = new JavaSingleton();
    
        private JavaSingleton() { }
    
        public static JavaSingleton getInstance() {
            return INSTANCE;
        }
    
    }

    - 여기서 동시성 처리를 조금더 해주거나 enum class를 활용하는 방법도 있다

     

    object Singleton

     

    3. 익명 클래스

    - 특정 인터페이스나 클래스를 상속받은 구현체를 일회성으로 사용할 때 쓰는 클래스

    fun main() {
        moveSomething(object : Movable {
            override fun move() {
                println("움직인다~~")
            }
    
            override fun fly() {
                println("난다~~")
            }
    
        })
    }
    
    private fun moveSomething(movable: Movable) {
        movable.move()
        movable.fly()
    }

    - Java에서는 new 타입이름() , Kotlin에서는 object : 타입이름

     

    정리

    - Java의 static 변수가 함수를 만드려면, Kotlin에서는 companion object를 사용해야 한다

    - companion object도 하나의 객체로 간주되기 때문에 이름을 붙일 수 있고, 다른 타입을 상속받을 수도 있다.

    - Kotlin에서 싱글톤 클래스를 만들 때 object 키워드를 사용한다.

    - Kotlin에서 익명 클래스를 만들 때 object : 타입을 사용한다.

     

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

    1. 중첩 클래스의 종류

    - 어딘가에 소속되어 있는 클래스, 여러 종류가 있었다.

        ㆍStatic을 사용하는 중첩 클래스 : 클래스 안에 static을 붙인 클래스! 밖의 클래스 직접 참조 불가

        ㆍStatic을 사용하지 않는 중첩 클래스

              ○ 내부 클래스(Inner Class) : 클래스 안의 클래스, 밖의 클래스 직접 참조 가능!

              ○ 지역 클래스(Local Class) : 메소드 내부에 클래스를 정의 ( 실제로 잘 사용 X )

              ○ 익명 클래스(Anonymous class) : 일회성 클래스

     

    - Effective Java - Item24, Item86

        ㆍ1. 내부 클래스는 숨겨진 외부 클래스 정보를 가지고 있어, 참조를 해지하지 못하는 경우 메모리 누수가 생길 수 있고,

               이를 디버깅하기 어렵다.

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

    * 클래스 안에 클래스를 만들 때에는 static 클래스를 사용하라

    - Kotlin에서는 이러한 Guide를 충실히 따르고 있다.

     

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

    - Java의 static 중첩 클래스 (권장되는 클래스 안의 클래스)

    class JavaHouse (
        private val address: String,
        private val livingRoom: LivingRoom
    ) {
    
        class LivingRoom(
            private val area: Double
        )
    }

        ㆍ기본적으로 바깥 클래스에 대한 연결이 없는 중첩 클래스가 만들어진다

     

     

    - Java의 내부 클래스 (권장되지 않는 클래스 안의 클래스)

    class House (
        private val address: String,
        private val livingRoom: LivingRoom
    ) {
    
        inner class LivingRoom(
            private val area: Double
        ) {
            val address: String
               get() = this@House.address
        }
    }

        ㆍinner 키워드 추가

        ㆍ바깥클래스 참조를 위해 this@바깥클래스 를 사용한다.

     

    - Kotlin에서는 이러한 Guide를 충실히 따르고 있다

    - 기본적으로 바깥 클래스를 참조하지 않는다.

    - 바깥 클래스를 참조하고 싶다면 inner 키워드를 추가한다.

     

    정리

    - 클래스 안에 클래스가 있는 경우 종류는 두 가지였다.

        ㆍ(Java기준) static을 사용하는 클래스

        ㆍ(Java기준) static을 사용하지 않는 클래스

    - 권장되는 클래스는 static을 사용하는 클래스이다.

     

    - 코틀린에서는 이러한 가이드를 따르기 위해

        ㆍ클래스 안에 기본 클래스를 사용하면 바깥 클래스에 대한 참조가 없고

        ㆍ바깥 클래스를 참조하고 싶다면, inner 키워드를 붙여야 한다.

    - 코틀린 inner class에서 바깥 클래스를 참조하려면 this@바깥클래스를 사용해야 한다.

     

    Java Kotlin
    클래스 안의 static 클래스 바깥 클래스 참조 없음
    권장되는 유형
    클래스 안의 클래스 바깥 클래스 참조 없음
    권장되는 유형
    클래스 안의 클래스 바깥 클래스 참조 있음 클래스 안의 inner 클래스 바깥 클래스참조 있음

     

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

    1. Data Class

    - 계층간의 데이터를 전달하기 위한 DTO( Data Transfer Object )

        ㆍ데이터(필드)

        ㆍ생성자와 getter

        ㆍequals, hashCode

    ctoString

    - IDE를 활용할 수도 있고, lombok을 활용할 수도 있지만 클래스가 장황해지거나, 클래스 생성 이후 추가적인 처리를 해줘야 하는 단점이 있다.

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

    - data 키워드를 붙여주면 equals, hashCode, toString 을 자동으로 만들어 준다.

     

    - 여기에 named argument 까지 활용하면 builder pattern 을 쓰는 것 같은 효과가 있다.

    - 사실상 buidler, equals, hashCode, toString이 모두 있는 것!

     

    - Java 에서는 JDK16부터 Kotlin의 data class 같은 record class 를 도입

    2. Enum Class

    - Java의 Enum 특징

        ㆍ추가적인 클래스를 상속받을 수 없다.

        ㆍ인터페이스는 구현할 수 있으며, 각 코드가 싱글톤이다.

     

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

     

    - when은 Enum Class 혹은 Sealed Class와 함께 사용할 경우, 더욱더 진가를 발휘한다.

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

    - 조금 더 읽기 쉬운 코드

    - 컴파일러가 country의 모든 타입을 알고 있어 다른 타입에 대한 로직(else)을 작성하지 않아도 된다.

    - Enum에 변화가 있으면 알 수 있다.

     

    3. Sealed Class, Sealed Interface

    - 컴파일 타임 때 하위 클래스의 타입을 모두 기억한다.

    - 즉, 런타임때 클래스 타입이 추가될 수 없다.

    - 하위 클래스는 같은 패키지에 있어야 한다.

    - Enum과 다른 점

        ㆍ클래스를 상속받을 수 있다.

        ㆍ하위 클래스는 멀티 인스턴스가 가능하다.

    sealed class HyundaiCar(
        val name: String,
        val price: Long
    )
    
    class Avante : HyundaiCar("아반떼", 1_000L)
    class Sonata : HyundaiCar("소나타", 2_000L)
    class Grandeur : HyundaiCar("그렌저", 3_000L)
    fun main() {
        handleCar(Avante())    
    }
    
    private fun handleCar(car: HyundaiCar) {
        when (car) {
            is Avante -> TODO()
            is Grandeur -> TODO()
            is Sonata -> TODO()
        }
    }

    - 추상화가 필요한 Entity or DTO 에 sealed class 활용

    - 추가로 JDK17 에서도 sealed Class 가 추가

     

    정리

    - Kotlin의 Data class를 사용하면 equals, hashCode, toString을 자동으로 만들어 준다.

    - Kotlin의 Enum Class는 Java의 Enum Class와 동일하지만, when과 함께 사용함으로써 큰 장점을 갖게 된다.

    - Enum Class 보다 유연하지만, 하위 클래스를 제한하는 Sealed Class 역시 when 과 함께 주로 사용된다.

    댓글

Designed by Tistory.