추상 클래스를 알아보기로 한 계기는 LiveData를 알아보면서 시작됐다.

구글을 MVVM패턴을 권장하고 이 패턴에서 ViewModel을 사용할 때 데이터 관리는 LiveData를 통해서 한다. (MVVM의 ViewModel과 AAC의 ViewModel은 다르다) 링크를 따라가면 좋은 글이!
시작은 다음과 같다. ViewModel클래스에서 데이터를 관리할때 아래와 같이 한다.
private val _num = MutableLiveData<Int>()
val num: LiveData<Int> = _num
MutableLiveData를 통해 값을 수정하고 LiveData를 통해 view에서 관찰을 한다. view에서는 관찰만 할 수 있고 데이터를 조작할 수는 없다.
이렇게 잡소리를 하면 언제 추상클래스를 알아보는가? 바로 지금이다!! MutableLiveData는 추상 클래스인 LiveData를 상속받아 구현한 클래스라는 사실!!
(왼쪽이 LiveData클래스의 일부이고 오른쪽이 MutableLiveData클래스의 일부이다. )
항상 이 두 클래스를 사용해서 데이터를 관리하는데 추상클래스가 뭐냐! 라고 물으면 대답할 자신이 없었다… 그리고 추상클래스를 검색해보면 항상 인터페이스와 함께 설명을 해준다. 그럼 이제 진짜 추상클래스와 인터페이스를 알아보자!
아래의 내용들은 블로그내용을 필사한 것입니다.


추상 클래스
추상 클래스 vs 인터페이스
추상 클래스는 대략적인 설계의 명세와 공통의 기능을 구현한 클래스이다. 즉, 구체적이지 않은 것이다. 추상 클래스를 상속하는 하위 클래스는 추상 클래스의 내용을 더 구체화 해야한다. 그럼 인터페이스와 같은 것이 아니냐는 질문이 나올 수 있다. 답변은 다음과 같다. 인터페이스도 대략적인 설계 명세를 구현하고 인터페이스를 상속하는 하위 클래스에서 이를 구체화 하는 것은 동일하다. 하지만! 인터페이스에서는 프로퍼티의 상태 정보를 저장할 수 없다. 다시 말하면 인터페이스에서는 프로퍼티의 초기화가 불가능하다는 것이다.

프로퍼티를 초기화 하려면 위 사진과 같이 오류를 표시해준다. Property initalizers are not allowed in interfaces (도와줘요 파파고!!) 이해는 했지만 정확함을 위해… 인터페이스에서 속성 초기화를 사용할 수 없습니다.
추상 클래스 사용해보기
추상 클래스는 위에서 말한 것처럼 구체화되지 않은 클래스이기 때문에 일반적인 객체를 생성하는 방법으로 인스턴스화될 수 없다. 일단 한번 정의해보자. 우선 추상 클래스를 정의하기 위해선 abstract라는 키워드를 사용해야 한다. 또한, 클래스 내에서의 프로퍼티나 메소드도 abstract로 선언할 수 있다. 이를 상속받는 클래스에서 구체화하겠다는 의미가 된다. 하지만 이를 사용하기 위해선 해당 클래스가 추상 클래스가 되어야 한다.
// abstract로 정의한 추상 클래스이다. 주생성자를 사용했다.
abstract class Vehicle(val name: String, val color: String, val weight: Double) {
// abstract로 정의한 추상 프로퍼티이므로 하위 클래스에서 반드시 재정의해야한다.
abstract var maxSpeed: Double
// 초기값을 갖는 일반 프로퍼티 (인터페이스에서는 불가능)
var year = "2023"
// abstract로 정의한 추상 메소드이므로 하우 ㅣ클래스에서 반드시 재정의해야한다
abstract fun start()
abstract fun stop()
fun disPlaySpecs() {
println("Name : $name, Color : $color, Weight : $weight, Year : $year, MaxSpeed : $maxSpeed")
}
}
Vehicle클래스는 abstract로 정의한 추상 클래스이므로 기본적인 설계만 정의하고 있다. abstract를 사용한 maxSpeed, start(), stop()은 반드시 하위 클래스에서 재정의 해줘야한다.
Q. 클래스를 상속하기 위해선 부모 클래스를 open키워드로 정의해야 하는데 추상 클래스에서도 해줘야 하나요?
위 코드에서 보듯, 추상 클래서에서는 open키워드를 사용하지 않아도 된다. 추상크로퍼티나 추상 메소드도 마찬가지다.
이제 Vihicle클래스를 상속해보자.
abstract class Vehicle(val name: String, val color: String, val weight: Int) {
abstract var maxSpeed: Int
var year = "2023"
abstract fun start()
abstract fun stop()
fun disPlaySpecs() {
println("Name : $name, Color : $color, Weight : $weight, Year : $year, MaxSpeed : $maxSpeed")
}
}
class Car(name: String, color: String, weight: Int, override var maxSpeed: Int): Vehicle(name, color, weight) {
override fun start() {
println("Car start")
}
override fun stop() {
println("Car Stop")
}
}
class Bicycle(name: String, color: String, weight: Int, override var maxSpeed: Int): Vehicle(name, color, weight) {
override fun start() {
println("Bicycle start")
}
override fun stop() {
println("Bicycle Stop")
}
}
@Test
fun abstractTest() {
val car = Car("Matiz", "Yellow", 1000, 150)
val bicycle = Bicycle("Bike", "Red", 150, 100)
car.year = "2024"
car.disPlaySpecs()
car.start()
bicycle.disPlaySpecs()
bicycle.start()
}
displaySpec은 추상 클래스가 갖고 있던 일반 메소드이다. start와 stop은 추상클래스를 상속받은 자식 클래스에서 오버라이딩 한 메소드이다. 추상 클래스에서 abstract로 정의한 프로퍼티나 메소드들은 자식 클래스에서 반드시 재정의되어야한다.
Q. 추상 클래스를 상속받는 하위 클래스를 정의하지 않고도 추상 클래스를 사용할 수 있을까? A. object 키워드를 사용하면 된다!
val human = object : Vehicle("gang", "black", 60) {
override var maxSpeed = 10
override fun start() {
println("human start")
}
override fun stop() {
println("human Stop")
}
}
인터페이스
코틀린에서는 다른 언어오 ㅏ다르게 메소드에 구현 내용을 포함할 수 있다.
interface Pet {
var category: String
fun feeding()
fun patting() {
println("Keep patting")
}
}
추상클래스와 다르게 abstract키워드를 사용하지 않는다. 또한, 앞서 말한 것처럼 상태를 저장할 수가 없으므로 프로퍼티에 기본 값을 초기화 할 수 없다.
인터페이스 사용하기
class Cat(override var category: String) : Pet {
override fun feeding() {
println("Feeding 메소드가 구현되었습니다.")
}
}
fun test() {
val obj = Cat("Small")
obj.feeding() // 인터페이스로 구현된 메소드
obj.patting() // 일반 메소드
}
인터페이스에서 값 저장하기
안된다며…. 하지만 예외는 있다. val로 선언한 프로퍼티는 게터를 통해서 필요한 내용을 구현할 수 있다.
interface Pet {
var category: String // 추상 프로퍼티
val message: String // val로 선언하면 게터의 구현이 가능하다.
get() = "I''m cutty"
fun feeding()
fun patting() {
println("Keep patting")
}
}
게터가 사용 가는ㅇ하지만 그렇다고 보조필드가 사용가능한 것은 아니다.
인터페이스의 이점
인터페이스를 사용하면 뭐가 좋을까? 일단 코드의 재사용성이 올라간다.
인터페이스를 이용해서 클래스 간의 의존성을 제거할 수 있다.
다중 상속도 가능하다. 코틀린에서는 자바와 다르게 클래스에서 부모 클래스를 상속할 때는 1개의 클래스만 가능하다. 하지만 인터페이스를 활용하면 다중 상속이 가능해진다.
interface Bird {
val wings : Int
fun fly()
fun jump() { // 구현된 일반 메소드
println("Bird jump")
}
}
interface Horse {
val maxSpeed : Int
fun run()
fun jump() { // 구현된 일반 메소드
println("jump!, max speed : $maxSpeed")
}
}
class Pegasus : Bird, Horse {
override val wings : Int = 2
override val maxSpeed : Int = 100
override fun fly() {
println("The Pegasus Fly!")
}
override fun run() {
println("The Pegasus Run!")
}
override fun jump() {
super<Horse>.jump() {
println("The Pegasus jump")
}
}
}
구현부가 있는 메소드의 경우 필요에 따라 오버라이딩을 진행하면 된다. 만약 인터페이스에서 구현한 메소드의 이름이 같은 경우 super<인터페이스 이름> .메소드 명 을 통해 구분할 수 있다.
Uploaded by N2T