Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
iOS Developer/Objective-C

19. Objective-C 다형성(polymorphism)

by Toughie 2024. 5. 22.

KAKAO-Choonsik

[다형성, 오버라이딩, 업캐스팅, 다운캐스팅]


 

다형성은 말 그대로 형태가 다양하다는 것이다. 

프로그래밍에서 다형성은 주로 클래스간의 상속관계가 있을 때 나타난다.

 

메서드 오버라이딩(Method overriding)

같은 이름의 메서드라도 어떤 타입으로 참조하고 있는지에 따라 내부 동작이 달라진다.

좀 더 정확히는 서브클래스에서 슈퍼클래스의 메서드를 재정의 하는 것을 말한다.

즉 어떤 객체를 부모타입으로 참조하느냐? 본인 타입으로 참조하느냐에 따라 호출되는 메서드가 다른 것이다.

 

메서드 디스패치 테이블(Method Dispatch Table)

옵젝씨에서 메서드는 메서드 디스패치 테이블을 통해 관리된다.

(내부적으로는 클래스의 메타 데이터 등이 저장되는 구조체로 되어 있다.)

메서드는 런타임에 메서드 호출을 처리하는 방식인 동적 디스패치(dynamic dispatch) 방식이 사용된다.

 

1. 객체의 메서드가 호출되면 런타임은 객체의 클래스 정보를 확인한다.

2. 클래스의 메서드 디스패치 테이블을 조회해 호출된 메서드의 포인터를 찾는다. 

3. 해당 메서드 포인터를 통해 실제 메서드가 호출된다. 

(본인 타입으로 먼저 찾고, 없으면 부모 테이블로 찾아가는 방식)

 

메서드 테이블에 대한 관련 내용은 자바 포스팅에서도 확인할 수 있다. 

(완전 동일하지는 않더라도 거의 비슷한 방식일 것이다.)

https://toughie-ios.tistory.com/368

 

8. 상속, 오버라이딩, super

객체지향의 4대 특징(캡슐화, 상속, 추상화, 다형성) 중 상속에 대해 Araboza. 추상화 먼저 추상화는 '복잡한 것을 단순하게' 만드는 것을 말한다고 할 수 있다. 그 방법 중 하나는, 복잡한 객체들 사

toughie-ios.tistory.com

 

메서드의 동적 바인딩 덕분에 메서드의 다형성이 구현되는 것이다.

만약 정적 바인딩이 이루어진다면 컴파일 타임에 호출할 메서드가 정해져버리기 때문이다. (ex static 메서드)


상속과 오버라이딩 개념을 적용할 수 있는 예시 코드를 작성해 보았다.

같은 이름의 함수를 호출해도 내부 동작 방식이 다를 수 있다는 것과, 상속 관계에 있어서

인스턴스를 어떤 타입으로 참조하느냐에 따라 접근 가능한 범위 및 메서드 동작이 달라지는 것을 확인해보자.

 

부모 클래스

#import <Foundation/Foundation.h>

@interface Human: NSObject
-(void) whatIsBench;
@end

@implementation Human
-(void) whatIsBench {
  NSLog(@"sit on a bench");
}
@end

 

간단하게 자식클래스에서 오버라이딩 할 메서드 하나만 존재한다.

 

자식 클래스

@interface PowerLifter: Human
-(id) init;
-(void) onlyAvailableLift;
@property(assign) NSString *strap;
@end

@implementation PowerLifter
-(id) init {
  [super init];
  _straps = @"versa";
  return self;
}
-(void) whatIsBench {
  NSLog(@"bench press");
}

-(void) onlyAvailableLift {
  NSLog(@"only lifting");
}
@end

 

참조타입에 따라 접근 가능 여부를 확인하기 위한 프로퍼티, 

그리고 오버라이딩 된 메서드(whatIsBench)가 있다.

또한 자식 클래스에서만 접근 가능한 onlyAvailableLift 메서드도 존재한다.


활용

int main(void) {
  @autoreleasepool {
    Human *human = [[Human alloc] init];
    [human whatIsBench]; // sit on a bench

    PowerLifter *lifter = [[PowerLifter alloc] init];
    [lifter whatIsBench]; // bench press
    NSLog(@"%@", lifter.straps);
    [lifter onlyAvailableLift]; //only lifting

    Human *upcasting = [[PowerLifter alloc] init];
    [upcasting whatIsBench]; //bench press (인스턴스의 타입에 따라 동적으로 결정됨)

    // NSLog(@"%@", upcasting.straps) //Property 'straps' not found on object of type 'Human'
    // [upcasting onlyAvailableLift] //Human may not respond to 'onlyAvailableLift'

    if ([upcasting isKindOfClass: [PowerLifter class]]) {
      PowerLifter *downcasting = (PowerLifter *) upcasting;
      downcasting.straps = @"VERSA";
      [downcasting whatIsBench]; //bench press
      NSLog(@"%@", downcasting.straps) // VERSA
      [downcasting onlyAvailableLift];
    }
  }
  return 0;
}

 

메서드 오버라이딩

Human 인스턴스를 통한 whatIsBench의 동작과

PowerLifter 인스턴스를 통한 whatIsBench의 동작이 다른 것을 알 수 있다.

핵심은 상속관계가 있을 때 메서드를 호출하면 참조타입과 상관없이 해당 인스턴스의 메서드가 호출된다는 것이다.

즉 오버라이딩이 되었다면 부모타입으로 참조를 하더라도 오버라이딩 된 메서드가 호출된다는 의미이다.


자식 인스턴스의 생성 과정

[메모리 할당]

자식 클래스의 인스턴스를 생성되기 위해서는 자식 클래스와 부모 클래스의 모든 인스턴스 변수(iVar)를 포함하는 크기의 메모리가 할당된다.

 

[초기화]

할당된 메모리 공간은 부모 클래스에서 자식 클래스 순으로 진행된다.

생성자는 상속되지 않으며, 자식 클래스에서 생성자를 구현하는 경우 [super init]을 호출하는 것을 생각하면 자연스럽다.

자식 클래스 인스턴스의 초기화는 부모 생성자를 통해 초기화가 완료 되면(부모 iVar),

자식 클래스의 초기화가 진행된다.(자식 iVar) 


업캐스팅

위와 같은 자식 인스턴스 생성 과정 덕분에, 자식 인스턴스를 부모 타입을 참조할 수 있다.

이를 업캐스팅이라고 하며, 이는 한가지 타입으로 여러가지 타입의 인스턴스를 참조할 수 있기 때문에

다형성의 중요한 역할을 한다.

 

위 예시 코드에서는 PowerLifter 인스턴스를 Human 타입으로 참조하고 있다.

whatIsBench 메서드를 호출하면, 부모타입으로 참조를 하더라도 오버라이딩 된 자식의 메서드가 호출된다.

 

하지만 자식 클래스에만 있는 프로퍼티나, 메서드에는 접근할 수 없다.

(자바에서는 자식 클래스에만 있는 메서드에 접근하려 하면 아예 컴파일 에러가 났는데,

Xcode에서 이상하게 응답하지 않을 수도 있다는 경고만 띄우고 빌드는 잘 되었다.)

하지만 이것은 올바른 접근법이 아니기에 그냥 안된다고 생각하자.


다운캐스팅

그렇다면 확장성을 위해 부모타입으로 자식 인스턴스를 참조했다가,

자식 인스턴스의 변수나 메서드에 접근하려면? 다시 자식 타입으로 캐스팅해주면 된다.

 

이것을 다운캐스팅이라고 하는데 다운캐스팅은 런타임에서 에러가 발생할 수도 있기 때문에

인스턴스가 해당 타입으로 캐스팅 될 수 있는지 확인하는 것이 좋다.

 

예시 코드에서는 isKindOfClass 메서드를 통해서 안전하게 인스턴스 여부를 확인하고 있다.

이는 객체를 새로 만드는 것이 아니라, 객체는 메모리 상에 하나만 존재하는 상태에서

이 객체를 참조하는 새로운 참조 변수를 잠시 추가한 것이다.


 

이렇게 간단한 예시를 통해서

메서드 디스패치 테이블을 통한 메서드 관리 방식,

메서드 오버라이딩, 업캐스팅 및 다운캐스팅을 통한 다형성 구현에 대해 알아보았다.

 

사실 위와 같은 참조 방식보다는 객체가 특정 클래스에 최대한 의존하지 않도록

자바에서는 인터페이스, Objective-C와 Swift에서는 프로토콜을 통해서

다형성을 구현하는 경우가 더 많다.

 

클래스에서 category, extension에 대해 먼저 알아본 다음 protocol도 살펴보도록 하겠다.