객체 지향 프로그래밍 (Object-Orientation Programming)
지금까지 배운 객체지향 프로그래밍은 대체로 C++ 문법에 대한 내용, 그 중에서도 상속에 대한 C++ 문법이었다.
C++ 뿐만 아니라 JAVA와 같은 대표적인 OOP 프로그래밍 문법들도 배웠을 것이다.
그러나 이번에는 문법을 배우는 것이 아니라, 실제 소프트웨어 개발에 어떻게 적용되는지를 배울 것이다.
소프트웨어를 개발하는 이상적인 방법
인터페이스
컴퓨터(하드웨어)를 생각해보자.
컴퓨터를 한 회사에서 처음부터 끝까지 다 만들지는 않는다.
삼성에서 만든 컴퓨터의 스펙을 보면 아래와 같지만, 이 모든 부품을 삼성에서 다 만든 것이 아니다.
다른 회사들이 만든걸 모아서 만든 것이다.
그런데 굉장히 신기하게도 각 부품들을 다 다른 회사가 만들었음에도 부품을 꽂으면 정상적으로 동작한다.▼
어떻게 모든 회사들이 합을 맞춘 것일까?
그건 인터페이스가 정해져있기 때문이다.
여기서 인터페이스는 하나의 규범으로 생각하면 이해하기 쉽다.
전 세계가 지키는 인터페이스를 정해두고, 그 인터페이스를 토대로 동작하게 설계를 했기에 부품을 생산하는 회사들이 달라도 동작할 수 있는 것이다.
하드웨어는 소프트웨어에 비해 로직이 간단하다고 한다. (교수님 피셜입니다. 허접인 제 의견이 아니에요.)
그 이유는 위에서 이야기한 인터페이스 덕분인데, 소프트웨어도 하드웨어처럼 인터페이스를 통한 ‘모듈화’를 하면 전에 비해 더 간단해질 수 있다.
모듈화
그럼 모듈화 했다는건 어떤 이야기일까?
소프트웨어 개발을 할 때, 한 소프트웨어 내부에서도 여러가지의 소프트웨어가 들어가게 된다.
모듈화는 그 여러 소프트웨어들 중 한 소프트웨어가 독립적으로 동작할 수 있다는 것을 말한다.
즉, 다른 소프트웨어들의 도움 없이도 동작할 수 있다는 것이다.
그런데 앞에서 잠깐 이야기 했듯 모듈화는 인터페이스가 필요하다.
그래서 모듈간의 인터페이스를 정의해야한다.
모듈화의 장점
그렇다면 모듈화를 하면 어떤 장점이 있길래 모듈화를 하려고 하는 것일까?
그 이유는 다음과 같다.
- 재사용(reuse) 하기 좋다.
다른 모듈을 쓰기 위해서 인터페이스만 알면 된다.
다른 모듈이 어떤 일을 해주는지만 알면 되는 것이지, 모듈 내부의 내용을 알 필요가 전혀 없다.
컴퓨터 조립을 할 때 RAM을 끼운다고 하면 RAM이 어떤 원리로 돌아가는지 알 필요없이, 컴퓨터에 맞는지만 알면 된다는 것이다. - 유지보수(maintenance) 하기 좋다.
새로운 requirement가 들어오면 그에 맞게 코드를 수정해야한다.
모듈화를 해놓으면 내가 코드를 수정해야할 부분이 한 점으로 한정이 된다.
모두 독립적이기 때문에 새 모듈을 추가하거나, 필요한 모듈만 바꾸는 식으로 유지보수가 가능해진다. - 소프트웨어 분석과 이해가 쉬워진다.
maintenance와 연관 되어있는 것도 있다.
유지보수가 잘 된다는 것은 그만큼 분석과 이해 하기 쉽다는 말과도 비슷하다.
모듈화가 되어있으면 마치 레고 부품을 보듯이 이해할 수 있고, 현실세계와 비유적으로 연결짓기도 쉽다.
사실 다른 방식의 프로그래밍 기법들도 Object-Orientation과 같은 방식으로 접근을 한다.
Object-Orientation 만의 방법은 아니지만, 이를 가장 만족시키기 쉬운 언어 혹은 방법이 Object-Orientation이라는데에 의의가 있다.
객체 지향(Object-Orientation)
객체 지향의 4가지 개념들
객체 지향은 아래의 4가지 개념으로 구성이 된다.
- 추상화 (Abstraction)
- 캡슐화 (Encapsulation)
- 상속 (Inheritance)
- 다형성 (Polymorphism)
전에 객체지향 수업을 들었거나 C++ 문법에 대해 공부했다면 친숙하게 느껴질 것으로 예상이 된다.
추상화부터 하나씩 보자.
추상화(Abstraction)
객체와 추상화는 뗄레야 뗄 수 없는 관계이기 때문에, 객체에 대해 알기 위해서는 추상화에 대해서 알아야 한다.
객체(Object)란?
객체는 어떤 구현하려는 영역에 있던 “무엇”을 추상화한 것을 말한다.
“무엇”은 정보를 저장하거나 기능을 제공하는 것을 말한다.
그 무언가를 attribute와 operation으로 추상화하여 객체를 만든다.
추상화(Abstaction)란?
그러면 추상화는 무엇일까?
추상화는 필요한 것만 뽑아내는 것을 말한다.
한 마디로 표현하자면,
“specific한 예시들로부터 공통된 것들을 뽑아냄으로서 일반화된 아이디어와 컨셉을 만들어내는 것을 말함.”
이라고 할 수 있다.
예시1.
학생을 예시로 보자.
학생들은 모두가 독특한 특성을 지닌다.
모두가 다 다르고, 수많은 특성을 가진다.
우리는 그런 것들 중에서 공통된 특성만 뽑는다.
학교 입장에서보면 우리의 공통된 특성은 학번과 이름, 들은 수업들이다.
이것들을 뽑아내서 일반화 시키는 과정을 abstraction이라고 한다.
이번에는 동아리에서 학생들을 관리하는 걸 만들고 싶다고 하자.
동아리에서는 학점, 수업 뭐 들었는지 신경 안쓰고, 동아리에서 사용하는 스킬들이 필요하다.
이렇게 같은 것을 대상으로 하지만 추출한 건 다르다.
예시2.
이번에는 우리학교 캠퍼스의 지도를 만들고 싶다고 해보자.
캠퍼스의 지도를 만드는 목적은 길 찾기이다.
길 찾기를 위해서는 길과 건물, 장소가 필요하고, 이를 추출하는 것으로 abstract를 수행한다.
그렇게 abstract 하고 보면 길과 건물외에 나머지는 남지 않는다. ▼
그리고 다시 abstract해보자.
이번에는 학교의 지형도를 그린다고 해보자.
지형도를 만드는 목적은 건물이 높은지 뒤에 있는 산이 높은지 알고 싶기 때문이다.
이 때는 길이 필요하지 않고, 높이 정보만 필요하기에 높이 정보를 추출하는 것으로 abstract를 수행한다.
이번 abstract로는 높이 정보 외에는 아무것도 남지 않는다. ▼
이러면 같은 학교를 대상으로 했지만 추출된 것은 다르다.
Class VS Object
둘의 차이에 대해서 이해하기 위해서는 syntax와 semantics라는 개념을 알아두면 좋다.
syntax는 문법(grammar)이라는 의미이고, semantics는 의미(meaning)라는 의미다.
우리가 말을 할 때에 문법만으로는 말이 통하지 않는다.
문법은 단어들을 배치하는 하나의 가이드라인이지 이 자체에는 의미가 없기 때문이다.
하지만 각 단어들에는 의미가 있고, 문법을 잘 따르면 그 의미는 조금 더 분명해져 의미가 생기게 된다.
Class와 object의 차이가 바로 이것이다.
Class는 semantic인 Object를 만드는 syntax라고 생각하면 된다.
위의 개념으로는 이해가 어렵다면 Class는 ‘붕어빵 틀’이고 Object는 ‘붕어빵’으로 기억하면 쉽다. ▼
Object는 클래스의 instance이다.
instance란 무언가가 실체화 된 것을 말한다.
실체화됐다는건 개념적으로만 만들어진게 아니라 실제로 쓸 수 있게 됐다는 것이다.
즉, 메모리를 할당받았다는 것을 말한다.
붕어빵 틀의 개념으로 보면, 붕어빵 틀은 밀가루라는 메모리를 할당받아 붕어빵을 실체화할 수 있게 도와주는 것이다.
캡슐화 (Encapsulation)
앞에서 본 Abstraction은 사실 객체 지향만의 특성이라기 보다는 일반적으로 사용되는 용어다.
진짜 핵심적인 특성은 캡슐화(Encapsulation)이다.
Encapsulation의 목적은 모듈화이다.
하나를 수정해도 다른 모듈에 영향을 안주게끔 개발하는게 목적이다.
캡슐하면 가장 먼저 떠오르는 건 역시 캡슐 약일 것이다.
약을 진단받으러 약국에 갔다고 해보자.
우리가 감기약을 달라고 부탁을 했을 때, 약사가 이게 감기약이 맞는지 직접 까보고 주지 않는다.▼
약사는 약의 내부를 보지 않아도 약의 외부를 보고 이게 어떤 약인지 알 수 있다.
그리고 이런 특성이 객체 지향에서의 캡슐화와 굉장히 유사하다.
내부적으로 어떻게 돌아가는지는 사용자 입장에서는 몰라도 된다.
겉으로 어떻게 동작하는지만 알면 된다.
Message-passing
Message-passing은 어떤 객체가 필요한 객체의 참조를 이용해 public 함수를 호출하는 것을 말한다.
그런데 호출을 할 때에 내부를 알 필요가 없다는 것이 핵심이다.
양파 껍질을 비유로 들 수 있다. ▼
데이터는 양파 껍질의 가장 내부에 있고, 이에 접근하려면 여러 개의 양파 껍질을 거쳐야 한다.
하지만 우리는 양파 껍질 내부를 알 필요가 없다는 것이다.
이 데이터가 양파 껍질 외부에 있을 거면 OOP를 사용하는 이유가 없다.
데이터를 public으로 사용할 거면 C언어를 사용하는 것이 좋다.
그렇기에 friend 기능은 쳐다도 보면 안된다.
잠깐 보고 가는 다형성 (Polymorphism)
Polymorphism에서는 인터페이스 하나만 공개되고 나머지는 모두 감춰져야한다.
이를 위해서는 클래스 단위의 Encapsulation을 하게 되고, 이런 측면에서 Polymorphism은 클래스 단위의 Encapsulation이라고 할 수 있다.
개발을 하다보면 여러 개의 component가 나오는데, 이 component들을 사용할 때 해당 component안에 뭐가 들어있는지를 알아야 돌아가게끔 개발을 하면 안된다.
위의 양파껍질과 비슷하게, 내부적으로 어떤 클래스들이 어떻게 동작하는지를 몰라도 돌아가게 설계를 해야한다는 것이다.
즉, 클래스든 컴포넌트든 뭐든 간에 내부적으로 뭐가 어떻게 돌아가는지 알아야 돌아가게 하면 안된다.
인터페이스만 보이고 나머지는 다 감춰져 있어야 하는게 모듈화의 기본 원칙이다.
Encapsulation 원칙
최소화
최소화는 필요한 기능만 공개하는 것을 말한다.
외부에 필요한 기능들만 공개하고 나머지는 모두 숨긴다.
무변경
무변경은 인터페이스를 변경하지 않는 것을 말한다.
인터페이스를 변경하게 되면 그에 맞게 다른 것도 싹 다 바꿔야하기 때문이다.
앞에서 이야기한 하드웨어 이야기를 다시 예로 들면, 열심히 현재 포트대로 개발을 했는데 포트가 바뀌게 되면 부품의 설계를 바꿔야한다. ▼
그렇기에 인터페이스는 최대한 바뀌지 않게 처음부터 조심스럽게 정해야한다.
바뀔 수 있을 것 같다면 정말 general하게 해야한다.
나중에 기능이 추가가 되어도 영향을 적게 해야한다.
인터페이스를 바꾸는 행위 = 모든 사람을 힘들게 하는 행위
장점
다 감추고 인터페이스만 공개하는 것을 통해 독립적으로 공개할 수 있다.
인터페이스 하나만 고정시켜놓고, 나머지 부분들은 뿔뿔이 흩어져서 만들어도 된다.
이런 과정을 통해 코드간의 의존성이 획기적으로 감소하게 된다.
또한 내 코드를 수정했다고 내 코드를 사용하는 다른 코드를 바꿀 일이 확 줄어들게 된다.
상속 (Inheritance)
상속은 ISA 관계라고 생각하면 쉽다.
상속이라고 하면 보통 위에서 아래로 계승되는 것을 생각하지만(이것도 관점에 따라서는 맞다), OOP에선 아래에서 위로 올라가는 것이 일반적이다. ▼
ISA 관계는 기본적으로 Generalization이다.
Generalization은 특징을 뽑아서 위에 superclass를 만드는 것을 말한다.
그래서 위로 올라갈 수록 더 General 해지고, 아래로 내려갈 수록 더 Specialized 해진다.
Specialized 된 요소들은 공통된 부분에 자신만의 요소를 포함한 것으로, 강아지는 동물이 가지는 모든 특성을 가지면서 강아지 만의 특징들을 가진다.
이를 상속이라고 하며, 상속은 과정이 아닌 결과물을 말하는 것이다.
상속의 장점
상속의 장점은 superclass를 재사용할 수 있다는 것이다.
만약에 상속을 안한다고 생각해보면, 강아지와 고양이 클래스는 동물이라는 클래스를 중복해서 갖게 된다.
이렇게 되면 일단 코드의 양이 많아지게 되고, 중복되는 부분이 바뀌게 되면 중복되는 부분을 갖는 모든 클래스들을 수정해야한다.
이 과정에서 시간을 많이 소모하게 된다.
상속을 찾는 방법
상속은 어거지로 끼워맞추며 찾는게 아니라 자연스럽게 아래의 방법들로 만들어진다.
Bottom-up approach
클래스들의 유사성을 찾는 것을 말한다.
Generaization을 통해 superclass를 만드는것이 Bottom-up approach라고 할 수 있다.
말로는 유사성이라고 했지만, 실상은 중복된 것을 찾는 것에 가깝다.
Top-down approach
그러나 이미 superclass가 만들어져 있으면 그 때부턴 Top-down 방식으로 진행한다.
만들어놓은 것들에 연결시켜준다고 생각하면 쉽다.
만약에 만들어놓은 것들과 맞는 것이 없다고 하면 새롭게 만들면 그만이다.
객체들 간의 두가지 관계
IS-A
is a kind of 관계로 생각하면 쉽다.
HAS-A
자신의 객체 내에 다른 객체의 instance를 포함하는 관계를 HAS-A 관계라고 한다.
IS-A관계나 HAS-A 관계 모두 [object A] IS-A/HAS-A [object B]로 나타낸다.
그런데 HAS-A는 다른 객체를 어떻게 가진다는 것일까?
Class가 가질 수 있는 요소는 Attribute와 Operation 두가지고, 객체는 Operation보다 Attribute에 적합하므로 Attribute에 표현한다.
"그렇다면 어떻게 표현하는건지?"
해당 객체 타입에 데이터 멤버를 정의하는 식으로 표현한다. ▼
sonata class가 attribute로 engine을 가진다.
위의 그림을 코드로 나타내면 아래와 같다.
class Sonata : public Car { // IS-A 관계
private Engine engine; // HAS-A 관계
}
관계는 HAS-A가 당연하게도 IS-A보다 많다.
IS-A, HAS-A 예제
College School MiddleSchool Depts. |
College IS-A School College HAS-A Depts. MiddleSchool IS-A School |
Kitchen House Building Office |
House IS-A Building Office IS-A Building House HAS-A Kitchen |
다형성 (Polymorphism)
다형성, Polymorphism은 Poly - multiple - 여러개 와 Morph - shape - 모양 이 합쳐진 말이다.
즉, 여러개의 모양이라는 뜻이다.
Polymorphism의 핵심을 한 문장으로 표현하는 것이 아래의 문장이다.
One interface, multiple implementations
그러나 polymorphism이라는 이름에는 이 중에 multiple만 반영되어있다.
하지만 정말 핵심은 One interface이다.
One interface는 외부에 공개되는 public function을 말하고 하나의 Super-class를 말하며, Multiple implementations는 뒤에 감춰진 Sub-classes를 말한다.
앞의 Encapsulation도 잠깐 언급했지만, Class 차원의 Encapsulation인 셈이다.
Encapsulation의 관점
위에서는 super-class의 함수를 호출한 거 뿐인데, 실행은 그 이면에 감춰진 sub-class들의 함수가 실행된다. ▼
요청을 보내는 쪽은 sub class중 누가 받을 지 알 필요가 없다.
그냥 공개된 Super-class에게 보내면 된다.
Inheritance의 관점
Inheritance를 사용하는 입장에서 역시도 내부 상속 구조에 대해서 알 필요가 없다.
심지어 상속이 있는지 조차도 생각할 필요가 없다.
내부 클래스들이 아무리 바뀌어도 사용하는 입장은 ‘바뀌든 말든 우리는 인터페이스로 나오는 결과만 정상이면 됐어’ 라 아무 문제 없이 사용할 수 있다.
즉, 인터페이스만 생각하면 개발과 제공된 Public function사용에는 문제 없다.
장점
앞의 모든 특성들이 그러하듯 재사용에 용이하다.
그런데 이제는 component 단위로 모듈화가 되어 좀 더 넓은 범주의 재사용이 되며 독립적인 구현이 가능해진다.
Super-class의 인터페이스만 호출하기에 Sub-class를 아무리 고쳐도 다른 쪽에 영향을 안주게 되고, 이런 특성으로 완전히 독립된 모듈이 된다.
이러면 코드의 질의 획기적으로 증가하게 되고, 유지보수에 용이해진다.
일부분을 고치기 위해 전체를 고치지 않아도 되기 때문이다.
구현
다형성은
$$ Operation overriding(inheritance) + Dynamic binding $$
을 통해 구현된다.
Operation Overloading VS Operation Overriding
오버 로딩(Overloading)은 함수 이름만 같은 여러가지 프로토 타입들을 정의할 수 있는 것을 말한다.
반환 타입과 매개변수를 다 다르게 설정할 수 있다.
다른 말로는 다중 함수 정의라고 할 수 있고 일차원적인 기술이라고 한다.
(교수님 의견이구 제게는 아직 어려운 기술입니다…)
오버 라이딩(Overriding)을 쉽게 이해하는 것은 위에 탄다고 이해하는 것이다.
무언가의 위에 올라타게 되면 위에서 봤을 때 그 아래의 것은 감춰지게 된다. ▼
여기에서도 마찬가지다.
Super-class에 무언가를 정의했는데, Sub-class에서 그걸 다시 정의함으로 Super-class 위에 올라탄다.
이렇게 되면 Super-class의 함수는 감춰지게 되고, Sub-class의 함수가 실행되게 된다.
Static binding과 Dynamic binding
Static binding은 컴파일 시간에 정해지는 것을 말하고,
Dynamic binding은 실제 동작시, 즉 런타임에 정해지는 것을 말한다.
마치며
객체지향에 대해서는 전에 공부한 적이 있기에 엄청나게 생소한 이야기는 아니었다.
개념에 대해서는 잘 알겠지만, 이를 어떤 식으로 적용하는지에 대해서는 아직 이해가 덜 된 느낌이다.
그래서 다음 글에서는 다형성의 구현 방식에 대해서 알아보도록 하겠다.
'CS > 소프트웨어 공학' 카테고리의 다른 글
12. Configuration and Version Management (0) | 2024.01.31 |
---|---|
11. 다형성(Polymorphism) 예제 (0) | 2024.01.31 |
9. 요구사항 문서화 (0) | 2024.01.29 |
8. 요구사항 포착 (0) | 2024.01.26 |
7. Modeling Concepts (0) | 2024.01.26 |