Index
- 코틀린에서 클래스를 다루는 방법
- 코틀린에서 상속을 다루는 방법
- 코틀린에서 접근 제어를 다루는 방법
- 코틀린에서 object 키워드를 다루는 방법
- 코틀린에서 중첩 클래스를 다루는 방법
- 코틀린에서 다양한 클래스를 다루는 방법
코틀린에서 클래스를 다루는 방법
클래스와 프로퍼티
코틀린에서 클래스는 아래와 같이 선언한다.
class Person(
val name: String,
var age: Int,
)
클래스 옆 괄호()
는 자바 생성자 역할을 한다. 코틀린에서는 val은 getter, var는 getter, setter를 모두 자동으로 생성하기 때문에 필드라 부르지 않고
프로퍼티라고 부른다.
fun main() {
val person = Person("고범석", 28)
println(person.name) // getter
person.age = 20 // setter
}
생성자와 init
코틀린에서의 생성자의 로직은 아래와 같다.
class Person (
val name: String,
var age: Int,
) {
init { // 초기화 블록, 생성자가 호출될 때 초기화되는 블록
if (age <= 0) {
throw IllegalArgumentException("...")
}
}
}
추가적인 생성자가 필요하다면 다음과 같이 작성한다.
fun main() {
val person = Person()
}
class Person (
val name: String,
var age: Int,
) {
init {
if (age <= 0) {
throw IllegalArgumentException("1살 이상이어야 합니다.")
}
println("초기화 블록")
}
// 부생성자, 이 this는 밑의 파라미터 한개짜리 부생성자를 호출
constructor(): this("홍길동") {
println("파라미터 없는 부생성자")
}
constructor(name: String): this(name, 1) {
println("파라미터 1개 부생성자")
}
}
그렇다면 init과 부생성자가 여러 개면 어떤 순서대로 호출될까?
초기화 블록
파라미터 1개 부생성자
파라미터 없는 부생성자
코틀린에서는 먼저 초기화 블록을 먼저 실행한 다음 알맞은 부생성자들을 호출한다.
코틀린은 부생성자를 여러개 만드는 것보다 디폴트 파라미터 사용을 권장한다. 그리고 다른 객체를 받아서 객체를 생성하는 경우는 정적 팩토리 메서드 패턴을 권장한다.
커스텀 getter, setter
코틀린에서 클래스에 프로퍼티가 있는 것처럼 작성할 수 있다. 이를 custom getter라 한다.
// 기본적인 함수
fun isAdult(): Boolean = this.age >= 20
// custom getter, 프로퍼티처럼 보이게 한다.
val isAdult: Boolean
get() = this.age >= 20 // Expression이라 바로 반환 가능
그렇다면 함수, custom getter를 언제 사용해야할까?
- 객체의 속성이라면 custom getter
- 그렇지 않고 기능처럼 접근해야하면 함수를 사용하자
backing field
만약, 이름을 get할 때 항상 대문자로만 가져와야하는 요구사항이 있다고 가정하면 다음과 같이 custom getter를 활용할 수 있다.
class Person (
name: String,
var age: Int = 1,
) {
val name: String = name
// field?
get() = field.toUpperCase()
}
field 예약어?
- 코틀린은
클래스.프로퍼티명
을 통해 getter를 호출한다. - 위 코드에서 get() = name.toUpperCase()를 호출할 때 name부분도 결국 getter를 호출한다.
- 무한루프가 일어나게 되는데 이를 field라는 예약어를 통해 필드를 호출한다.
- 이를 코틀린에서 자기 자신을 가리키는 보이지 않는 필드다. 라고 하여 backing field라고 부른다.
코틀린에서 상속을 다루는 방법
추상 클래스
자바, 코틀린 모두 추상클래스를 인스턴스화 할 수 없다.
abstract class Animal(
protected val species: String,
protected open val legCount: Int,
) {
abstract fun move()
}
class Cat(
species: String
): Animal(species, 4) // 추상 클래스 생성자 호출 {
// override 지시어 사용
override fun move() {
println("고양이가 움직임")
}
}
class Penguin(
species: String
): Animal(species, 2) {
private val wingCount: Int = 2
override fun move() {
println("펭귄이 움직인다.")
}
override val legCount: Int
get() = super.legCount + this.wingCount
}
추상 클래스 프로퍼티의 getter를 override 한다면 해당 추상 클래스 프로퍼티에는 open
키워드를 붙인다. 기본적으로 코틀린에서는 상속을 막는다.
그리고 custom getter를 통해 추상 클래스 프로퍼티의 getter를 구현한다.
인터페이스
자바, 코틀린 모두 인터페이스를 인스턴스화 할 수 없다.
구현시에도 콜론 옆에 인터페이스를 기입한다.
특정 인터페이스의 default 메서드를 호출할 때는 super<인터페이스타입>.메서드
로 호출한다.
그리고 코틀린에서는 backing field가 없는 프로퍼티를 인터페이스에서 만들 수 있다.
interface Flyable {
fun act() {
println("파닥파닥")
}
}
interface Swimable {
// backing field가 없는 프로퍼티
val swimAbility: Int
get() = 3
fun act() {
println("어푸어푸")
}
}
class Penguin(
species: String
): Animal(species, 2), Flyable, Swimable {
private val wingCount: Int = 2
override fun move() {
println("펭귄이 움직인다.")
}
override val legCount: Int
get() = super.legCount + this.wingCount
override fun act() {
super<Swimable>.act()
super<Flyable>.act()
}
// Swimable 인터페이스에서 프로퍼티를 선언했다면
// 구현체에서는 getter 혹은 setter를 구현해주길 기대할 것이다.
override val swimAbility: Int
get() = 4
}
클래스를 상속할 때 주의할 점
아래와 같은 상황에서 Derived 클래스를 인스턴스화 했을 때 출력값은 어떻게 될까?
fun main() {
val instance = Derived(10)
println(instance.number)
}
open class Base(
open val number: Int = 100
) {
init {
println("Base class")
println(number)
}
}
class Derived (
override val number: Int,
): Base(number) {
init {
println("Derived Class")
}
}
// console
Base class
0
Derived Class
10
생성자에 10을 넣어주었지만 init 에서는 0으로 출력되었다. 왜 이런 일이 벌어졌을까?
- 하위 클래스인 Derived를 인스턴스화 할 때 상위 클래스 init이 호출된다.
- 이 때, 상위 클래스에서 하위 클래스의 프로퍼티를 가져와야하는데, 아직 하위 클래스가 초기화되지 않았다.
- 따라서 기본값인 0이 호출되었다.
따라서, 상위 클래스를 설계할 때 생성자 또는 초기화 블록에 사용되는 프로퍼티에는 open을 피해야 한다.
상속 관련 지시어 정리
final
: override를 할 수 없게 한다. default로 보이지 않게 존재한다.open
: override를 열어준다.abstract
: 반드시 override 해야한다.override
: 상위 타입을 오버라이드 하고 있다.
코틀린에서 접근 제어를 다루는 방법
자바와 코틀린의 가시성 제어
자바는 public
, protected
, default
, private
이라는 가시성 제어가 있다.
코틀린에서는 다음과 같이 바뀌었다.
가시성 제어 | 자바 | 코틀린 |
---|---|---|
public | 모든 곳에서 접근 가능 | 동일 (기본 지시어) |
protected | 같은 패키지 또는 하위 클래스에서만 접근 가능 | 선언된 클래스 또는 하위 클래스에서만 접근 가능 |
default | 같은 패키지에서만 접근 가능 (기본 지시어) | 같은 모듈에서만 접근 가능 (internal 로 변경) |
private | 선언된 클래스 내에서만 접근 가능 | 동일 |
- 코틀린의 패키지는 namespace를 관리하기 위한 용도로만 사용하며 가시성 제어에는 사용되지 않는다. 그래서 코틀린의
protected
는 같은 패키지가 아닌 선언된 클래스에서 접근을 제어한다. - 모듈은 한 번에 컴파일되는 코틀린 코드를 의미한다. 만약
internal
키워드를 하위 모듈에 작성했다면 상위 모듈에는internal
에 접근할 수 없다.
코틀린 파일의 접근 제어
코틀린은 하나의 kt 파일에 클래스, 변수, 함수를 선언할 수 있다. 그리고 이 파일에 대한 접근을 제어할 수 있다.
접근 제어자 | 설명 |
---|---|
public | 기본값으로 어디서든 접근 가능 |
protected | 파일(최상단) 에서는 사용 불가능 |
internal | 같은 모듈에서만 접근 가능 |
private | 같은 파일 내에서만 접근 가능 |
다양한 구성요소의 접근 제어
제어자 키워드를 클래스에 붙이면 앞에서 설명한 것과 동일하다.
생성자도 동일하다. 단, constructor
키워드를 붙여야한다.
class Person private constructor(...)
// protected의 경우 자기 자신과 상속받은 하위 클래스에서 사용할 수 있다.
// 클래스 앞에 open 키워드가 없을 경우 자기 자신만 쓸 수 있어서 인텔리제이에서는 노란줄로 경고가 뜬다.
open class Person protected constructor(...)
자바에서 유틸성 코드를 만들때 abstract class + private constructor를 통해 인스턴스화를 막았다. 코틀린에서는 파일 최상단에 유틸 함수를 작성하면 매우 편하다.
프로퍼티도 가시성 범위는 동일하다. 가시성을 설정하는 방법은 다음과 같다.
class Car(
// 바로 선언, 모듈에서만 접근 가능한 getter만 존재
internal val name: String,
// 바로 선언, 클래스 내부에서만 접근 가능한 getter, setter 존재
private var owner: String,
_price: Int,
) {
// setter는 private, getter는 public
// 애초에 필드가 아닌 프로퍼티가 생성되기 때문에 getter, setter의
// 접근 제어자를 분리할 경우 사용
var price = _price
private set
}
자바와 코틀린을 함께 사용할 경우 주의할 점
internal
을 바이트 코드로 변경하면 자바에서는 public으로 변경된다.- 만약 상위 모듈이 자바, 하위 모듈이 코틀린이라 가정했을 때 하위 모듈에
internal
로 작성된 접근 제어자가 있다면 이를 바이트 코드로 변경할 때public
으로 변경된다. 즉, 상위 모듈 자바에서 접근이 가능하게 된다.
- 만약 상위 모듈이 자바, 하위 모듈이 코틀린이라 가정했을 때 하위 모듈에
- 코틀린의
protected
와 자바의protected
는 다르다.- 자바는 같은 패키지의 코틀린
protected
멤버에 접근이 가능하다. - 자바는 동일한 패키지에서 접근이 가능하기 때문이다.
- 자바는 같은 패키지의 코틀린
코틀린에서 object 키워드를 다루는 방법
static 함수와 변수
코틀린에서 static
키워드가 없어졌다. static
은 동반 객체와 함께 다룬다.
class Person private constructor(
var name: String,
val age: Int,
) {
// 자바에서의 static 역할
companion object Factory {
private const val MIN_AGE = 1
fun newBaby(name: String) = Person(name, MIN_AGE)
}
}
잠깐 용어 정리
static
: 클래스가 인스턴스 화 될 때 새로은 값이 복제되는게 아닌 인스턴스끼리 해당 값을 정적으로 공유한다.companion object
: 클래스와 동행하는 유일한 오브젝트const
:val MIN_AGE
로 선언하면 해당 값이 런타임에 선언된다.const
키워드를 통해 컴파일 시에 변수에 값이 할당된다. 즉, 진짜 상수에 붙이기 위한 용도로 쓰인다. 기본 타입과 String에 붙일 수 있다.
주의 사항
companion object
도 하나의 객체이다. 이 객체에게 이름을 붙일 수 있다.- 객체라는건? 인터페이스를 구현할 수 있다.
companion object
에 유틸성 함수를 넣어도 되지만, 최상단 파일을 활용하는 것을 권장한다.- 자바에서
companion object
를 사용하려면@JvmStatic
을 사용해야한다.
싱글톤
코틀린에서 싱글톤은 object
키워드를 통해 만들 수 있다.
익명 클래스
익명 클래스는 특정 인터페이스나 클래스를 상속받은 구현체를 일회성으로 사용할 때 쓰는 클래스다. 코틀린에서는 익명 클래스를 다음과 같이 표현한다.
fun main() {
moveSomething(object: Movable {
override fun fly() {
println("fly")
}
override fun move() {
println("move")
}
})
}
private fun moveSomething(movable: Movable) {
movable.fly()
movable.move()
}
interface Movable {
fun fly()
fun move()
}
코틀린에서 중첩 클래스를 다루는 방법
중첩 클래스의 종류
자바에서는 어딘가에 소속되어 있는 클래스가 여러 종류 있었다.
static
이 붙은 중첩 클래스- 바깥의 클래스를 직접 참조할 수 없다.
static
이 붙지 않은 중첩 클래스- 내부 클래스
- 밖의 클래스를 직접 참조할 수 있다.
- 지역 클래스
- 익명 클래스
- 일회성 클래스
- 내부 클래스
static 키워드가 붙고 안붙고의 차이를 그림으로 표현하면 아래와 같다.
Effective Java에서는 아래와 같이 내부 클래스에 대해 언급한다.
-
내부 클래스는 숨겨진 외부 클래스 정보를 갖고 있어 참조를 해제하지 못하는 경우 메모리 누수가 생길 수 있고, 이를 디버깅하기 어렵다.
-
내부 클래스의 직렬화 형태가 명확하게 정의되지 않아 직렬화에 대한 제한이 있다.
따라서, 클래스 내에 클래스를 만들 때는 static을 사용하라는 가이드가 있다. 그리고 코틀린은 이 가이드를 따르고 있다.
코틀린의 중첩 클래스와 내부 클래스
코틀린에서는 기본적으로 바깥 클래스에 대한 연결이 없는 중첩 클래스가 만들어진다. (권장 방법)
class House(
val address: String,
val livingRoom: LivingRoom,
) {
class LivingRoom (
var area: Double,
)
}
바깥 클래스를 참조해야하는 경우는 아래와 같이 작성한다. (권장하지 않는 방법)
class House(
private val address: String,
private val livingRoom: LivingRoom,
) {
// inner 키워드 명시
inner class LivingRoom (
private var area: Double,
) {
// 바깥 클래스를 참조하려면 {this@바깥클래스.프로퍼티명}을 통해 접근
val address: String
get() = this@House.address
}
}
코틀린에서 다양한 클래스를 다루는 방법
Data class
자바에서 DTO는 주로 필드와 생성자, getter
, equals
, hashCode
, toString
과 같은
부가적인 메서드를 가지고 있다. IDE를 활용하거나 lombok을 활용할 수 있지만 클래스 생성 이후 추가적인 처리를 해줘야하는 단점이 있다.
코틀린에서는 이를 Data class
로 정의하여 간단하게 만들 수 있다.
data class PersonDto(
val name: String,
val age: Int,
)
Enum class
자바에서 Enum
은 추가적인 클래스를 상속받을 수 없다. 인터페이스는 구현할 수 있으며 각 코드가 싱글톤이다. 코틀린의 Enum
도 자바와 비슷하게 작성한다.
enum class Country(
private val code: String,
) {
KOREA("KO"),
AMERICA("US"),
;
}
when과 같이 사용해보기
아래는 자바에서 Enum의 코드에 따라 분기처리할 때의 로직이다.
private static void handleCountry(Country country) {
if (country == Country.KOREA) {
// logic
}
if (country == Country.AMERICA) {
// logic
}
}
위 코드에서 만약 코드들이 많이지게 된다면 else 로직에 대한 처리가 애매하며 가독성이 떨어진다.
코틀린에서 when으로 받는 변수가 Enum이라면 else를 작성하지 않아도 된다. 그리고 Enum에 코드가 추가될 때도 IDE에서 warning이나 error가 뜬다. 이유는 컴파일러가 해당 타입의 코드들을 모두 알고 있기 때문이다.
fun handleCountry(country: Country) {
return when (country) {
Country.KOREA -> TODO()
Country.AMERICA -> TODO()
}
}
Sealed Class, Sealed Interface
Sealed는 ‘봉인된’ 이라는 뜻이다. 추상 클래스를 만들고 싶지만 외부에서는 이 클래스를 상속받지 않았으면 하는 경우가 있다. Sealed class
는 외부에서 상속하지
못하도록 막고, 상속한 하위 클래스를 봉인하는 의미에서 사용된다.
특징으로는 컴파일 타임 때 하위 클래스의 타입을 모두 기억하며 런타임 때 클래스 타입이 추가될 수 없다. 또한, 하위 클래스는 같은 패키지에 있어야 한다.
Enum과 차이점?
Sealed Class
는 클래스를 상속받을 수 있다.- 하위 클래스는 멀티 인스턴스가 가능하다.
when과 같이 사용해보기
fun handleHyundaiCar(hyundaiCar: HyundaiCar) {
when (hyundaiCar) {
is Avante -> TODO()
is Grandeur -> TODO()
is Sonata -> TODO()
}
}
활용 방안?
- 추상화가 필요한 Entity
- 추상화가 필요한 DTO
abstract class vs sealed class
이 둘의 가장 큰 차이점은 ‘구현체가 같은 패키지에 있어야 하는지? 아니면 어디에 있어도 상관 없는지?’ 이다.
멀티 모듈 사용 시 A 모듈에서 만들어둔 class를 B 모듈과 C 모듈에서 구현하게 하고 싶다면 abstract class
를 사용한다. 즉, 다른 패키지에서 접근해야할 경우
abstract class
를 사용한다.