객체지향 설계를 위한 추상화 메커니즘 정리
하이!
오늘은 객체지향의 개념중 가장 중요하다 할 수 있는 추상화에 대해 총정리 해두려고 한다.
java나 c#등 객체를 이용한 , 객체를 지향하는 다양한 언어가 있다. 객체지향으로 짜여진 프로그램에서는 무수히 많은 객체가 서로 메시지를 주고 받으며 협력한다. 너무나 많은 객체가 있다보니 이를 효율적이고 단순하게 관리할 필요가 있다. 객체지향에서는 대표적으로 추상화라는 방식을 통해 복잡한 도메인을 단순화 시키고 직관적으로 만든다.
추상화란 현실을 어느정도 반영하되,
구체적인 사물간의 공통점은 취하고 차이점은 버리는 분류를 통해,
그리고 중요한 부분을 강조하기 위해 불필요한 세부사항은 버리는 일반화를 통해 이루어진다.
아래는 객체지향에서 활용하는 추상화 메커니즘을 정리해 놓은것이다.
-분류와 인스턴스화
-일반화와 특수화
-집합과 분해
하나씩 자세하게 알아보자.
분류와 인스턴스화
모든 객체는 각각의 개념을 적용하여 그 개념의 범주에 넣을 수 있다.
예를들어 '아이스아메리카노' 는 커피다. '바닐라라떼', '카라멜 마키야또' 도 커피다.
우리는 보통 커피나무의 열매의 씨를 갈아 끓이거나 물에 타서 먹는 음료를 커피라 칭한다. 이렇게 무언가에 대해 공통적인 요소를 가지고 정의한 것이 개념이다. 그리고 이러한 '커피'라는 개념의 범주에 '아메리카노', '바닐라라떼', '카라멜 마키야또' 등의 커피가 포함되게 된다.
이렇게 세상에 존재하는 모든 객체는 공통의 속성을 가진 개념이 존재한다. 여기서 분류란 객체에 개념을 적용하는 과정이다. 다양한 객체는 각 개념을 통해 특정한 범주의 구성요소로 포함이 된다.
지금 내가 글을 쓰기 위해 사용하고 있는 '키보드'도 개념이 있고, 블로그 화면을 보여주는 '모니터'도 개념이 있고, 어제 자기전에 읽은 '책' 도 개념이 존재 한다.
어제 읽다 잠든 '클린코드'라는 책은 '여러장의 종이로 구성되고, 각 종이마다 글,그림,사진 등이 일정한 순서에 따라 있는 물건' 이라는 책의 특성을 가졌다. 와이프가 아기 이유식을 만들기 위해 보고 있는 '삐뽀삐뽀 119 이유식'이라는 책도 책의 개념을 그대로 가지고 있다. 우리는 누구나 무의식적으로 책하면 떠오르는 개념이 있다.
객체지향에서 이러한 개념을 '타입' 이라고 부른다.
객체지향언어에서 타입을 구현하는 아주 효과적인 방법은 바로 클래스를 만드는 것이다.
그리고 이러한 타입(개념)을 바탕으로 만들어진 객체를 타입의 '인스턴스'라 부른다. 같은 집합에 속하는 객체는 동일한 클래스의 인스턴스가 되도록 한다.
객체지향의 세계에서 객체는 각각의 타입에 따라 분류되어 생성되게 된다.
분류는 단일분류(single classification)와 다중 분류(multiple classification) 로 나뉠수 있다.
다중 분류의 개념에 따르면 '사람'인 '나'는 '가족' 집합의 '아빠' 역할을 하는 동시에 '회사' 집합의 '팀원' 에 포함될 수 있다.
여기서 단일 분류를 따르게 되면 '나'는 두 집합 중 하나에만 포함되어야 한다.
보통의 객체지향 프로그래밍 언어에서는 단일분류만을 지원한다. 이말은 곧 생성된 객체는 오직 하나의 클래스만의 인스턴스여야 한다는 것이다.
여기서 다중 분류와 다중 상속의 개념이 헷갈릴 수 있는데 다중상속은 하나의 타입이 다수의 슈퍼타이틀을 가질 수 있도록 허용하는 개념이지 여러 클래스의 인스턴스가 될 수 있다는 의미는 아니다.
또한 분류는 동적 분류(dynamic classification) 과 정적 분류(static classification)으로도 나눠 볼 수 있다.
동적분류는 객체가 자신이 포함된 집합을 , 즉 타입을 변경할 수 있는 경우를 의미한다. 정적 분류는 반대로 변경할 수 없는 경우를 말한다.
예를 들어 '나'는 회사에 있을 때는 '회사'의 '팀원' 으로 분류 되었다가 퇴근하고나서는 '가족'의 '아빠' 로 분류 된다. 동적 분류가 허용 될 때는 시간의 흐름에 따라 변경되기도 하고, 경우에 따라서는 동시에 몇가지 종류의 타입을 사용할 수도 있다.
우리가 객체지향의 개념을 이해하기 어려운 이유가 바로 여기에 있다. 현실세계에서는 수많은 일들이 다중분류와 동적분류로 나눠진다.
하지만 객체지향 프로그래밍에서 다중분류와 동적분류를 통해 구현을 하는것은 거의 불가능에 가깝다. 현실의 도메인을 다중분류와 동적분류를 통해 모델링의 큰 골격을 만들고 단일분류와 정적분류 방식을 통해 설계를 완성하는 것이 현실적인 방안이라 생각한다.
일반화와 특수화
일반화와 특수화는 타입간의 관계를 계층적인 구조로 바라보는 것이다. 어떤 타입이 다른 어떤 타입보다 더 범용적이고 일반적일 경우 이 타입을 슈퍼타입(supertype)이라 칭한다. 반면 어떤 타입이 다른 어떤 타입보다 더 세밀하고 특수하다면 이타입을 서브타입(subtype)이라고 부른다. 객체지향에서 슈퍼타입은 서브타입의 일반화이고, 서브타입의 경우 슈퍼타입의 특수화이다.
만약 슈퍼타입에 특정한 속성이 있으면, 하위 범주인 서브타입도 해당 속성을 공유한다.
예를 들어 '동물' 이라는 타입이 슈퍼 타입이고, '사람' 이라는 타입이 서브타입이라고 가정해보자.
우리는 '사람'이 '동물' 처럼, 움직이고, 먹고, 자고 하는 것을 쉽게 예상할 수 있다. 이는 동물의 공통적인 특징이기 때문이다.
그런데 '사람'은 움직일때, 좀더 세부적으로 2족 보행으로 걸어서 움직이고, 먹을때 수저를 사용하고, 잘때는 침대에서 이불을 덮고 자는 것처럼 조금더 세밀하고 특수한 형태의 속성을 지닌다. 그리고 동물에는 없는 '공부하고', '일하고' 하는 등의 세부적인 특징도 추가적으로 가진다.
우리가 여기서 확실히 알 수 있는 것은 서브타입은 슈퍼타입의 모든 속성을 포함하고 있다는 것이다.
그리고 서브 타입의 모든 인스턴스는 슈퍼타입의 집합에 포함이 되어야한다.
모든 '사람'은 '동물'이라는 범주에 포함된다. 한단계 더 계층을 나눠서 설명해보면 모든 '사람'은 '포유류'라는 범주에 포함이 되고, 모든 '포유류'는 '동물' 이라는 범주에 포함이 된다.
흔히 일반화 관계를 IS-a 관계라 칭한다. '사람은 동물이다' 처럼 is-a 관계는 서브타입이 슈퍼타입의 부분집합임을 알수 있게 해준다.
일반화 원칙에 의하면 어떤 타입이 다른 타입의 서브 타입이 되기 위해서는 슈퍼타입에 구조적 , 그리고 행위적 순응을 해야한다. 구조적 순응은 타입의 속성관 관련이 있다. 서브 타입은 슈퍼타입에 있는 속성을 100% 가지고 있어야한다.
행위적 순응은 행동에 관련되어 있다. 행위적인 순응을 우리는 객체지향 5대 원칙인 리스코프 치환 원칙 이라한다. 즉 슈퍼타입이 A() 라는 메시지의 기능을 수행하면 서브타입도 A() 라는 메시지를 동일한 형태로 수행할 수 있어야한다.
우리는 일반화 특수화 관계를 일반적으로 상속을 이용하여 구현할 수 있다.
상속은 보통 서브타이핑(subtyping) 과 서브클래싱(subclassing) 이렇게 두가지 용도로 사용할 수 있는데,
서브타이핑은 서브클래스가 슈퍼클래스를 대체할 수 있는 경우를 말하며, 보통 설계의 유연성을 확보해야할 때 사용한다. 다른말로 인터페이스 상속(interface inheritance) 라고도 부른다.
반대로 대체할 수 없는 경우를 서브클래싱이라 하는데, 서브클래싱은 코드 중복제거, 재사용을 위한 목적으로 사용한다. 다른 말로 구현 상속(implementation inheritance) 라고도 한다.
상속으로 연결된 클래스들이 수신된 메시지를 처리하는 방식에서 우리는 위임(delegation) 을 알고 있어야 한다. 만약 어떤서브타입의 클래스가 수신한 메시지를 처리하지 못한다면 해당 메시지를 부모클래스로 위임한다. 그 클래스도 역시나 처리할 수 없다면 또 상위 클래스인 부모클래스로 메시지를 위임한다. 이는 메시지를 처리할 수있는 클래스가 메시지를 처리하거나 최상위 부모클래스로 위임될 때까지 계속된다.
집합과 분해
자동차 공장에서 자동차 완성품이 만들어지기 위해서는 엔진, 차체, 조향장치, 변속장치 등 다양한 부품이 합쳐져서 자동차가 만들어진다. 그리고 엔진 역시 세부적인 단위의 부품들로 만들어진다. 차체, 조향장치, 변속장치 모두 마찬가지이다.
작은 단위의 부분으로부터 전체를 만드는 것을 집합이라 하고, 전체를 부분으로 분할하는 것을 분해라고 한다.
집합은 불필요한 세부사항을 추상화할 수 있다. 반대로 필요에 따라 전체를 분해하여 부분들을 세부적으로 다룰 수 있다.
집합은 전체의 내부에 노출이 불필요한 부분들을 감춰야한다. 즉 집합-분해는 추상화를 추구하는 동시에 캡슐화를 지킨다.
우리가 자동차를 탈때 엔진이 어떤식으로 동작하는지, 차체의 구조는 어떤지, 조향장치는 어떤 원리인지 등을 몰라도 차를 운전할 수 있는 것처럼 외부에서는 전체에 대해서만 알고 내부의 세부사항에 대해서는 알지 않아도 된다. (외부에서는 내부의 세부사항을 알고 싶어도 알수도 없다) 그리고 내부의 구성이 변하더라도 외부에 영향을 미치지 않는다. 자동차의 엔젠이 다른 것으로 바뀌더라도, 자동차 탑승자는 전혀 몰라도 되는 것처럼 말이다.
우리는 객체와 객체 사이의 전체-부분 관계를 구현하기 위해 합성 관계를 이용한다.
합성관계에서는 부분을 전체안으로 캡슐화한다. 합성 관계를 이용하면 부분의 객체들의 복잡성이 완화된다. 이러한 복잡성 완화를 위해 우리는 합성관계를 이용하여 그룹을 만든다.
관계는 있지만 전체와 부분 관계가 아닌 단순한 물리적인 연결고리만 존재하는 경우를 연관관계라 부른다.
두가지의 차이는 생명주기와 연관되어 있는데, 합성관계에 있는 객체는 전체의 객체가 사라지면 부분의 객체도 사라지는 반면, 연관관계에 있는 객체는 하나의 객체가 사라지더라도 다른 객체의 생명주기에 영향을 주지 않는다.
즉, 합성관계는 연관관계보다 더 강한 결합도를 지닌다.
합성을 이용해 객체 그룹을 단순화 시키더라도 프로그램이 커지고 각 객체간 의존도가 높아지면 프로그램을 관리하는 것이 굉장히 어려워진다.
이럴때 패키지를 이용해 클래스 집합을 논리적인 단위로 묶는다. 클래스를 논리적인 단위로 묶어 관리하여 복잡도를 낮춘다.
합성관계를 이용해 내부 객체들을 캡슐화시켜 내부구조를 추상화하고, 패키지를 이용해 클래스를 감춰 시스템 구조를 추상화하여 객체지향 시스템의 완성도를 높인다.