Class
1 Class
1.1 클래스 정의
class Person(val name:String)
동일한 Java
public class Person{
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
1.2 프로퍼티
- 자바에서는 필드와 접근자를 한데 묶어 프로퍼티라고 한다.
- 코틀린은 프로퍼티를 언어 기본 기능으로 제공한다.
- 코틀린의 프로퍼티는 자바의 필드와 접근자 메서드를 완전히 대신한다
- val로 선언한 프로퍼티는 읽기 전용이다.
- 비공개 필드와 public getter를 만들어낸다.
- var로 선언한 프로퍼티는 읽기/쓰기가 가능하다.
- 비공개 필드와 public setter와 getter를 만들어 낸다.
1.3 커스텀 접근자
- 직사각형 클래스인 Rectangle을 정의하면서 자신이 정사각형인지 알려주는 기능을 만들어보자.
class Rectangle(val height:Int, val width:Int) {
val isSquare: Boolean
get () {
return height == width
}
}
- 정사각형인지를 별도의 필드에 저장할 필요가 없다.
- isSquare 프로퍼티에는 자체 값을 저장하는 필드가 필요없다.
- 이 프로퍼티에는 자체 구현을 제공하는 getter만 존재한다.
- 클라이언트가 프로퍼티에 접근할 때마다 getter가 프로퍼티 값을 매번 계산한다.
2 자바 클래스와 차이점
- 코틀린의 클래스는 기본적으로
final
,pulic
이다. - 중첩 클래스는 기본적으로 내부 클래스가 아니다.
- 외부 클래스에 대한 참조가 없다.
3 abstract class
- abstract로 선언한 클래스는 인스턴스화할 수 없다.
- 추상 멤버는 항상 열려있어 추상 멤버 앞에
open
변경자를 명시할 필요가 없다.
예시
- 추상 클래스에는 abstract 변경자를 붙인다.
- 추상 메서드에도 abstract 변경자를 붙인다.
- 메서드에 abstract 변경자를 붙이지 않으면 추상 메서드가 아니다.
abstract class Animated {
// 추상 메서드
abstract fun animate()
// 비추상 메서드도 기본적으로 final이기 때문에 원한다면 open을 명시해야 함
open fun stopAnimating() {}
// 비추상 메서드 기본적으로 final
fun animateTwice() {}
}
4 nested class
- 자바의 nested class는 기본적으로 Inner Class로 선언된다.
- 자바의 Inner Class는
static
키워드 없이 선언된 중첩 클래스를 의미한다. - Inner Class 클래스는 암묵적으로 바깥 클래스에 대한 참조를 가진다.
- 자바에서 암묵적인 바깥 클래스에 대한 참조를 없애려면
static
키워드를 사용해야 한다.
- 자바의 Inner Class는
- 코틀린에서는 nested class는 기본적으로 바깥 클래스에 대한 참조가 없다.
- 즉 자바의 Static Nested Class와 같다.
- 자바의 Inner Class처럼 바깥 클래스에 대한 참조 포함하고 싶다면
inner
변경자를 붙여야 한다.
- 코틀린에서는 바깥 클래스의 인스턴스를 가리키는 참조를 표기하는 법이 자바와 다르다.
- 바깥쪽 클래스의 인스턴스에 접근하려면
this@Outer
라고 써야 한다.
- 바깥쪽 클래스의 인스턴스에 접근하려면
5 생성자와 초기화 블록
- 코틀린에서는 클래스의 인스턴스를 생성할 때 사용되는 두 가지 타입의 생성자가 있습니다
- 주 생성자(primary constructor)와 부 생성자(secondary constructor).
- 이들은 클래스의 인스턴스화와 초기화 과정에서 중요한 역할을 합니다.
- constructor 키워드는 주 생성자나 부 생성자를 정의할 때 사용한다.
5.1 주 생성자 (Primary Constructor)
- 클래스를 초기화할 때 주로 사용하는 생성자로 클래스 본문 밖에 정의한다.
- 주 생성자는 생상자 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 두 가지 목적에 쓰인다.
초기화 블록
- init 키워드는 초기화 블록을 시작한다.
- 초기화 블록에는 클래스의 객체가 만들어질 때 실행될 초기화 코드가 들어간다.
- 초기화 블록은 주로 주 생성자와 함께 사용된다.
- 주 생성자는 제한적이기 때문에 별도의 코드를 포함할 수 없어 초기화 블록이 필요하다.
예시
class User constructor(_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
}
간략한 버전
- 프로퍼티를 초기화하는 식이나 초기화 블록 안에서 주 생성자의 파라미터를 참조할 수 있다.
class User(_nickname: String) {
val nickname: String = _nickname
}
더 간결한 버전
- 주 생성자의 파라미터로 프로퍼티를 초기화한다면 그 주 생성자 파라미터 이름 앞에 val을 추가하는 방식으로 프로퍼티 정의화 초기화를 간략히 아래와 같이 쓸 수 있다.
class User(val nickname: String)
5.2 Secondary Constructor
- 클래스 본문 안에 정의한다.
- 일반적으로 코틀린에서 생성자가 여럿 있는 경우가 적다
- 오버로드한 생성자가 필요한 경우 디폴트 파라미터 값과 이름 붙인 인자 문법을 사용해 해결할 수 있다.
6 data class
- 어떤 클래스가 데이터를 저장하는 역할만 수행한다면 toString, equals, hashcode를 반드시 오버라이드해야 한다.
- 코틀린에서는 data 라는 변경자를 클래스 앞에 붙이면 필요한 메서드를 컴파일러가 자동으로 만들어준다.
- data 변경자가 붙은 클래스를 데이터 클래스라고 부른다.
예시
- equals와 hashcode는 주 생성자에 나열된 모든 프로퍼티를 고려해 만들어진다.
- 주 생성자 밖에 정의된 프로퍼티는 고려 대상이 아니다!
data class Client(val name: String, val postalCode: Int)
6.1 data 클래스와 불변성
- data 클래스의 프로퍼티가 꼭 val일 필요는 없다.
- 하지만 data 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 data 클래스를 불변 클래스로 만들길 권장한다.
- HashMap 컨테이너에 data 클래스를 담는 경우 불변성이 필수다.
- 키로 쓰인 data 클래스의 프로퍼티를 변경하면 컨테이너 상태가 잘못될수 있기 때문
- data 클래스를 불변 객체로 더 쉽게 사용할 수 있게 코틀린 컴파일러는 copy라는 메서드를 제공한다.
- 복사본은 원본과 다른 생명주기를 가지며 복사하면서 일부 프로퍼티 값을 바꿀 수 있다.
- 복사복을 제거해도 원본에 전혀 영향을 미치지 않는다.
7 by
- 상속에 의해서 두 객체가 강력하게 결합하는 것을 막는 방법으로 데코레이터 패턴을 사용할 수 있다.
- 데코레이터 패턴의 단점은 준비 코드가 상당히 많이 필요하다는 점이다.
- 코틀린은 이러한 준비 코드 를 컴파일러가 만들어 준다.
- 데코레이터가 기존 클래스 객체의 포워딩하는 메서드를 자동으로 만들어준다.
예시
- 새로운 기능 없이 기존 클래스의 기능을 그대로 쓰는 경우
class DelegatingCollection<T> (
innnerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innnerList {}
- add와 addAll 메서드에 대해서 새로운 기능을 정의한 경우
class CountingSet<T>(
val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet {
var objectsAdded = 0
override fun add(element: T): Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(c: Collection<T>): Boolean {
objectsAdded += c.size
return innerSet.addAll(c)
}
}
데코레이터 패턴
- 클래스에 새로운 동작을 추가해야할 때 데코레이터 패턴을 사용한다.
- 데코레이터 패턴을 사용해 기존 클래스와 같은 인터페이스를 가지는 데코레이터를 만든다.
- 데코레이터 내부에 기존 클래스에 대한 참조를 가지고 있다.
- 컴포지션
- 새로운 기능은 데코레이터의 메서드에 새로 정의한다.
- 기존 클래스의 메서드나 필드를 활용할 수 있다.
- 기존 기능이 필요한 경우 데코레이터 메서드에서 기존 클래스의 메서드에게 요청을 전달한다.(포워딩)
8 object
- object 키워드는 객체를 선언하거나 동반 객체를 선언할 때 사용한다.
- 객체 선언
- 싱글턴을 정의하는 방법 중 하나다.
- 동반 객체
- 인스턴스 메서드는 아니지만 어떤 클래스와 관련있는 메서드와 팩토리 메서드를 담을 때 쓰인다.
- 동반 객체 메서드에 접근할 때는 동반 객체가 포함된 클래스의 이름을 사용한다.
8.1 싱글턴 만들기
- 코틀린은
객체 선언
기능을 통해 싱글턴을 언어에서 기본 지원한다. - 객체 선언(
object
)은 클래스를 정의하고 그 클래스의 인스턴스를 만들어서 변수에 저장하는 모든 작업을 단 한 문장으로 처리한다 - 객체 선언에 생성자를 사용할 수 없다.
- 일반 클래스 인스턴스와 달리 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어진다.
예시
- 객체 선언도 클래스와 인터페이스를 상속할 수 있다.
- 특정 인터페이스를 구현해야 하는데 상태가 필요하지 않은 경우 object 키워드가 유용하다.
import java.util.Comparator
import java.io.File
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(file1: File, file2: File): Int {
return file1.path.compareTo(file2.path, ignoreCase = true)
}
}
fun main(args: Array<String>) {
println(CaseInsensitiveFileComparator.compare(File("/User"), File("/user")))
val files = listOf(File("/Z"), File("/a"))
println(files.sortedWith(CaseInsensitiveFileComparator))
}
8.2 companion object
- 클래스 안에 정의된
object
중 하나에companion
이라는 키워드를 붙이면 그 클래스의 동반 객체로 만들 수 있다.- 동반 객체는 클래스 안에 정의된 일반 객체다.
- 동반 객체의 프로퍼티나 메서드에 접근하려면 동반 객체가 정의된 클래스 이름을 사용한다.
- 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다.
- 따라서 동반 객체는 팩토리 메서드를 구현하기 가장 적합한 위치다.
예시
class A {
companion object {
fun bar() {
println("Companion object called")
}
}
}
fun main(args: Array<String>) {
A.bar()
}
예시
fun getFacebookName(accountId: Int) = "fb:$accountId"
class User private constructor(val nickname: String) {
companion object {
fun newSubscribingUser(email: String) =
User(email.substringBefore('@'))
fun newFacebookUser(accountId: Int) =
User(getFacebookName(accountId))
}
}
fun main(args: Array<String>) {
val subscribingUser = User.newSubscribingUser("bob@gmail.com")
val facebookUser = User.newFacebookUser(4)
println(subscribingUser.nickname)
}
- 생성자의 접근 지시자가 private이므로 팩터리 메서드를 통해서만 인스턴스를 만들 수 있다.
- 클래스 이름을 사용해 동반 객체의 메서드를 호출할 수 있다.
9 sealed class
9 sealed 클래스란?
sealed
클래스는 특정 클래스 계층을 정의할 때 사용된다.sealed
클래스는 상위 클래스의 하위 클래스들을 제한할 수 있다.- 이를 통해 특정 클래스의 하위 클래스를 미리 정의된 집합으로 한정할 수 있다.
9.1 sealed 클래스의 특징
sealed
클래스의 하위 클래스들은 반드시 동일한 파일에 정의되어야 한다.- 이를 통해 클래스 계층의 구조를 명확하게 하고, 코드의 가독성을 높인다.
sealed
클래스는 암시적으로abstract
이므로 직접 인스턴스화할 수 없다.sealed
클래스의 하위 클래스들은 상위 클래스의 모든 추상 멤버를 구현해야 한다.sealed
클래스로 표시된 클래스는 자동으로open
이 되어 하위 클래스에서 상속이 가능하다.
9.2 사용 예시
sealed
클래스를 사용하여 결과 타입을 정의할 수 있다.- 아래 예시는 성공(Success)과 오류(Error) 상태를 나타내는
Response
클래스를 정의한다.
sealed class Response<out T> {
data class Success<out T>(val data: T) : Response<T>()
data class Error(
val errorCode: ErrorCode,
val errorMessage: String,
val fieldErrors: List<FieldError> = emptyList()
) : Response<Nothing>()
}
data class FieldError(
val field: String,
val message: String
)
enum class ErrorCode {
INVALID_REQUEST,
NOT_FOUND,
INTERNAL_ERROR
}
Response
클래스는 성공 응답과 오류 응답을 포함하는 sealed 클래스이다.Success
클래스는 데이터만 포함하며,Error
클래스는 오류 코드, 메시지, 필드 오류를 포함한다.
9.3 sealed 클래스의 장점
sealed
클래스는 컴파일러가 모든 하위 클래스를 알고 있어,when
구문에서 모든 경우를 처리하도록 강제할 수 있다.- 이는
when
구문에서 누락된 하위 클래스에 대해 컴파일러가 경고를 제공할 수 있음을 의미한다. sealed
클래스를 사용하면 디폴트 분기를 사용하지 않아도 된다.
예시
fun handleResponse(response: Response<Any>) {
when(response) {
is Response.Success -> {
println("Success: ${response.data}")
}
is Response.Error -> {
println("Error: ${response.errorCode}, ${response.errorMessage}")
}
}
}
Response
클래스의 모든 하위 클래스가 처리되었는지 확인할 수 있다.handleResponse
함수는Response
타입의 모든 하위 클래스에 대해 적절히 처리한다.
9.4 sealed 인터페이스와 클래스
- 코틀린 1.5부터
sealed
인터페이스를 사용할 수 있다. - 인터페이스도 클래스와 마찬가지로 특정 구현체들을 제한할 수 있다.
예시
sealed interface Shape
data class Circle(val radius: Double) : Shape
data class Rectangle(val height: Double, val width: Double) : Shape
Shape
인터페이스를 상속하는Circle
과Rectangle
클래스는 반드시 동일한 파일에 정의되어야 한다.- 이는
sealed
클래스와 동일한 방식으로 작동한다.