Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

부록 A: 추상화 기법 #205

Closed
Tracked by #40
fkdl0048 opened this issue Jan 10, 2024 · 0 comments
Closed
Tracked by #40

부록 A: 추상화 기법 #205

fkdl0048 opened this issue Jan 10, 2024 · 0 comments

Comments

@fkdl0048
Copy link
Owner

fkdl0048 commented Jan 10, 2024

부록 A

추상화 기법

추상화는 도메인의 복잡성을 단순화하고 직관적인 모델을 만드는 데 사용할 수 있는 가장 기본적인 인지 수단이다.

사람들은 도메인에 존재하는 개념들을 구조화하고 단순화하기 위해 다양한 추상화 기법을 사용한다.

특성을 공유하는 객체들을 동일한 타입으로 분류하는 것은 객체지향 패러다임에서 사용하는 추상화 기법의 한 예다.

다음은 사람들이 세계를 이해하는 데 사용하는 중요한 추상화 기법의 종류를 나타낸 것이다.

각 추상화 기법은 복잡성을 낮추기 위해 사물의 특정한 측면을 감춘다.

  • 분류와 인스턴스화: 분류는 객체의 구체적인 세부 사항을 숨기고 인스턴스 간에 공유하는 공통적인 특성을 기반으로 범주를 형성하는 과정이다. 분류의 역은 범주로부터 객체를 생성하는 인스턴스화 과정이다.
  • 일반화와 특수화: 일반화는 범주 사이의 차이를 숨기고 범주 간에 공유하는 공통적인 특성을 강조한다. 일반화의 역을 특수화라고 한다.
  • 집합과 분해: 집합은 부분과 관련된 세부 사항을 숨기고 부분을 사용해서 전체를 형성하는 과정을 가리킨다. 집합의 반대 과정은 전체를 부분으로 분리하는 분해 과정이다.

image

객체지향의 가장 큰 장점은 동일한 추상화 기법을 프로그램의 분석, 설계, 구현 단계에 걸쳐 일관성 있게 적용할 수 있다는 점이다.

분류와 인스턴스화

개념과 범주

사람들은 공통점을 바탕으로 서로 다른 객체에 대해 하나의 개념으로 분류할 수 있다. (자동차라는 개념)

객체를 분류하고 범주로 묶는 것은 객체들의 특정 집합에 공통의 개념을 적용하는 것을 의미한다.

개념이란 속성과 행위가 유사한 객체에 공통적으로 적용되는 관념이나 아이디어다.

객체들을 공통적인 특성을 기반으로 범주로 묶고 개념을 적용하는 것은 범주라는 정신적인 렌즈를 통해 세상을 바라보는 것과 유사하다.

세상에 존재하는 객체에 개념을 적용하는 과정을 분류라고 한다.

분류는 객체를 특정한 개념을 나타내는 집합의 구성 요소로 포함시킨다.

같은 범주로 묶음으로써 세상에 존재하는 복잡성을 낮출 수 있다.

사람들은 분류를 통해 개별 현상을 하나의 개념으로 다룬다. (이데아)

이때 '수많은 개별적인 현상들'을 객체라고 하고, '하나의 개념'을 타입이라고 한다.

다시 말해서 분류는 객체를 타입과 연관시키는 것이다.

분류의 역은 타입에 해당하는 객체를 생성하는 과정으로 인스턴스화 또는 예시라고 한다.

객체지향의 세계에서 개념을 가리키는 표준 용어는 타입이다.

따라서 타입은 개념과 동의어이며 속성과 행위가 유사한 객체에 공통적으로 적용되는 관념이나 아이디어를 의미한다.

이런 관점에서 분류란 객체들을 동일한 타입 또는 범주로 묶는 과정을 의미하므로 객체를 타입의 인스턴스라고 한다.

요약하자면 분류는 객체와 타입 간의 관계를 나타낸 것이다.

어떤 객체가 타입의 정의에 부합할 경우 그 객체는 해당 타입으로 분류되며 자동으로 타입의 인스턴스가 된다.

타입

객체를 타입에 따라 분류하기 위해서는 객체가 타입에 속하는지 여부를 확인할 수 있어야 한다.

어떤 객체의 타입을 말할 수 있으려면 명확한 정의가 필요하다.

  • 심볼: 타입을 가리키는 간략한 이름이나 명칭
  • 내연: 타입의 완전한 정의, 내연의 의미를 이용해 객체가 타입에 속하는지 여부를 확인할 수 있다.
  • 외연: 타입에 속하는 모든 객체들의 집합

image

만약 어떤 객체가 내연을 만족한다면 그 객체는 우측 하단에 표시된 자동차 집합인 외연의 한 원소로 포함된다.

자동차라는 심볼은 내연과 외연에 모호하지 않은 이름을 붙임으로써 타입을 쉽게 참조하고 커뮤니케이션할 수 있게 한다.

이처럼 도메인을 분석하는 동안 이름과 의미, 객체들의 집합을 이용해 개념을 정의할 수 있다.

외연과 집합

타입의 외연은 타입에 속하는 객체들의 집합으로 표현한다. (집합 == 외연)

객체들은 동시에 서로 다른 집합에 포함될 수도 있다.

한 객체가 한 시점에 하나의 타입에만 속하는 것을 단일 분류라고 한다면, 한 객체가 한 시점에 여러 타입에 속할 경우 이를 다중 분류라고 한다.

대부분의 객체지향 프로그래밍 언어들은 단일 분류만을 지원한다. (C#은 단일, C++은 다중)

객체를 특정한 타입으로 분류하면 해당 객체는 타입의 집합에 포함된다.

만약 객체가 타입을 변경할 수 있다면 어떻게 될까?

객체가 한 집합에서 다른 집합의 원소로 자신이 속하는 타입을 변경할 수 있는 경우 이를 동적 분류라고 한다.

객체가 자신의 타입을 변경할 수 없는 경우 이를 정적 분류라고 한다.

컴퓨터는 교육용 컴퓨터인 동시에 사무용 컴퓨터로도 사용될 수 있다.

다중 분류와 동적 분류는 서로 배타적인 개념이 아니다.

개념적인 관점에서 다중 분류와 동적 분류를 함께 적용하는 것이 실세계의 복잡성을 모델링하는 데 유용하다.

클래스 기반의 객체지향 언어에서 타입은 클래스를 이용해서 구현된다.

대부분의 언어는 일단 클래스로부터 인스턴스를 생성한 후 클래스를 변경할 수 있는 방법을 제공하지 않는다.

즉, 객체의 타입을 변경할 수 없다.

다중 분류와 동적 분류는 개념적인 관점에서 도메인을 분석하는 데는 유용하지만 객체지향 프로그래밍 언어의 제약으로 인해 이를 구현으로 옮기기는 쉽지 않다.

개인적인 경험에 따르면 다중 분류와 동적 분류관점에서 도메인 모델의 초안을 만든 후 실제 구현에 적합하도록 단일 분류와 정적 분류 방식으로 객체들의 범주를 조정하는 편이 분석과 구현 간의 차이를 메울 수 있는 가장 현실적인 방법이다.

디자인 탬플릿을 사용하여 유연성을 강화할 수 있지만 단순함을 위해서는 항상 단일 분류와 정적 분류를 선택하는 것이 현명하다. (필요할 때만 사용)

클래스

객체지향 프로그래밍 언어를 이용해 타입을 구현하는 가장 보편적인 방법은 클래스를 이용하는 것이다. (타입 != 클래스)

클래스는 타입을 구현하는 용도 외에도 코드를 재사용하는 용도로 사용되기도 한다.

클래스 외에도 인스턴스를 생성할 수 없는 추상 클래스나 인터페이스를 이용해 타입을 구현할 수도 있다.

현재의 객체지향 패러다임은 아리스토텔레스의 분류법의 근간을 형성하는 아이디어를 기반으로 한다.

만약 객체들이 동일한 특성을 가진다면 그것들은 동일한 카테고리에 속한다.

따라서 객체들의 카테고리는 객체들이 공유하는 공통적인 특성에 의해 정의된다.

또한 아리스토텔레스는 객체의 특성을 본질적인 속성우연적인 속성으로 분류했다.

본질이란 한 사물의 가장 핵심적이고 필수불가결한 속성이다.

본질적이지 않은 속성을 우연적 속성이라고 한다.

한 사람이 취직하여 회사원이 되었더라도 그 사람은 여전히 사람일 뿐이다.

클래스는 객체가 공유하는 본질적인 속성을 정의한다.

대부분의 객체지향 프로그래밍 언어에서 동일한 범주에 속하는 객체는 동일한 클래스의 인스턴스여야 한다.

대부분의 객체지향 언어는 본질적인 속성을 표현할 수 있지만 우연적인 속성은 표현할 수 없다.

따라서 동일한 범주에 속하는 객체는 모두 동일한 속성을 가져야만 한다.

일반화와 특수화

범주의 계층

린네의 계층 구조는 좀 더 세부적인 범주가 계층의 하위에 위치하고 좀 더 일반적인 범주가 계층에 상위에 위치한다.

이때 계층의 상위에 위치한 범주를 계층의 하위에 위치한 범주의 일반화라고 하고, 계층의 하위에 위차한 범주는 계층의 상위에 위치한 범주의 특수화라고 한다.

서브 타입

객체지향 세계에서 범주는 개념을 의미하고, 개념은 타입을 의미하므로 일반화와 특수화는 계층 구조 안에 존재하는 타입 간의 관계를 의미한다.

따라서 좀 더 일반적인 타입을 이용해 더욱 세부적인 타입을 정의함으로써 타입 간의 계층 구조를 구축할 수 있다.

어떤 타입이 다른 타입보다 일반적이라면 이 타입을 슈퍼타입이라고 한다.

반대로 다른 타입보다 특수하다면 이를 서브타입이라고 한다.

슈퍼타입은 서브타입의 일반화이고 서브타입은 슈퍼타입의 특수화다.

새로운 범주의 속성은 자신이 정의한 본질적인 속성에 기존 범주의 본질적인 속성을 추가한 것이다.

서브타입은 슈퍼타입의 본질적인 속성을 모두 포함하기 때문에 계층에 속하는 모든 서브타입들이 슈퍼타입의 속성을 공유한다는 것을 쉽게 예상할 수 있다.

이러한 사실을 통해 복잡한 사실에 대한 논리적 추론이 가능해지게 된다. (파편화 된 사실을 모르더라도)

크레이그 라만은 어떤 타입이 다른 타입의 서브타입이 되기 위해서는 '100% 규칙'과 'is-a 규칙'을 준수해야 한다고 말한다.

  • 100% 규칙: 슈퍼타입의 정의가 100% 서브타입에 적용돼야만 한다. 서브 타입의 속성과 연관관계 면에서 슈퍼타입과 100%일치해야 한다.
  • is-a 규칙: 서브타입의 모든 인스턴스는 슈퍼타입 집합에 포함돼야 한다. 이는 대게 영어로 서브타입은 슈퍼타입이다라는 구문을 만듦으로써 테스트할 수 있다.
이러한 상속 관계가 코드에서 표현하기 어려운 이유는 아마 완벽한 슈퍼타입을 설계하기 어려움에서 오는 것 같다.

실세계는 규칙적이고 말에 의해서 변경될 수 있지만 코드는 불확실하고 슈퍼타입의 변경이 서브타입에 영향을 많이 주기 때문에 Depth가 깊어질수록 100%의 관계나 is-a 관계를 만족시키기 어려워진다.

상속

프로그래밍 언어를 이용해 일반화 특수화 관계를 구햔하는 가장 일반적인 방법은 클래스간의 상속을 사용하는 것이다.

그러나 모든 상속 관계가 일반화 관계인 것은 아니다.

일반화의 원칙은 한 타입이 다른 타입의 서브타입이 되기 위해서는 슈퍼타입에 순응해야 한다는 것이다.

순응에는 구조적인 순응과 행위적인 순응이 있다.

두 가지 모두 특정 기대 집합에 대해 서브타입의 슈퍼타입에 대한 대체 가능성을 의미한다.

구조적인 순응의 경우 기대 집합은 속성과 연관관계에 관한 것이며, 행위적인 순응의 경우 기대 집합은 행위가 동일한 계약을 기반으로 하느냐에 관한 것이다.

구조적인 순응은 타입의 내연과 100% 규칙을 의미한다.

즉, 서브타입은 슈퍼타입이 가지고 있는 속성과 연관관계 면에서 100% 일치해야 한다.

행위적인 순응은 타입의 행위에 관한 것이며, 서브타입은 슈퍼타입을 행위적으로 대체 가능해야 한다.

행위적인 순응을 흔히 리스코프 치환 원칙(Liskov Substitution Principle, LSP)이라고 한다.

상속의 또 다른 용도는 코드 중복을 방지하고 공통 코드를 재사용하기 위한 언어적 메커니즘을 제공하는 것이다.

만약 한 클래스가 상속한다면 타입은 부모 클래스의 데이터와 메서드를 사용하고, 수정하고, 확장할 수 있다.

상속은 서브타이핑서브클래싱의 두 가지 용도로 사용될 수 있다.

서브 클래스가 슈퍼 클래스로 대체할 수 있는 경우 이를 서브타이핑이라고 한다.

서브클래스가 슈퍼클래스를 대체할 수 없는 경우에는 서브클래싱이라고 한다.

서브타이핑은 설계의 유연성이 목표인 반면 서브클래싱은 코드의 중복 제거와 재사용이 목적이다.

흔히 서브타이핑을 인터페이스 상속이라고 하고, 서브클래싱을 구현 상속이라고 한다.

클래스가 다른 클래스를 상속받았다는 사실만으로 두 클래스 간의 관계가 서브타이핑인지, 서브클래싱인지 여부를 결정할 수는 없다.

서브타이핑의 전제 조건은 대체 가능성이기 때문에 서브타이핑인지 여부를 확인하려면 클라이언트 관점에서 실제로 어떻게 사용되고 있는지를 확인해야 한다.

요약하면 일반화를 위한 서브타이핑은 특정 기대 집합에 대한 서브타입과 슈퍼타입 간의 구조적, 또는 행위적 순응 관계를 의미하며, 대체 가능성을 내포한다.

가능한 모든 상속관계가 서브타이핑의 대체 가능성을 준수하도록 주의 깊게 사용하는 것은 코드를 유연하게 만들고 재사용성을 높이는 한 가지 방법이다.

여러 클래스로 구성된 상속 계층에서 수신된 메시지를 이해하는 기본적인 방법은 클래스 간의 위임(delegation)을 사용하는 것이다.

어떤 객체의 클래스가 수신된 메시지를 이해할 수 없다면 메시지를 부모 클래스로 위임한다. (반복)

집합과 분해

계층적인 복잡성

호라와 템프스의 우화에서 얻을 수 있는 교훈은 다음과 같다.

  • 복잡성은 '계층'의 형태를 띈다.
  • 단순한 형태로부터 복잡한 형태로 진화하는 데 걸리는 시간은 그 사이에 존재하는 '안정적인 형태'의 수와 분포에 의존한다.

작은 부품으로부터 큰 부품을 만들어내는 과정은 연쇄적으로 이뤄지며 계층적인 형태를 가진다.

이와 같은 안정적인 형태의 부분으로부터 전체를 구축하는 행위를 집합이라고 하고 집합과 반대로 전체를 부분으로 분할하는 행위를 분해라고 한다.

집합의 가치는 많은 수의 사물들의 형상을 하나의 단위로 다룸으로써 복잡성을 줄일 수 있다는 데 있다.

집합은 불필요한 세부 사항을 배제하고 큰 그림에서 대상을 다룰 수 있게 한다.

즉, 불필요한 세부 사항을 추상화한다.

그러나 필요한 시점에는 전체를 분해함으로써 그 안에 포함된 부분들을 새로운 전체로 다룰 수 있다.

전체와 부분 간의 일관된 계층 구조는 재귀적인 설계를 가능하게 한다.

집합은 전체의 내부로 불필요한 세부 사항을 감춰주기 때문에 추상화 메커니즘인 동시에 캡슐화 메커니즘이다.

외부에서는 전체에 관해서만 알고 있고 내부의 세부 사항에 대해서는 알지 못하기 때문에 내부의 구성을 변경하더라도 외부에 영향을 미치지 않는다.

집합의 경계가 시계처럼 물리적으로 명확한 경우도 있지만 모호한 경우도 많다.

그럼에도 인간은 집합의 경계를 결정하는 데 큰 어려움을 느끼지 않는데 인간이 본능적으로 세계를 안과 밖으로 세계를 안과 밖 지향성을 가진 그릇으로 보기 때문이다.

인간은 실제로 경계가 존재하지 않는 곳에서도 쉽게 추상적인 경계를 찾는다.

합성 관계

상품 주문을 생각해보면 여러 상품을 한 번에 주문할 수 있다.

이때 각 상품을 몇 개 주문했는지를 가리켜 주문 항목이라고 한다.

각 주문 항목은 주문과 독립적으로 준재할 수 없다.

주문 항목은 반드시 어떤 한 주문의 일부로 생성되기 때문에 주문의 일부여야 한다.

객체와 객체 사이의 전체-부분 관계를 구현하기 위해서는 합성 관계를 사용한다. (has-a)

합성 관계는 부분을 전체 안에 캡슐화함으로써 인지 과부화를 방지한다.

주문 항목은 주문의 일부이므로 이 모델을 다루는 사람은 주문 항목과 관련된 세부 사항은 무시하고 주문과 상품만이 존재하는 것처럼 모델을 다룰 수 있다.

필요하다면 주문 내부로 들어가 주문 항목과 관련된 세부 사항을 확인할 수 있다.

주문 내부의 세부 사항을 다루는 동안에는 주문 외부의 상품에 대해서는 신경 쓰지 않아도 무방하다.

따라서 객체들의 그룹과 관련된 복잡성이 완화된다.

상품과 주문 항목 사이에도 관계가 존재하지만 상품은 주문 항목의 일부가 아니다.

따라서 주문과 주문 항목 사이의 관계는 전체와 부분 간의 관계를 나타내는 합성 관계인데 비해 주문 항목과 상품 간에는 단순한 물리적 통로가 존재한다는 사실만 나타낸다.

이를 연관 관계라고 한다.

합성 관계로 연결된 객체는 포함하는 객체가 제거될 때 내부에 포함된 객체도 함께 제거된다. (주문이 존재하지 않는 주문 항목은 의미가 없기 때문이다.)

이에 반해 연관 관계로 연결된 두 객체는 생명주기와 관련된 어떤 제약도 부과하지 않는다. (독립적으로 제거 가능)

합성 관계는 생명주기 측면에서 연관 관계보다 더 강하게 객체들을 결합한다.

패키지

비록 합성 관계를 이용해 커다란 객체 그룹을 단순화하더라도 클래스의 수가 많아지면 많아질수록 얽히고 설킨 클래스 간의 의존성을 관리하는 일은 악몽으로 변해 간다.

복잡한 클래스의 미로 속에서 길을 잃고 헤매지 않으려면 구조에 관한 큰 그림을 안내해줄 지도가 필요하다.

지도는 소프트웨어의 전체적인 구조를 쉽게 이해할 수 있게 적절한 높이와 적절한 축척으로 소프트웨어를 표현할 수 있어야 한다. (균형트리처럼)

소프트웨어는 물리적 형체가 존재하지 않기 때문에 구조를 단순화하기 위해서는 서로 관련성이 높은 클래스 집합을 논리적인 단위로 통합해야 한다.

이처럼 상공에서 바라본 소프트웨어의 전체적인 구조를 표현하기 위해 관련된 클래스 집합을 하나의 논리적인 단위로 묶는 구성 요소를 패키지또는 모듈이라고 한다.

패키지를 이용하면 시스템의 전체적인 구조를 이해하기 위해 한 번에 고려해야 하는 요소의 수를 줄일 수 있다.

또한 개별 클래스가 아닌 클래스의 집합을 캡슐화함으로써 전체적인 복잡도를 낮출 수 있다.

함께 협력하는 응집도 높은 클래스 집합을 하나의 패키지 내부로 모으면 코드를 이해하기 위해 패키지 경계를 넘나들 필요가 적어진다.

C#에서는 namespace

@fkdl0048 fkdl0048 self-assigned this Jan 10, 2024
@fkdl0048 fkdl0048 added this to Todo Jan 10, 2024
@github-project-automation github-project-automation bot moved this to Todo in Todo Jan 10, 2024
@fkdl0048 fkdl0048 moved this from Todo to Two-Week Plan in Todo Jan 10, 2024
@fkdl0048 fkdl0048 moved this from Two-Week Plan to In Progress in Todo Jan 13, 2024
@github-project-automation github-project-automation bot moved this from In Progress to Done in Todo Jan 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

No branches or pull requests

1 participant