Wireframe

[Objective-C] 카테고리

카테고리는 쉽고도 정말 강력한 Objective-C의 기능 중 하나이다. 카테고리는 기존에 정의된 어떤 클래스를 쉽게 확장할 수 있도록 한다. 만약 A라는 클래스에 부족한 기능이 있어 이를 추가하고자 할 때는 해당 클래스를 상속 받는 클래스 B를 만들 수 있고, 이를 사용하면 된다.1 하지만 이 새롭게 추가된 기능을 사용하기 위해서는 이전에 작성한 코드에서 클래스 A를 사용하는 부분을 모두 클래스 B에 맞도록 수정해야 한다.

왜 확장이 아니라 카테고리인가

하지만 카테고리를 사용하게 되면 A+NewFeature 라는 파일을 만들고 새롭게 추가되는 기능을 추가해서 기존 클래스 A에 새로운 기능이 이미 있는 것처럼 사용할 수 있게 된다. 그런데 기존 카테고리의 기능을 확장하는데 왜 이름이  카테고리일까?
일반적인 클래스는 2개의 파일 (헤더와 구현부)로 만들어지는데, 대략의 구성은 다음과 같다.

만약 어떤 클래스를 만들어야 하는데, 이 클래스는 제법 범용적인 용도를 가지고 있어서 널리 쓰이는 만큼 그 속에 구현되어야 하는 기능이 매우 많다고 가정하자. 이런 경우 이 클래스를 1개의 소스 (이 한 개의 소스는 결국 .h 파일과 .m의 쌍으로 구성되겠지만)에 모두 코딩한다고 하면 이렇게 덩치가 큰 소스코드를 작성하는 것은 추후 유지 보수 측면에서도 불리할 것이다.
만약 최소한의 공통 요소만으로 기본 클래스를 지정한 후 비슷한 기능들을 묶어서 분류하고, 이들 기능들을 다시 각 분류별 하나의 파일로 분리한다면 관리 및 유지보수도 수월하게 할 수 있게 된다. 즉,

이런 식으로 ClassA라는 클래스를 쪼개어 만드는 것이다. 카테고리라는 명칭은 이처럼 특정 기능들을 카테고리로 묶어내고 이들을 별개의 파일로 정의하는 것을 의미한다.
이렇게 클래스를 디자인할 경우(모든 객체지향언어에서 이렇게 디자인할 수 있는 것은 아니다) 해당 클래스를 필요한 기능만 컴팩트하게 가져다 쓸 수 있는 장점이 있다. 거대한 SomeClass를 다른 프로젝트에서 쓰려고할 때 단지 네트워킹 및 문자열 처리와 관려된 기능만 사용하려고 한다면 SomeClass, SomeClass+Network, SomeClass+String의 세 구성 요소만 가져다 쓰면 되는 것이다.
또한 카테고리는 클래스 원본에 대해 새로운 기능을 추가하여 확장하는 것이므로, 기존에 있는 클래스, 심지어는 Foundation이나 UIKit 등에 있는 클래스도 같은 문법을 사용하여 확장하는 것이 가능하다.

카테고리 작성하기

카테고리는 기존 클래스에 대해 어떤 분류로 묶을 수 있는 하나 혹은 그 이상의 기능의 추가적인 집합이다. 따라서 해당 메소드들을 선언하는 인터페이스 파일과, 그 기능들의 실제 정의를 담은 구현부 파일이 한 쌍으로 이루어진다. (반대로 프로토콜은 기능의 선언만 있으면 되므로 인터페이스 파일 하나로만 구성된다.) 카테고리의 파일 이름은 강제적인 내용은 아니지만 기존 클래스에 특정 기능을 추가하는 것이므로 클래스이름+카테고리명으로 이루어진다.  단순히 더하기가 아니라 중간에 플러스 기호를 붙인다. 즉 NSString+Reorder.h / NSString+Reorder.m 과 같은 형태로 만든다.

인터페이스 파일

인터페이스 파일은 기능을 추가하고자 하는 클래스 본체의 인터페이스 파일을 임포트해야 한다. 그리고 추가하고자 하는 메소드의 목록을 선언한다. 인터페이스 블럭은 클래스 이름 뒤에 카테고리 이름을 괄호에 넣어 붙여주면 된다. 또한 카테고리에서는 인스턴스 변수는 추가할 수 없다.2
카테고리의 헤더는 다음과 같은 식으로 작성된다. 확장하려는 클래스명 뒤에 괄호로 카테고리명을 추가해주면 된다.

//NSString+Reorder
#import <Foundation/NSString.h>
@interface NSString (Reorder)
    -(NSString *)reversedString;
@end

구현부

구현부 파일은 카테고리의 인터페이스를 반입한다. 만약 구현하고자 하는 어떤 메소드에서 동일 클래스의 다른 카테고리에 정의된 메소드를 참조해야 한다면, 해당 카테고리의 인터페이스 파일을 반입해 주어야 한다.

#import "NSString+Reorder.h"
#import "NSString+PathComp.h"
@implementation NSString
-(NSString *)reversedString
{
    ...
}
@end

카테고리의 적용

특정 클래스3에 대해 카테고리를 만든 경우, 다른 클래스에서 이 클래스의 인스턴스를 참조하는 경우에 카테고리에서 추가한 기능까지 모두 포함한 한 덩어리로 인식하게 된다. 또한 해당 클래스를 상속받는 클래스 역시 수퍼 클래스가 카테고리로 추가한 기능을 그대로 상속받을 수 있다.
즉 카테고리를 통해 기존 클래스에 메소드를 추가해준 경우, 이 클래스는 그 프로젝트 내에서는 해당 메소드가 추가된 형태로 인식된다. 애초에 클래스 자체가 여러 개의 소스로 분리되어 구현되어 있다고 인식되는 셈이다.
카테고리에서 본체에 정의된 메소드를 다시 추가하면 기존의 메소드는 폐기되고 카테고리에서 구현한 메소드로 동작하게 된다. 서브클래싱할 때 오버라이드하는 메소드의 경우, 원본(부모)의 메소드를 사용하고자 할 때는 super로 메시지를 보내 기존의 메소드를 그대로 사용할 수 있는 것과는 차이가 있다.
특히 많은 클래스들의 “원시” 메소드는 해당 클래스의 다른 메소드들이 참조하고 있으므로, 이를 카테고리에서 덮어 써버리면 프로그램이 예측하지 못한 방식으로 동작할 위험이 있다. 따라서 카테고리에서 이들 기존 메소드를 오버라이딩하는 것은 바람직하지 않다.
또 클래스 하나에 여러 개의 카테고리를 붙일 수 있다. 카테고리의 개수에는 제한이 없지만 카테고리의 이름은 모두 달라야 한다. 그리고 각 카테고리는 서로 다른 메소드들을 선언해야 한다. 만약 카테고리 간에 같은 이름을 가진 메소드를 선언하고 구현했다면 어떤 것이 적용될지는 알 수 없다.

클래스 확장

카테고리는 기존의 클래스에 메소드를 추가할 때 사용한다. 이 문법을 활용하여 클래스 확장(extension)에 적용할 수도 있다. 구현부 파일의 @implemetation 부분 앞쪽에 추가적인 @interface 구문을 사용하고 카테고리 이름을 비워두는 방식을 사용한다. 이를 private interface라 부르기도 하는데, 여기서 선언한 프로퍼티나 메소드들은 클래스 외부에서는 접근할 수 없고, 클래스 내부에서만 사용하게 된다.

기존의 클래스에 인스턴스 변수를 추가하기

카테고리는 기존 클래스에 메소드를 추가할 수는 있지만, 인스턴스 변수를 추가할 수는 없다. 그렇다고 해서 Objective-C에서 기존 클래스에 인스턴스 변수를 추가할 방법이 없는 것은 아니다. 흔하게 사용되는 것은 아니지만 Asscosiative References라는 방법을 사용하여 기존 클래스에 추가적인 인스턴스 변수를 부여하는 효과를 만들 수 있는 방법이 존재한다. 이는 별도로 공부해서 다뤄야 할 듯 하다.


  1. 서브클래싱은 객체지향 패러다임의 가장 큰 장점 중 하나이지만, 상속으로 인한 가계도가 커지고 그 과정에서 오버라이딩되는 멤버가 많으면 많을수록 관리상의 어려움과 성능 이슈들이 발생할 수 있다. 그래서 Objective-C는 서브클래싱이 아닌 다른 방법으로 객체를 확장하는 것을 더 장려하는 편이다. 
  2. 카테고리로는 원칙적으로 인스턴스 변수를 추가할 수 없지만 associated object를 이용하여 이와 유사한 효과를 낼 수 있다. 
  3. 신규로 만든 클래스나 프레임워크가 제공하는 클래스든 상관없다 
Exit mobile version