실용주의 프로그래머 4. 실용주의 편집증

jiffydev·2021년 8월 2일
0

1. 계약에 의한 설계

소프트웨어 모듈이 서로 소통하는 것을 돕기 위해 만들어진 것이 계약에 의한 설계(DBC)이다.
계약은 선행조건, 후행조건, 클래스 불변식으로 구성되어 있는데, 모든 선행조건을 충족한다면 종료시 모든 후행조건과 불변식이 참이 될 것을 보증해야 한다는 내용이다.

계약의 경우 게으른 코드를 작성해야 한다. 자신이 수용해야 할 것에 대해서는 엄격하게 평가하고, 내어줄 것에 대해서는 최소한도를 약속하는 것이다.
또한 객체지향 언어에서 계약을 사용해 클래스가 올바르게 구현되었는지 확인할 수 있다.

DBC를 지원하지 않는 언어에서도 계약을 코드에 제한적으로나마 넣을 수 있다.
단정문(assertion)을 사용하면 컴파일러를 통해 계약을 검사할 수 있지만 기반 클래스 메서드를 재정의하면 단정문이 제대로 호출되지 않는다는 문제와, 메서드 진입 시점에 존재했던 값을 별도로 추가해야 한다는 점, 런타임 시스템과 라이브러리가 계약을 지원하도록 설계되지 않았다는 점 때문에 한계가 존재한다.

단정문 외에는 각 언어에서 제공하는 전처리기를 통해 주석을 가지고 계약을 처리할 수 있다.
비록 내장 기능보다 성능은 떨어지지만 찾아내지 못했을 문제를 발견하는데 사용할 수 있다.

앞에서 클래스 내 모든 메서드에 적용되는 불변식에 대해 언급했지만, 그 외에도 불변식을 사용하는 방법들이 있다.
루프 불변식의 경우, 루프 시작 전과 진행 중 매 반복마다 적용된다. 따라서 루프 불변식을 하나의 축소된 계약으로 사용할 수 있다.
의미론적 불변식은 불변의 요구사항을 표현하는데 사용된다. 의미론적 불변식은 일시적인 정책에 영향을 받지 않는다.

2. 죽은 프로그램은 거짓말을 하지 않는다

망가진 프로그램이 계속 돌아다니게 하는 것보다 빨리 멈추게 하는 것이 피해를 최소화할 수 있는 길이다.

3. 단정적 프로그래밍

프로그래밍에서 '이런 일은 절대 일어날 리 없어'라는 믿음은 항상 틀린 믿음이다.
내가 작성한 코드에서 그런 생각이 든다면 진짜 그럴지 확인하는 코드를 추가해야 한다.
이는 단정문을 통해 확인할 수 있는데, 몇 가지 주의할 점이 있다.
먼저 실행되어야만 하는 코드는 절대 assert 안에 넣어서는 안된다. 또한 진짜 에러처리 대신으로 단정문을 사용해서는 안된다.

4. 언제 예외를 사용할까

예외를 사용하면 코드의 흐름을 명확하게 파악할 수 있다.
하지만 문제가 되는 것은 언제 예외를 사용해야 할지이다.
예외는 의외의 상황을 위한 것이다. 만약 '모든 예외 처리기를 제거해도 코드가 실행될까'에 대한 대답이 '아니오'라면 예외가 잘못 사용된 것이다.

5. 리소스 사용의 균형

리소스를 할당하면 사용하고 해제해야 한다. 이 때 할당과 해제를 일관되게 다루는 것이 필요한데, 결국은 리소스를 할당하는 객체가 리소스를 해제해야 한다는 것을 뜻한다.

void readCustomer(const char *fName, Customer *cRec) {
  cFile = fopen(fName, "r+");
  fread(cRec, sizeof(*cRec), 1, cFile);
}

void writeCustomer(Customer *cRec) {
  rewind(cFile);
  fwrite(cRec, sizeof(*cRec), 1, cFile);
  fclose(cFile);
}

void updateCustomer(const char *fName, double newBalance) {
  Customer cRec;
  readCustomer(fName, &cRec);
  cRec.balance = newBalance;
  writeCustomer(&cRec);
}

위 코드가 수행하는 일은 간단하다. 레코드를 읽고 잔액을 업데이트한 후 레코드를 재기록한다.
하지만 자세히 보면 readCustomerwriteCustomer는 긴밀히 결합되어 있는 상태이다.
만약 updateCustomer에서 잔액이 음수가 아닐때만 업데이트 되도록 수정한다면 다음과 같을 것이다.

void updateCustomer(const char *fName, double newBalance) {
  Customer cRec;
  readCustomer(fName, &cRec);
  if (newBalance >= 0.0) {
    cRec.balance = newBalance;
    writeCustomer(&cRec);
  }
}

수정된 코드가 괜찮아 보일지 모르지만, 계속 실행하다 보면 어느 순간 파일이 너무 많이 열려 있게 된다. 이는 조건문으로 인해 writeCustomer가 호출되지 않는 경우가 있기 때문이다.
이를 수정한다고 else문만 추가해주면, 결국 세 개의 루틴 모두가 결합되는 최악의 상황이 발생한다.

결국 필요한 것은 위에서 언급한 '리소스를 할당하는 객체가 리소스를 해제해야' 하는 것이다.
그래서 위의 코드를 리팩토링하면 다음과 같다.

void readCustomer(FILE *cFile, Customer *cRec) {
  fread(cRec, sizeof(*cRec), 1, cFile);
}

void writeCustomer(FILE *cFile, Customer *cRec) {
  rewind(cFile);
  fwrite(cRec, sizeof(*cRec), 1, cFile);
}

void updateCustomer(const char *fName, double newBalance) {
  FILE *cFile;
  Customer cRec;
  cFile = fopen(fName, "r+");
  readCustomer(cFile, &cRec);
  if (newBalance >= 0.0) {
    cRec.balance = newBalance;
    writeCustomer(cFile, &cRec);
  }
  fclose(cFile);
}

파일에 대한 모든 책임을 updateCustomer가 갖고 파일을 열고 닫는 것도 여기서 행해진다.
전역변수도 제거될 수 있다.

한 번에 하나 이상의 리소스를 필요로 하는 경우에는
1. 리소스를 할당한 순서의 반대로 해제해야 다른 리소스를 참조할 때도 리소르를 고아로 만들지 않는다.
2. 코드의 여러 곳에서 동일한 리소스 집합을 할당한다면 할당 순서를 언제나 같게 해야 교착 가능성이 줄어든다.

하지만 리소스 사용의 균형을 잡을 수 없는 경우도 있다. 보통 동적 자료 구조형을 사용하는 프로그램에서 발생하는데, 이 때는 메모리 할당에 대한 의미론적인 불변식을 정함으로써 자료에 대해 책임을 지는게 누구인지 정해야 한다.

profile
잘 & 열심히 살고싶은 개발자

0개의 댓글