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

18. Objective-C Inheritance 상속, encapsulation(캡슐화)

by Toughie 2024. 5. 21.

KAKAO-Choonsik

[상속, 캡슐화, 깊은복사]

 

객체지향 프로그래밍에서 상속은 굉장히 중요하다. 

코드의 재사용성 및 확장성 증대 & 개발 속도에서 효율을 끌어올릴 수 있으며 다형성 구현에도 필요하기 때문이다.

 

새로운 클래스가 필요할 때, 모든 데이터 및 멤버 메서드를 다시 정의하는 대신, 기존에 만들어둔 다른 클래스의 멤버를 상속받게 할 수 있다.

이 경우 기존 클래스는 부모 클래스(Super Class), 부모 클래스를 상속받아 만들어진 클래스는 자식 클래스(Sub Class) 라고 말한다.

 

OBC에서는 단일 상속만 지원한다. 즉 단 하나의 부모 클래스만 가진다는 것이다.(Java,Swift와 마찬가지)

이는 다이아몬드 문제(어느 클래스에서 상속받은 건지 모호한 문제)를 피하고 복잡한 상속 구조 대신

코드의 단순성과 명료성을 유지하기 위함이다.

대신 확장성을 위해서 프로토콜 및 카테고리 등의 기능이 제공되는데 이는 독립 챕터로 이후에 살펴보겠다.


부모 클래스 만들기

먼저 최상위 클래스로 '사람' 클래스를 만들어 보았다.

겨우 두 개의 프로퍼티와 두 개의 메서드만 있을 뿐인데, 고려할 부분이 꽤 많았다.

 

#import <Foundation/Foundation.h>

// super class
@interface Person : NSObject
    
@property (copy, nonnull) NSString *name ;
@property int age ;

-(id)initWithName: (NSString*)name andAge:(int)age;
-(void) introduce;
@end

@implementation Person

-(id)initWithName: (NSString *)name andAge: (int) age {
    _name = [name copy];
    _age = age;
    return self;
}

-(void) introduce {
    NSLog(@"I'm %@ and %d years old",_name,_age);
}
@end

 

인스턴스 변수 vs 프로퍼티

먼저 이름과 나이를 인스턴스 변수로 만들지, 프로퍼티로 만들지 고민했다.

인스턴스 변수의 경우 private이 기본이기 때문에, 내부 인스턴스 메서드에서만 쓴다면 상관 없지만

외부에서 이름과 나이에 접근할 상황이 생길 수도 있고 이름과 나이는 변할 가능성이 있기 때문에 property가 적절하다 생각했다.

DNA 정보 같은건 인스턴스 변수로 하는게 맞으려나..

 

NSString vs NSMutableString

이름의 경우 변경 가능성이 낮기 때문에, 그냥 새로운 이름 문자열 포인터로 참조만 변경해주면 될 것 같아서 

NSMutableString 대신 NSString으로 정했다.

NSMutableString은 타입명처럼 언제든 변경 가능성이 있는 상황에서 사용하는 것이 맞다고 생각한다.

 

copy

또한 기본타입인 int같은 경우는 크게 상관 없지만, 이름 같은 경우는 초기화 단계에서 문자열 상수 포인터가 전달될 수도 있고,

다른 문자열 객체가 전달될 수도 있다. 포인터만 전달하는 경우에는 원본에 영향이 갈 수도 있기 때문에 (물론 원본을 조작하는 상황이 필요할 수도 있겠지만) 객체에 따로 복사해서 관리하기 위해 copy 속성을 추가해 주었다.

이로써 Person의 인스턴스들은 각각 이름 문자열의 독립적인 복사본을 참조하게 될 것이다. 

 

nonnull

이름없는 사람은 이 세상에 없길 바라며 nonnull 속성을 추가해 주었다.

nil값은 허용하지 않는다.

 

weak

단순한 문자열만 전달하는 상황이기도 하고, 불변 객체의 경우 강한 순환 참조 사이클이 발생하지 않기 때문에 

현재는 크게 필요하지 않은 속성이라 생각했다. 


자식 클래스 만들기

이제 사람을 만들 수 있게 됐으니, 상속을 통해 좀 더 구체적인 특성을 지닌 개발자 클래스를 만들어 보자.

 

@interface Developer : Person

@property (copy, nonnull) NSMutableArray *skills;
-(id)initWithName: (NSString *)name andAge: (int)age andSkills: (NSMutableArray *)skills;
-(void)introduce;
@end

@implementation Developer

-(id)initWithName:(NSString *)name andAge:(int)age andSkills:(NSMutableArray *)skills {
    //부모 생성자 호출 먼저
    self = [super initWithName:name andAge:age];
    if (self) {
        _skills = [skills mutableCopy];
    }
    return self;
}

-(void) introduce {
    [super introduce];
    NSLog(@"Skill List");
    
    //Fast Enumeration
    for(NSString *skill in _skills) {
        NSLog(@"%@ ",skill);
    }
}
@end

 

기본적으로 Person 클래스를 통해 name, age, introduce 멤버들이 상속된다.

또한 개발스킬의 리스트를 담을 수 있는 가변배열 프로퍼티를 하나 추가해 주었다.

extension의 개념은 아직 나오지 않았지만, 상속관계에 있어 멤버 변수의 접근 제어 수준이 헷갈릴 때는

아래 포스팅의 접근제어 부분을 살펴보도록 하자.

 

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

 

17. Objective-C 클래스와 객체 (메모리), 접근제어

Objective-C는 이름만 봐도 알 수 있듯이 C언어에서 객체지향의 개념을 더한 슈퍼셋 언어이다.객체지향 프로그래밍에서 클래스는 핵심 기능인데 클래스는 객체를 찍어내기 위한 틀 혹은,데이터와

toughie-ios.tistory.com

 

 

생성자는 자동상속되지 않는다.

부모 클래스의 생성자가 자동 상속된다면, 모든 하위 클래스가 동일한 방식으로 초기화 될것이다.

따라서 다형성을 유지하고, 서브클래스마다 다른 초기화 방식을 허용하기 위해서 명시적으로 부모 생성자를 호출한 이후에

자신만의 생성자를 정의하도록 한다. 

 

부모 생성자 호출 이후 본인 멤버 초기화 순서를 지켜주도록 하자. (부모의 메서드를 호출할 때는 super 키워드를 사용한다.)

위 예시에서는 부모 생성자를 통해 생성이 잘 되었는지 if문으로 nil 체크를 해주고 있다.

 

메서드 오버라이딩

부모로부터 상속받은 메서드를 그대로 사용할 수도 있지만, 자식 클래스에서 내부 동작 방식을 다르게 할 수 있다.

위 예시코드의 introduce에는 부모의 메서드도 호출하고 로직이 추가되어 있다.

for문을 사용해서 iterable한 객체의 Fast-Enumeration을 통해 출력을 하고 있다.

메서드 오버라이딩 또한 다형성의 대표 예시이다.

 

copy vs mutableCopy

copy 메서드는 '불변 객체'를 반환한다.

반환된 객체는 원본 객체와 값은 동일하나 수정은 할 수 없다.

주로 NSString이나 NSArray와 같은 불변 객체를 복사할 때 사용한다.

 

mutableCopy는 '가변 객체'를 반환한다.

반환된 객체는 원본 객체와 동일한 값을 가지며 '수정 가능하다.'

즉 원본의 가변 복사본을 만든다는 뜻으로 이 복사본은 원본 객체와 독립적으로 존재하고 서로 다른 메모리 공간에 저장된다.

즉 복사본의 변경 사항이 원본에 영향을 끼치지 않는다는 의미이다.

 

따라서 변경여부에 따라 상황에 맞는 복사 방법을 사용하도록 하자.

기본적으로 복사를 하지 않고 포인터만 전달하게 되면 원본 배열에 변경사항이 적용되기 때문에 조심해야 한다.


Data Encapsulation

클래스를 정의할 때 @interface를 통해 선언을 하고, @implementation을 통해 구현으로 분리를 하면서

외부 인터페이스를 변경하지 않는 유연성을 챙기면서 내부 구현은 은닉화 했다. 

(필요한 인터페이스만 노출하고 구현부는 숨기는 측면에서 추상화 됐다고 표현할 수도 있다.)

 

꼭 필요한 부분이 아니면 숨기는 객체지향 프로그래밍의 특징 중 캡슐화에 대해서도 간단히 알아보자.

 

모든 프로그램은 함수에 의해 조작되는 데이터 & 데이터를 조작하는 코드(statesments), 메서드로 이루어져 있다.

캡슐화(Encapsulation)는 데이터와 관련 메서드를 묶어서 외부에서의 방해나 잘못된 사용을 방지하고자 하는 

OOP의 개념이다. 결국 OOP에서 캡슐화, 추상화는 꼭 필요한 것만 외부에 노출시키겠다는 것이다.

 

#import <Foundation/Foundation.h>

@interface MyNumber : NSObject {
    NSInteger total;
}
-(id) initWithNum: (NSInteger) initNum;
-(void) addNumber: (NSInteger) num;
-(NSInteger) getCurrentNum;
@end

@implementation MyNumber

-(id) initWithNum:(NSInteger)initNum {
    total = initNum;
    return self;
}

-(void) addNumber:(NSInteger)num {
    total += num;
}

-(NSInteger) getCurrentNum {
    return total;
}
@end

int main(void) {
    @autoreleasepool {
        MyNumber *cal1 = [[MyNumber alloc] initWithNum:7];
//        NSLog(@"%d",cal1.total); //protected type! encapsulated
        NSLog(@"current: %ld", (long)[cal1 getCurrentNum]);
        [cal1 addNumber:3];
        NSLog(@"current: %ld", (long)[cal1 getCurrentNum]);
    }
    return 0;
}

 

지난 시간 접근제어에 대한 개념을 학습한 것이 캡슐화에 도움이 된다.

사실상 위의 예시는 @property를 사용하지 않고 getter와 setter를 직접 구현해준 것으로 볼 수 있다.

 

메서드는 명시적인 접근제어 지정자가 없어서 기본적으로 @interface에서 선언된 메서드는 public으로 간주된다.

 

만약 private한 메서드가 필요하면 extension을 활용할 수 있는데, 이는 확장을 다루면서 정리해보겠다.

또한 swift에서 singleton 패턴을 구현하면서 private init을 사용했었는데

Objective-C에서는 dispatch_once와 static 등을 활용한다.

+ (instancetype)sharedInstance {
    static MySingleton *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

이렇게 Objective-C에서 상속과 캡슐화에 대해 간단하게 알아보았다.

너무 깊은 상속은 코드의 복잡성을 증가시킬 수 있기에, 이후 다룰 프로토콜을 더 잘 활용하고 싶은 것이 욕심이다.

(Swift가 다중 패러다임이지만 프로토콜 지향 프로그래밍이라고 알려져 있기 때문에 더 ..)

 

또한 캡슐화를 잘 적용하면서 협업시 혼란을 방지하고 안전한 코드를 작성할 수 있도록 노력해야겠다!