ice rabbit programming

[소공] 객체지향의 개념 본문

Development/Software Architecture

[소공] 객체지향의 개념

판교토끼 2021. 1. 24. 21:59

OOP(Object Oriented Programming), 객체지향 패러다임은 문제를 해결하기 위한 계산을 객체(object)를 기본으로 수행하는 방법이다. 아마 학부 수업을 듣다 보면 real world의 개념을 가져오는 설명을 많이 들었을 것이다.

객체지향 이전에 많이 쓰이던 절차지향 프로그램은, 무엇을 수행한다는 것에 초점을 맞추고 진행하기 때문에, main 함수에서 실행 트랜잭션, 그리고 각 기능들로 이어지는 flow가 이어진다.

반면에 객체지향은 클래스 간의 상속 및 관계, 클래스 내의 멤버변수/멤버함수를 통해 프로그램 구조가 이루어진다.

객체는 클래스의 인스턴스이고, 하나의 개체라고 보면 된다. 가장 많이 예를 드는 것이 붕어빵 틀이 클래스이고, 그 틀로 찍어낸 붕어빵이 객체이다. 조금 더 전산적인 용어를 사용하자면, 실행되는 소프트웨어 시스템에 존재하는 구조화된 데이터의 집합이다. 객체는 클래스의 구체화된 형태이기 때문에, property와 behavior를 가진다.

클래스는 자료를 추상화하여 묶은 것이고, 객체를 조작하는 프로시저(함수) 또한 추상화하여 묶은 것이다.

언어마다, 사람마다 네이밍 규칙이 많지만 클래스 이름을 정할 때에는 보통 대문자로 시작하는 Pascal Case를 사용하고, 단수형 명사를 사용한다. 그 외에도 좋은 이름 짓기에 관한 내용이 많지만 생략하고, 조금 더 궁금하신 분은 클린코드를 검색해보시면 좋을 듯하다.

클래스는 멤버변수/멤버함수를 가지는데, 멤버변수는 각 인스턴스에 존재하는 데이터로, 속성이나 클래스 간의 관계를 보관한다. 객체 자체를 변수에 저장할 수도 있는데, 그 변수는 객체를 reference한다. 변수의 타입은 객체의 유형이 된다.

Account ac; // Account 클래스 타입의 object를 담는 변수
cout << ac.name << endl; // object의 멤버변수를 호출

앞에서 말한 변수는 객체 각각의 변수이고, 클래스 전역적인 변수도 존재한다. static 키워드를 붙인 변수인데, 디폴트나 상수 값 저장에 주로 쓰인다. 클래스 자체의 변수이기 때문에 다른 곳에서 바꾸어도 해당 클래스 타입의 객체의 static 변수도 변경이 함께 이루어진다.

멤버함수는 클래스의 행위를 구현하는데, 당연히 클래스 내부에서 오버로딩/오버라이딩을 통한 선언/정의가 가능하다. OOP에서는 멤버변수에 대해 은닉성을 중요시하기 때문에, 대부분의 경우에 변수에 대한 직접 접근을 허용하지 않고(public일지라도) 함수를 통해 객체의 속성을 조작하도록 한다.

cout << ac.name << endl; // 직접 접근, 좋지 않다.
cout << ac.getName() << endl; // 함수를 통한 접근

 

OOP에서 클래스를 다룰 때 빼놓을 수 없는 개념이 상속이다. 중요하면서도 어렵고, 실제로 적용할 때도 어떤 부분을 상속으로 처리하고 구현으로 처리할지 헷갈리기도 하는 부분이다.

보통 상속을 사용할 때에는, 공통 부분 묶기로 시작한다. 여러 클래스가 있는데, 공통적으로 가지는 속성이나 함수가 존재한다면, 일반화하는 클래스를 만들어 슈퍼 클래스(부모 클래스)로 두는 것이 상속의 기본이다. 상속이 위로 올라갈수록 일반화(추상화)된 클래스이고, 아래로 내려갈수록 구체화된 클래스이다. 상속을 하게 되면 부모 클래스가 정의한 기능들을 자식 클래스가 가지게 된다.

이런 식으로 Account의 구체화된 형태들이 상속을 받는 형태이다. 코드로 간략하게 보자면

class Account {
    private:
        string name;
        int money;
    // ...
}

class SavingsAccount : Account {
    // ...
    // 적어주지 않아도 Account의 name과 money를 상속받는다.
    // ...
}

SavingsAccount sa;
sa.name; // SavingsAccount의 name 멤버

 

만약 상속 구조를 설계할 때 이것이 상속인지, 관계를 가지는 것인지 모호하다면 IS-A 관계를 만족시키는지를 보면 된다. 위의 예를 보면 SavingsAccount is an account.가 성립한다. 즉, 계좌의 일종이므로 IS-A 관계가 되고, 상속 관계를 만들어도 문제가 없는 것이다. 하지만 I am my father.는 말이 되지 않으므로, 상속받기 어렵다. I am a human. My father is a human.으로 상속한 후에 관계를 만드는 것이 더 적절할 것이다.

아래는 이해를 돕기 위해 보편적으로 많이 쓰이는 도형의 상속 관계이다. 아래로 내려갈수록 구체화된다.

다형성 또한 중요한 개념인데, 하나의 추상 오퍼레이션이 서로 다른 클래스에서 다르게 구현될 수 있는 성질이다. 즉, Account에서 정의된 함수를 SavingsAccount와 InstallmentAccount에서 각각 다르게 정의할 수 있다는 것이다. 어떤 함수가 실행될지는 변수 내의 객체의 종류가 결정하게 된다.

class Account {
    public:
        void myFunc() {
            // ...
        }
}

class SavingsAccount : Account { 
    public:
        void myFunc() {
            cout << "sa" << endl;
        }
}

class InstallmentAccount : Account {
    public:
        void myFunc() {
            cout << "ia" << endl;
        }
}

SavingsAccount sa;
sa.myFunc(); // sa 출력
InstallmentAccount ia;
ia.myFunc(); // ia 출력

// 상속 개념을 함께 가져온다면
Account a;
a = new SavingsAccount(); // 자식 객체는 부모 그릇에 담을 수 있다.
a.myFunc(); // sa 출력
a = new InstallmentAccount();
a.myFunc(); // ia 출력

 

마지막으로 추상 클래스(abstract class)는, 이름 그대로 추상화를 위한 클래스이다. 즉, 기능에 대한 선언만 하고 구현은 자식 클래스에서 구현한다. C++에서는 virtual 함수를 하나 이상 가지면 추상 클래스로 명명하고, 자바나 C#에서는 interface, 파이썬에서는 abc 패키지와 pass 키워드를 통해서 구현할 수 있다. 계속 C++로 예시 코드를 작성했으니 아래에서도 C++로 예시 코드를 작성하고 포스팅을 마무리하겠다.

// 추상 클래스는 인스턴스화 할 수 없다.
// 간단히 생각해보면 가지고 있는 가상 함수의 구현부가 없으므로 당연히 실행할 수도 없다.
class VirtualAccount {
    // ...
    virtual void myFunc(); // 선언만 한다.
}

class Account : VirtualAccount {
    // ...
    // 부모 클래스의 가상 함수를 구현하지 않으면 에러가 난다.
    virtual void myFunc() { // virtual을 안붙여 주어도 동작은 하지만, 혼란을 회피하기 위해 보통 붙여준다.
        // ...
    }
}

'Development > Software Architecture' 카테고리의 다른 글

[소공] 클래스 모델링  (0) 2023.10.14
[소공] 요구분석  (0) 2021.03.27
[소공] 소프트웨어 공학의 개요  (0) 2020.04.19