[오브젝트] 객체 분해

객체 분해

현대적인 프로그래밍 언어를 특징 짓는 중요한 두 가지 추상화 메커니즘은 프로시저 추상화데이터 추상화이다. 프로시저 추상화는 소프트웨어가 무엇을 해야하는지를 추상화 한다. 데이터 추상화는 소프트웨어가 무엇을 알아야 하는지를 추상화 한다.

프로시저 추상화를 결정했다면 이는 기능 분해로 향하는 것이다. 알고리즘 분해라고도 한다. 데이터 추상화를 결정했다면 두 가지로 다시 나뉜다. 하나는 데이터를 중심으로 타입 추상화이고 하나는 데이터를 중심으로 프로시저 추상화이다. 전자는 추상 데이터 타입이라 하고 후자는 객체 지향이라 한다.

프로시저 추상화와 기능 분해

프로시저 추상화는 Top-Down 방식의 추상화이다. 즉, 최상위 기능을 정의하고 이 최상위 기능을 더 작은 하위 기능으로 분해해 가는 방식이다. 다음 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char* employees[3] = {"Employee A", "Employee B", "Employee C"};
int payment[3] = {100, 200, 300};

int getPay(char* name) {
for(int index = 0; index < 3; index++) {
if(employees[index] == name)
return payment[index];
}
return 0;
}

int main() {
char* name;
printf("%d", getPay(name));
}

위 코드는 직원명을 입력받아 급여를 출력하는 프로그램이다. 하향식 기능 분해는 논리적이고 체계적인 시스템 개발 절차를 제시한다. 커다란 기능을 더 작은 기능으로 단계적으로 정재해 가는 과정에서 구조적이며 체계적인 동시에 이상적인것 처럼 보인다. 하지만 하향식 접근법의 문제는 이제부터 나타난다.

  • 시스템은 하나의 메인 함수로 구성돼 있지 않다.
  • 기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 한다.
  • 비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.
  • 하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시켜 유연성과 재사용성이 저하된다.
  • 데이터 형식이 변경될 경우 사이드 이펙트를 예측할 수 없다.

모듈

정보은닉은 시스템을 모듈 단위로 분해하기 위한 기본 원리로 시스템에서 자주 변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 뒤로 감춰야 한다는 것이 핵심이다. 그 측면에서 위의 코드는 employees와 payment가 전역변수로 선언되어 있어 네임스페이스를 어지럽힌다. 모듈은 다음과 같은 두 가지 비밀을 감춰야 한다.

  • 복잡성 : 모듈이 너무 복잡한 경우 이해하고 사용하기 어렵다. 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해 복잡성을 낮춘다.
  • 변경 가능성 : 변경 가능한 설계 결정이 외부에 노출될 경우 실제로 변경이 발생했을 때 파급효과가 커진다. 변경 발생 시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모듈 내부로 감추고 외부에는 쉽게 변경되지 않을 인터페이스를 제공한다.

C와 같은 언어에서는 소스 파일과 헤더 파일로 분리해서 모듈을 관리하고 C++과 C#에서는 네임 스페이스로, Java에서는 패키지를 통해 관리한다. 이에 따른 모듈에 장점은 다음과 같다.

  • 모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다.
  • 비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다.
  • 전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염을 방지한다.

하지만 모듈의 한계도 존재한다. 위에서 보았던 프로시저 추상화보다 높은 단계의 추상화를 제공하지만 변경 관리를 위한 기법이기 때문에 추상화 관점에서 한계가 존재한다. 인스턴스란 개념을 제공하지 않기 때문에 높은 추상화 수준을 위해선 직원 전체가 아닌 직원 독립적으로 추상화를 하는 메커니즘이 필요로 하게 된다.

추상 데이터 타입

추상 데이터 타입은 추상 객체의 클래스를 정의한 것으로 추상 객체에 사용할 수 있는 오퍼레이션을 이용해 규정된다. 이는 오퍼레이션을 이용해 추상 데이터 타입을 정의할 수 있음을 의미한다. 추상 데이터 객체를 사용할 때 프로그래머는 오직 객체가 외부에 제공해야 하는 행위에만 관심을 가지며 행위가 구현되는 세부적인 사항에 대해서는 무시한다. 객체가 저장소 내에서 어떻게 표현되는지와 같은 구현 정보는 오직 오퍼레이션을 어떻게 구현할 것인지에 집중할 때만 필요하다. 객체의 사용자는 이 정보를 알거나 제공받을 필요가 없다. 그리고 추상 데이터 타입을 구현하기 위해선 언어 레벨에서 다음과 같은 지원이 있어야 한다.

  • 타입 정의를 선언할 수 있다.
  • 타입 인스턴스를 다루기 위해 사용할 수 있는 오퍼레이션의 집합을 정의할 수 있어야 한다.
  • 제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 외부로부터 보호할 수 있어야 한다.
  • 타입에 대해 여러 개의 인스턴스를 생성할 수 있어야 한다.

그리고 이런 개념을 지원하는 것이 struct이다. 다음 코드는 정규직과 아르바이트생의 월급여를 출력하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Employee{
string name;
bool isHourly;
int pay;
int workedTime;

Employee(string name, bool isHourly, int pay, int timePay) : name(name), isHourly(isHourly), pay(pay), timePay(timePay) {}

int getPay() {
if(isHourly)
return pay * timePay;
else
return pay;
}
};

int main() {
Employee employees[3] = {
Employee("Salaried Employee A", false, 100, 0),
Employee("Salaried Employee B", false, 200, 0),
Employee("Hourly Employee C", true, 1, 80),
};

for(int i = 0; i < 3; i++) {
cout<<employees[i].name<<"'s monthly pay is "<<employees->getPay()<<endl;
}
}

클래스

클래스는 추상 데이터 타입이 아니다. 위의 코드에서 Employee는 정규직과 아르바이트생이라는 두 개념을 포괄적으로 가진다. 또한 getPay() 함수는 직원 유형에 따라 상이하게 동작한다. 즉 정규직과 아르바이트생 두 개의 타입이 공존하는 것이다. 추상 데이터 타입이 오퍼레이션을 기준으로 타입을 묶는 거라면 객체지향은 타입을 기준으로 오퍼레이션을 묶는다. 그렇게 되면 두 타입의 공통 로직을 어디에 둘 것인가가 요점이 된다. 공통 로직을 포함한 부모 클래스를 정의하고 상속으로 해결한다. 이것이 다형성이다. 즉, 객체지향은 절차 추상화이다. 다음은 위 코드를 클래스로 변경한 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Employee{
private:
string name;

public:
virtual int getPay() = 0;
Employee(string name) : name(name) {}
string getName() {
return name;
}
virtual ~Employee() {}
};

class SalariedEmployee : public Employee{
private:
int pay;

public:
SalariedEmployee(string name, int pay) : Employee(name), pay(pay) {}
int getPay() override {
return pay;
}
};

class HourlyEmployee : public Employee{
private:
int pay;
int workedTime;

public:
HourlyEmployee(string name, int pay, int workedTime) : Employee(name), pay(pay), workedTime(workedTime) {}
int getPay() override {
return pay * workedTime;
}
};

int main() {
Employee* employees[3] = {
new SalariedEmployee("Salaried Employee A", 100),
new SalariedEmployee("Salaried Employee B", 200),
new HourlyEmployee("Hourly Employee C", 1, 80)
};

for(int i = 0; i < 3; i++) {
cout<<employees[i]->getName()<<"'s monthly pay is "<<employees[i]->getPay()<<endl;
}
}
Author: Song Hayoung
Link: https://songhayoung.github.io/2021/01/14/Object/object7/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.