java, c# 등 OOP 기반 언어를 주로 사용할때 가장 중요한 것들 중 하나가 클래스, 메서드 간 상속 구조, 오버라이딩, 오버로딩을 적절히 활용하여 SW 아키텍처를 효율적으로 만드는 것이다.
Kotlin은 Java 와 매우 흡사한 구조를 가지고 있지만 엄연히 용법은 다르다.
Kotlin 기반의 Interface , abstract 의 specific 한 구현 예제를 언어 적응 차원에서복기 해본다.
Interface
개요
SW 아키텍처에서 의존성 역전을 통한 아키텍처 경계 설정을 위해 자주 사용되는 패턴으로, 클래스, 함수, 변수의 틀을 지정한다.
즉, 구현 부분이 없게 틀을 만들어 여러 종류의 비슷한 형태의 오브젝트들을 만들 수 있도록 frame을 잡아 주는 역할을 한다.
단, 인터페이스의 생성자는 만들어 낼 수 없기 때문에 인터페이스의 객체를 직접 만들 수 없으며, 구현 클래스를 통해 해당 인터페이스를 상속받아 구현한 후 구현한 클래스의 객체를 통해 사용할 수 있게된다.
정의하기
Interface 안에는 val, var 등의 프로퍼티를 포함할 수 있는데, 인터페이스 자체에서 초기화 해줄 수는 없다.
따라서 인터페이스는 내부 프로퍼티들을 구현체에서 반드시 재정의 해야한다는 정책을 보장한다.
단, 멤버 함수의 겨우 부모 인터페이스에서 정의가 되어 있다면 반드시 재정의해야 할 필요는 없지만, 선언만 되어있을 경우 이를 반드시 오버라이딩 하여 구현 해줘야 한다.
public interface Coupon {
var name: String
fun use(amount: Long): Boolean
}
< Coupon.kt - 인터페이스>
사용하기
클래스를 통하여 구현되며, 구현한 클래스는 인터페이스의 하위 클래스가 된다.
java 의 경우 implements를 사용해서 구현 되지만 kotlin 에서는 별도의 예약어를 쓰지 않는다.
구현하는 클래스에서는 인터페이스의 디폴트 구현이 없는 모든 함수와 프로퍼티를 구현 해야 한다.
public class AmazonCoupon: Coupon {
override var name: String = "AMAZON"
override fun use(amount: Long): Boolean {
println("successfully used amazon coupon")
return true
}
}
< AmazonCoupon.kt - 구현 클래스1>
public class PaperCoupon: Coupon {
override var name: String = "지류 쿠폰"
override fun use(amount: Long): Boolean {
println("지류 쿠폰을 사용 하였습니다.")
return true
}
}
< PaperCoupon.kt - 구현 클래스2>
fun main(){
var amazonCoupon = AmazonCoupon()
println(amazonCoupon.use(300))
var paperCoupon = PaperCoupon()
println(paperCoupon.use(500))
}
< Main 함수 - 클라이언트 >
특징
- 인터페이스 그 자체의 객체를 생성 할 수 없다.
- 인터페이스도 구현 부분을 가질 수 있는데, 자체 구현하면 해당 메서드 구현이 디폴트 구현으로 적용된다.
- 특정 클래스는 하나의 클래스만 상속 받을 수 있지만, 인터페이스는 여러 개를 구현할 수 있다.
- 인터페이스도 인터페이스를 상속 받을 수 있다.
- 인터페이스의 접근 제어자는 public, open 이다. 실제로 사용할때는 주로 public 으로 놓고 쓰고 있긴한데 open 접근 제어자는 클래스에 대한 상속에 주로 사용한다.
Abstract
개요
구현되어 있지 않은 abstract 로 정의된 메소드가 포함 된 클래스를 의미한다.
인터페이스와 일반 클래스의 중간 정도의 개념이라 볼 수있다.
클래스 안에 메소드 하나라도 추상 메소드가 포함되어 있다면 해당 클래스는 반드시 abstract 클래스로 표기 되어야 한다.
추상 클래스 역시 인터페이스와 같이 타 클래스에서 상속 받는다면, 모두 구현 해줘야한다.
인터페이스와 다르게 추상 클래스는 생성자를 가질 수 있다.
정의하기
일반 클래스 정의 할 때와 같지만 class 앞에 Java 와 같이 abstract 라는 키워드를 써준다.
추상 클래스의 메소드는 추상 메소드, 일반 메소드가 섞여서 들어갈 수 있고, 추상 메소드로 정의하려면 역시 추상 메소드의 함수 정의 앞에abstract 키워드를 붙여야 한다.
클래스 내부에 추상 메소드나 추상 프로퍼티가 어디 하나라도 존재 한다면, 해당 클래스는 무조건 추상 클래스가 되어야 한다.
추상 메소드는 블록으로 둘러 쌓인 구현부를 가질 수 없다. 또한 추상 클래스도 인터페이스와 마찬가지로 객체를 생성 할 수 없다.
abstract class Animal {
abstract var count: Int
abstract fun eating() // 추상 메서드
open fun alive(): String { // 일반 메서드의 상속 후 재사용 허용
return "It is alive"
}
fun noting(): String { // 일반 메서드이지만 상속 불가
return "상속 불가 함수"
}
}
< Animal.kt >
사용하기
인터페이스와 마찬가지로 다른 클래스가 추상 클래스를 상속받고, 그 클래스에서 추상 메서드들의 구현 부분을 채운 후 하위 클래스의 객체를 통해 사용할 수 있다.
즉, 추상 클래스의 주목적 역시 같은 상위 클래스에 대하여 각 파생 클래스마다 다른 코드를 실행할 수 있도록 하는 상속에 기본이 있다.
java 의 경우 extends를 사용해서 구현 되지만 kotlin 에서는 별도의 키워드를 쓰지 않는다.
class Dog : Animal(){
override var count: Int = 0 // abstract 변수 초기화
override fun eating() { // abstract 함수 구체화
count++
println("DOG 의 eating() 호출, meat 을 먹습니다")
}
override fun alive(): String { // open 함수 상속
return "DOG의 일반 함수 alive()"
}
// override fun nothing() 상속 불가 함
}
< Dog.kt >
class Cat : Animal() {
override var count: Int = 0
override fun eating() {
count--
println("CAT 의 eating() 호출, fish를 먹습니다")
}
// open 으로 선언된 일반 함수는 상속 안해도 그만
// overrie fun nothing() 상속 불가 함
}
< Cat.kt >
fun main(){
var dog = Dog()
println(dog.eating())
println(dog.alive())
println(dog.noting()) // dog 에서 상속 받지 않았어도 추상 클래스 내 초기화 메서드 리턴이 그대로 출력
var cat = Cat()
println(cat.eating())
println(cat.alive()) // cat 에서 오버라이드 하지 않았으므로 추상 클래스 내 초기화 메서드 리턴이 그대로 출력
println(cat.noting()) // cat 에서 상속 받지 않았어도 추상 클래스 내 초기화 메서드 리턴이 그대로 출력
}
< Main 함수 - 클라이언트 >
특징
- 추상 클래스 그 자체의 객체를 생성 할 수 없다.
- 생성자를 가질 수는 있다. 이 생성자는 하위 클래스에서 추상 클래스를 상속 받을 때 사용하며, 추상 클래스의 concrete 프로퍼티를 초기화 하는데 쓸 수 있다.
- 추상 메소드/프로퍼티의 접근 범위는 public, open 이고 override가 반드시 필요하다.
- 일반 메소드/프로퍼티의 기본 집근 제어자는 public, final 이다.
Interface 와 Abstract 의 실전
- 인터페이스
- 구성요소들이 자주 바뀔 때 쓰면 유용
- 메소드 형태만 서로 공유해서 구현(재정의) 되는 상황일 때 사용
- 즉, 공통된 메소드(객체 행위)로 만들어 사용하고 싶을 때 인터페이스를 이용하면 효과적
- 서로 관련성이 없는 클래스들이 인터페이스를 구현하게 되는 경우에 사용한다.
- 특정 데이터 타입의 행동을 명시하고 싶은데, 어디서 그 행동이 구현 되는지 신경쓰고 싶지 않은 경우
- 예를 들어, payment 의 핵심 기능은 결제, 결제취소, 환불 등의 행동이 어느 페이옵션이든지 동일하게 존재 하는데 이때 상위 인터페이스로서 행동의 틀을 잡는다.
- 추상 클래스
- 객체의 추상적인 상위 개념으로 공통된 개념을 표현 할때
- 예를 들어 공통 된 속성을 가진 Car 라는 추상 클래스에 추상 메소드로 구현하여 하위 개념인 Van 등의 객체에 상속시켜 구현하면 객체의 재사용성과 객체를 표현하는 것이 더욱 명확해 짐
- 추상 클래스를 상속받은 클래스들이 공통으로 가지는 메소드와 필드가 많은 경우
- public 이외에 protected 나 private 가 필요한 경우
abstract class 는 절대로 바뀌지 않는 개념에 대한 공통 부분을 만들때는 재사용성을 보장하므로 매우 유용하다.
하지만 바뀌지 않는 개념이란 사실 상 거의 없고, 특히 클라이언트를 개발 할때는 모든 것이 뒤집어 질 수 있으므로 사용할 일이 거의 없는 것 같다.
따라서, 변화에 대한 유연성을 쉽게 가져가려면 abstract 보다는 interface 를 쓰는것이 합리적이다.
abstract class 처럼 내부에 정해진 변수나 함수가 있으면 구현체에 의존적이게 되어 변화에 대한 유연성이 감소하기 때문이다.
'Engineering > Kotlin' 카테고리의 다른 글
Companion object (0) | 2023.05.18 |
---|---|
Constructor (0) | 2023.05.16 |
댓글