코틀린 OOP (1) - 클래스, 상속, 접근 제어

de_sj_awa·2023년 4월 15일
0
post-custom-banner

1. 클래스

1) 클래스와 프로퍼티, 생성자와 init

  • 프로퍼티 = 필드 + getter + setter, Kotlin에서는 필드만 만들면 getter, setter를 자동으로 만들어준다.
  • Kotlin에서는 클래스의 필드 선언과 생성자를 동시에 선언할 수 있다.
  • Kotlin에서 클래스 Body를 생략할 수 있다.
  • Kotlin의 주 생성자는 반드시 존재해야 하나, 파라미터가 하나도 없다면 생략 가능하다.
  • Kotlin의 주 생성자에는 constructor라는 키워드를 생략할 수 있다.
  • .필드를 통해 getter와 setter를 바로 호출한다.
  • Kotlin에서 Java 클래스에 대해서도 .필드로 getter, setter를 사용한다.
  • Kotlin에서 init 블록은 클래스가 초기화되는 시점에 한 번 호출되는 블록이다. 따라서 값을 적절히 만들어주거나 validation 로직을 넣거나 하는 용도로 사용된다.
  • Kotlin의 부 생성자는 클래스 Body에 constructor라는 키워드와 함께 만들어 줄 수 있다. 최종적으로 주생성자를 this로 호출해야 한다. 부 생성자에는 block을 통해 코드를 넣을 수도 있다.
  • Kotlin에서는 부생성자보다는 default parameter를 권장한다.
  • convertin의 경우 부생성자를 사용할 수는 있지만 그보다는 정적 팩토리 메소드를 권장한다.
fun main() {
	val person = Person("person", 100)
    println(person.name)
    person.age = 10
    println(person.age)
}

class Peron (
    val name: String, 
    var age: Int
) {
	init {
    	if (age <= 0) {
        	throw IllegalArgumentException("나이는 ${age}일 수 없습니다")
        }
    }
    
    constructor(name: String): this(name, 1)
}

2) 커스텀 getter, setter, backing field

fun main() {
	val person = Person("person", 100)
    println(person.name)
    person.age = 10
    println(person.age)
}

class Peron (
    name: String, 
    var age: Int
) {
	init {
    	if (age <= 0) {
        	throw IllegalArgumentException("나이는 ${age}일 수 없습니다")
        }
    }
    
    val name = name
    	get() = field.uppercase()
    
    constructor(name: String): this(name, 1)
    
    val isAdult: Boolean
    	get() = this.age >= 20
}
  • Kotlin에서는 실제 메모리에 존재하는 것과 무관하게 custom getter와 custom setter를 만들 수 있다.
  • Java로 디컴파일 했을때, custom getter는 자바 클래스 내부에 함수가 있는 것처럼 표현된다.
  • custom getter를 사용하면 자기 자신을 변형해 줄 수도 있다. 또한 field라는 키워드를 사용하는 이유는 내부에서 필드를 호출해도 getter를 호출하는 것이기 때문에 무한루프가 발생하기 때문이다. 즉 field는 무한 루프를 막기 위한 예약어로 자기 자신을 가리킨다. 그래서 field를 backing field로 부른다. 그러나 실제로는 custom getter를 사용하는 이유는 프로퍼티가 있는 것처럼 사용하기 위해서인데 field를 사용할 때는 진짜 프로퍼티에 대해 getter를 만들어 줄 때 쓴다. 따라서 다음과 같이 처리할 수도 있다.
fun getUppercaseName(): String = this.name.uppercase()

val uppercase: String
	get() = this.name.uppercase()
fun main() {
	val person = Person("person", 100)
    println(person.name)
    person.age = 10
    println(person.age)
}

class Peron (
    name: String, 
    var age: Int = 1
) {

	var name = name
    	set(value) {
        	field = value.uppercase()
        }
        
	init {
    	if (age <= 0) {
        	throw IllegalArgumentException("나이는 ${age}일 수 없습니다")
        }
    }
    
    val isAdult: Boolean
    	get() = this.age >= 20
}
  • 그러나 setter를 지양하고 있기 때문에 custom setter도 잘 사용되지 않는다.

2. 상속

1) 추상 클래스

abstract class Animal(
	protected val species: String,
    protected open val legCount: Int,
) {
	
    abstract fun move()
}
class Cat(
	species: String
) : Animal(species, 4) {
	
    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
}
  • Kotlin에서 상속받을 때는 상속받은 클래스의 생성자를 바로 명시해 주어야 한다.
  • Kotlin 컨벤션에서 타입을 명시할 때는 :를 공백 없이 붙여주고 상속받을 때는 한 칸 뛰고 :를 붙여준다.
  • Kotlin에서는 Java와 동일하게 상위 클래스에 접근하는 키워드는 super이다.
  • Java, Kotlin 모두 추상클래스는 인스턴스화 할 수 없다.
  • Kotlin에서는 Java와는 다르게 @Override라는 어노테이션을 사용하는 것이 아니라 override라는 지시어, 키워드를 사용한다.
  • Kotlin에서는 프로퍼티를 override 할 때 추상 프로퍼티가 아니라면 무조건 open을 붙여줘야 한다. 또한 프로퍼티를 override할 때 override 키워드와 custom getter를 사용한다.

2) 인터페이스

interface Flyable {
	
    fun act() {
    	println("파닥파닥")
    }
    
    fun fly()
        
}
interface Swimable {
	
    fun act() {
    	println("어푸어푸")
    }
}
class Penguin(
	species: String
) : Animal(species, 2), Swimable, Flyable {
	
    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()
    }
}
  • Java와 Kotlin 모두 인터페이스를 인스턴스화할 수 없다.
  • Kotlin에서 인터페이스 구현도 :를 사용한다.
  • Kotlin에서는 default 키워드를 쓰지 않아도 메소드 구현이 가능하다.
  • 중복되는 인터페이스를 특정할 때 Java에서는 타입.super.함수()라고 표현하나, Kotlin에서는 super<타입>.함수()라고 표현한다.
  • Kotlin에서는 backing field가 없는 프로퍼티를 interface에 만들 수 있다.
interface Swimable {
	
    val swimAbility: Int
    	get() = 3				// 기본 값
	
    fun act() {
    	println("어푸어푸")
    }
}
class Penguin(
	species: String
) : Animal(species, 2), Swimable, Flyable {
	
    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()
    }
    
    override val swimAbility: Int
    	get() = 3
}

3) 클래스를 상속할 때 주의할 점

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")
    }
}
  • 다른 클래스가 상속받게 해줄 수 있게 하기 위해 class 앞에 open 이라는 키워드를 붙여준다.
  • 프로퍼티도 override할 수 있도록 프로퍼티 앞에 open 이라는 키워드를 붙여준다.
  • IntelliJ에서 Base 클래스의 init 블록 println(number)에 Accessing non-final property number in constructor 라는 메시지로 경고하고 있다. 이 상위 클래스의 init 블록이 실행될 때 하위 클래스의 number에 접근하게 되는데 아직 하위 클래스가 초기화되기 전이기 때문이다. 즉, final이 아닌 property에 접근하기 때문이다. 따라서 상위 클래스를 설계할 때 생성자 또는 초기화 블록에 사용되는 프로퍼티에는 open을 피해야 한다.

4) 상속 관련 지시어

  1. final : override를 할 수 없게 한다. default로 보이지 않게 존재한다.
  2. open : override를 열어준다.
  3. abstract : 반드시 override 해야 한다.
  4. override : 상위 타입을 오버라이드 하고 있다.

3. 접근 제어

1) Java와 Kotlin의 가시성 제어

  • Java
    - public : 모든 곳에서 접근 가능
    - protected : 같은 패키지 또는 하위 클래스에서만 접근 가능
    - default : 같은 패키지에서만 접근 가능
    - private : 선언된 클래스 내에서만 접근 가능

  • Kotlin
    - public : 모든 곳에서 접근 가능
    - protected : 선언된 클래스 혹은 하위 클래스에서만 접근 가능(Kotlin에서는 패키지를 namespace를 관리하기 위한 용도로만 사용하고 가시성 제어에는 사용되지 않는다.), 파일 최상단에는 사용 불가능하다(클래스에만 사용 가능).
    - internal : 같은 모듈(한번에 컴파일되는 Kotlin 코드)에서만 접근 가능

    • private : 같은 파일 내에서만 접근 가능하다.
  • Java의 기본 접근 지시어는 default, Kotlin의 기본 접근 지시어는 public이다.
  • Kotlin은 .kt 파일에 변수, 함수, 클래스 여러개를 바로 만들 수 있다.
  • Kotlin의 생성자에 접근 지시어를 붙이려면 constructor 키워드를 써야 한다.
  • Java에서는 유틸성 코드를 만들 때 abstract class + private constructor를 만들어 인스턴스화를 막는다. Kotlin에서도 동일하게 사용가능하나, 파일 최상단에 유틸 함수를 바로 작성하면 매우 편리하다.
  • 프로퍼티도 앞에 접근 지시어를 작성해 getter, setter 한 번에 접근 지시어를 정할 수 있고, setter에만 추가로 가시성을 부여할 수도 있다.
class Car (
	internal val name: String,
    _price: Int
) {
	var price = _price
    	private set
}

2) Java와 Kotlin을 함께 사용할 경우 주의할 점

  • Internal은 바이트 코드 상 public이 된다. 때문에 Java 코드에서는 Kotlin 모듈의 internal 코드를 가져올 수 있다.
  • Java의 protected와 Kotlin의 protected는 다르다. Java는 같은 패키지의 Kotlin protected 멤버에 접근할 수 있다.

참고

profile
이것저것 관심많은 개발자.
post-custom-banner

0개의 댓글