[Swift]프로그래밍 패러다임이란? #2 객체 지향 프로그래밍, 객체 지향 프로그래밍의 특징

Eric·2022년 10월 9일
0
post-thumbnail

도입부

저번글에서 프로그래밍 패러다임의 개념과 명령형과 선언형 프로그래밍의 차이를 알아보았다. 이번 글에서는 Swift에서 차용하고 있는 패러다임들 중 객체 지향 프로그래밍에 대해 알아보려 한다.

텍스트만으로 설명하기엔 이해하기 쉽지 않은 개념들이 많아 예제를 많이 사용했다. 예제에 집중하면서 읽어주세요.


객체 지향 프로그래밍(Object-Oriented Programming, OOP)

객체 지향 프로그래밍은 프로그램을 명령문들의 목록으로 보는 것이 아닌, 프로그램에 필요한 객체들을 파악하고 정의하여 이들의 상호작용으로 프로그램을 구성하는 것을 말한다.

객체(Object)란?

넓은 의미로는 세상에 실존할 수 있는 존재 또는 생각할 수 있는 것이고,
프로그래밍적 관점으로는 속성과 특징을 가진 데이터 단위를 말한다.

객체는 클래스인스턴스를 통해 소프트웨어 상에 구현된다.

클래스는 객체의 속성과 행동을 나타내는 설계도이고,
인스턴스는 클래스에 의해 설계된 객체가 소프트웨어 상에 직접 구현된 것을 말한다.

코딩을 하던 중, 내가 만들고 있는 프로그램에 고양이들이 필요해, 소프트웨어로 고양이를 구현해야할 일이 생겼다고 상상해보자.

먼저, 고양이를 만들기 위해서는 먼저 고양이의 설계도를 그려야한다.

import SwiftUI
// 고양이 클래스
class Cat { 
    let foldedEar: Bool //  귀가 접혔는가?
    let hairColor: Color //  털 색
    let eyeColor: Color //  눈 색
    var weight: Double //  몸무게
    let breed: String //  품종
    var age: Int // 나이
    
    init(foldedEar: Bool, hairColor: Color, eyeColor: Color, weight: Double, breed: String, age: Int) {
        self.foldedEar = foldedEar
        self.hairColor = hairColor
        self.eyeColor = eyeColor
        self.weight = weight
        self.breed = breed
        self.age = age
    }
}

Cat 클래스를 만들었다. 클래스는 객체의 설계도라고 했다. 즉, 이 클래스 안에서 고양이의 특징이나 행동들을 정의해서 고양이를 올바르게 설계해야한다.
설계도를 다 그렸다면 고양이를 소프트웨어 상에 직접 구현해보자.

// 고양이 인스턴스
let russianblue = Cat(foldedEar: false, hairColor: .gray,
eyeColor: .green, weight: 4, breed: "Russian Blue", age: 7)

let ragdoll = Cat(foldedEar: false, hairColor: .white, 
eyeColor: .blue, weight: 5, breed: "Ragdoll", age: 1)

Cat 클래스 설계를 따르는 russianblue와 ragdoll라고 하는 인스턴스를 만들었다.
이 인스턴스들은 고양이의 특성을 가지고, 메모리에 직접 할당되어 프로그램 안에서 다른 객체들과 상호작용 할 것이다.

정리해보자면,
고양이라는 객체를
Cat 클래스로 설계하고,
그 설계도에서 정의한 특징과 기능을 가진 russianblue 인스턴스와 ragdoll 인스턴스를
소프트웨어 상에 구현한 것이다.

객체: 고양이
클래스: Cat class
인스턴스: russianblue, ragdoll

이런 방식으로 만들어진 여러가지 독립된 객체들이 상호작용하면서 프로그램을 구성하는 방식이 바로 객체 지향 프로그래밍이다.

객체 지향 프로그래밍의 특징들로부터 오는 이점 때문에 C/C++, Python, Swift, Objective-C, JAVA 등 매우 다양한 언어에서 이 패러다임을 채택하고 있다.
대표적으로 소개되는 객체 지향 프로그래밍의 특징들을 알아보자.

객체 지향 프로그래밍의 특징

객체 지향 프로그래밍에는 4가지 특징이 있다.

  • 추상화
  • 상속
  • 캡슐화
  • 다형성

추상화(Abstraction)

추상화란 객체들의 공통적이고 핵심적인 속성을 추출하여 일반화하는 것을 말한다.

추상화를 거듭하면 할 수록 객체의 디테일함은 점점 사라지고 공통된 특징만 남게 된다.
위 예시로 이어 설명하자면, 이번엔 난 고양이말고도 강아지, 코끼리, 돌고래, 소 객체들을 구현했다.

class Cat {}

class Dog {}

class Elephant {}

class Dolphin {}

class Cow {}

근데 만들고 보니, 이 객체들에게 공통된 특징이 있어 좀 더 일반적인 새로운 객체로 표현, 즉 추상화가 가능하다는 걸 발견했다.
이 객체들은 자신의 새끼에게 모유수유를 한다는 공통점을 가졌고, 그래서 난 ‘포유류’ 객체로 이 객체들의 특징(수유)을 추출하여 일반화했다.


class Mammal {
    func breastFeeding() {}
}

그 이후로도 계속해서 내 프로그램에 객체들을 추가하면서 추상화하다보니, 포유류 외에도 파충류, 양서류, 조류, 어류 등의 다양한 객체들을 만들 수 있었다.
하지만 추상화를 한 이 객체들조차도 공통적인 부분이 있어 또 다시 추상화하는 것이 가능했다.
난 이들을 ‘동물’이라는 객체로 추상화했다.

추상화를 거듭하면 거듭할수록 특징이 점점 희미해지는게 느껴지나?

추상화하기 전 객체들은

코끼리의 긴 코
돌고래의 높은 지능
고양이의 울음소리

같은 디테일한 특징들을 가지고 있었을텐데,

추상화를 거듭하다보니

광합성을 통해 스스로 영양분을 얻지 못해서,
다른 객체를 잡아먹어 영양분을 얻는 특징 정도만 남게 되었다.

왜 추상화 해야 하나요?

추상화가 객체 지향 프로그래밍의 특징인건 알겠지만,
프로그래머들이 왜 코드를 짤 때 추상화 해야하는지 의문이 들 수 있다.

위 예시에서 고양이, 강아지, 돌고래, 소, 코끼리를 포유류로 추상화하지 않았다고 생각해보자. 그럼 breastFeeding 메서드가 5개의 클래스 내부에 각자 구현되어있게 되는 것이다.
4줄 더 적는게 뭐가 그리 힘드냐라고 생각할 수 있지만,

지금이야 4줄이지 객체들을 추가해나가다보면 엄청난 수의 포유류 객체 클래스가 만들어 질 것이고, 여기에 다 동일한 변수, 메서드들을 추가하면, 코드량이 엄청나게 증가하고, 코드를 유지보수하기에는 더욱 어려워질 것이다.

하지만 추상화를 통해 상위 개념의 클래스에서 메서드를 단 한번만 정의해주면, 그 클래스를 상속받는 다른 클래스들도 그 메서드를 사용할 수 있게 된다.

추상화를 통해 코드 재사용성도 증가하고, 중복된 코드도 줄여, 코드 가독성을 높힐 수 있는 것이다.

상속(Inheritance)

상속이란 하나의 클래스가 다른 클래스의 특징을 물려받는 것을 의미한다.

상속을 받는 클래스를 자식 클래스라고 하며,
기능을 상속해주는 클래스를 부모 클래스라고 한다.

class Mammal {
    func breastFeeding() {
        print("수유중")
    }
}

class Cat: Mammal { 
    let foldedEar: Bool
    let hairColor: Color
    let eyeColor: Color
    private var weight: Double
    let breed: String
    var age: Int
    
    init(foldedEar: Bool, hairColor: Color, eyeColor: Color, 
         weight: Double, breed: String, age: Int) {

        self.foldedEar = foldedEar
        self.hairColor = hairColor
        self.eyeColor = eyeColor
        self.weight = weight
        self.breed = breed
        self.age = age
    }
    
    func walk() -> Double {
        let lostWeight = 0.1
        self.weight -= lostWeight
        
        return weight
    }
}

위는 Mammal 클래스를 상속받은 Cat 클래스이다.
Swift에서는 기능을 상속받을 클래스 이름 옆에 콜론(:)과 기능을 상속해줄 클래스 이름을 써주면 상속을 받을 수 있다.

Cat 클래스에서는 분명 breastFeeding 메서드를 구현해주지 않았지만, 메서드를 구현한 클래스를 상속받고 있기 때문에, 메서드를 사용할 수 있다.

앞서 설명한 추상화의 장점이 사실 상속으로부터 나오는 것이라고 할 수 있다.
클래스와 그 클래스를 추상화한 추상클래스를 연결해주는게 바로 상속이다.


let russianblue = Cat(foldedEar: false, hairColor: .gray, 
eyeColor: .green, weight: 4, breed: "Russian Blue", age: 7)

russianblue.breastFeeding() // 수유중

캡슐화(Encapsulation)

캡슐화란 객체의 속성과 행동, 즉 클래스의 변수와 메서드를 하나로 묶어 내부적으로만 필요한 연산이나 변수, 메서드를 외부로부터 감추는 것이다.

외부에서는 캡슐화된 객체 내부의 구조를 얻지 못하며 오직 캡슐화된 객체가 제공하는 변수와 메소드만 이용할 수 있다.

왜 묶고 감춰야하나요?

캡슐화를 했을 때 얻는 몇 가지 이점이 있다.

1. 개발자의 의도와 다른 조작으로 객체를 손상시킬 수 있는 행위를
방지할 수 있다.

예제로 사용했던 Cat 클래스로 예시를 들어보면,

import SwiftUI

class Cat { 
    let foldedEar: Bool
    let hairColor: Color
    let eyeColor: Color
    var weight: Double
    let breed: String
    var age: Int
    
    init(foldedEar: Bool, hairColor: Color, eyeColor: Color, weight: Double, breed: String, age: Int) {
        self.foldedEar = foldedEar
        self.hairColor = hairColor
        self.eyeColor = eyeColor
        self.weight = weight
        self.breed = breed
        self.age = age
    }
    
    func walk() -> Double {
        let lostWeight = 0.1
        self.weight -= lostWeight
        
        return weight
    }
}

고양이가 산책을 하는 행위를 추가했다. 산책을 하면 0.1kg씩 몸무게를 감량하게 된다.
난 고양이의 몸무게를 산책으로만 제어하고 싶어서 walk 메서드를 만들었는데, 사용자가 갑자기 고양이의 몸무게에 이상한 짓을 한다면?


let russianblue = Cat(foldedEar: false, hairColor: .gray, eyeColor: .green, weight: 4, breed: "Russian Blue", age: 7)

russianblue.weight = -3
print(russianblue.walk()) // weight: -3.1

러시안블루의 몸무게를 -3kg으로 만들었다.
러시안블루를 산책시키니 -3.1kg이 되었다.
이런 말도 안되는 대참사를 막기 위해서 접근제어자를 사용해서 변수에 대한 접근을 제한한다.

import SwiftUI
// 고양이 클래스
class Cat { 
    let foldedEar: Bool
    let hairColor: Color
    let eyeColor: Color
    private var weight: Double
    let breed: String
    var age: Int
    
    init(foldedEar: Bool, hairColor: Color, eyeColor: Color, weight: Double, breed: String, age: Int) {
        self.foldedEar = foldedEar
        self.hairColor = hairColor
        self.eyeColor = eyeColor
        self.weight = weight
        self.breed = breed
        self.age = age
    }
    
    func walk() -> Double {
        let lostWeight = 0.1
        self.weight -= lostWeight
        
        return weight
    }
}

weight 변수 앞에 private이라는 접근제어자가 추가되었다.
이 상태로 객체 외부에서 러시안블루의 몸무게에 접근하려하면 오류가 난다.

이제 고양이의 몸무게에 대한 컨트롤은 walk 메서드를 통해서만 할 수 있게 되었고, 이는 개발자의 의도에 어긋나는 객체를 망치는 행위를 제한한 것이다.

2. 유지보수나 확장시 오류의 범위를 최소화할 수 있다.

지금은 예제라서 코드가 몇 줄 없지만, 실제로 서비스를 하게 되어 고양이 천 마리, 만 마리, 1억 마리를 관리하게 된다고 가정해보자.
캡슐화를 제대로 해놓지 않아 몇 만줄이나 되는 코드 이곳저곳에서 1억 마리 고양이들의 변수를 수정해놓는 코드를 마구잡이로 짰다면?
그 코드에서 의도하지 않은 이상한 일이 일어 났다면?
아직 경험해본 적은 없지만 글자만 봐도 식은 땀이 난다.

3. 외부에 불필요한 정보를 노출시키지 않기 때문에 프로그래머의 생산성을 향상시킨다.

다른 개발자가 내가 만든 Cat 클래스를 사용한다고 가정해보자.
이 개발자는 내가 이상적으로 코드를 짰다면, 클래스가 캡슐화 되어있을 것이라고 생각하고 사용할 것이다.

하지만 Suggestion에서 weight가 뜬다면?

개발자는 캡슐화가 되어 어차피 메서드로만 처리할 변수라면 띄워놓지 않았을 거라고 생각하기 때문에 외부에서 weight 변수에 접근을 해야만 하는 이유를 일일이 분석하기 시작할 것이다. 이 시간동안 개발자의 생산성이 저해되는 것이다.

private으로 설정한 변수는 Suggestion에 뜨지 않는다.

4. 데이터가 변경되어도 다른 객체에 영향을 주지 않는다.

접근제어자를 통해 객체 내 변수를 철저히 지정한 영역 내에서만 제어하기 때문에, 다른 객체의 다른 변수들과 엮이기 쉽지 않다.
만약 캡슐화를 하지 않아 각 객체들이 서로 긴밀히 연결되어있다면, 한 곳에서 오류가 나면 연쇄적으로 다른 곳에서도 오류가 날 것이다.

5. 코드의 재사용성이 증가한다.

객체와 관련된 변수와 메서드를 하나로 묶어놓았기 때문에,
여러 인스턴스를 만들어도 클래스에서 정의한 메서드를 통해 제어할 수 있다.

let russianblue = Cat(foldedEar: false, hairColor: .gray, eyeColor: .green, weight: 4, breed: "Russian Blue", age: 7)
let ragdoll = Cat(foldedEar: false, hairColor: .white, eyeColor: .blue, weight: 5, breed: "Ragdoll", age: 1)
let britishshorthair = Cat(foldedEar: false, hairColor: .gray, eyeColor: .brown, weight: 2.5, breed: "British Short Hair", age: 5)
let scottishfold = Cat(foldedEar: true, hairColor: .gray, eyeColor: .brown, weight: 2.5, breed: "scottishfold", age: 3)

russianblue.walk()
ragdoll.walk()
britishshorthair.walk()
scottishfold.walk()
몇 마리의 고양이를 만들어도 전부 산책시킬 수 있다.

다형성(Polymorphism)

다형성이란 프로그래밍 언어의 각 요소들이 다양한 타입에 속하는 것이 허락되는 성질을 말한다.

그래서 다형성이 적용되는 변수, 클래스, 메서드는 같은 이름이더라도 상황에 따라 다르게 해석될 수 있다.

다형성은 크게 객체의 다형성메서드의 다형성으로 나눌 수 있다.

객체의 다형성은 말 그대로 하나의 객체가 여러가지 타입을 가지는 것이다. 하지만 아무 연관없는 타입으로는 허락되지 않는다.
상속을 받은 객체가 자신의 부모 객체의 타입을 가지는 것만이 가능하다.


class Mammal {
    
    func breastFeeding() {
        print("수유중")
    }
} 


class Cat: Mammal { 
    let foldedEar: Bool
    let hairColor: Color
    let eyeColor: Color
    private var weight: Double
    let breed: String
    var age: Int
    
    init(foldedEar: Bool, hairColor: Color, eyeColor: Color, weight: Double, breed: String, age: Int) {
        self.foldedEar = foldedEar
        self.hairColor = hairColor
        self.eyeColor = eyeColor
        self.weight = weight
        self.breed = breed
        self.age = age
    }
    
    func walk() -> Double {
        let lostWeight = 0.1
        self.weight -= lostWeight
        
        return weight
    }
}

class Dog: Mammal {
    
}

class Elephant: Mammal {
    
}

class Dolphin: Mammal {
    
}

class Cow: Mammal {
    
}

위는 앞서 예시를 든 Mammal 클래스와 이를 상속받은 Cat, Dog, Elephant, Dolphin, Cow 클래스들이다.
위 설명에 의하면 Mammal을 상속하고 있는 타입들에도 다형성이 적용될 것이다.


let russianblue = Cat(foldedEar: false, hairColor: .gray, eyeColor: .green, weight: 4, breed: "Russian Blue", age: 7)
let dolphin = Dolphin()
let cow = Cow()
let unknownMammal = Mammal()

var mammals: [Mammal] = [russianblue, dolphin, cow, unknownMammal]

Mammal 타입을 요소로 가지는 mammals 배열을 만들었다.
mammals 배열의 요소는 Mammal 타입이어야 하지만, 구성요소들을 보면
‘Cat 타입’인 russianblue
‘Dolphin 타입’인 dolphin
‘Cow 타입’인 cow
같이 Mammal 타입은 아니지만, 이를 상속하고 있는 다른 타입들이 존재한다.

왜 상속관계를 가진 클래스들끼리만 가능한가요??

상속을 받은 자식 클래스는 자신의 부모 클래스의 특징을 온전히 포함하고 있다. 따라서 자식 클래스는 부모 클래스의 역할을 충분히 할 수 있기 때문에 다형성이 적용되는 것이다.

메서드의 다형성은 다형성의 대표적인 예시인 오버로딩(overloading) 오버라이딩(overriding) 으로 설명할 수 있다.

class Mammal {
    func breastFeeding() {
        print("수유중")
    }
    
    func breath() {
        print("후하후하")
    }

    func breath(with: String) {
        print("저는 \(with)로 숨쉬어요")
    }
} 

Mammal 클래스에 breath라는 메서드를 추가했다. 근데 코드를 잘보면 breath라는 이름의 메서드가 두 개가 있는 것을 발견할 수 있을 것이다.
위 코드는 다형성이 적용된 예제로,
첫 번째 breath 메서드는 매개변수가 없고,
두 번째 메서드는 String 매개변수를 가지기 때문에
이름이 같음에도 불구하고 개별적인 메서드로 인식하여 오류가 나지 않는 것이다.

이렇게 같은 이름의 메서드라도 매개변수의 타입이나 개수가 다르면 개별적인 메서드로 인식하는 것을 오버로딩(overloading) 이라한다.

다형성의 개념이 메서드에 적용되지 않았다면, 동일한 기능을 하는 메서드가 매개변수가 다르다는 이유만으로 새로운 이름의 메서드를 만들어야 할 것이다.

 다형성이 없었다면 결과물을 출력하는 메서드 print가 매개변수의 타입에
따라 printInt, printChar, printString, printDouble 메서드로 나눠질수도 있었다.


class Cat: Mammal { 
    let foldedEar: Bool
    let hairColor: Color
    let eyeColor: Color
    private var weight: Double
    let breed: String
    var age: Int
    
    override func breath() {
        print("Purrr")
    }

      .
      .
      .

}

이번엔 Mammal 클래스를 상속하고 있는 Cat 클래스에서 똑같은 이름의 메서드인 breath를 새롭게 정의했다. 오류가 날 것 같지만, 이것도 다형성의 한 예로 breath는 정상적으로 동작한다.

이렇게 부모 클래스에 있는 메서드를 자식 클래스에서 재정의 할 수 있는데 이를 오버라이딩(overriding) 이라고 한다.

위 코드를 잘보면 breath 함수를 정의하는 부분에 ‘override’라는 키워드가 추가되어있다.
이는 함수를 오버라이딩할 때 새로 정의될 함수 앞에 반드시 붙여줘야하는 Swift 문법으로, 이 접두어를 통해 부모 클래스에도 같은 메서드가 있음을 알 수 있다.

부모 클래스로부터 상속받은 메서드가 자식 클래스에게 충분한 기능을 제공하지 않을 때, 오버라이딩을 활용하여 자식 클래스에 좀 더 적절하게 메서드를 재구성 할 수 있다.


참고

https://defacto-standard.tistory.com/51

https://blog.itcode.dev/posts/2021/08/12/polymorphism.html

https://www.hackingwithswift.com/read/0/20/polymorphism-and-typecasting

https://geonoo.tistory.com/151

마무리

코딩을 하다가 내가 뭘 잘하고 있고, 뭘 못하고 있는지 감이 안 잡힐 때가 있었다. 그럴 때마다 내가 공부하고 있는 언어의 패러다임들을 다시 천천히 둘러보곤 했는데, 이 과정이 감을 잡는 것에 도움이 많이 되었던 것 같다.

잘못된 내용이 있다면 어떤 방식으로든 피드백 부탁드립니다.

profile
IOS Developer DreamTree

0개의 댓글