5장 객체 지향 프로그래밍

캡슐화?

객체 지향(Object-Oriented) 언어는 데이터와 함수를 효과적으로 캡슐화하는 방법을 제공한다. 클래스는 private을 통해 데이터는 은닉되고 일부 public 함수로 표현된다.

객체 지향 언어가 아니더라도 캡슐화는 존재한다. c언어에선 헤더 파일에 데이터 구조와 함수를 선언하고 구현 파일에서 이를 구현한다.

// point.h
struct Point;
struct Point* makePoint(double x, double y);
double distance (struct Point *p1, struct Point *p2);
// point.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);
}

point.h 를 사용하는 측에선 struct Point의 멤버에 접근할 수 있는 방법이 전혀 없다.

하지만 C++에선 컴파일러가 클래스의 인스턴스 크기를 알아야 하기 때문에 멤버 변수를 클래스 헤더 파일에 선언해야 한다.

class Point {
  public:
  	Point(double x, double y);
	  double distance(const Point& p) const;
  
  private:
  	double x;
  	double y;
}

point.h 를 사용하는 측에서 멤버 변수를 알게되었다. 멤버 변수에 접근하는 것은 컴파일러가 막겠지만 멤버 변수의 이름이 변경될 경우 다시 컴파일 해야한다. public, private, protected 를 통해 보완하긴 했지만 임시 방편일 뿐이다.

객체 지향 언어는 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;
}

NamedPoint는 선언된 변수의 순서가 Point와 동일하기 때문에 Point 데이터 구조로부터 파생된 것 처럼 동작한다. 또한 NamedPoint 타입을 Point 타입으로 변환도 가능하다.

상속과 캐스팅이 기존에도 가능은 했지만 객체 지향 언어는 편리한 방식으로 제공했다. 캡슐화와 마찬가지로 상속 역시 완전히 새로운 개념은 아니라는 것이다.

다형성?

객체 지향 언어 이전에도 다형성을 표현할 수 있었다. C언어의 STDIN, STDOUT은 사실상 자바 형식의 인터페이스이다. 유닉스 운영체제의 경우 모든 입출력 장치 드라이버가 open, close, read, write, seek 표준 함수를 구현해야 한다.

따라서 STDINFILE* 타입으로 선언할 수 있고, getchar()는 다음과 같이 구현할 수 있다.

extern struct FILE* STDIN;

int getchar() {
  return STDIN->read();
}

즉, 함수를 가리키는 포인터를 응용한 것이 다형성이다. 하지만 함수의 포인터를 직접 사용하는 것은 위험하기 때문에 객체 지향 언어는 다형성을 좀 더 안전하고 편리하게 제공한다.

다형성이 가진 힘

위에서 작성한 프로그램에서 새로운 입출력 장치가 생기는 경우 프로그램엔 어떤 변화가 필요한가? 아무런 변경도 필요하지 않다. 입출력 드라이버가 FILE에 정의된 5가지 표준 함수를 구현한다면 얼마든지 사용할 수 있다.

입출력 드라이버가 프로그램의 플러그인이 된 것이다. 1950년대에 입출력 장치가 천공카드에서 자기테이프로 변화하면서 우리는 프로그램이 장치 독립적(device independent)이어야 한다는 사실을 배웠기 때문이다. 즉, 이러한 플러그인 아키텍처(plugin architecture는 입출력 장치 독립성을 위해 만들어졌고 모든 운영체제에서 구현되었다.

기존에는 함수 포인터를 사용해야 하기 때문에 대다수 프로그래머가 이러한 개념을 확장해서 적용하지 않았는데, 객체 지향 언어의 등장으로 어디서든 플러그인 아키텍처를 적용할 수 있게 되었다.

의존성 역전

객체 지향 언어가 다형성을 지원하기 전 까지는 프로그램의 호출 트리에서 소스 코드의 의존성 방향이 제어 흐름과 일치했다. main 함수가 고수준 함수를 호출하려면 고수준 함수가 포함된 모듈의 이름을 지정해야만(의존해야만) 했다.

이러한 제약조건으로 인해 제어흐름은 시스템의 행위에 따라 결정되고 소스 코드의 의존성은 제어흐름에 따라 결정됐다. 하지만 다형성으로 인해 소스 코드는 인터페이스를 참조하고 런타임엔 해당 모듈의 함수를 호출할 수 있게 되었다. 이것을 의존성 역전(dependency inversion) 이라고 한다.

객체 지향 언어가 다형성을 안전하고 편리하게 제공해주기 때문에 소스코드의 의존성을 어디서든 역전시킬 수 있다. 이것이 객체 지향 언어의 힘이고 객체 지향 언어가 지향하는 것이다.

이제는 UI와 데이터베이스가 비즈니스 규칙의 플러그인이 된다는 뜻이다. 따라서 비즈니스 규칙은 UI, 데이터베이스와 독립적으로 배포할 수 있다. UI, 데이터베이스의 변경사항이 비즈니스 규칙에 영향을 미치지 않는다.

UI, 비즈니스 규칙, 데이터베이스라는 각 컴포넌트는 개별적이도 독립적으로 배포가 가능해졌다. 이것이 바로 배포 독립성(independenct deployability)이다. 모듈을 독립적으로 배포할 수 있게 되면 서로 다른 팀에서 독립적으로 개발할 수 있다. 이것이 개발 독립성(independent developability)이다.

결론

  • 객체 지향이란 다형성을 이용해 모든 소스 코드에 대한 제어 권한을 획득할 수 있는 능력이다.

  • 객체 지향을 이용하면 플러그인 아키텍처를 구성할 수 있다.

  • 고수준 모듈은 저수준 세부사항 모듈에 대해 독립을 유지할 수 있다.

  • 저수준 모듈은 고수준 모듈과 독립적으로 개발하고 배포할 수 있다.

Last updated