추상 클래스와 인터페이스
추상 클래스는 선언 등의 대략적인 설계 명세와 공통의 기능을 구현한 클래스이다.
abstact라킄 키워드와 함께 선언하며 추상클래스로부터 일반적인 객체를 생성하는 방법으로 인스턴스화 될 수 없다.
추상 클래스를 상속하는 하위 클래스가 어떻게 만들어야 하는지를 나타내는 용도이다.
예시를 보자.
package chap07.section1
// 추상 클래스, 주 생성자에는 비추상 프로퍼티의 선언의 매개변수가 3개가 있음.
abstract class Vehicle (val name: String, val color: String, val weight: Double) {
// 추상 프로퍼티(반드시 하위 클래스에서 재정의해 초기화해야 함)
abstract var maxSpeed: Double
// 일반 프로퍼티(초깃값인 상태를 저장할 수 있음)
var year = "2018"
// 추상 메서드(반드시 하위 클래스에 구현해야 함)
abstract fun start()
abstract fun stop()
// 일반 메서드
fun displaceSpecs(){
println("Name: $name, Color: $color, Weight: $weight, Year: $year, Max Speed: $maxSpeed")
}
}
예시 코드를 봤을때, 앞으로 어떤식으로 세워야겠다. 약간 일종의 수도코드 느낌이 난다.
그러면 추상 클래스의 객체 인스턴스도 보자.
package chap07.section1
// 추상 클래스의 선언
abstract class Printer {
abstract fun print() // 추상 메서드
}
val myPrinter = object: Printer(){ // 객체 인스턴스
override fun print() { // 추상 메서드의 구현
println("출력합니다.")
}
}
fun main(){
myPrinter.print()
}
인터페이스
abstract로 정의된 추상 메서드나 일반 메서드가 포함된다.
현실 세계의 '계약서'와 비슷하다. 계약서에는 무엇 무엇을 하라는 추상적인 활동이 적혀 있다.
구체적인 내용이 아니라, 계약서 자체로는 실행되지가 않는다.
작업자에 의해 구체적은 작업이 구현되어야 실행되는것이다. 그래서 '기본 설계도'라고도 한다.
추상 클래스와 느낌이 비슷한데 쓰는 이유가 무엇일까?
추상클래스는 2개 이상의 클래스로부터 프로퍼티나 메서드를 상속받을 수 없다는 단점이 있다.
그리고 상위 클래스와 하위 클래스에 강한 연관이 생기면서 하위 클래스는 상위 클래스의 영향을 받는다.
인터페이스는 클래스가 아니다. 그래서 상속이라는 형태로 하위 클래스에 전달하는 느낌이 아니라, 하위 클래스를 구현 클래스라고 한다.
그래서 추상 클래스와 다르게 강한 연관을 가지지 않는다.
자바의 인터페이스는 default 키워드를 사용했지만 코틀린은 기본적으로 제공하기에 필요가 없다.
아래 예시를 보자.
package chap07.section1
interface Pet {
var category: String // abstract 키워드가 없어도 기본은 추상 프로퍼티
fun feeding() // 마찬가지로 추상 메서드
fun patting() { // 일반 메서드: 구현부를 포함하면 일반적인 메서드로 기본이 됨
println("Keep patting!") // 구현부
}
}
class Cat(override var category: String) : Pet {
override fun feeding(){
println("Feed the cat a tuna can!")
}
}
fun main() {
val obj = Cat("small")
println("Pet Category: ${obj.category}")
obj.feeding() // 구현된 메서드
obj.patting() // 기본 메서드
}
자바에서는 상속은 extends, 구현은 implements 키워드를 구별하고 있지만 코틀린에서는 둘 다 콜론(:)을 통해서 정의한다.
게터를 구현한 프로퍼티도 있다.
interface Pet {
var category: String // abstract 키워드가 없어도 기본은 추상 프로퍼티
val msgTags: String // val 선언 시 게터의 구현이 가능
get() = "I'm your lovely pet!"
fun feeding() // 마찬가지로 추상 메서드
fun patting() { // 일반 메서드: 구현부를 포함하면 일반적인 메서드로 기본이 됨
println("Keep patting!") // 구현부
}
}
val로 선언했기에 게터 선언이 가능하고, 잘 출력된다.
인터페이스 구현의 필요성을 다시 알아보자.
앞에서 작성한 InterfacePet.kt에 이어 반려동물을 관리하는 주인 클래스를 만들고 노는 동작을 넣어보자.
package chap07.section1
open class Animal(val name: String)
// feeding의 구현을 위해 인터페이스 Pet 지정
class Dog(name: String, override var category: String) : Animal(name), Pet {
override fun feeding() {
println("Feed the dog a bone")
}
}
class Master{
fun playWithPet(dog: Dog){ // 각 애완동물 종류에 따라 오버로딩됨
println("Enjoy with my dog.")
}
fun playWithPet(cat: Cat){ // 각 애완동물 종류에 따라 오버로딩됨
println("Enjoy with my Cat.")
}
}
fun main(){
val master = Master()
val dog = Dog("Toto", "small")
val cat = Cat("Coco", "BigFat")
master.playWithPet(dog)
master.playWithPet(cat)
}
나도 개와 고양이와 놀고싶다.
인터페이스의 장점인 여러 인터페이스를 이용한 다중 상속을 하는 경우도 있다.
package chap07.section1
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("Fly!")
}
override fun run() {
println("Run!")
}
override fun jump() {
super<Horse>.jump()
println("Pegasus Jump!")
}
}
fun main() {
val pegasus = Pegasus()
pegasus.fly()
pegasus.run()
pegasus.jump()
}
인터페이스도 위임(by)을 할 수 있다.
커피 제조기를 만들어보자.
용어 | 선언 | 설명 |
히터 | Heater | 일반적인 켜기, 끄기, 뜨거운 상태를 확인 |
전기 히터 | ElectricHeater | 추상적인 히터를 구체화한 클래스 |
펌프 | Pump | 열사이펀 과정을 위한 펌핑 기능을 가진 개념 |
열사이펀 | Thermosiphon | 커피 추출을 위한 방법 중 하나 |
커피 모듈 | CoffeeModule | 커피 모듈은 추출 방법을 선택할 수 있다(열사이펀 방법만 사용) |
드립 커피 모듈 | MyDripCoffeeModule | 커피 모듈을 전기 히터와 열사이펀 조합해 구체화한다. |
커피 제조기 | CoffeeMaker | 최종 제품으로 커피 모듈을 통해 브루잉 과정을 진행한다. |
커피 제조기의 구현
히터 인터페이스
package chap07.section1
interface Heater {
fun on()
fun off()
fun isHot() : Boolean
}
전기히터 클래스
package chap07.section1
class ElectricHeater(var heating: Boolean = false) : Heater {
override fun on() {
println("[ElectricHeater] heating...")
heating = true
}
override fun off() {
heating = false
}
override fun isHot(): Boolean = heating
}
펌프 클래스(인터페이스)
package chap07.coffeeMaker
interface Pump {
fun pump()
}
열사이펀 클래스
package chap07.coffeeMaker
class Thermosiphon (heater: Heater) : Pump, Heater by heater{ // 위임의 사용
override fun pump() {
if (isHot()) {
println("[Thermosiphon] pumping...");
}
}
}
커피 모듈 인터페이스
package chap07.coffeeMaker
interface CoffeeModule {
fun getThermosiphon() : Thermosiphon
}
드립 커피 모듈 클래스
package chap07.coffeeMaker
class MyDripCoffeeModule : CoffeeModule{
companion object{
val electricHeater : ElectricHeater by lazy{ // lazy를 이용한 지연 초기화
ElectricHeater()
}
}
private val _themosiphon : Thermosiphon by lazy { // 임시적인 private 프로퍼티
Thermosiphon(electricHeater)
}
// 오직 이 메서드에서만 Thermosiphon을 초기화
override fun getThermosiphon(): Thermosiphon = _themosiphon
}
커피제조기 클래스와 생성 테스트(main)
package chap07.coffeeMaker
class CoffeeMaker(val coffeModule: CoffeeModule) {
fun brew() {
val theSiphon: Thermosiphon = coffeModule.getThermosiphon()
theSiphon.on()
theSiphon.pump()
println("Coffee, here! Enjoy!~")
theSiphon.off()
}
}
fun main() {
val coffeeMaker = CoffeeMaker(MyDripCoffeeModule())
coffeeMaker.brew()
}
데이터 클래스와 기타 클래스
코틀린에서는 프로퍼티와 메서드가 없는 데이터만을 저장된 클래스를 제공한다.
데이터 전달을 위한 객체를 DTO(Data Transfer Object)라고 한다. 자바에서는 POJO라고도 부른다.
DTO는 구현 로직을 가지고 있지 않고 순수한 데이터 객체를 표현하기 때문에 보통 속성과 접근하고자 하는 게터/세터를 가진다.
추가적으로 자바는 toString(), equals()등과 같은 데이터를 표현하거나 비교하는 메서드를 가져야 한다.
하지만 코틀인은 내부적으로 자동 생성되기 때문에 데이터를 위한 프로퍼티만 신경쓰면 된다.
자동으로 생성되는 메서드는 다음과 같다.
제공된 메서드 | 기능 |
equals() | 두 객체의 내용이 같은지 비교하는 연산자 |
hashCode() | 객체를 구별하기 위한 고유한 정숫값 생성 데이터 세트나 해시 테이블을 사용하기 위한 하나의 생성된 인덱스 |
copy() | 빌더 없이 특정 프로퍼티만 변경해서 객체 복사하기 |
toString() | 데이터 객체를 읽기 편한 문자열로 반환하기 |
componentN() | 객체의 선언부 구조를 분해하기 위해 프로퍼티에 사응하는 메서드 |
데이터 클래스 선언하기
data키워드를 제공하는데
data class Customer(var name: String, var email: String) 이런 식으로 사용할 수 있다.
내부 클래스 기법
자바는 4가지의 내부클래스가 있는데,
코틀린은 2가지의 내부 클래스 기법이 있다.
중첩(Nested)클래스 : 객체 생성 없이 사용가능
이너(Inner)클래스 : 필드나 메서드와 연동하는 내부 클래스로 inner 키워드가 필요하다.
중첩 클래스
코틀린에서 중첩 클래스는 기본적으로 정적 클래스처럼 다뤄진다.
즉, 중첩 클래스는 객체 생성 없이 접근할 수 있다는 것이다.
아래 예시를 보자.
package com.acaroom.kotlin.chap03.section04.nested
class Outer {
val ov = 5
class Nested{
val nv = 10
fun greeting() = "[Nested] Hello ! $nv" // 외부의 ov에는 접근 불가
}
fun outside() {
val msg = Nested().greeting() // 객체 생성없이 중첩 클래스의 메서드 접근
println("[Outer]: $msg, ${Nested().nv}") // 중첩 클래스의 프로퍼티 접근
}
}
fun main() {
// static처럼 객체 생성 없이 사용
val output = Outer.Nested().greeting()
println(output)
// Outer.outside() // 오류! 외부 클래스의 경우는 객체를 생성해야함
val outer = Outer()
outer.outside()
}
이너 클래스
클래스 안에 이너 클래스를 정의할 수 있는데 이때 이너 클래스는 바깥 클래스의 멤버들에 접근할 수 있다.
심지어 private 멤버도 접근이 가능하다.
package chap07.section2
class Smartphone (val model: String) {
private val cpu = "Exynos"
inner class ExternalStorage(val size: Int){
fun getInfo() = "${model}: Installed on $cpu with ${size}Gb" // 바깥 클레스의 프로퍼티 접근
}
}
fun main() {
val mySdcard = Smartphone("S7").ExternalStorage(32)
println(mySdcard.getInfo())
}
지역 클래스
지역 클래스는 특정 메서드의 블록이나 init 블록과 같이 블록 범위에서만 유효한 클래스이다.
블록 범위를 벗어나면 더 이상 사용되지 않는다.
package chap07.section2
class Smartphone (val model: String) {
private val cpu = "Exynos"
fun powerOn(): String{
class Led(val color: String){ // 지역 클래스 선언
fun blink(): String = "Blinking $color on $model" // 외부의 프로퍼티는 접근 가능
}
val powerStatus = Led("Red") // 여기에서 지역 클래스가 사용됨
return powerStatus.blink()
} // powerOn() 블록 끝
inner class ExternalStorage(val size: Int){
fun getInfo() = "${model}: Installed on $cpu with ${size}Gb" // 바깥 클레스의 프로퍼티 접근
}
}
fun main() {
val mySdcard = Smartphone("S7").ExternalStorage(32)
val myphone = Smartphone("Note9")
myphone.ExternalStorage(128)
println(myphone.powerOn())
// println(mySdcard.getInfo())
}
여기서 사용된 Led 클래스는 Smartphone 클래스의 메서드인 powerOn()에서만 유효한 클래스다.
단 Led 클래스에서 외부의 멤버인 프로퍼티에는 접근할 수 있다.
익명 객체
자바에서는 익명 이너 클래스라는 것을 제공해 일회성으로 객체를 생성해 사용한다.
코틀린에서는 object 키워드를 사용하는 익명 객체로 이와 같은 기능을 수행한다.
실드 클래스와 열거형 클래스
실드(Sealed)란 '봉인된'이라는 의미로 무언가 안전하게 보관하기 위해 묶어 두는것을 말한다.
실드 클래스는 미리 만들어 놓은 자료형들을 묶어서 제공하기 때문에 어떤 의미에서는 열거형(Enum) 클래스의 확장으로도 볼 수 있다.
실드 클래스
sealed키워드를 class와 함께 사용한다.
실드 클래스는 그 자체는 추상 클래스와 같기 때문에 객체를 만들 수 없다.
또한 생성자도 기본적으로는 private이며 private가 아닌 생성자는 허용하지 않는다.
package chap07.section2
// 실드 클래스를 선언하는 첫 번째 방법
sealed class Result {
open class Success(val message: String): Result()
class Error(val code: Int, val message: String): Result()
}
class Status: Result() // 실드 클래스 상속은 같은 파일에서만 가능
class Inside: Result.Success("Status") // 내부 클래스 상속
fun main(){
// Success에 대한 객체 생성
val result = Result.Success("Good!")
val msg = eval(result)
println(msg)
}
// 상태를 검사하기 위한 함수
fun eval(result: Result) : String = when(result){
is Status -> "in progress"
is Result.Success -> result.message
is Result.Error -> result.message
// 모든 조건을 가지므로 else가 필요 없음
}
열거형 클래스
열거형 클래스란 여러 개의 상수를 선언하고 열거된 값을 조건에 따라 선택할 수 있는 특수한 클래스이다.
실드 클래스와 거의 비슷하나, 다양한 자료형을 다루지 못한다.
package chap07.section2
interface Score{
fun getScore(): Int
}
enum class MemberType(var prio: String) : Score{ // Score를 구현할 열거형 클래스
NORMAL("Third"){
override fun getScore(): Int = 100
},
SILVER("Second"){
override fun getScore(): Int = 500
},
GOLD("First"){
override fun getScore(): Int = 1500
}
}
fun main() {
println(MemberType.NORMAL.getScore())
println(MemberType.GOLD)
println(MemberType.valueOf("SILVER"))
println(MemberType.SILVER.prio)
for (grade in MemberType.values()){ // 모든 값을 가져오는 반복문
println("grade.name = ${grade.name}, prio = ${grade.prio}")
}
}
리플렉션이라는 개념도 있다.
프로그램을 실행할 때 프로그램의 특정 구조를 분석해 내는 기법으로 사용한다.
예를 들어 어떤 함수를 정의하는데 함수의 매개변수로 클래스 타입을 선언하고 실행할 때 매개변수로 전달된 클래스의 이름,
클래스의 메서드나 프로퍼티를 알아내는 작업이다.
연산자 오버로딩
연산자 오버로딩도 클래스의 다형성의 한 경우로 같은 연산자에 여러 가지 다른 의미의 작동을 부여할 수 있다.
package chap07.section3
class Point(var x: Int = 0, var y: Int = 10){
// plus()함수의 연산자 오버로딩
operator fun plus(p: Point) : Point{
return Point(x+p.x, y+p.y)
}
}
fun main(){
val p1 = Point(3, -8)
val p2 = Point(2, 9)
var point = Point()
point = p1 + p2 // Point 객체의 + 연산이 가능하게 됨
println("point = (${point.x}, ${point.y})")
}
9주차 - 컬렉션 (1) | 2024.01.11 |
---|---|
8주차 - 제네릭과 배열 (1) | 2024.01.11 |
6주차 - 프로퍼티와 초기화 (3) | 2024.01.09 |
5주차 - 클래스와 객체의 정의 (1) | 2024.01.06 |
4주차 - 프로그램의 흐름 제어 (0) | 2024.01.04 |
댓글 영역