Skip to main content

2부. 벽돌부터 시작하기: 프로그래밍 패러다임

3장. 패러다임 개요#

이 장에서는 세 가지 패러다임인 구조적 프로그래밍(structured programming) , 객체지향 프로그래밍(object-oriented programming) , 함수형 프로그래밍(functional programming)

구조적 프로그래밍#

  • 1968년도에 등장
  • if/then/elsedo/while/until 과 같은 익숙한 구조입니다.
  • 구조적 프로그래밍은 제어흐름의 직접적인 전환에 대해 규칙을 부과합니다.

객체 지향 프로그래밍#

  • 1966년도 등장
  • 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 대해 규칙을 부과합니다.

함수형 프로그래밍#

  • 1958년도에 만들어졌으나, 최근에 들어서야 만들어 졌습니다.
  • 함수형 프로그래밍은 할당문에 대해 규칙을 부과합니다.

생각할 거리#

  • 각 패러다임은 프로그래머에게서 권한을 박탈합니다.
    • 즉, 어떤 패러다임도 새로운 권한을 부여하지 않습니다.
  • 위 패러다임은 각각 goto문, 함수 포인터, 할당문을 가져갑니다.
  • 아마도 프로그래밍 패러다임은 앞으로도 딱 세가지만 있을 것으로 예상됩니다.
    • 위 패러다임은 1958년부터 1968년에 나온 것이며 현재까지도 새로운 패러다임은 없습니다.

4장. 구조적 프로그래밍#

증명#

  • 데이크스트라는 증명(proof)라는 수학적 원리를 적용하여 어려운 프로그래밍을 해결할려고 노력했습니다.
    • 공리, 정리. 따름정리, 보조정리로 구성되는 유클리드 계층구조를 만드는 것입니다.
  • 입증된 구조를 이용하고, 이들 구조를 코드와 결합시키며 코드가 올바르다는 사실을 스스로 증명하게 되는 방식입니다.
  • 데이크스트라가 이를 진행하며 goto 문장이 모듈을 더 작게 분해하는 과정에 방해되는 경우가 있다는 사실을 발견하빈다.
  • 이러한 제어 구조는 순차 실행(sequential execution)에 결합했을 때 특별합니다.
    • 모든 프로그램을 순차(sequence), 분기(selection), 반복(iteration) 이라는 세 가지 구조만으로 표현할 수 있습니다.
    • 모듈을 증명 가능하게 하는 바로 그 제어 구조가 모든 프로그램을 만들 수 있는 제어 구조의 최소 집합과 동일하다는 사실입니다.
  • 데이크스트라는 아래를 증명했습니다.
    • 열거법을 이용해 순차 구문(sequential statement)이 올바름을 증명할 수 있다는 사실을 증명합니다.
    • 분기(selection)의 경우, 열거법을 재적용하는 방식으로 처리했습니다.
    • 반복(iteration)은 귀납법으로 증명했습니다.

해로운 성명서#

  • goto문의 해로움은 1968년도에 실렸습니다.
  • 그 후 goto 문장은 없어졋습니다.

기능적 분해#

  • 구조적 프로그래밍을 통해 모듈을 증명 가능한 더 작은 단위로 재귀적으로 분해할 수 있게 되었고, 결국 모듈을 기능적으로 분해할 수 있음을 뜻합니다.
  • 즉, 거대한 문제 기술서를 받더라도 문제를 고수준의 기능들로 분해할 수 있습니다. 그리고 이러한 각 기능은 다시 저수준의 함수들로 분해할 수 있고, 이러한 분해 과정을 끝없이 반복할 수 있습니다.
  • 이를 토대로 구조적 분석(structured analysis)이나 구조적 설계(structured design)와 같은 기법이 1970~1980년대에 인기를 끌었습니다.
  • 프로그래머는 대규모 시스템을 모듈과 컴포넌트로 나눌 수 있고, 더 나아가 모듈과 컴포넌트는 입증할 수 있는 아주 작은 기능들로 세분화할 수 있습니다.

엄밀한 증명은 없었다#

  • 결국 증명은 이루어지지 않았습니다.
  • 현재의 개발자는 엄밀한 증명이 고품질의 소프트웨어를 생산하기 위한 적절한 방법이라고 생각하지 않습니다.
  • 무엇인가를 증명할 때는 유클리드 방식같이 수학적인 증명만이 있는 것이 아닌, 과학적 방법(scientific method)가 있습니다.

과학이 구출하다#

  • 꽈학적 이론과 법칙이 지닌 본성입니다. 즉, 과학적 방법은 반증은 가능하지만 증명은 불가능합니다.
  • 과학은 서술된 내용이 사실임을 증명하는 방식이 아니라 서술이 틀렸음을 증명하는 방식으로 동작합니다.
  • 수학은 증명 가능한 서술이 참임을 입증하는 윈리라면 과학은 증명 가능한 서술이 거짓임을 입증하는 원리입니다.

테스트#

  • "테스트는 버그가 있음을 보여줄 뿐, 버그가 없음을 보여줄 수는 없습니다."
  • 소프트웨어는 일종한 과학과 같습니다.
  • 부정확함에 대한 증명은 입증 가능한 프로그램에만 적용할 수 있습니다.
  • 구조적 프로그래밍은 프로그램을 증명 가능한 세부 집합으로 재귀적으로 분해할 것을 강요합니다.

결론#

  • 구조적 프로그래밍이 오늘날까지 가치 있는 이유는 프로그래밍에서 반증 가능한 단위를 만들어 낼 수 있는 능력 때문입니다.
  • 가장 작은 기능에서부터 가장 큰 컴포턴트에 이르기까지 모든 수준에서 소프트웨어는 과학과 같고, 따라서 반증 가능성에 의해 주도됩니다.
  • 소프트웨어 아키텍트는 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록 만들기 위해 분주히 노력해야 합니다.

5장. 객체 지향 프로그래밍#

  • 좋은 아키텍처를 만드는 일은 객체 지향 설계 원칙을 이해하고 응용하는 데서 출발합니다.
  • 이러한 객체지향의 본질을 설명하기 위해 캡슐화(encapsulation), 상속(inheritance), 다형성(polymorphism) 을 사용합니다.

캡슐화?#

  • 캡슐화를 통해 데이터와 함수가 응집력 있게 구성된 집단을 서로 구분 짓는 선을 그을 수 있습니다.
  • 구분선 바깥에서는 데이터는 은닉되고, 일부 함수만 외부에 노출됩니다.
// point.h
struct Point;
struct Point* makePoint(double x, double y);
double distance (struct Point *p1, struct Point *p2);
// 구현부 point.c 코드는 생략
  • C++ 컴파일러는 기술적인 이유로 클래스의 멤버 변수를 해당 클래스의 헤더 파일에 선언한 것을 요구하면서, 완벽한 캡슐화를 깨졌습니다.
// point.h
class Point {
public:
Point(double x, double y);
double distance(const Point& p) const;
private:
double x;
double y;
}
  • 언어에 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;
}
// 아래 코드 생략
// 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, 0.0, "upperRight");
printf("distance=%f\n", distance((struct Point*) origin, (struct Point*) upperRight));
}
  • 위 코드는 일종의 눈속임처럼 보이지만 단일 송석을 c로 구현한 방법입니다.
  • 다만, 이러한 방법은 상속을 흉내내는 방법이며 상속만큼 편리한 방식은 아닙니다. 또한 다중 상속을 구현하기는 더 어려운 일이 되었습니다.
  • 객체지향언어는 완전히 새로운 개념을 만들지 못했지만, 데이터 구조에 가면을 씌우는 일을 상당히 편리한 방식으로 제공했다고 볼 수 있습니다.

다형성?#

  • 유직스 운영체제의 경우 모든 입출력 장치 드라이버가 열기(open), 닫기(close), 읽기(read), 쓰기(write), 탐색(seek) 가는 다섯 가지 표준 함수를 제공할 것을 요구합니다.
  • 이렇게 단순한 기법은 모든 객체지향이 지닌 다형성의 근간이 됩니다.
  • c++에서는 클래스의 모든 가상 함수(virtual function)은 vtable이라는 테이블에 포인터를 가지고 있고, 모든 가상 함수 호출은 이 테이블을 거치게 됩니다. 그러나 이러한 방법은 매우 위험합니다.
  • 객체지향 언어는 이러한 과례를 없애주며, 실수의 위험을 없애줍니다.

다형성이 가진 힘#

  • 프로그램은 장치 독립적(device independent) 이어야 합니다. 즉, 불필요한 수정이 없습니다.
  • 플러그인 아키텍처(plugin architecture) 은 이처럼 입출력 장치 독립성을 지원하기 위해 만들어졌으며,거의 모든 운영체제에서 구현되었습니다.

의존성 역전#

  • 다형성을 안전하고 편리하기 사용하기 전에는 아래의 구조를 따라 진행했습니다.

소스 코드 의존성 vs 제어 흐름

실선은 소스코드 의존성, 점선은 제어흐름

  • main함수가 고수준 함수를 호출하려면 고수준 함수가 포함된 모듈의 이름을 지정해야하며, 이러한 제약 조건으로 인해 소프트웨어 아키텍트에게 남은 선택지는 없었습니다.
  • 즉, 제어흐름은 시스템의 행위에 따라 결정되며 소스 코드 의존성은 제어흐름에 따라 결정됩니다.

의존성 역전

  • 위의 내용을 보면, ML1과 I 인터페이스 사이의 소슼 코드 의존성(상속 관계)이 제어흐름과는 반대입니다. 이를 의존성 역전(dependency inversion) 이라고 부릅니다.
  • 다형성을 안전하고 편리하게 제공한다는 사실은 소스 코드 의존성을 어디에서든 역전시킬 수 있다는 뜻입니다.
  • 이러한 접근법을 통해 소프트웨어 아키텍트는 시스템의 소스 코드 의존성 전부에 대해 방향을 결정할 수 있는 절대적인 권한을 갖습니다.
    • 소스 코드 의존성이 제어흐름의 방향과 일치되도록 제한되지 않습니다.

데이터베이스와 사용자 인터페이스가 업무 규칙에 의존

  • 즉, UI와 데이터베이스가 업무 규칙의 플러그인이 됩니다.
  • 결과적으로 업무 규칙, UI, 데이터베이스는 세 가지로 분리된 컴포넌트 또는 배포 가능한 단위로 컴파일할 수 있습니다.
    • 이 배포 단위들의 의존성 역시 소스 코드 사이의 의존성과 같습니다.
  • 업무 규칙을 UI와 데이터베이스와는 독립적으로 배포할 수 있습니다.
  • 즉, 특정 컴포넌트의 소스 콪드가 변경되면 해당 코드가 포함된 컴포넌트만 다시 배포하면 뙵니다.
    • 이를 배포 독립성(independent deployability) 라고 합니다.
  • 시스템의 모듈을 독립적으로 배포할 수 있게 되면, 서로 다른 팀에서 각 모듈을 독립적으로 개발할 수 있으며 이를 개발 독립성(independent developability) 입니다.

결론#

소프트웨어 아키텍트 관점에서 정답은 명백합니다. 객체지향은 다형성을 이용하여 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력입니다. 이를 통해 이카텍트는 플러그인 아키텍처를 구척할 수 있고, 이를 통해 고수준의 정책을 포함하는 모듈은 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장할 수 있습니다.


6장. 함수형 프로그래밍#

함수형 프로그래밍은 프로그래밍 그 자체보다 앞서 나왔습니다. 이 패러다임에서 핵심은 람다 계산법입니다.

정수를 제곱하기#

다음의 예시코드를 볼 수 있습니다.

public class Squint {
public static void main(String[] args[]) {
for(int i=0; i<25; i++)
System.out.println(i*i);
}
}

이를 함수형 언어인 클로저를 이용하면 아래와 같이 할 수 있습니다.

(println (take 25 (map (fn [x] (x * x)) (range))))

차이의 핵심은 다음과 같습니다.

  • 자바 프로그램은 가변 변수(mutable variable)을 사용하는데, 가변 변수는 프로그램 실행 중에 상태가 변할 수 있습니다. 그러나 클롲ㅓ 프로그램에서는 가변 변수는 전혀 없습니다.
  • 함수형 언어에서는 변수는 변경되지 않습니다.

불변성과 아키텍처#

  • 경합 조건, 교착상태 조건, 동시 업데이트 문제가 모두 가변 변수로 인해 발생하기 때문에 아키텍처를 고려합니다.
  • 동시성 애플리케이션에서 마주치는 모든 문제, 즉 다수의 스레드와 프로세스를 사용하는 애플리케이션에서 마주치는 모든 문제는 가변 변수가 없다면 생기지 않스빈다.
  • 아키텍트는 동시성(concurrency) 문제에 지대한 관심을 가져야 합니다.
    • 일반적으로 긍정적이지만, 저장 공간과 프로세스의 제한이 있습니다.

가변성의 분리#

  • 불변성과 관련하여 가장 주요한 타협 중 하나는 애플리케이션, 또는 애플리케이션 내부의 서비스를 가변 컴포턴트와 불변 컴포넌트로 분리하는 일입니다.
  • 불변 컴포넌트에서는 순수하게 함수형 방식으로만 작업이 처리되며, 어떤 가변 변수도 사용되지 않습니다.

상태 변경과 트랜잭션 메모리

  • 상태 변경은 컴포넌트를 갖가지 동시성 문제에 노출하는 꼴이므로 흔히 트랜잭션 메모리(transactional memory)와 같은 실천법을 사용하여 동시 업데이트와 경합 조건 문제로부터 가변 변수를 보호합니다.
  • 현명한 아키텍트라면 가능한 많은 처리를 불변 컴포넌트로 옮겨야 하고, 가변 컴포넌트에서는 가능한 많은 코드를 빼야햡니다.

이벤트 소싱#

  • 저장 공간과 처리 능력의 한계는 늘어나고 있습니다.
  • 더 많은 메모리를 확보할수록, 기계가 더 빨라질수록 필요한 가변 상태는 더 적어집니다.
  • 이벤트 소싱(event sourcing) 의 기본 전략은 상태가 아닌 트랜잭션을 저장하자는 전략입니다. 상태가 필요해지면 단순히 상태의 시작점부터 모든 트랜잭션을 처리합니다.
  • 저장 공간과 처리 능력이 충분하다면 애플리케이션이 환전한 불변성을 갖도록 만들 수 있고, 완전한 함수형으로 만들 수 있습니다.

결론#

  • 구조적 프로그래밍은 제어흐름의 직접적인 전환에 부과되는 규율입니다.
  • 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 부과되는 규율입니다.
  • 함수형 프로그래밍은 변수 할당에 부과되는 규율입니다.

소프트웨어는 급격히 발전하는 기술이 아닙니다. 즉, 컴퓨터 프로그램은 순차(sequence), 분기(selection), 반복(iteration), 참조(indirection) 으로 구성됩니다.

Last updated on