Today I Learned

객체와 클래스 본문

Kotlin/다재다능 코틀린 프로그래밍

객체와 클래스

하이라이터 2023. 8. 1. 16:38
728x90

객체와 싱글톤

객체 표현식으로 사용하는 익명 객체

  • 코틀린의 객체 표현식은 Java의 익명 클래스를 생성하는 데 유용하게 쓰이지만, JavaScript의 객체, C#의 익명 타입과 비슷하다.
fun drawCircle() {
  val circle = obejct { // an expression
    val x = 10
    val y = 20
    val radius = 30
  }
  println("Circle x: ${circle.x} y: ${circle.y} radius: ${circle.radius}")
}
drawCircle()
  • 객체 표현식은 지역변수들을 그룹핑 할때 유용하지만, 다음과 같은 한계를 가진다.
    • 익명 객체의 내부 타입은 함수나 메소드의 리턴 타입이 될 수 없다.
    • 익명 객체의 내부 타입은 함수나 메소드의 파라미터가 될 수 없다.
    • 클래스 안에 저장된 속성들이 있다면, 해당 속성들은 Any로 간주된다. 그러면 모든 속성이나 메소드에 직접 접근할 수 없게 된다.
  • 작은 변화만 주면 익명 객체는 인터페이스의 구현체가 된다.
fun createRunnable(): Runnable {
  val runnable = object: Runnable {
    override fun run() { println("You called...") }
  }
  return runnable
}

val aRunnable = createRunnable()
aRunnable.run() //You called...
  • 함수에서 생성된 인스턴스를 리턴할 수도 있고, 익명 내부 객체가 구현하고 있는 인터페이스를 리턴 타입으로 사용할 수도 있다.
  • 인스턴스를 인터페이스가 아니라 클래스로 확장한 경우라면 타입으로 사용될 수도 있다.
  • 익명 내부 클래스에 의해 구현된 인터페이스가 싱글 추상 메소드 인터페이스라면 다음과 같이 메소드 일름을 명명하지 않고 바로 구현할 수 있다.
fun createRunnable(): Runnable = Runnable {println("You called...") }
  • 익명 내구 클래스가 둘 이상의 인터페이스를 구현해야 한다면 리턴이 필요한 경우에는 반드시 리턴할 인스턴스 타입을 명시해줘야 한다.
fun createRunnable(): Runnable = object: Runnable, AutoCloseable {
  override fun run() {println("You called...") }
  override fun close() {println("closing...") }
}

 

객체 선언을 위한 싱글톤

  • object 키워드와 {} 블록 사이에 이름을 넣으면, 명령문 또는 선언으로 인식한다.
  • 익명 이너클래스의 인스턴스는 객체 표현식으로 만들고, 싱글톤은 객체선언을 사용한다.
object Util {
  fun numberOfProcessors() = Runtime.getRuntime().availableProcessors()
}

println(Util.numberOfProcessors())
  • Util은 이미 싱글톤 객체인 상태이며 클래스로 취급하지 않는다.
  • 싱글톤은 메소드 뿐만 아니라 val과 var로 선언된 속성 모두 가질 수 있다.
  • 싱글톤이 부모 클래스나 인터페이스를 가지고 있다면 싱글톤은 참조로 할당되거나 부모 타입의 파라미터로 전달될 수 있다.
object Sun : Runnable {
  val radiusInKM = 696000
  var coreTemperatureInC = 15000000
  override fun run() { println("spin...") }
}

fun moveIt(runnable: Runnable) {
  runnable.run()
}

println(Sun.radiusInKM) //696000
moveIt(Sun) //spin..

 

탑레벨 함수 vs 싱글톤

  • 다음 예제는 탑레벨 함수와 싱글톤이 모두 포함되어 있는 패키지이다.
package com.agiledeveloper.util
fun unitsSupported() = listOf("Mertric", "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
}
  • package 키워드를 이용해서 패키지를 정의하고 파일에 직접 탑레벨 함수들을 정의했다.
  • 그리고 관련있는 메소드들을 그룹핑한 싱글톤을 만들었다.
  • 이제 com.agiledeveloper.util 패키지의 메소드들을 사용해보자.
package com.agiledeveloper.use
import dom.agiledeveloper.util.*
import dom.agiledeveloper.util.Temperature.c2f

fun main(){
  println(unitSupported())
  println(Temperature.f2c(75.253))
  println(c2f(24.305))
}
  • 싱글톤의 c2f()는 이미 import 해두었기 때문에 검증하지 않고, f2c() 메소드를 호출할땐 Temperature로 메소드명을 검증해준다.
  • 사용할 함수들이 하이레벨이거나 일반적이거나 넓게 사용될 예정이라면 패키지 안에 직접 넣어서 탑레벨 함수로 사용하는게 좋고,
    반대로 함수들이 연관되어 있다면 싱글톤을 사용하는 게 좋다.

 

클래스 생성

  • Java에서 클래스를 작성하려면 매우 많은 보일러플레이트 코드가 필요하지만, 코틀린은 코드 생성을 IDE에서 컴파일러로 옮겨버렸다.

 

읽기전용 속성

class Car(val yearOfMake: Int)
  • 이 간결한 문법을 통해서 생성자가 정수형 파라미터를 받도록 만들었고, yearOfMake라는 이름으로 Int 타입의 읽기전용 속성을 만들었다.
  • 코틀린 컴파일러는 생성자를 작성했고, 필드를 정의하고, 해당 필드에 접근하게 해주는 getter를 추가헀다.
  • 위 코드는 아래의 내용을 축약해서 적은 것이다.
public class Car public constructor(public val yearOfMake: Int)
  • 기본적으로 클래스의 멤버는 생성자의 멤버와 같고, constructor 키워드는 접근제어자나 주 생성자용 표기를 사용할 게 아니라면 필요없다.

 

인스턴스 생성하기

  • 코틀린에서 객체를 만들때는 new 키워드를 사용하지 않고 함수를 사용하듯 그냥 클래스 이름을 이용한다.
val car = Car(2019)
println(car.yearOfMake) //2019

 

읽기-쓰기 속성

  • 원한다면 var를 사용해서 변경가능한 속성으로 만들 수 있다.
class Car(val yearOfMake: Int, var color: String)
val car = Car(2019, "red")
car.color = "green"
println(car.color) //green

 

들여다보기 - 필드와 속성

  • 앞선 예제에서 yearOfMake와 color는 Java의 관점에서 봤을때 속성이라기보다는 필드에 가깝다.
  • 하지만 이것들은 모두 속성이며, 코틀린에서는 클래스에 필드가 없다.
  • car.yearOfMake를 호출하면 실제로는 car.getYearOfMake()를 호출한 것이다.

 

속성 제어 변경

class Car(val yearOfMake: Int, theColor: String) {
  var color = theColor
  set(value) {
    if(value.isBlank()) {
      throw RuntimeException("empty")
    }
    field = value
  }
}
  • 코틀린에서 개발자는 필드를 만들 수 없으며, 커스텀 getter와 setter를 가진 필드를 정의하고 field 키워드를 사용한 백킹 필드를 사용하지 않는다면 백킹 필드가 생성되지 않는다.
  • color 속성은 getter만 만들고 setter는 제공되는 코드를 사용하게 된다.
  • 전달받은 값이 사용 가능하다면 값을 스페셜 키워드인 field에 의해서 참조되고 있는 필드에 할당한다.

 

접근 제어자

  • 코틀린에는 public, private, protected, internal 네 개의 접근 제어자가 있다.
  • proteced는 파생 클래스의 메소드가 속성에 접근할 수 있는 권한을 준다.
  • internal은 같은 모듈(함께 컴파일된 소스코드)에 있는 모든 코드에서 속성이나 메소드에 접근이 가능하다.
  • getter의 접근 권한은 속성의 접근 권한과 동일하지만, setter은 원하는 대로 접근 권한을 설정할 수 있다.
  var fuelLevel = 100
  private set //setter의 접근 제어

 

초기화 코드

  • 코틀린은 생성자용 바디를 만들기위해 init 블록을 제공한다.
  • 클래스는 0개 이상의 init 블록을 가질 수있으며, 이 블록들은 주 생성자의 실행의 한 부분으로써 실행된다.
class Car(val yearOfMake: Int, theColor: String) {
  var fuelLevel = 100
    private set
  var color = theColor
  set(value) {
    if(value.isBlank()) {
      throw RuntimeException("empty")
    }
    field = value
  }
  
  init {
    if (yearOfMake <2020) { fuelLevel = 90}
  }
}
  • init 블록 대신 fuelLevel을 정의하는 시점에 처리하려면 다음과 같이 사용할 수 있다.
var fuelLevel = if (yearOfMake <2020) 90 else 100
  private set

 

보조생성자

  • 주생성자 외에 만들어진 생성자를 보조 생성자라고 하며, 보조 생성자는 주 생성자를 호출하거나, 다른 보조 생성자를 호출해야만 한다.
  • 보조 생성자에서는 속성을 선언할 수 없으며, 주 생성자와 클래스 내부에서만 속성을 정의할 수 있다.
class Person(val first: String, val last: String) {
  val fulltime = true
  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() = "$first $last $fulltile $location"
}

 

인스턴스 메소드 정의

  • 클래스 안의 메소드를 정의할 때는 fun 키워드를 사용한다.
  • 메소드는 기본적으로 public이고 private, protected, internal 키워드로 권한을 설정할 수 있다.
class Person(val first: String, val last: String) {
  //...
  internal fun fullName() = "$last, $first"
  private fun yearOfService(): Int =
  throw RuntimeException("Not implemented yet")
}
val jane = Person("Jane","Doe")
println(jane.fullName()) //Doe, Jane
//jave.yearsOfService() //ERROR: private 메소드에 접근할 수 없다.

 

인라인 클래스

  • inline 클래스는 컴파일 시간에는 클래스의 장점을 취할 수 있고, 실행 시간에는 프리미티브 타입으로 취급되어 함수 호출로 인한 오버헤드가 발생하지 않는다.
inline class SSN(val id: String)
fun receiveSSN(ssn: SSN) {
  println("Received $ssn")
}

 

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

  • 컴패니언 객체는 클래스 안에 정의한 싱글톤이다.
  • 컴패니언 객체는 인터페이스를 구현할 수도 있고 다른 클래스를 확장할 수도 있다.

 

클래스 레벨 멤버

class MachineOperator(val name: String) {
  fun checkin() = checkedIn++
  fun checkout() = checkedIn--
  companion object {
    var checkedIn = 0
    fun minimumBreak() = "15 minutes every 2 hours"
  }
}
  • 클래스에서 컴패니언 객체는 companion Object 키워드를 이용해서 정의되었다.
  • 컴패네이넌 객체 안에 있는 속성 checkedIn은 MachineOperator 클래스의 클래스 레벨 속성이 되었다.
  • 클래스 이름을 참조하여 클래스의 컴패니언 객체의 멤버에 접근할 수 있다.
MachineOperator("Mater").checkin()
println(MachineOperator.minimumBreak())  // 15 minutes every 2 hours
println(MachineOperator.checkedIn) // 1
  • 단, 멀티 스레드인 경우 컴패니언 객체에 뮤터블 속성을 사용하면 스레드 안정성 문제가 발생할 수 있으니 주의해야 한다.

 

컴패니언에 접근하기

  • 컴패니언 객체의 속성이 아닌 컴패니언 객체 자체의 참조가 필요한 경우 , 클래스에 .Companion을 붙여서 접근할 수 있다.
var ref = MachineOperator.Companion
  • 클래스 컴패니언을 빈번하게 사용해야 한다면 직접 이름을 지정해줄 수도 있다.
//companion object {
companion object MahineOperatorFactory {
  var checkedIn = 0
  // ...
}}

val ref MachineOperator.MahineOperatorFactory

 

팩토리로 사용하는 컴패니언

  • 컴패니언을 팩토리로 사용하기 위해서는 클래스에 private 생성자를 만들고, 컴패니언 객체에서 생성된 인스턴스를 리턴하기 전에 인스턴스를 처리하는 메소드를 하나 이상 생성해야 한다.
class MachineOperator private constructor(val name: String) {
  //...
  companion object {
    //...
    fun create(name: String): MachineOperaotr {
      var instance = MachineOperaotr(name)
      instance.checkin()
      return instance
    }
  }
}
  • MachineOperaotr 클래스의 생성자가 private가 되었고, 클래스 외부에서는 어떤 인스턴스도 직접 생성할 수 없다.
  • create() 메소드가 인스턴스를 생성하는 유일한 방법이기 때문에 컴패니언 객체가 클래스의 팩토리로서 동작한다.

 

static과는 다르다

  • 동작하는 방식을 보면 컴패니언 객체의 멤버가 static 멤버가 되는 것처럼 보이지만 그렇지 않다.

 

제네릭 클래스의 생성

  • 제네릭 클래스는 타입 안정성을 지키면서 일반화를 할 때 사용된다.
  • 코틀린의 제네릭 기능은 여러 부분에서 Java와 유사하지만, 가변성과 제약들은 다르게 선언되었다.
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() = "${first}, ${second}"
}
println(PriorityPair(2,1)) //2,1
println(PriorityPair("A","B")) //B,A
  • PriorityPair<T>의 인스턴스는 Comparable<T>의 인터페이스를 구현하고 있는 타입이라면 어떤 타입이든 이용해서 만들 수 있다.

 

데이터 클래스

  • data class는 특정한 행동, 동작보다는 데이터를 옮기는 데 특화된 클래스이다.
  • 데이터 클래스에선 val이나 var를 사용한 속성 정의가 하나 이상 필요하며, val이나 var가 아닌 파라미터는 사용할 수 없다.
  • 데이터 클래스에는 자동으로 equals(), hasCode(), toString() 메서드가 만들어지며, copy() 메서드도 제공된다.
  • 주 생성자에 의해서 정의된 각각의 속성에 접근할 수 있게 해주는 component로 시작하는 이름의 메서드도 제공된다.
data class Task(val id: Int, val name: String,
  val completed: Boolean, val assinged: Boolean)
val task1 = Task(1, "Create Project", false, true)
println(task1) // Task(id=1,name=Create Project, completed=false, assinged=true)
println("Name: #{task1.name}") // Name: Create Project

val task1Completed = Task(completed=true, assigned=false)
println(task1Completed) // Task(id=1,name=Create Project, completed=true, assinged=false)
  • copy() 함수는 프리미티브 참조에 대한 쉘로우 카피만 가능하며, 객체 내부의 참조들은 딥카피 되지 않는다.
  • componentN() 메소드의 주된 목적은 구조분해이다.
  • 구조분해를 사용해서 주 생성자가 만든 속성 순서와 동일한 속성 순서로 속성을 추출할 수 있다.
val (id, _, _, isAssigned) = task1
println("Id: $id Assinged: $isAssigned") //Id:1 Assinged: true
  • 만약 뒤에 더 많은 속성이 있었더라도 사용하지 않는다면 언더 스코어를 쓰지 않더라도 무시된다.
  • 코틀린의 구조 분해는 이름이 아닌 주 생성자에 전달되는 프로퍼티 순서에 기반하기 때문에, 개발자가 현재 파라미터 사이에 새로운 파라미터를 넣게 된다면 구조 분해에 심각한 영향을 끼친다.
728x90
Comments