이재용의 iOS

객체지향의 다형성(Polymorphism)이란?

2022년 4월 1일 • ☕️ 5 min read

clean code

객체 지향의 4가지 주요 개념인 ‘추상화(Abstraction)’, ‘캡슐화(Capsulation)’, ‘상속(Inheritance)’, ‘다형성(Polymorphism)‘. 소프트웨어를 객체지향적으로 디자인하는 방법에서 이 네가지를 빼놓고 이야기 할 수 없다.

특히 필자는 객체지향에서 캡슐화가 가장 핵심이라고 생각한다. 캡슐화는 객체 지향 뿐만 아니라 다른 패러다임에서도 중요할 뿐만 아니라, 다형성 또한 class차원의 캡슐화를 구현한 구조이기 때문이다. 그런데 왜 다형성에 대해 이야기를 하려고 하는가? 객체 지향 패러다임에서 다형성은 변화에 유동적인 코드를 짜기 위해서는 필수적이기 때문이다.

그래서 이번 글에서는 swift에서 다형성을 구현하는 방법을 예제를 통해 소개하고 그 장점을 알아보려고 한다.

객체지향에서의 다형성

다형성은 하나의 메시지에 대해 서로 다른 객체가 서로 다른 방법으로 응답할 수 있는 기능이라고 할 수 있다. 이는 컴포넌트의 재사용성을 돕고, 컴포넌트 단위로 모듈화 되어 독립적으로 구현되므로 서로 간의 의존성이 감소된다.

다형성이 필요한 이유

구현하는 방법을 보기 전에 객체지향에서 다형성이 왜 중요한 지 예제를 통해 알아보자.

class TaxiDriver {
    private var driverName: String
    private var baseSalary: Int
    private var bonusMoney: Int
    
    init(driverName: String, baseSalary: Int, bonusMoney: Int) {
        self.driverName = driverName
        self.driverAge = driverAge
        self.baseSalary = baseSalary
        self.bonusMoney = bonusMoney
    }
    
    func getDriverName() -> String {
        driverName
    }
    
    func getSalary() -> Int {
        // ✅ 합
        return baseSalary + bonusMoney
    }
}

TaxiDrvier 클래스는 택시 드라이버의 이름과 기본급, 보너스급을 프로퍼티로 가진 클래스이다. 이들은 기본급과 보너스의 합으로 월급을 받는다.

class BusDriver {
    private var driverName: String
    private var workingHours: Int
    private var payPerHour: Int
    
    init(driverName: String, workingHours: Int, payPerHour: Int) {
        self.driverName = driverName
        self.workingHours = workingHours
        self.payPerHour = payPerHour
    }
    
    func getDriverName() -> String {
        driverName
    }
    
    func getSalary() -> Int {
        // ✅ 곱
        return workingHours * payPerHour
    }
}

BusDriver 클래스도 마찬가지로 이름을 프로퍼티로 가지지만 일한시간과 시간당 급여를 프로퍼티로 가져 월급의 계산방식이 기본급과 시간당 급여의 곱이다.

위 두 클래스와 HAS-A 관계를 가진 DriverList 클래스는 운전사들의 리스트이다.

class DriverList {
    private var taxiDriverList: [TaxiDriver] = []
    private var busDriverList: [BusDriver] = []
    
    private var numTaxiDrivers: Int = 0
    private var numBusDrivers: Int = 0
    
    func addTaxiDriver(taxiDriver: TaxiDriver) {
        taxiDriverList.append(taxiDriver)
        numTaxiDrivers += 1
    }
    
    func addBusDriver(busDriver: BusDriver) {
        busDriverList.append(busDriver)
        numBusDrivers += 1
    }
}

위 클래스들처럼 만들면 무엇이 문제일까?

클래스가 매우 길고 반복적인 코드가 많다. 택시와 버스 모두 같은 이름 프로퍼티와 이름 메소드를 가지고 있다. DriverList 클래스는 택시와 버스 각각 리스트를 만들어야 저장이 가능하고 다른 클래스의 구조를 알아야 사용이 가능하다. 설상가상으로, 새로운 클래스가 생성 혹은 삭제 된다면 수정해야 한다.

그럼 요구사항이 변경되어 Driver 클래스들이 수정되더라도 외부 코드(이 예제에서는 DriverList 클래스)는 영향을 받지 않도록 모듈화되는 구조로 수정해보자. 여기서 다형성을 떠올릴 수 있다.

swift로 다형성을 구현하는 방식에는 여러가지가 있지만 그 중 2가지 방법을 소개하려고 한다.

  • 오버라이드
  • 프로토콜을 이용

오버라이드

오버라이드는 subclass가 superclass 함수의 동일한 함수를 재정의하는 것을 말한다.

class Driver {
    private var driverName: String
    
    init(driverName: String) {
        self.driverName = driverName
    }
    
    func getDriverName() -> String { driverName }

    func getSalary() -> Int { return 0 }
}

택시와 버스의 superclass인 Driver를 만들고 공통된 프로퍼티와 메소드로 정의 및 구현해주었다.

그럼 이제 택시와 버스 클래스를 재정의해보자.

class TaxiDriver: Driver {
    private var baseSalary: Int
    private var bonusMoney: Int
    
    init(driverName: String, baseSalary: Int, bonusMoney: Int) {
        self.baseSalary = baseSalary
        self.bonusMoney = bonusMoney
        super.init(driverName: driverName)
    }
    
    override func getSalary() -> Int {
        // ✅ 합
        return baseSalary + bonusMoney
    }
}

버스도 택시 클래스와 동일하여 생략하도록 하겠다.

여기서 getSalary 메소드에 주목해보자. 택시와 버스에서 구현이 다르게 되어있지만 오버라이드를 통해 subclass에서 재정의를 해준다.

class DriverList {
    private var driverList: [Driver] = []
    private var numDrivers: Int = 0
    
    func addDriver(driver: Driver) {
        driverList.append(driver)
        numDrivers += 1
    }
 
    func getSalary() -> Int {
       var sum: Int = 0
        driverList.forEach {
            sum += $0.getSalary()
        }
        return sum
    }
}

재정의한 DriverList 클래스이다.

외부에서 Driver 클래스의 subclass의 구조를 몰라도 getSalary 메소드에 접근이 가능하며 호출할 경우 버스 혹은 택시의 getSalary 메소드가 응답한다. driverList 프로퍼티의 getSalary 하나의 요청에 서로 다른 방법으로 응답하는 것을 알 수 있다.

그럼 테스트를 해보자.

let driverList = DriverList()

let taxiDriver1 = TaxiDriver(driverName: "taxi1", baseSalary: 10, bonusMoney: 10)
driverList.addDriver(driver: taxiDriver1)

let busDriver1 = BusDriver(driverName: "bus1", workingHours: 10, payPerHour: 10)
driverList.addDriver(driver: busDriver1)

let salary = driverList.getSalary()
print(salary)

// 120

택시는 10 + 10 = 20, 버스는 10 x 10 = 100, 합으로 120이 나오는 것을 확인할 수 있다.

프로토콜

프로토콜은 정의를 하고 제시를 하는 구조체이다. 자바의 인터페이스와 비슷하다.

protocol Driver {
    var driverName: String { get }
    func getDriverName() -> String
    func getSalary() -> Int
}

extension Driver {
    func getDriverName() -> String {
        driverName
    }
}

위 프로토콜과 같이 구현할 수 있다. 오버라이드보다 더욱 간단하게 구현할 수 있지만 프로토콜을 사용한다면 driverNameprivate하게 보호할 수 없다는 단점이 있다.

추가로, 만약 트럭 운전사 클래스를 추가하고 싶다면 DriverList 클래스의 수정 없이 추가가 가능하다.

class TruckDriver: Driver {
    var driverName: String
    private var numTrucks: Int
    private var payForTruck: Int
    
    init(driverName: String, numTrucks: Int, payForTruck: Int) {
        self.driverName = driverName
        self.numTrucks = numTrucks
        self.payForTruck = payForTruck
    }
    
    func getSalary() -> Int {
        // ✅ 곱
        return numTrucks * payForTruck
    }
}

정리

다형성을 구현하기 위해서는 객체지향의 “상속”과 “캡슐화”, “추상화”를 알고 있어야 한다. 하지만 다형성을 구현하게 되면 반복된 코드를 줄이고, 객체끼리 서로 의존하지 않으며 변화에 유연하게 설계할 수 있다.

사실 다른 언어를 많이 써보지는 못 했지만 swift에서 다형성을 구현하는데 다른 언어와 차이점은 프로토콜이라고 생각한다. 이 프로토콜을 얼마나 유연하게 잘 쓰는가가 중요한 것 같다. 프로토콜을 잘 정의하고 적용해보자.

사진출처