본문 바로가기
Study/다재다능 코틀린 프로그래밍

Day 7. 객체와 클래스

반응형

본 포스트는 지인들과 스터디한 내용을 정리한 포스트입니다

 

http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788931463422

 

다재다능 코틀린 프로그래밍 - 교보문고

다양한 프로그래밍 패러다임으로 살펴보는 코틀린 기본서 | 코틀린은 멀티패러다임 프로그래밍 언어다. 코틀린은 스크립트로 사용할 수도 있고, 객체지향 코드나 함수형 코드, 비동기 프로그램

www.kyobobook.co.kr


1. 객체와 클래스

  • 코틀린은 클래스 작성 등을 위한 장황한 보잉러플레이트 코드 없이 바로 객체지향 프로그래밍을 할 수 있도록 해준다
  • 코틀린은 클래스 생성자를 함수처럼 사용할 수 있다( 자바처럼 new 키워드가 필요없다 )
  • 개발자는 속성만 정의하면 코틀린이 필요한 부븐에 백킹 필드를 만들어준다 ( 백킹필드: 클래스의 속성 정보 )
  • 데이터 클래스를 이용하면 데이터를 처리하는 것보다 데이터를 보여주는데 집중할 수 있다

 

2. 객체와 싱글톤

  • 코틀린은 싱글톤을 직접 지원함으로써 싱글톤을 구현할 때 발생할 수 있는 부담과 구현이 잘못될수 있는 리스크를 제거했다
  • 개발자가 필요하다면 클래스 정의 없이 객체를 생성할 수 있다
  • 객체 표현식으로 사용하는 익명 객체
    • 익명객체가 지닌 한계
      • 익명 객체의 내부 타입은 함수나 메소드의 리턴타입이 될 수 없다
      • 익명 객체의 내부 타입은 함수나 메소드의 파라미터가 될 수 없다
      • 클래스 안에 저장된 속성들이 있다면, 해당 속성들은 Any로 간주된다. 그러면 모든 속성이나 메소드에 직접 접근할 수 없게 된다
    • Java Style vs Kotlin Style
      // anonymous.kts
      fun createRunnalble(): java.lang.Runnable {
          val runnable = object : java.lang.Runnable {
              override fun run() {
                  println("You called...")
              }
          }
          return runnable
      }
      
      val aRunnable = createRunnalble()
      aRunnable.run() // You called...
      
      
      fun createRunnable2(): java.lang.Runnable = java.lang.Runnable { println("You called...") }
      val aRunnable2 = createRunnable2()
      aRunnable2.run() // You called...​
    • 익명 내부 클래스에 의해 구현된 인터페이스가 싱글 추상 메소드 인터페이스(함수형 인터페이스)라면 다음과 같이 메소드 이름을 명명하지 않고 바로 구현할 수 있다
    • 익명 내부 클래스가 둘 이상의 인터페이스를 구현해야 한다면 리턴이 필요한 경우에는 반드시 리턴할 인스턴스 타입을 명시해줘야 한다

3. 객체 선언을 이용한 싱글톤

  • object 키워드와 {} 블록 사이에 이름을 넣는다면, 코틀린은 이를 표현식이 아니라 명령문 또는 선언으로 인식한다
  • 익명 이너클래스의 인스턴스를 만들 땐 객체 표현식을 사용하고, 싱글톤을 만들 땐 객체 선언을 사용하라
  • 대표적인 싱글톤 Unit
  • 예제코드1
    // util.kts
    object util {
        fun numberOfProcessors() = Runtime.getRuntime().availableProcessors()
    }
    
    println(util.numberOfProcessors())​

    • 객체 선언을 이용해서 만들 Util 객체가 싱글톤이다
    • Util로는 객체를 생성할 수 없다
    • 코틀린 컴파일러는 Util을 클래스 취급하지 않고, 객체 자체로 취급한다
    • 즉 Java에서 private 생성자와 static 메서드만 가지고 있는 클래스라고 생각하면 된다
  • 예제코드2
    // singleton.kts
    object Sun : java.lang.Runnable {
        val radiusInKM = 696000
        var coreTemperatureInC = 15000000
        override fun run() {
            println("spin...")
        }
    }
    
    fun moveIt(runnable: java.lang.Runnable) {
        runnable.run()
    }
    
    println(Sun.radiusInKM) // 696000
    moveIt(Sun) // spin...​

4. 탑 레벨 함수 vs 싱글톤

  • 패키지 정의
    // UtilTest.kt
    fun unitSupported() = listOf("Metric", "Imperial")
    fun precision(): Int = throw RuntimeException("Not implemented yet")
    
    object Temperature {
        fun c2f(c: Double) = c * 9.0 / 5 + 32
        fun f2c(f: Double) = (f - 32) * 5.0 / 9
    }
    
    object Distance {
        fun milesToKm(miles: Double) = miles * 1.609344
        fun kmToMiles(km: Double) = km / 1.609344
    }​
  • 패키지 import 및 사용
    // UseUtil.kt
    package chapter7
    
    import Temperature.c2f
    import unitSupported
    
    fun main() {
        println(unitSupported())
        println(Temperature.f2c(75.253))
        println(c2f(24.305))
    }​
     
  • 탑레벨 함수 선택
    • 사용할 함수들이 하이레벨
    • 일반적이거나 넓게 사용될 예정
  • 반면 함수들이 연관되어 있다면 싱글톤을 사용하는 게 좋다

 

5. 클래스 생성

  • class 키워드를 이용해 정의
    class Car​
  • 읽기 전용 속성 부여
    class Car(val yearOfMake: Int)​

    • yearOfMake라는 이름으로 Int 타입의 읽기전용 속성을 만들었다
    • 코틀린 컴파일러는 생성자를 작성했고, 필드를 정의하고 해당 필드에 접근하게 해주는 getter를 추가했다
  • 인스턴스 생성하기
    • 코틀린은 new 키워드를 쓰지 않는다
    • 함수를 사용하듯 그냥 클래스 이름을 이용한다
      // property.kts
      class Car(val yearOfMonth: Int)
      
      var car = Car(2019)
      println(car.yearOfMonth)​
  • 읽기-쓰기 속성
    • 예제코드
      // readwrite.kts
      class Car(val yearOfMake: Int, var color: String)
      val car = Car(2019, "Red")
      car.color = "Green"
      println(car.color) // Green​
    • 읽기전용 속성을 만들 때는 val
    • 변경 가능한 속성을 만들때는 var
  • 들여다보기 - 필드와 속성
    • 코틀린에서는 클래스에 필드가 없다 ( public으로 열리는 멤버변수가 없다..? )
    • 코틀린에선 getter, setter 대신 속성의 이름을 이용해서 속성에 접근할 수 있다
  • 속성 제어 변경
    • 예제코드
      // setter.kts
      import java.lang.RuntimeException
      
      class Car(val yearOfMake: Int, theColor: String) {
          var fuelLevel = 100
          var color = theColor
          set(value) {
              if (value.isBlank()) {
                  throw RuntimeException("no empty, please")
              }
              field = value
          }
          get() = field + "hi"
      }
      
      val car = Car(2021, "Green")
      println(car.color)
      println(car.fuelLevel)​
    • 코틀린은 정의한 속성에 사용되는 getter와 setter를 생성한다
    • 전달받은 값이 사용 가능하다면 값을 스페셜 키워드인 field에 의해서 참조되고 있는 필드에 할당한다
    • 코틀린이 필드를 내부적으로 만들었기 떄문에 코드에서 필드에 접근할 수 있는 방법이 없다
    • 즉 개발자는 getter나 setter에 있는 field 키워드를 통해서만 필드를 사용할 수 있다
  • 접근 제어자
    • 코틀린에서 클래스의 속성과 메소드는 public이 기본이다
    • 코틀린에는 4개의 접근 제어자가 있다
      • public
      • private
      • protected
      • internal
        • 같은 모듈에 있는 모든 코드에서 속성이나 메소드에 접근이 가능
        • 모듈이라 함은 함께 컴파일된 모든 소스 코드를 뜻한다
        • 바이트 코드에 직접 나타나지 않는다
        • 네이밍 컨벤션에 의해서 코틀린 컴파일러에 의해 다뤄진다
    • getter의 접근 권한은 속성의 접근 권한과 동일하다
    • setter의 경우 개발자가 원하는 대로 접근 권한을 설정할 수 있다
      // mySetter.kts
      class myClass(param: Int) {
          var fuelLevel = param
          private set(value) {
              field = value
          }
      }
      
      val testObject = myClass(1000)
      println(testObject.fuelLevel)
      //testObject.fuelLevel = 3000 // Cannot assign to 'fuelLevel': the setter is private in 'myClass'​
    • setter를 다시 구현할 생각이 없다면 파라미터인 value와 메소드 바디는 생략하면 된다
    • setter 역시 작성하지 않거나 접근 제어자 설정을 하지 않는다면 속성과 동일한 권한으로 접근이 가능하다
  • 초기화 코드
    • 생성자를 위해서 파라미터와 속성이 파라미터 리스트로 정의된다 ( 주 생성자, 첫 번째 줄에 정의 )
    • 객체를 초기화하는 코드가 값들을 설정하는 것보다 더 복잡하다면 생성자용 바디를 만들 필요가 있다
    • 클래스는 0개 이상의 init 블록을 가질 수 있다
    • init 블록의 코드는 top-down으로 순차적으로 실행된다
    • 주 생성자에서 선언된 속성과 파라미터는 클래스 전체에서 사용 가능
    • 클래스 내부에서 선언된 속성을 사용하기 위해서는 init 블록을 해당 속성 아래에 위치시켜야 한다
    • 예제 코드
      // initialization.kts
      import java.lang.RuntimeException
      
      class Car(val yearOfMonth: Int, theColor: String) {
          var fuelLevel = 100
          private set
      
          var color = theColor
          set(value) {
              if(value.isBlank()) {
                  throw RuntimeException("no empty, please")
              }
              field = value
          }
      
          init {
              if (yearOfMonth < 2020) {
                  fuelLevel = 90
              }
          }
      }​
    • 예제코드 ( init 블록을 표현식으로 )
      // initialization.kts
      import java.lang.RuntimeException
      
      class Car(val yearOfMonth: Int, theColor: String) {
          var fuelLevel = if(yearOfMonth < 2020) 90 else 100
          private set
      
          var color = theColor
          set(value) {
              if(value.isBlank()) {
                  throw RuntimeException("no empty, please")
              }
              field = value
          }
      }​
    • 가급적 init 블록은 1개만 만들고, 가능하다면 1개도 만들지 않도록 하라 ( 생성자에서 최대한 아무런 작업도 안 하는 것이 프로그램의 안정성과 퍼포먼스 측면 모두에서 더 장점이 크다 )
  • 보조 생성자
    • 주 생성자를 작성하지 않았다면 코틀린은 아규먼트가 없는 기본 생성자를 생성한다
    • 예제코드
      // seondary.kts
      class Person(val first: String, val last: String) {
          private var fulltime = true
          private var location: String = "-"
          constructor(first: String, last: String, fte: Boolean): this(first, last) {
              fulltime = fte
          }
          constructor(first: String, last: String, loc: String): this(first, last, false) {
              location = loc
          }
      
          override fun toString(): String {
              return "$first $last $fulltime $location"
          }
      }
      
      println(Person("Jane", "Doe")) // Jane Doe true - 
      println(Person("John", "Doe", false)) // John Doe false -
      println(Person("Baby", "Doe", "home")) // Baby Doe false home​

      • Person의 주 생성자는 2개의 속성 first, last를 val로 선언했다
      • 주 생성자에서 constructor 키워드는 선택사항이다
      • 두 개의 보조 생성자는 constructor 키워드로 선언
      • 모든 보조 생성자는 주 생성자나 보조 생성자를 호출할 수 있다, 단 생성자끼리 서로를 호출하는 순환은 일어나선 안된다
  • 인스턴스 메소드 정의
    • 클래스 안의 메소드를 정의할 때는 fun 키워드를 사용한다
    • 기본적으로 public, fun 키워드 앞에 private, protected, internal 키워드를 이용해서 메소드의 권한을 설정할 수 있다
  • 인라인 클래스
    • inline 클래스는 primitive type과 class 생성간 발생하는 오버헤드(객체 생성과 메모리 사용에 관한)를 균형잡게 해주는 좋은 기능이다
    • 컴파일 시간에는 클래스의 장점을 취할 수 있고, 실행 시간에는 primitive type으로 취급된다 ( 바이트 코드로 변환되었을 때 primitive type으로 변경되는 것 )
    • 예제코드
      // ssn.kt
      package chapter7
      
      @JvmInline
      value class SSN(val id: String)
      
      fun receiveSSN(ssn: SSN) {
          println("Received $ssn")
      }​

      • 코틀린 컴파일러는 receiveSSN() 함수를 호출할 때 raw 문자열이 아닌 SSN 인스턴스를 이용했는지를 검증한다
    • inline 클래스는 속성과 메서드를 가질 수 있고, 인터페이스를 구현할 수도 있다
    • 내부를 살펴보면 메소드는 primitive type을 받는 static 메소드가 inline 클래스로 둘러싸여있다
    • inline 클래스는 final이 되어야 하고, 다른 클래스에 의해서 확장될 수 없다

 

6. 컴패니언 객체와 클래스 멤버

  • 지금까지 만든 클래스는 속성과 인스턴스 메소드만 가지고 있었다
  • 클래스 레벨(static, 클래스 멤버 변수, 메소드)의 속성이나 메소드를 컴패니언 객체로 만든다
  • 컴패니언 객체는 클래스 안에 정의한 싱글 톤이다
  • 예제코드
    // companion.kts
    class MachineOperator(val name: String) {
        fun checkIn() = checkedIn++
        fun checkOut() = checkedIn--
    
        companion object {
            var checkedIn = 0
            fun minimumBreak() = "15 minutes every 2 hours"
        }
    }
    
    MachineOperator("Mater").checkIn()
    println(MachineOperator.minimumBreak()) // 15 minutes every 2 hours
    println(MachineOperator.checkedIn) // 1​
  • 컴패니언에 접근하기
    • 컴패니언 객체가 인터페이스를 구현하고 있는 등의 이유로 컴패니언 객체에 참조가 필요한 경우가 생긴다
    • 클래스 .Companion을 붙여서 접근할 수 있다 ( C는 반드시 대문자로 )
    • companion object {name} 을 통해 companion 객체에 적절한 이름도 지어줄 수 있다
  • 팩토리로 사용하는 컴패니언
    • 컴패니언 객체는 클래스의 팩토리로 사용할 수 있다
    • 객체를 사용 가능한 상태로 만들기까지 몇 가지 단계를 두고 진행을 해야 할 때가 있다 ( 객체를 생성 후 이벤트 핸들러에 등록하거나 타이머에 등록하는 등 )
    • 위에서 소개한 필수 작업들이 진행된 후에 객체를 사용할 수 있기에 생성자의 작업 완료 이후 등록 메소드를 호출하는 것은 실수와 빼먹음을 유발할 수 있어 좋은 선택이 아니다.
    • 또한 생성자에 해당 기능들을 넣는 것 또한 좋은 생각이 아니다
    • 이런 경우 팩토리처럼 동작하는 클래스의 컴패니언 객체를 설계하는 것을 고려해볼 수 있다
    • 예제코드
      // companionFactory.kts
      class MachineOperator private constructor(val name: String) {
          var num = 0
          fun checkIn() = num++
          fun checkOut() = num--
      
          companion object {
              fun create(name: String): MachineOperator {
                  val instance = MachineOperator(name)
                  instance.checkIn()
                  return instance
              }
          }
      }
      
      val operator = MachineOperator.create("hi")
      println(operator.num) // 1
      operator.checkIn()
      println(operator.num) // 2​
    • 컴패니언 객체의 멤버에 접근하면 코틀린 컴파일러는 싱글톤 객체로 라우팅을 한다

 

7. 제네릭 클래스 생성

  • 제네릭 클래스는 타입 안정성을 지키면서 일반화를 할 때 사용된다
  • 코틀린의 제네릭 기능은 이와 비슷하지만 가변성과 제약들은 다르게 선언된다
  • 예제코드
    // prioritypair.kts
    class PriorityPair<T: Comparable<T>>(member1: T, member2: T) {
        val first: T
        val second: T
        init {
            if (member1 >= member2) {
                first = member1
                second = member2
            } else {
                first = member2
                second = member1
            }
        }
    
        override fun toString(): String {
            return "$first, $second"
        }
    }
    
    println(PriorityPair(2, 1)) // 2, 1
    println(PriorityPair("A", "B")) // B, A​
  • 제네릭 클래스는 일반적인 클래스를 만드는 문법을 기반으로 가변성과 제약조건을 추가해서 정의한다

 

8. 데이터 클래스

  • 코틀린의 data class는 특정한 행동, 동작보다는 데이터를 옮기는 데 특화된 클래스이다
  • 각각의 데이터 클래스에 코틀린은 자동으로 equals(), hashCode(), toString() 메소드를 만들어준다
  • 추가적으로 셀렉트 속성의 업데이트된 값을 제공하면서 인스턴스를 복사해주는 copy() 메소드도 제공해준다
  • 주 생성자에 의해서 정의된 각각의 속성에 접근할 수 있게 해주는 메소드인 이름이 component로 시작되는 특별한 메소드도 제공해준다
  • 예제코드
    // taskdataclass.kts
    data class Task(val id: Int, val name: String, val completed: Boolean, val assigned: Boolean)
    
    val task1 = Task(1, "Create Project", false, true)
    println(task1) // Task(id=1, name=Create Project, completed=false, assigned=true)
    println("name: ${task1.name}") // name: Create Proejct
    
    val task1Completed = task1.copy(completed = true, assigned = false)
    println(task1Completed) // Task(id=1, name=Create Project, completed=true, assigned=false )
    println(task1 === task1Completed) // false
    
    val (id, _, _, isAssigned) = task1
    println("Id: $id Assigned: $isAssigned") //Id: 1 Assigned: true​

    • copy 메서드
      • primitive type과 참조에 대한 쉘로우 카피만 가능하다, deep copy는 되지않는다 ( 조심해야함 )
    • componentN()
      • 마지막 구조분해를 살펴보자
      • 코틀린에서는 주 생성자에 전달되는 프로퍼티 순서에 기반한다 ( 즉 순서가 섞이거나, 추가, 제거된다면 다른 결과를 낼 수 있다... )
  • 데이터 클래스 선택을 고려해야하는 선택지
    • 행동, 동작보다는 데이터 자체에 집중된 모델링을 할 경우
    • equals(), hashcode(), toString()과 copy()가 생성되길 원하거나 copy()만 생성되길 원할 경우 
    • 주 생성자에 적어도 하나 이상의 속성이 포함되어야 할 경우 ( 아규먼트가 없는 생성자는 데이터 클래스에서 불가능 )
    • 주 생성자를 속성만으로 구성해야 할 경우
    • 구조분해 기능을 이용해서 데이터를 쉽게 추출하고 싶은 경우 ( 데이터 추출은 속성의 이름이 아닌 순서 기반이다, 한계... )
728x90
반응형