서브클래싱과 익스텐션

jonghwan·2022년 9월 28일
0

멋쟁이사자처럼

목록 보기
15/28
post-thumbnail

상속, 클래스, 그리고 하위 클래스

상속의 개념은 현실 세계의 관점을 프로그래밍으로 가져왔다.

이것은 클래스에 어떤 특성(예를 들어, 메서드와 프로퍼티)을 정의할 수 있게 해주었고, 그 클래스를 상속받은 다른 클래스를 생성할 수 있게 해준다.

상속된 클래스는 부모 클래스의 모든 기능을 상속받으며 자신만의 기능을 추가하게 된다.

클래스의 상속을 통하여 클래스 계층구조(class hierarchy)를 만든다.

계층 구조의 최상위에 있는 클래스를 베이스 클래스(base class) 또는 자식 클래스 (child class)라고 부른다.

하나의 클래스는 하위 클래스를 얼마든지 가질 수 있다.

하위 클래스가 상속받은 클래스를 부모 클래스(prarent class) 또는 상위 클래스(super class)라고 부른다.

클래스는 루트 클래스를 상속받을 수도 있다.

그리고 하위 클래스는 또 다른 하위 클래스를 둘 수도 있어서 클래스 계층 구조를 크고 복잡하게 만들 수 있다.

상속 예제

class BankAccount {
  var accountBalance: Float
  var accountNumber: Int

  init(number: Int, balance: Float) {
    accountNumber = number
    accountBalance = balance
  }

  func displayBalance() {
    print("Number \(accountBalance)")
    print("Current balance is \(accountNumber)")
  }
}

BankAccount 클래스의 선언부를 간단하게 하면 이와 같이 줄어들 것이다.

이것은 다소 단순한 클래스이지만, 계좌 번호와 잔고를 저장하는 것이 우리가 하는 일의 전부라면 이것으로도 충분하다.

그런데 BankAccount 클래스 뿐만 아니라 저축 계좌로 사용할 클래스가 필요하다고 가정해보자.

저축 계좌 역시 계좌 번호와 잔고를 저장해야 할 것이며, 이러한 데이터에 접근할 수 있는 메서드도 필요할 것이다.

class SavingsAccount {
  var accountBalance: Float
  var accountNumber: Int

  init(number: Int, balance: Float) {
    accountNumber = number
    accountBalance = balance
  }

  func displayBalance() {
    print("Number \(accountBalance)")
    print("Current balance is \(accountNumber)")
  }
}

이를 구현하기 위한 하나의 방법으로 BankAccount 클래스의 모든 기능을 복사하여 새로운 클래스를 만들고, 저축 계좌에 필요한 새로운 기능을 넣는 방법이 있다.

하지만, 좀 더 효율적인 방법은 BankAccount의 하위 클래스로 새로운 클래스를 만드는 것이다.

새로운 클래스는 BankAccount의 모든 기능을 상속받을 것이며, 저축 계좌에 필요한 기능들을 추가하여 확장될 수 있다.

class SavingsAccount: BankAccount {

}

SavingsAccount라고 불릴 BankAccount의 하위 클래스를 생성하기 위하여 새로운 클래스를 선언하고, 부모 클래스로 BankAccount를 지정하자.

아직은 어떠한 인스턴스 변수나 메서드를 추가하지 않았지만, 이 클래스는 부모인 BankAccount 클래스의 모든 메서드와 프로퍼티를 실제로 상속받았음을 알 수 있다.

SavingsAccount는 BankAccount 클래스로 했던 것과 동일한 방법으로 변수를 설정하고 메서드를 호출하는 것이 가능하다.

하위 클래스의 기능 확장하기

class SavingsAccount: BankAccount {

  var interestRate: Float = 0.0

  func calculateInterest() -> Float {
    return interestRate * accountBalance
  }
}

실제로 하위 클래스가 어떻게 되는지 확인하기 위하여 저축 계좌 정보를 저장할 수 있도록 하위 클래스를 확장해보자.

원하는 프로퍼티와 메서드를 클래스에 추가해보자.

상속받은 메서드 오버라이딩 하기

상속을 사용할 경우 우리에게 필요한 작업과 거의 비슷한 메서드를 부모 클래스에서 찾기란 어렵지 않다.

하지만, 우리에게 필요한 정확한 기능을 제공하기 위해서는 수정이 필요하다.

다시 말해, 우리는 원하는 작업을 정확하게 표현하는 이름의 메서드를 상속받을 수 있지만, 실제로는 우리에게 필요한 동작이 아닐 수 있다는 의미다.

이러한 조건에서 할 수 있는 한 가지 방법은 상속된 메서드를 무시하고 완전히 새로운 이름의 메서드를 새롭게 만드는 것이다.

하지만, 좀 더 좋은 방법으로는 상속받은 메서드를 오버라이드(override)하여 하위 클래스 내에 새로운 버전의 메서드를 만드는 것이다.

메서드를 오버라이딩을 할 때 반드시 따라야 할 두 가지 규칙이 있다.

하위 클래스에서 오버라이딩 하는 메서드는 오버라이딩 되는 부모 클래스 메서드의 매개변수 개수와 타입이 정확하게 일치해야 한다.

새롭게 오버라이딩 하는 메서드는 반드시 부모 클래스 메서드가 반환하는 타입과 일치해야 한다.

class SavingsAccount: BankAccount {

  var interestRate: Float = 0.0

  func calculateInterest() -> Float {
    return interestRate * accountBalance
  }

  override func displayBalance() {
    print("Number \(accountNumber)")
    print("Current balance is \(accountBalance)")
    print("Prevailing interest rate is \(interestRate)")
  }
}

BankAccount 클래스에는 은행 계좌 번호와 현재 잔고를 표시하는 displayBalance라는 이름의 메서드가 있다.

여기에 더불어, 하위 클래스인 SavingsAcount에서는 계좌에 할당된 현재 이자율도 출력하고 싶다.

이를 위하여 override 키워드가 앞에 붙은 displayBalance 메서드의 새로운 버전을 SavingsAccount 클래스에 선언한다.

class SavingsAccount: BankAccount {

  var interestRate: Float = 0.0

  func calculateInterest() -> Float {
    return interestRate * accountBalance
  }

  override func displayBalance() {
    super.displayBalance()
    print("Prevailing interest rate is \(interestRate)")
  }
}

또한, 하위 클래스에서 오버라이드된 상위 클래스의 메서드를 호출할 수도 있다.

예를 들어, 상위 클래스의 displayBalance 메서드는 이자율을 표시하기 전에 계좌 번호와 잔고를 출력하도록 호출될 수 있으므로 코드의 중복을 없앨수 있다.

하위 클래스 초기화하기

class BankAccount {
  
  
  init(number: Int, balance: Float) {
    accountNumber = number
    accountBalance = balance
  }
  
}

현재 SavingAccount 클래스는 다음과 같이 부모 클래스인 BankAccount의 초기화 메서드를 상속하고 있다.

이 메서드는 클래스의 계좌 번호와 잔고 모드를 초기화하는 데 필요한 과정을 진행한다.

class SavingsAccount: BankAccount {

  var interestRate: Float
  init(number: Int, balance: Float, rate: Float) {
    interestRate = rate
    super.init(number: number, balance: balance)
  }
  
}

하지만 SavingsAccount 클래스는 이자율에 대한 변수가 추가로 필요하다.

따라서 SavingsAccount 클래스의 인스턴스가 생성될 때 interestRate 프로퍼티가 초기화되도록 해야 한다.

SavingsAccount의 init 메서드는 이자율을 초기화하는 작업을 한 다음, 부모 클래스의 init 메서드를 호출하여 모든 변수가 초기화되도록 한다.

초기화 과정에서 발생할 수 있는 잠재적인 문제를 피하기 위해서 상위 클래스의 init 메서드는 항상 하위 클래스의 초기화 작업이 완료된 후에 호출되도록 해야 한다.

SavingsAccount 클래스 사용하기

class SavingsAccount: BankAccount {

}

let savings1 = SavingsAccount(
  number: 12311,
  balance: 600.00,
  rate: 0.07
)

print(savings1.calcurateInterest())
savings1.displayBalance()

이제 SavingsAccount 클래스에 대한 작업이 끝났으니 부모 클래스인 BankAccount를 사용했던 이전의 예제 코드처럼 SavingsAccount 클래스를 사용할 수 있다.

클래스 익스텐션

Swift 클래스에 새로운 기능을 추가하는 또 다른 방법은 익스텐션(extension)을 이용하는 것이다.

익스텐션은 하위 클래스를 생성하거나 참조하지 않고 기존 클래스에 메서드, 초기화(initializer), 그리고 연산 프로퍼티와 서브스크립트(subscript) 등의 기능을 추가하기 위하여 사용될 수 있다.

Swift 언어와 iOS SDK 프레임워크에 내장된 클래스에 기능을 추가할 때 익스텐션을 이용하면 매우 효과적일 수 있다.

클래스는 다음의 구문을 이용하여 익스텐션된다.

extension ClassName {
  //새로운 기능을 여기에 추가한다.
}

이번 예제에서는 표준 Double 클래스에 제곱 값을 반환하는 프로퍼티와 세제곱 값을 반환하는 프로퍼티를 추가하고자 한다고 가정하자.

이 기능은 다음의 익스텐션 선언부를 이용하여 추가할 수 있다.

extension Double {
  var squared: Double {
    return self * self
  }

  var cubed: Double {
    return self * self
  }
}

Double 클래스에 두 개의 새로운 연산 프로퍼티를 갖도록 확장했으니 이제는 다음과 같이 이용할 수 있다.

코드를 실행하면 print문은 9.0의 값을 출력할 것이다.

extension Double {
  var squared: Double {
    return self * self
  }

  var cubed: Double {
    return self * self
  }
}
         
           |
           v
           
let myValue: Double = 3.0
print(myValue.squared)

여기서 주목해야 할 점은 myValue 상수를 선언할 때 Double형이 되도록 선언하고 익스텐션 프로퍼티를 사용했다는 것이다.

실제로 프로퍼티는 하위 클래스를 사용하는 것이 아니라 익스텐션으로 추가된 것이므로, 우리는 Double 값에서 이 프로퍼티에 직접 접근할 수 있다.

let myValue: Double = 3.0
print(myValue.squared)

print(3.0squred)
print(6.cubed)

요약

상속은 새로운 클래스가 기존의 클래스로부터 파생되어 새로운 기능이 추가되도록 하는 방법으로 객체지향 프로그래밍에서의 객체 재사용성 개념을 확장시켜 준다.

기존의 클래스가 프로그래머가 원하는 일부 기능을 제공하고 있는 경우, 상속은 기존 클래스를 새로운 하위 클래스의 기본형처럼 사용할 수 있게 해준다.

Swift의 익스텐션은 하위 클래스를 생성하지 않고도 기존의 클래스에 기능을 추가할 수 있는 유일한 방법을 제공한다.

0개의 댓글