Swift - 클래스(Class) 훑어보기

객체지향 프로그래밍(OOP - Object Oriented Programming)의 핵심인 클래스(Class)는 객체(Object)를 디자인하기 위한 기능이다. 스위프트(Swift)의 클래스는 역시나 class 라는 명령어로 제공된다. 이 글에서는 이 클래스에 대해 훑어보겠다.

우선 아래 예제 코드는 이 블로그의 [스위프트의 struct 훑어보기 글]에 나오는 마지막 예제에서 struct 라는 키워드를 그저 class로 바꾼 코드이다.
class MyDate {
    var BC = false
    var year = 0
    var month = 0
    var day = 0
    var name: String {
        return (self.BC ? "BC" : "AC") + " \(self.year)-\(self.month)-\(self.day)"
    }
    
    init() { }
    
    init(y: Int, m: Int, d: Int, isBC: Bool = false) {
        self.BC = isBC
        self.year = y
        self.month = m
        self.day = d
    }
    
    func print() {
        println(self.name)
    }
}
struct와 동일하게 클래스도 프로퍼티메소드, 생성자(init) 등으로 구성되어 있다. 실제로 실행 결과도 struct의 때와 동일하다.
var date = MyDate()
date.name           // "AC 0-0-0"

var someDate = MyDate(y: 1999, m: 1, d: 21, isBC: false)
someDate.name       // "AC 1999-1-21"
someDate.year = 2001
someDate.name       // "AC 2001-1-21"
someDate.print()    // AC 2001-1-21
struct 글에서도 이야기 했지만, 스위프트의 구조체와 클래스는 문법이 거의 동일하다. 프로퍼티와 메소드를 가진다는 구조적인 면이나 기능도 비슷하고 구현 문법도 거의 동일하다.

상속(Inheritance)

OOP에서 클래스의 특징은 상속(Inheritance)에 있다. 부모 클래스의 내용을 이어받는 자식 클래스는 부모 클래스의 기능과 속성을 이어 받으면서 자신(자식클래스)만을 위해 특정 기능을 추가하는 것이 가능하다. 일반적인 코드 디자인 관점에서 부모클래스는 공통된 부분을 구현하고 자식클래스는 자식클래스들 만의 고유한 기능을 추가로 구현한다.

클래스와 구조체의 가장 큰 차이는 이 상속에 있다고 볼 수 있다. 상속은 클래스에서만 가능한 개념이다.

다시 예제를 보자. 이번에는 위 MyDate 클래스 코드를 상속받는 형식으로 고치려고 한다. 우선은 부모로 쓰일 BaseDate 라는 클래스를 만들었다.
class BaseDate {
    var year = 0
    var month = 0
    var day = 0
    var name: String {
        return "\(self.year)-\(self.month)-\(self.day)"
    }
    
    init() { }
    
    init(y: Int, m: Int, d: Int) {
        self.year = y
        self.month = m
        self.day = d
    }
    
    func print() {
        println(self.name)
    }
}
바뀐 부분을 생각하지 말자. 그냥 년/월/일 데이터를 가지는 클래스를 하나 만들었다. 테스트를 해 보면 아래와 같다.
var baseDateInstance = BaseDate(y: 2002, m: 5, d: 8)
baseDateInstance.name       // "2002-5-8"
baseDateInstance.print()    // 2002-5-8
굳이 설명할 필요는 없을 것 같다.

이제 MyDate 클래스를 BaseDate를 상속받는 형태로 재창조해보자. 이번에 구현하는 MyDate 클래스의 기능은 상속을 사용하기 이전과 동일한 것을 목표로 한다.
class MyDate : BaseDate {
    var BC = false
    override var name: String {
        return (self.BC ? "BC" : "AC") + " "  + super.name
    }
    
    init(y: Int, m: Int, d: Int, isBC: Bool = false) {
        super.init(y: y, m: m, d: d)
        self.BC = isBC
    }
}
class 정의를 시작 할 때 이름 오른쪽에 콜론(:)을 찍고 상속받을 부모클래스(BaseDate)의 이름을 넣어준 것이 보인다. 이것이 상속을 사용한다는 의미이다.

BC라는 자식클래스 만의 프로퍼티를 추가했다. 이건 자식클래스(MyDate)만의 특징이다.

name 이라는 프로퍼티의 getter 선언 부분에 override라는 용어가 보인다. 이는 부모의 기능을 자식이 그대로 이어받는게 아니라 자식만의 기능을 덧붙여서 이어받는다는 의미이다.

그리고 MyDate 만의 생성자(init)를 추가했다.

코드 몇몇 부분에서 self와 super가 보이는데 self는 자기자신(자식클래스), super는 부모클래스를 지칭한다.

새롭게 구현한 MyDate 클래스를 이용해 테스트 해 보자.
var someDate = MyDate(y: 2003, m: 6, d: 9, isBC: false)
someDate.name           // "AC 2003-6-9"
someDate.year           // 2003
someDate.year = 2004
someDate.BC = true
someDate.print()        // "BC 2004-6-9"
결과로써 MyDate 클래스는 부모인 BaseClass의 모든 성질을 이어받았음을 알 수 있다.

생성자인 init의 경우 super.init()을 호출하는 부분이 있다. super 라는 부모클래스 인스턴스를 의미하므로 부모의 생성자(init())를 호출한다는 의미다. 따라서 .year, .month, .day 프로퍼티도 부모 덕분에 자동으로 초기화된다.

.name 프로퍼티는 override 라는 지시어로 부모의 .name을 이어받긴 했지만 getter를 변경하여 자신만의 추가기능을 가지도록 구현되었다. 그래서 부모는 가지고 있지 않던 .BC라는 프로퍼티를 이용해 문자열을 더 추가하는 기능을 가지고 있다. super.name 을 사용한 이유도 부모의 .name getter 를 그대로 활용한다는 의미이다.

여기서 테스트 한 year 라는 프로퍼티는 부모클래스인 BaseDate에 소속되어 있다. 그리고 BaseDate를 상속받은 MyDate도 그대로 year 프로퍼티를 이어받았음을 알 수 있다.

BaseDate의 print() 메소드는 MyDate에서 그대로 상속받았다. 하지만 동작하는 것은 MyDate의 오버라이드된 .name 프로퍼티를 이용한다. 그래서 아무런 수정이 없었음에도 부모와 동작이 다르다.

참고로 스위프트는 단일상속(Single Inheritance)만 되는 언어이다. 상속받을 부모클래스 이후에 콤마로 무언가를 적는 것은 프로토콜을 명시하는 것이니 주의하자.

상속의 또다른 이야기

앞의 이야기에 이어서, 이제 BaseDate를 상속받는 다른 클래스를 또 하나 정의해 보려 한다.
class YourDate : BaseDate {
    var hour = 0
    var minute = 0
    var second = 0
    override var name: String {
        return super.name + " \(self.hour):\(self.minute):\(self.second)"
    }
    
    init(y: Int, m: Int, d: Int, hh: Int, mm: Int, ss: Int) {
        super.init(y: y, m: m, d: d)
        self.hour = hh
        self.minute = mm
        self.second = ss
    }
}
이름이 뭐 같지만 -_- 어쨌든 BaseDate를 상속받은 또다른 클래스다. 이 클래스는 시/분/초 데이터를 프로퍼티로 가지고 있다. name의 getter 부분을 오버라이드 해서 시/분/초도 함께 출력하도록 기능이 추가되었다.

위 클래스의 동작은 어느 정도 이해될 것이다. 그렇다면 이제 앞서 만든 MyDate와 YourDate를 같이 테스트 하는 코드를 보자.
var someDate = MyDate(y: 1999, m: 1, d: 9, isBC: false)
someDate.print()        // AC 1999-1-9

var anotherDate = YourDate(y: 2011, m: 2, d: 14, hh: 9, mm: 55, ss: 16)
anotherDate.print()     // 2011-2-14 9:55:16

func PrintDate(dateInstance: BaseDate) {
    dateInstance.print()
}

PrintDate(someDate)
// 위 함수는 콘솔에 AC 1999-1-9를 출력한다

PrintDate(anotherDate)
// 위 함수는 콘솔에 2011-2-14 9:55:16를 출력한다
핵심은 PrintDate 라는 함수의 매개변수이다. 타입은 부모클래스로 만들어 놓은 BaseDate가 들어갔다. 그런데 이 함수에 BaseDate가 아닌 BaseDate를 상속받은 클래스 MyDate와 YourDate 클래스의 인스턴스를 넣어서 실행시켰다.

결과는 주석으로 표기해 놓았다. 결과적으로 BaseData의 print() 메소드를 호출하는 것 처럼 보이는 PrintDate() 함수이지만, 실제 실행 결과는 각자의 .name 을 참고로 다르게 동작하였다. 즉 각 자신클래스의 입장에서 print() 라는 메소드가 실행된 것이다.

이것이 상속의 또다른 특징이다. 부모클래스를 자식클래스 인스턴스의 공통 타입으로 사용이 가능하다. 그리고 각 자식 클래스들은 부모클래스 타입의 변수에도 대입이 가능해진다. 하지만 부모클래스 타입의 변수에 대입되었더라도 실제 인스턴스는 자식클래스의 것이다.

쉽게 설명할 방법을 몰라서 이렇게 마무리 하지만, 상속이 쓰이는 디자인 패턴은 이런 방식이 가장 유용하다고 볼 수 있다. 만약 위의 코드가 왜 저런지 이해되었다면 상속에 대해서 거의 다 이해한 셈이라고 봐도 된다.

class

이제 정리를 해 보자.

클래스(class)는 프로퍼티(클래스 안의 변수)와 메소드(클래스 안의 함수)를 가질 수 있는 컨테이너 타입을 정의하기 위한 용도이다.

클래스의 가장 큰 특징은 상속에 있다. 상속이란 유전이란 의미와 비슷하다. 부모와 자식 간의 생김새가 비슷하다는 점에서, 부모클래스를 상속받은 자식클래스는 부모의 기능을 동일하게 가져간다. 하지만 부모와 자식이 같지 않은 것과 동일하게, 자식클래스는 부모클래스를 이어받지만 자식 만의 기능(프로퍼티와 메소드)을 추가로 가질 수 있다.

이 외에 클래스(class)는 구조체(struct)에 비해 몇 가지 특징이 있는데 형변환(Type Casting)과 관련된 기능과 파괴자(Deinitializer) 등의 기능을 가질 수 있다는 점 등이다.

클래스는 struct에 비해 좀 더 다양한 용도로 활용된다. 타입의 정의가 주인 struct와는 다르게 클래스는 상속의 관점을 중요하게 본다. 대표적인 예로 Cocoa Touch의 UIView 는 클래스가 있다. 이 UIView는 독자적으로도 활동하지만 다른 UI~~~View라는 이름이 붙은 모든 클래스의 부모클래스이기도 하다.

추가 개념 정리

프로퍼티(Property): 클래스에 정의된 변수. 일반적으로 멤버 변수(Member Variable)이라 부르고 언어에 따라 속성(Attribute)라고 부르기도 한다. Objective-C에서는 이를 프로퍼티라 불렀고 이것이 그대로 스위프트에 옮겨왔다. 자세한 내용은 아래 관련글을 참고바란다.

[관련글] Swift - 프로퍼티(Properties)

메소드(Method): 클래스에 정의된 함수(Function). 클래스 함수라고 부르는 경우도 있지만 일반적으로 메소드라고 불린다.

인스턴스(Instance): 클래스가 타입이라면 특정 변수에 클래스의 생성자를 이용해 데이터를 담았다면 이 변수는 클래스의 인스턴스를 가지고 있다라고 표현한다. 클래스는 그저 타입에 불과하며 실체화 되면 메모리 상에 올라가는데 이 메모리에 올라간 내용물을 인스턴스라고 하기도 한다.

오브젝트(Object): 인스턴스의 의미와 동일하다. 일반적으로 OOP에서는 오브젝트라는 용어를 더 많이 사용한다. 인스턴스는 클래스 뿐만 아니라 다른것을 지칭하는 경우도 많기 때문이다. 하지만 개인적으론 인스턴스라는 용어를 선호한다 -_-;

Setter/Getter: 일반적으로 프로퍼티는 그냥 변수처럼 사용하지만 특별한 경우 그냥 대입하는 것이 아니라 추가 연산을 하던가 데이터를 가공해서 넣는 등의 대입 전 특정 기능을 동작시키는 경우가 있다. Setter나 Getter는 이런 추가 기능을 의미한다. 대입 용도로 사용하는게 Setter, 참조 용도로 사용하는게 Getter이다.

추가 개념 정리: 오버로드(Overload)와 오버라이드(Override)

오버라이드에 관한건 따로 정리할 생각이지만 여기서 간단히 짚고 넘어간다.

오버로드(Overload) 개념은 OOP의 상속과는 직접적인 관계가 없는 용어다. 이 용어는 이름이 동일한 함수를 여러개 만드는 것을 의미한다. 단지, 매개변수의 종류나 갯수를 다르게 해서 컴파일러가 함수를 구분 할 수 있도록 해야 하는 제한이 있다.

오버라이드(Override)는 OOP의 상속에서 부모의 기능에 자식만의 기능을 덧붙이거나 아예 바꾸고자 하는 경우에 사용된다. 스위프트의 경우에는 override 라는 지시어를 반드시 명기해야 하기에 무엇이 오버라이드 되는지는 눈으로 확인이 가능하다.

오버로드는 이름이 아니라 호출 시 넘겨주는 인자(Arguments)의 구조에 따라 실제 실행될 함수나 메소드가 결정된다. 따라서 컴파일 타임 때 컴파일러가 확인이 가능하다는 점 때문에 속도 상 불이익이 없다. 하지만 오버라이드는 컴파일 타임이 아닌 런타임 때 어떤 메소드가 실행되어야 하는지가 판단되기 때문에 퍼포먼스 면에서 불이익이 있을 수 있다. (라고 해도 사람 입장에선 별 차이 없는 속도차이니 걱정할 건 없다)

[관련글] Swift - 구조체(Structure) 훑어보기
[관련글] Swift - 프로퍼티(Properties)
[관련글] Swift - 메소드(Method)
[관련글] Swift - 생성자와 파괴자(Initialization and Deinitialization)
[관련글] Swift - 프로토콜(Protocols)
[관련글] Swift - 서브스크립트(Subscripts)

[돌아가기] 스위프트(Swift) 가이드

댓글

이 블로그의 인기 게시물

버전(Version)을 제대로 이해하기

소수점 제거 함수 삼총사 ceil(), floor(), round()