[소프트웨어공학] 7. 객체지향 (1) - abstraction, 캡슐화, 상속
이번 글부터는 객체지향의 주요 개념을 소프트웨어 개발 방법의 관점에서 살펴보려고 한다.
먼저 객체지향이 등장한 배경은 SW의 모듈화를 위해서였다.
모듈은 소프트웨어를 설계할 때 '독립적인 기능을 수행하는 개발단위'를 말한다.
기능이 독립적이라는 뜻은 이 기능이 다른 모듈의 변화에 영향을 주지도, 받지도 않는다는 것을 말한다.
HW에서의 모듈화를 생각해보자.
컴퓨터를 조립할 때, CPU, RAM, SSD, 그래픽카드, 네트워크카드, 파워, 팬, 마더보드 등의 부품이 필요하다.
그런데 이 각각의 부품들은 모두 서로 다른 제조사에서 만든다.
하지만 이 부품들을 잘 모아서 조립하면 하나의 온전한 컴퓨터로서 기능한다.
다른 부품을 그대로 두고 RAM 만 바꾸거나, 그래픽카드만 바꾸거나 해도 다른 하드웨어의 동작에는 전혀 영향이 없다.
따라서 각 부품들은 모두 독립적이다.
이게 가능한 이유는 각각의 하드웨어 별로 통일된 인터페이스가 정의되어 있기 때문이다.
모든 RAM 카드는 어떤 사이즈의 규격을 갖고 있어야 하고, SSD는 어떤 규격을 갖고 있어야 하고.. 이렇게 인터페이스가 모두 규격화되어 있기 때문에, 각각의 제조사는 인터페이스 형태만 맞추어 개발하고, 다른 부품의 동작은 전혀 신경쓰지 않고 생산할 수 있다.
SW에서도 같은 원리로 모듈을 나눠 개발해야 한다.
전체 소프트웨어를 모듈 단위로 분리하고, 각각의 모듈마다 인터페이스를 먼저 정의한다.
이렇게 아키텍처를 설계한 뒤에 실제 개발에 들어가면 인터페이스에 맞춰서 개발하기만 하면 독립적으로 개발해도 전체 모듈을 조립했을 때 문제없이 동작할 수 있다. 그리고 다른 모듈의 구현이 변하더라도 인터페이스에 맞기만 한다면 다른 모듈이 동작에는 전혀 영향을 주지 않는다.
이와 같은 방법을 통해 모듈 간 의존성을 획기적으로 감소시킬 수 있고, 모듈의 재사용, 소프트웨어 유지보수, 코드 분석이 용이해진다.
소프트웨어 기능을 업그레이드 할 때 모듈 단위로만 필요한 기능을 업그레이드 하고, 분석하면 되기 때문이다.
이렇게 소프트웨어를 모듈 단위로 나눠 개발하는 것은 객체지향만 가능한 것은 아니지만,
객체지향은 소프트웨어를 모듈 단위로 나눠 개발하기에 좋은 점이 많다.
이제 객체지향에서 중요하게 언급되는 4가지 개념인 abstraction, 캡슐화, 상속, polymorphism에 대해 정리해보자.
Abstraction
한국어로는 추상화라고 많이 알려져있다.
교수님은 컴퓨터공학 용어 중에서 가장 잘못 번역된 용어라고 하셨는데, 설명을 들으면서 나도 공감되었다.
abstraction 은 abstract 의 명사형이다.
abstract 는 무슨 뜻인지 살펴보면 '추출하다' 라는 뜻을 가진 동사다.
기존에 있는 어떤 개념이나 존재에서 일부를 빼서 가져오는 느낌인데, 한국어의 '추상화' 라는 단어에서는 잘 느껴지지 않는 느낌이다.
객체지향의 abstraction도 이와 완전히 같은 의미이다.
Object
객체지향에서 '객체(Object)' 는 다음과 같이 정의된다.
An abstraction of something in a problem domain that is capable of
- keeping information about it (attribute)
- providing its functionalities (operation)
문제 영역에 존재하는 어떤 실체로부터 문제에 대한 정보를 갖고 있는 요소 (attribute) 나, 그 실체가 가진 기능 (operation) 을 뽑아내 가져온 것이 객체라고 한다.
쉽게 말하면, 문제 상황을 구성하는 요소로부터 그 문제를 SW로 해결하는데 있어 꼭 필요한 요소와 기능만 추출해서 가져온 것이 객체이다.
객체지향에서 '객체'란, 시스템에서 반드시 필요한 상태(state)와 행위(behavior)를 추출한 것이다.
구체적인 예시를 보면 더 잘 와닿는다.
문제를 해결한데 꼭 필요한 요소만 추출한다는 것은, 각 문제마다 필요한 요소가 달라질 수 있음을 내포한다.
그래서 문제로부터 꼭 필요한 요소를 잘 선별해서 뽑아내는 것이 중요하다.
예를 들어 지도를 만들고자 한다.
지도는 실제 지형 정보를 추상화한 것이다.
그런데 지도의 종류마다 그 지도의 목적에 따라 추출해야 하는 정보가 달라진다.
단순히 길을 찾기 위한 지도라면 실제 지형 정보에서 '건물'과 '길' 정보만 추출해서 보여주면 된다.
반면 지형 지도를 그리고자 한다면 실제 지형 정보에서 바위, 산과 같이 지표면의 높낮이가 변화하는 정보를 추출해야 한다.
똑같은 지형 정보가 주어져도 만들고자 하는 지도에 따라 추출하는 정보가 달라진다.
이번엔 조금 더 실제에 가까운 예시를 살펴보자.
학생을 대상으로하는 서비스를 만들려고 한다.
그러면 그 '학생' 객체에는 어떤 정보를 담아야 할까?
이것도 이 서비스에서 필요로 하는 상태와 행위가 무엇인지에 따라 달라진다.
만약 학과 사무실에서 학생 정보를 관리하는 서비스를 개발하려고 한다면 실제 학생과 관련된 정보 중에서 '학번, 이름, 성적, 수강 과목, 성적' 등의 정보가 필요할 것이다.
반면 동아리에서 학생 정보를 관리하는 서비스를 개발하려고 한다면 '학번, 이름' 은 똑같이 필요하겠지만 성적과 같은 정보는 필요없다.
오히려 해당 동아리에서 활동하기에 적합한 기술이나 경험 등이 더 필요한 정보일 것이다.
똑같은 학생에 대해서 해결하고자 하는 문제 상황에 따라 추출한 정보가 달라진 것이다.
이처럼 객체란 문제 상황 속 개념에서 시스템이 필요로 하는 상태와 행위를 추출한 것이다.
이때 객체로 추출하는 것은 유무형의 모든 개념이 가능하다.
학생, 지형 정보과 같은 유형의 존재도, 강의, 스터디와 같은 무형의 개념도 모두 객체로 추출해낼 수 있다.
이렇게 필요한 데이터를 추출하여 실제 존재하는 큰 개념을 시스템에 맞춰 축소시키는 것을 '모델링' 이라고 표현하기도 하는데, 결국 모든 모델링은 abstraction 과 같다.
(참고) 객체의 상태와 행위는 다양한 용어로 불린다.
state - behavior / attribute - operation / property - method / data member - member function
Class
클래스는 위에서 abstract object 들 중 비슷한 object 들의 집합을 정의한 것
object가 갖고 있는 specified logical similarity 에 대한 abstract description 을 말한다.
이때 specified logical similarity 라는 것은 attribute, operation, link (다른 오브젝트에 대한 참조) 와 더불어 symmantics (오브젝트가 갖고 있는 내재적인 의미) 까지 비슷하다는 것을 말한다.
예를 들어 우리 학교의 학생과 다른 학교의 학생의 attribute, operation, link 는 비슷할 수 있어도, symmantics 는 서로 다를 것이다.
똑같이 '학번' 이라는 속성을 갖고 있어도 우리 학교에서 사용하는 학번 형식과 다른 학교의 학번 형식은 다를 것이기 때문이다.
그리고 객체지향에서의 Object는 클래스로부터 state 와 behavior 가 인스턴스화 된 것을 말한다.
따라서 모든 object는 어떤 클래스의 instance 이다.
컴퓨터 관점에서는 실제로 메모리르 할당하고 데이터를 저장한 것이 인스턴스라는 것
Encapsulation
한국어로 캡슐화라고 부른다.
캡슐하면 자연스럽게 '알약'이 생각난다.
캡슐 알약을 열어보면 그 안에 여러 하얀색 가루가 들어있다.
그런데 우리는 알약을 먹을 때, 그 가루를 원심분리해서 어떤 종류의 가루들이 들어있고 각각의 가루가 몸 속에서 어떤 역할을 수행하는지 이해한 뒤에 먹지 않는다.
그냥 알약 캡슐에 적혀있는 글자를 보고 '감기약이구나' 하고 이해한 다음에 먹는다.
객체지향에서의 캡슐화도 이와 똑같다.
실제로 객체가 어떻게 동작하는지 구현을 숨기고 인터페이스만 공개하는 것을 캡슐화라고 부른다.
그리고 객체를 사용할 때는 그 객체의 인터페이스만 보고도 대충 어떻게 동작할지 다 파악할 수 있어야 한다.
- message passing & information hiding
객체지향에서 message passing 이라는 말이 종종 등장한다.
message passing은 객체가 갖고 있는 public function 을 호출하는 것을 말한다.
또는 '해당 객체에 message 를 전달한다' 라고 표현하기도 한다.
그래서 하나의 시스템을 구성하는 여러 객체들이 서로서로에게 메세지를 전달하면서 소통한다.
이때 각각의 객체들은 상대 객체가 어떻게 구현되어 있는지 전혀 알 필요가 없다.
캡슐화를 이미지로 표현하면 위와 같이 표현할 수 있다.
다른 객체로부터 받은 메세지가 서비스를 요청하면 (= 다른 객체가 자신 객체의 메서드를 호출하면)
그 객체가 갖고 있는 메서드에 의해서만 해당 객체의 데이터에 접근이 되어 데이터를 다루게 된다.
즉, 객체가 가지고 있는 데이터는 철저하게 외부로부터 숨겨져있다.
이렇게 객체의 데이터가 외부로부터 숨겨져있는 것을 가리켜 information hiding 이라고 한다.
이렇게 캡슐화를 통해 객체가 갖고 있는 세부적인 데이터와 그 데이터에 접근하는 로직 구현을 숨기고,
외부에 보여질 인터페이스에 집중할 수 있게 된다.
그리고 캡슐화는 System 아키텍처를 설계할 때 컴포넌트를 나누고 각 컴포넌트가 서로 소통하는데 필요한 인터페이스를 정의할 때 필수적인 개념이다.
- Encapsulation 원칙
캡슐화를 할 때는 다음 2가지를 지켜야 한다.
1. 최소화
외부에 보여질 기능은 최소한으로 보여주고 나머지는 숨겨야 한다.
2. 무변경
한번 정의한 인터페이스는 가급적 변경하지 않도록 최대한 일반화해서 만들어야 한다.
그래서 public function 과 같은 인터페이스는 최대한 유지되도록 하고, 바뀌어도 되는 private function 이나 내부 구조같은 요소는 분리하여 따로 관리한다.
인터페이스가 바뀌게 되면 전체 소프트웨어가 다 흔들리게 된다.
하드웨어에서 램 슬롯 크기가 바뀌게 되면 램을 꽂는 메인보드 제도사도, 렘을 제조하는 회사들도 모두 새로 부품을 만들어야 한다.
OSI 7계층에서도 각 계층은 이전 계층의 전송 / 수신 메서드만 알고 있다. (application layer 에서 '소켓' 이라는 인터페이스로 소통하듯이) 만약 이 인터페이스가 바뀌게 되면 네트워크 전체 시스템도 흔들릴 것이다.
따라서 인터페이스는 최대한 변경되지 않도록 설계해야 한다.
캡슐화를 잘하면 모듈화를 할 때 도움이 많이 된다.
먼저 다른 모듈을 가져다가 사용할 때, 다른 모듈의 구현을 자세히 알지 못해도 사용할 수 있으므로 재사용하기 용이해진다.
그리고 소프트웨어의 각 컴포넌트가 모듈화되어 독립적으로 구현할 수 있으므로 코드간 의존성이 낮아진다.
코드간 의존성이 낮아진다는 것은, 하나의 모듈의 변경 때문에 다른 모듈을 수정할 가능성이 낮아진다는 것과 같다.
다른 모듈에서는 가져다 쓰는 모듈의 인터페이스를 사용하여 코드를 작성했을 것이기 때문에 인터페이스가 바뀌지 않는다면 다른 모듈은 바뀌지 않기 때문이다.
Inheritance
어떤 개념을 분류할 때, 자연스럽게 계층구조가 그려진다.
예를 들어서 사람을 분류한다고 하면 회사 내부를 기준으로 '직원 / 고객 / 거래처'로 나눌 수 있다.
직원은 다시 월급 / 주급 / 시급 직원으로 나눌 수 있고,
시급 직원은 구체적인 역할에 따라 driver, cleaner, sales assistant 로 나눌 수 있다.
이렇게 하나의 큰 개념이 아래로 내려갈수록 점점 구체화된다.
이때 위에 있는 큰 개념일수록 general 하고 (super class), 하위에 있는 작은 개념일수록 specific 하다. (subclass)
하위의 개념은 상위의 개념을 그대로 가지고 있으면서 추가적인 정보를 가지고 있다.
예를 들어 상위 개념인 Person 은 이름, 생일, 성별과 같은 정보를 가질 수 있고, 하위 개념인 HourlyPaidDriver는 Person이 갖고 있는 정보에 더해 근무 시작일, 면허 종류와 같은 정보를 추가로 가질 수 있다.
이렇게 상위 개념의 정보에 추가적인 정보를 더해 하위 개념을 정의하는 것을 가리켜 specialization 이라고 한다.
반대로 하위 개념들이 갖고 있는 정보 중에 공통된 정보들을 묶어 상위 개념으로 정의하는 것을 가리켜 generalization 이라고 한다.
그리고 객체지향 프로그래밍에서는 inheritance(상속)를 통해 specialization 과 generalization 을 구현한다.
super class (또는 parent/base class) 의 attribute와 operation은 모든 subclass 에 적용된다.
또한 모듈화 개념에서 보면 상속을 통해 super class의 속성과 기능을 재사용하는 효과를 얻을 수 있다.
실제로 프로그래밍을 할 때 상속을 활용하는 방법은 크게 top-down 과 bottom-up 방식이 존재한다.
Bottom-Up 방식은 클래스들 사이의 유사성을 찾아서 공통 속성을 묶은 super class 를 만드는 방법이다.
상속은 '내가 상속을 사용해야지' 해서 사용하는 것이 아니라 자연스럽게 찾아지는 것이므로, 처음에는 보통 바텀-업 방식으로 상속 관계를 찾아낸다.
예를 들어 직원 관리 프로그램을 설계하면서 어떤 직원이 있나 보니 월급을 받는 직원과 주급을 받는 직원이 있다고하면, 그 각각을 클래스로 정의한 뒤, '직원' 이라는 공통적인 속성을 묶어 Employee 클래스를 만든다.
Top-Down 방식은 Employee 라는 상위 클래스를 기반으로 추가적인 하위 클래스를 찾아 추가해나가는 방식이다.
즉, 기존에 super class 와 sub class 가 존재하는 상황에서 새로운 sub class 를 추가하려고 할 때 적용하는 방식이다.
기존의 월급/주급 직원 외에 시급을 받는 직원을 추가하고 싶다면 Hourly paid 라는 subclass 를 추가하면 되는 것이다.
- 객체와 객체 사이의 관계
서로 다른 두 객체는 아무런 관계를 갖고 있지 않을 수도 있고, 어떤 관계를 가질 수도 있다.
이때 두 객체가 관계를 맺고 있다면 그 종류는 정확히 2가지 중 하나이다.
IS-A 관계
데이터베이스에서 ERD를 그릴 때 봤던 그 관계다.
상속관계인 두 객체는 IS-A 관계를 갖는다.
A is a B 라고 하면, A는 subclass 이고, B는 superclass 이다.
예를 들어 sonata is a car 라고 하면, 소나타는 자동차의 기종이므로 subclass 이고, car 가 모든 자동차의 개념을 포괄하는 superclass가 된다. 위 그림에서 자동차는 탈것이므로 car is a vehicle 이 성립한다.
is-a 관계는 'is a kind of' 로 표현하기도 한다. (car is a kind of vehicle)
HAS-A 관계
객체 내에 다른 객체를 갖고 있는 관계를 말한다.
클래스 안에 다른 객체를 private 멤버 변수로 갖고 있는 것이다.
예를 들어 위 그림에서 보듯 모든 자동차는 엔진을 갖고 있다.
그러면 car has a engine 이므로 car 객체 내에는 engine 객체가 존재한다.
그렇다고 engine 객체가 자동차의 한 종류는 아니므로 is-a 관계가 되지는 않는다.
example 1
객체와 객체 사이의 관계를 파악하는 또 다른 예시로 house, building, kitchen 사이의 관계를 파악해보자.
house is a building
house has a building
자연스러운 것은 house is a building 이다.
따라서 house 와 building 은 is-a 관계 (=상속관계) 이며, superclass 는 building 이다.
house is a kitchen
house has a kitchen
자연스러운 것은 house has a kitchen 이다.
따라서 house 와 kitchen 은 has-a 관계이며, house 가 kitchen 을 필드로 갖고 있는 관계이다.
example 2
다른 예시로 college, school, department 사이의 관계를 파악해보자
college is a school
college has a school
자연스러운 것은 college is a school 이다.
따라서 college 는 school 의 subclass 이다.
college is a department
college has a department
자연스러운 것은 college has a department 이다.
따라서 college 는 department 를 내부에 필드로 갖고 있는 has-a 관계이다.
지금까지 객체지향의 개념과 객체지향이 갖고 있는 4가지 속성 중 abstraction, encapsulation, inheritance 에 대해서 정리해보았다.
다음 글에서는 교수님이 객체지향에서 제일 중요하다고 강조하신 polymorphism 과 그 예제들에 대해 정리해보려고 한다.