Skip to content

Latest commit

 

History

History
208 lines (136 loc) · 8.19 KB

5장.md

File metadata and controls

208 lines (136 loc) · 8.19 KB

5장 객체지향 프로그래밍

객체지향의 핵심

  1. 캡슐화
  2. 상속
  3. 다형성

캡슐화란?

객체지향의 중요한 특징으로 뽑는 이유는 데이터와 함수를 쉽고 효과적으로 캡슐화 하는 방법을 객체지향은 제공하기 때문.

이를 통해 데이터를 응집력 있게 구성된 집단을 서로 구분 짓는 선을 그을 수 있다.

구분선 바깥에 있는 데이터는 은닉되고 함수의 일부만 외부에 노출 된다.

(Java에서는 private와 public으로 구분된다.)

이러한 개념은 OO에서만 국한된 것이 아닌 C에서도 가능하다.

```
c
struct Point;
struct Point* makePoint(double x, double y);
double distance (struct Point *p1, struct Point *p2);

```
```
c
#include "point.h"
#include <stdlib.h>
#include <math.h>

struct Point {
  double x,y;
};

struct Point* makepoint(double x, double y) {
  struct Point* p = malloc(sizeof(struct Point));
  p->x = x;
  p->y = y;
  return p;
}

double distance(struct Point* p1, struct Point* p2) {
  double dx = p1->x - p2->x;
  double dy = p1->y - p2->y;
  return sqrt(dx*dx+dy*dy);
}

```

위 코드에서 x,y 멤버에 접근할 방법이 없으므로 완벽한 캡슐화 이다.

단, C++에서 OO가 등장했고 C에서 지원하던 완벽한 캡슐화가 깨지게 되었다. 헤더에 클래스의 멤버변수를 선언할 것을 요구한다.

캡슐화를 지원하기 위해서는 다음과 같이 바꿔야 한다.

//point.h
class Point {
public:
 	Point(double x, double y);
  double distance(const Point& p) const;

private:
  double x;
  double y;
}

이로서 헤더파일에 선언 된 변수에는 접근할 수 없다. 하지만 존재는 알 수가 있는데 만약에 이 헤더파일을 사용하는 파일에 변수명이 바뀐다면 다시 컴파일 해야한다.

언어에 protected, public, private 키워드를 추가함으로서 부족한 캡슐화를 어느정도 보완하기는 하였지만 java와 C#으로 오면서 헤더와 구현체를 분리하는 방식을 모두 버렸고 이로인해 캡슐화는 더 훼손되게 되었다.

따라서 OO 언어가 캡슐화를 거의 강제하지 않으며 프로그래머가 올바르게 사용한다는 믿음을 기반으로 한다.

상속

OO언어는 캡슐화는 완벽하지 않지만 상속만은 완벽하게 지원한다. 사실 C언어에서도 상속을 지원하지는 않지만 상속을 구현할 수는 있다.

namedPoint.h

struct NamedPoint;

struct NamedPoint* makeNamedPoint(double x, double y, char* name); 
void setName(struct NamedPoint* np, char* name);
char* getName(struct NamedPoint* np);

namedPoint.c

#include "namedPoint.h" #include <stdlib.h>
struct NamedPoint { 
  double x,y;
	char* name;
};

struct NamedPoint* makeNamedPoint(double x, double y, char* name) { 
  struct NamedPoint* p = malloc(sizeof(struct NamedPoint));
	p->x = x;
	p->y = y;
	p->name = name;
	return p; 
}
void setName(struct NamedPoint* np, char* name) { 
  np->name = name;
}

char* getName(struct NamedPoint* np) { 
  return np->name;
}

Main.c

#include "point.h" 
#include "namedPoint.h" 
#include <stdio.h>
int main(int ac, char** av) {
	struct NamedPoint* origin = makeNamedPoint(0.0, 0.0, "origin"); 
  struct NamedPoint* upperRight = makeNamedPoint(1.0, 1.0, "upperRight"); 
  printf("distance=%f\n",distance(
		(struct Point*) origin,
		(struct Point*) upperRight));
  }

데이터 구조가 마치 Point 데이터 구조로부터 파생된 것 "처럼" 동작한다.(이는 선언된 두 변수의 위치가 똑같기 때문)

이런 눈속임 처럼 보이는 방식은 OO가 등장하기 전부터 사용해 왔으며 C++도 실제로 초기에는 이런식으로 단일 상속을 구현했다.

또한, NamedPoint인자를 Point로 강제로 형병환 하는 것도 보이는데 진짜 OO언어에서는 Upcasting을 묵시적으로 지원한다.

따라서 OO는 새로운 개념을 만든것이라기 보다는 데이터 구조에 편하게 가면을 씌우는 정도라고 생각하면 된다.

다형성

OO이전에도 사실 다형성을 표현할 수 있다. C언어를 보자

#include <stdio.h>
void copy() { int c;
	while ((c=getchar()) != EOF) putchar(c);
}

저기 있는 getchar와 putchar가 사실 다형적이다.

getchar와 putchar 함수는 FILE 데이터 구조의 read 포인터가 가리키는 함수를 단순히 호출할 뿐이지만 다형적으로 사용할 수 있다.

이렇게 단순한 기법이 모든 OO가 가진 다형성의 근간이 되며 C++에서는 클래스의 모든 가상함수는 vtable이라는 테이블에 포인터를 가지고 있고, 모든 가상함수 호출은 이 테이블을 거치게 된다. 파생 클래스의 생성자는 생성하려는 객체의 vtable을 단순히 자신의 함수로 덮어쓸 뿐이다.

따라서 폰노인만 아키텍쳐 이후로 다형적 행위를 위해 함수를 가르키는 포인터를 사용했을 뿐 OO에서 새롭게 나온 것은 없다.

하지만 이러한 함수 포인터 방식은 매우 위험하기 때문에 OO언어에서는 이를 안전하게 실수 없이 사용할 수 있도록 도와준다.

다형성이 가진 힘

다형성이 가진 힘은 책에 있는 예제가 아닌 차를 예로 들겠다.

자동차가 가진 특성들을 모아놓은 "엔진"이라는 클래스가 있고 이 클래스를 상속받는 다양한 "차"들은 공통적인 "운전"인터페이스를 가져 차의 종류와는 관계 없이 "장치 독립적으로" 동작할 수 있다. 이를 활용해 "플러그인 아키텍처"가 등장하였다.

의존성 역전

다형성을 안전하게 사용할 수 있기 전에 의존성과 제어 흐름은 매우 수직적이며 동일했다.

![스크린샷 2021-12-29 오후 9.52.33](/Users/gwonjin-u/Library/Application Support/typora-user-images/스크린샷 2021-12-29 오후 9.52.33.png)

main함수가 고수준 함수를 호출하려면 고수준 함수가 포함된 모듈의 이름을 지정해야 했다. C언어의 경우 #include, 자바는 import

따라서, 이전에는 소프트웨어 아키텍쳐에 서택지가 없었다. 시스템의 행위에 따라 제어 흐름이 결졍되었고 의존성도 제어흐름에 의해 결정되었다.

하지만 다형성 덕분에 다른 것을 할 수 있게 되었다.

![스크린샷 2021-12-29 오후 9.55.55](/Users/gwonjin-u/Library/Application Support/typora-user-images/스크린샷 2021-12-29 오후 9.55.55.png)

HL 모듈은 ML1 모듈의 F()함수를 호출하고 소스 코드에서는 HL은 단순히 ML의 F함수를 호출한다.

하지만 ML1의 I인터페이스 사이의 소스 코드 의존성이 제어흐름과는 반대인 것을 알 수 있으며 이를 "의존성 역전" 이라고 부른다.

즉, OO의 다형성을 안전하게 편리하게 제공하기 떄문에 소스코드의 의존성을 어디서든 역전시킬 수 있다는 뜻이다. 이러한 의존성 역전을 사이에 인터페이스를 추가함으로서 방향을 역전 시킬 수 있게 한다.

이를 통해 OO로 개발된 시스템을 다루는 소프트웨어 아키텍트는 시스템의 소스 코드 의존성 전부에 대한 방향을 결정할 수 있는 "절대적인 권한"을 가진다.

이 힘을 통해서 업무 규칙이 데이터베이스와 사용자 인터페이스에 의존하는 대신에, 시스템의 소스 코드 의존성을 반대로 배치하여 데이터베이스와 UI가 업무 규칙에 의존하게 만들 수 있다. 즉, UI와 데이터베이스가 업무 규칙의 플러그인이 된다는 뜻이며, 다시 말해 업무규칙의 소스 코드에서는 UI나 데이터베이스를 호출하지 않는다.

결과적으로 업무 규칙, UI, 데이터베이스는 세 가지로 분리된 컴포넌트 또는 배포 가능한 단위(ex: jar)로 컴파일 할 수 있고, 이 배포단위들의 의존성 역시 소스코드 사이의 의존성과 같다.

컴포넌트 간의 의존성이 깨지게 된다.

![스크린샷 2021-12-29 오후 10.01.37](/Users/gwonjin-u/Library/Application Support/typora-user-images/스크린샷 2021-12-29 오후 10.01.37.png)