다재다능 코틀린 프로그래밍 책으로 코틀린 스터디를 진행하면서 발표를 위해 준비했던 글입니다.
객체와 싱글톤
- 자바에서 싱글톤은 구현하기 까다로운 디자인 패턴이다, 방법도 여러가지고..
- 코틀린에서는 싱글톤을 직접 지원한다.
(역시 사기언어) - 클래스를 정의할 것인가 정의없이 객체를 생성할 것인가는 개발자의 선택이다.
- ex) 추상클래스를 정의해야하는 복잡한 상황에선 클래스를 정의할 수 있다.
객체 표현식으로 사용하는 익명 객체
// 객채 표현식으로 원을 표현할 데이터를 가진 circle 익명 객체 생성
fun createCircle() {
val circle = object {
val x = 10;
val y = 20;
val radius = 30;
}
...
}
- 익명 객체에 메소드를 추가할 수도 있다. 근데 그럴바엔 그냥 클래스를 정의하자.
- 대부분의 기초적인 객체 표현식은 지역변수를 그룹핑하고 의도를 표현할 때만 유용하다.
// 객체 표현식으로 인터페이스 구현체 만들기
}fun createRunnable(): Runnable {
return object: Runnable {
override fun run() { println("run") }
}
}
createRunnable().run()
// 싱글 추상 메소드 인터페이스(자바에선 함수형 인터페이스)는 이렇게도 가능
fun createRunnable2(): Runnable = Runnable { println("run") }
createRunnable2().run()
// 둘 이상의 인터페이스 구현 -> 리턴 타입을 꼭 명시해줘야한다.
fun createRunnable3(): Runnable = object: Runnable, AutoCloseable {
override fun run() { println("run") }
override fun close() { println("close") }
}
createRunnable3().run()
createRunnable3().close() // 당연 ERROR
객체 선언을 이용한 싱글톤
- object 키워드와 {} 블록 사이에 이름을 넣는다면 명령문 또는 선언으로 인식
- 코틀린의 대표적인 싱글톤 Unit
- 싱글톤은 객체 선언을 아래 예제와 같이 객체 선언을 사용해라.
// 객체 선언을 이용해서 만든 Util 싱글톤 객체
object Util {
fun numberOfProcessors() = Runtime.getRuntime().availableProcessors()
}
println(Util.numberOfProcessors()) //16
- 코틀린 컴파일러는 Util을 클래스로 취급하지 않아서 Util로는 객체를 생성할 수 없다.
- 자바의 유틸 클래스라고 생각해라.
- 코틀린에서는 내부적으로 싱글톤 객체를 Util 클래스의 static 인스턴스라고 표현
- 그러나
@JvmStatic
을 사용하지 않으면 바이트코드에서는 static이 되지 않는다. 이 내용은 뒤에서 설명 예정 - 당연히 뮤터블한 상태는 두면 안된다.
탑레벨 함수 vs 싱글톤
package chapter07.util
// 탑레벨 함수
fun unitsSupported() = listOf("A", "B")
// 싱글톤
object Temperature {
fun add(c: Int) = c + c
}
package chapter07.use
import chapter07.util.Temperature as Singleton // 패키지명에 얼라이어스 사용
import chapter07.util.*
fun main() {
println(unitsSupported())
println(Singleton.add(3))
}
- 사용할 함수들이 하이레벨이거나 일반적이거나 넓게 사용될 예정이라면 패키지 안에 직접 넣어 탑레벨 함수로 사용하자.
- 함수들이 연관되어 있다면 싱글톤을 사용하는게 의도 표현에 좋다.
- 함수들이 상태와 연관되어 있다면 클래스로 분리하자. 상태가 상수면 무관
클래스 생성
class Car(val yearOfMake: Int)
- yearOfMake, Int형 읽기 전용 속성 생성
- 생성자 자동 생성
- getter 자동 생성
인스턴스 생성
val car = Car(2019)
println(car.yearOfMake) // 2019
- new 키워드가 없다.
필드와 속성
class Car(val yearOfMake: Int, var color: String)
// 바이트코드의 일부
public final class Car {
private final int yearOfMake;
private java.lang.String color;
public final int getYearOfMake();
public final java.lang.String getColor();
public final void setColor(java.lang.String);
pulbic Car(int, java.lang.Stirng);
}
- 코틀린에선 클래스에 필드가 없다. 속성이다.
필드와 속성의 차이: 필드는 클래스에 비공개로 유지되고 get 및 set 속성을 통해 액세스 속성은 클래스를 사용하는 것들에 의해 액세스되는 외부 방식에 영향을 미치지 않으면 서 필드를 변경할 수있는 추상화 수준을 제공
-
car.yearOfMake
는 실제로는car.getYearOfMake()
를 호출한 것 → 코틀린은 캡슐화를 깨뜨리지 않는다. - val로 선언한 속성은 getter만 생성
- var로 선언한 속성은 getter,setter 생성
- 선언한 생성자 자동생성
속성 제어 변경
class Car(val yearOfMake: Int, theColor: String) {
var fuelLevel = 100
var color = theColor
set(value) {
if (value.isBlank()) {
throw RuntimeException("no empty!")
}
field = value
}
}
fun main() {
val car = Car(2019, "RED")
car.color = "" // 에러 발생
}
- setter나 getter 중 하나만 작성하면 백킹 필드가 자동 생성된다. 개발자가 생성하는 것은 아니다.
- 같은 이름의 속성과 파라미터를 사용하지말자. this를 사용하지 않으니 더 혼란스럽다.
- 코틀린이 필드를 내부적으로 만들었기 때문에 코드에서는 getter나 setter에 있는 field 키워드를 통해서만 필드를 사용할 수 있다.
접근 제어자
- 코틀린은 자바에서 제공하는 public, private, protected 와 더불어 internal 접근 제어자를 제공한다.
- internal은 같은 모듈에 있는 모든 코드에서 속성이나 메소드에 접근 가능하게 해줌
- internal은 바이트코드에 직접 나타나지 않고 네이밍 컨벤션을 이용해서 코틀린 컴파일러에 의해 다뤄지므로 실행시간 오버헤드가 전혀 없다.
- getter의 적븐 권한은 속성의 접근 권한과 동일하지만 setter의 경우엔 아래 예제와 같이 개발자가 직접 설정해줄 수 있다.
var fuelLevel = 100
private set
초기화 코드
class Car(val yearOfMake: Int, theColor: String) {
var fuelLevel = 100
private set
var color = theColor
set(value) {
if (value.isBlank()) {
throw RuntimeException("no empty!")
}
field = value
}
init {
if (yearOfMake < 2020) {
fuelLevel = 90
}
}
}
- 클래스는 0개 이상의 init 블록을 가질 수 있으며 top-down으로 순차적으로 실행된다.
- init 블록을 속성 선언 아래에 위치시키면 어디에서든 속성과 파라미터를 사용할 수 있다.
- 가급적 init 블록은 만들지 말고 만들더라도 1개만 만들어라. 생성자에서 최대한 아무런 작업도 안하는 것이 프로그램의 안정성과 퍼포먼스 측면 모두에서 장점이 크다.
보조 생성자
- 주 생성자에 모든 파라미터가 디폴트값을 가지고있으면 주 생성자와 함께 아규먼트가 없는 기본 생성자를 생성해준다.
- 보조 생성자는 주 생성자를 호출하거나 다른 보조 생성자를 호출해야만 한다.
- 보조 생성자의 파라미터는 val, var를 사용할 수 없다.
- 보조 생성자에서는 속성을 선언할 수 없다.
class Person(val first: String = "junseong", val last: String = "hong") {
var 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 $fullTime $location"
}
fun main() {
println(Person()) // 자동 생성된 기본 생성자 사용, junseong hong true -
println(Person("Jane", "Doe")) // Jane Doe true -
println(Person("Jane", "Doe", false)) // Jane Doe false -
println(Person("Jane", "Doe", "home")) // Jane Doe false home
}
인라인 클래스
- 프리미티브 타입을 Wrapping 클래스로 사용하면 의도가 명확해지고 안정성이 생기지만 객체 생성과 메모리 사용에 관한 오버해드와 함께 퍼포먼스가 나빠질 가능 성이 있다.
- inline 클래스는 아직 실험적인 기능이지만 컴파일 시간에는 Wrapping 클래스의 장점을 취할 수 있고 실행 시간에는 프리미티브 타입으로 취급된다. → 바이트코드로 변화됐을 때 프리미티브 타입으로 변경
inline class SSN(val id: String)
fun receiveSSN(ssn: SSN) {
println("$ssn")
}
- 위 코드에서
receiveSSN
메소드는 컴파일하면 SSN이 아닌 String을 받도록 변경되지만 코틀린 컴파일러는 SSN 인스턴스를 사용했는지 검증한다. - inline 클래스 기능은 기본 클래스와 동일하지만 내부를 살펴보면 메소드는 프리미티브 타입을 받는 static 메소드가 inline 클래스로 둘러싸여있다.
컴패니언 객체와 클래스 멤버
- 코틀린은 static 메소드를 만들 수 없다. 대신 컴패니언 객체를 사용할 수 있다.
- 컴패니언 객체는 클래스 안에 정의한 싱글톤이다.
- 컴패니언 객체는 인터페이스를 구현할 수도 있고 다른 클래스를 확장할 수도 있고 코드 재사용에도 유용하다.
- 클래스 변수와 클래스 메소드를 컴패니언 객체에 몰아서 정의한 느낌
class Machine(val name: String) {
fun checkIn() = checkedIn++
fun checkOut() = checkedIn--
companion object {
var checkedIn = 0
fun minimumBreak() = "15 minutes"
}
}
Machine("Mater").checkIn()
println(Machine.minimumBreak())
println(Machine.checkedIn)
컴패니언 접근
// 컴패니언 객체 자체의 참조에 접근하기
var ref = Machine.Companion
- 컴패니언 객체에 이름을 지정해주면 더 접근하기 수월하다.
class Machine(val name: String) {
companion object MachineFactory{
...
}
}
var ref = Machine.MachineFactory
팩토리로 사용하는 컴패니언
- 컴패니언 객체를 Java의 static factory method 처럼 사용할 수 있다.
class Machine(val name: String) {
...
companion object MachineFactory {
...
fun withName(name: String): Machine {
return Machine(name)
}
}
}
컴패니언 객체와 Static은 다르다.
- 컴패니언 객체의 멤버에 접근하면 코틀린 컴파일러는 싱글톤 객체로 라우팅한다.
- Java와의 상호 운용성 측면에서 문제를 야기할 수 있지만 @표기가 문제를 해결해준다. → 17-3 static 메소드 생성에서 배울 예정
제네릭 클래스 생성
class PriorityPari<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
}
}
}
- compareTo() 메소드를 직접 사용하는 대신
>=
연산자를 사용해서 compareTo 메소드를 사용했다.- 비교연산자 >, <, >=, <= 는 compareTo 호출로 컴파일 된다.
데이터 클래스
- 데이터 클래스에선 val, var가 아닌 파라미터는 사용할 수 없다. 필요하면 바디 {}안에 속성이나 메소드를 추가할 수 있다.
- 데이터 클래스에 equals(), hashCode(), toString(), copy() 메소드를 자동으로 만들어준다.
- 주생성자에 의해 정의된 각 속성에 접근할 수 있게 해주는 component 메소드들도 제공하고 componentN()이라고 부른다
data class Task(val id: Int, val name: String)
val task1 = Task(1, "일기쓰기")
val task2 = task1.copy(id = 2)
println(task1) // Task(id=1, name=일기쓰기)
println(task2) // Task(id=2, name=일기쓰기)
- 자동으로 만들어준 toString()과 새로운 값으로 업데이트해서 복사해주는 copy 메소드를 확인하자.
- copy 메소드는 프리미티브 참조에 대한 쉘로우 카피만 가능하다.
- componentN 메소드는 구조분해를 위해 주로 사용한다.
val task1 = Task(1, "일기쓰기")
val (_, name) = task1
println(name)
- js에서의 구조분해는 객체 속성 이름에 기반하는데 반해 코틀린은 주 생성자 파라미터 순서에 기반한다.
- 파라미터 순서가 바뀐다면 심각한 영향을 끼친다.
- 자동화 테스트를 강화하자!
- 순서가 안바뀔 것 같은 data class에만 구조분해를 사용하자.
- 혹시 모르니 생성자의 추가된 속성은 파라미터 맨 뒤에 위치시키자?(아닐지도..)
'스터디 > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글
9. 델리게이션을 통한 확장 (0) | 2021.11.29 |
---|---|
8. 클래스 계층과 상속 (0) | 2021.11.29 |
6. 오류를 예방하는 타입 안전성 (0) | 2021.11.29 |
5. 콜렉션 사용하기 (0) | 2021.11.29 |
4. 외부 반복과 아규먼트 매칭 (0) | 2021.11.29 |