article thumbnail image
Published 2022. 4. 7. 23:11
728x90


🙋‍♂️  들어가면서

객체 지향이 주는 장점은 구현 변경의 유연함이다.
유연함을 얻을 수 있도록 해주는 방법에는 추상화가 있는데 그 전에 추상화를 가능하게 해주는 다형성에 대해 살펴보자.

 

📌  상속

상속(Inheritance) 은 한 타입을 그대로 사용하면서 구현을 추가할 수 있도록 해주는 방법을 제공한다. 상속받은 하위 클래스는 필요에 따라 상위 클래스에 정의된 메서드를 새롭게 구현할 수 있다. 이를 재정의(overriding)라 한다. 메서드를 재정의하면, 해당 메서드를 실행할 때 상위 타입의 메서드가 아닌 하위 타입에서 재정의한 메서드가 실행된다.

 

상속의 장점

  • 기존에 작성된 클래스를 재활용할 수 있다.
  • 자식 클래스 설계 시 중복되는 멤버를 미리 부모 클래스에 작성해 놓으면, 자식 클래스에서는 해당 멤버를 작성하지 않아도 된다.
  • 클래스 간의 계층적 관계를 구성함으로써 다형성의 문법적 토대를 마련한다.

 

 

상속의 종류

  1. 인터페이스 상속
    • 다중 상속을 지원하지 않는 언어에서 다형성을 구현하는 방법
    • 순수하게 타입만을 상속받는 상속
    • 인터페이스에서 정의만한 오퍼레이션을 직접 구현하는 방식으로 상속
    • 추상화를 구현하기 위한 핵심
  2. 구현 상속(클래스 상속)
    • 상위 클래스에 정의된 기능을 재사용하기 위한 목적 ( 재사용성이 핵심 목적)
    • 재사용성 + 다형성의 기능을 제공

 

코틀린에서의 상속 예시

open class Beginner(val name: String) {
    open fun attack() {
        println("$name 가 기본 공격을 수행합니다.")
    }
}

class Wizard(name: String) : Beginner(name) {
    override fun attack() {
        super.attack() // super 키워드를 통해서 부모클래스의 attack 메서드 수행
        println("$name 가 마법 공격을 수행합니다.")
    }
}

fun main() {
    val wizard = Wizard("stark")
    wizard.attack()
}

// 실행 결과
// stark 가 기본 공격을 수행합니다.
// stark 가 마법 공격을 수행합니다.

 

 

📌  추상클래스 vs 인터페이스

  1. 추상 클래스
    • abstract 메서드를 가지고 있는 클래스
    • 자식 클래스는 추상 메소드를 반드시 구현해야 한다.
    • 생성자를 가질 수 있다.
    • 다중 상속 불가
  2. 인터페이스
    • 일반적으로 모든 메서드가 abstract인 클래스를 의미하지만, java 8 부터는 default 키워드를 통해 메소드 구현이 가능
    • 생성자 X
    • 다중 상속 가능
public interface Animal {
  void eat(); 
  default void introduce() { 
  Systemp.out.println("안녕하세요");
  } 
}

 

코틀린에서는 인터페이스는 프로퍼티, abstarct 메서드, 일반 메소드 모두 가질 수 있다

interface Runner { 
    fun run() 
} 

interface Eater { 
    fun eat() { 
        println ("음식을 먹습니다")
    } 
} 

class Dog : Runner, Eater { 
    override fun run() {
        println ("뜁니다")
    }
    override fun eat() {
        println ("사료를 먹습니다")
    } 
}

 

 

 

📌  다형성

이름이 동일한 것처럼 보이지만 실행 결과를 다르게 가질 수 있는 것을 다형성(polymorphism)이라고 한다.

 

 

타입 상속을 통한 다형성 구현

코틀린에서 타입 상속을 통한 다형성 구현

open class Plane {
    open fun fly() {
        // 비행 관련 동작
    }
}

interface Turbo {
    fun boost()  // 인터페이스에서 메서드는 open 키워드 없이 해당 인터페이스를 구현하는 클래스에서 사용 가능하다.
}

class TurboPlane : Plane(), Turbo {
    override fun fly() {
        super.fly()
        // 구현할 추가 동작 구문
    }
    override fun boost() {
        // 구현할 구문
    }
}

 

위의 코틀린 코드에는 두 개의 클래스와 한 개의 인터페이스가 있다.


이 중 TurboPlane 클래스는 Plane 클래스를 상속받고 있고, Turbo 인터페이스도 같이 상속받고 있다.
이런 타입 상속 관계를 갖는 경우 다음과 같이 TurboPlane 타입의 객체에 Plane 타입이나 Turbo 타입에 정의된 메서드의 실행을 요청할 수 있다.

val tp: TurboPlane = TurboPlane()
tp.fly() // TurboPlane 에서 부모 클래스인 Plane 에 정의/ 구현된 메서드 실행
tp.boost() // Turbo 인터페이스에 정의되고 TurboPlane 에 구현된 메서드 실행

 

또한 TurboPlane 타입의 객체를 Plane 타입이나 Turbo 타입에 할당하는 것도 가능하다.

 

val tp: TurboPlane = TurboPlane()

val p: Plane = tp // TurboPlane 객체는 Plane 타입도 된다.
p.fly()

val t: Turbo = tp // TurboPlane 객체는 Turbo 타입도 된다.
t.boost()

 

 

 

📌  추상화

컴퓨터 과학에서 추상화란 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말한다.
추상화는 이처럼 구체적인 사물들 간의 공통점을 취하고 차이점을 버리는 일반화를 사용하거나, 중요한 부분을 강조하기 위해 불필요한 세부 사항을 제거함으로써 단순하게 만든다. 결국 핵심은 불필요한 코드를 제거하고 중요한 부분을 살리는 것이다.

 

예를 들어, for, while, do while 등과 같은 문법은 반복한다는 구문을 추상화한 것이다. 실제로는 CPU의 명령을 통해서 반복이 구현되겠지만, 이 구현으로부터 반복이라는 개념을 뽑아내서 for, while, do~while 등으로 추상화한 것이라고 할 수 있다.

 

 

객체 지향의 유명한 규칙 : 인터페이스에 대고 프로그래밍하기(program to interface)

  • 여기서 말하는 인터페이스는 오퍼레이션을 정의한 인터페이스
  • 코틀린이나 자바 같은 언어는 자체적으로 인터페이스나 추상 클래스를 이용해서 개념적인 인터페이스를 제공하고 있다.
  • 인터페이스에 대고 프로그래밍하기(program to interface) 규칙은 추상화를 통한 유연함을 얻기 위한 규칙이다.

 

 

추상화 예시

LogCollector Interface를 통한 추상화

 

interface LogCollector {
    fun collect()
}

class FtpLogFileDownloader : LogCollector {
    override fun collect() { println("FtpLogFileDownloader") }
}

class SocketLogReader : LogCollector {
    override fun collect() { println("SocketLogReader") }
}

class DbTableLogGateway : LogCollector {
    override fun collect() { println("DbTableLogGateway") }
}


class Logs(private val logCollector: LogCollector) {
    fun read() = logCollector.collect()
}

fun main() {
    val ftpLogReader = FtpLogFileDownloader()
    var logs = Logs(ftpLogReader)
    logs.read()

    val socketLogReader = SocketLogReader()
    logs = Logs(socketLogReader)
    logs.read()

    val dbTableLogGateway = DbTableLogGateway()
    logs = Logs(dbTableLogGateway)
    logs.read()
}

 

 

추상 타입 구현에서 발생할 수 있는 문제점

  • 변경이 일어날 가능성이 적은 부분에 대해 인터페이스를 만들면, 구조의 복잡성만 올라가고, 유연성의 장점을 누릴 수 없는 상황이 된다
  • 변경이 일어나는 부분을 정확히 캐치하여 인터페이스를 만들지 않으면, 상속 & 다형성으로 얻을 수 있는 장점이 깨질 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

'OOP' 카테고리의 다른 글

OOP - DI(Dependency Injection)와 서비스 로케이터  (0) 2023.02.26
OOP - SOLID 원칙  (0) 2022.10.27
OOP - 다형성과 추상 타입  (0) 2022.02.14
OOP - 객체 지향  (0) 2022.02.04
복사했습니다!