[Effective C++] 18. 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자

Effective C++ 제 3판 - Chapter 4 - 1


새로운 타입을 들여와 인터페이스를 강화하자


날짜를 나타내는 어떤 클래스에 넣을 생성자를 설계하고 있다고 가정합시다.

1
2
3
4
5
6
class Date
{
public:
  Date(int month, int day, int year);
  ...
};

첫 인상이 나쁘지 않지만, 사용자가 실수를 저지를 구멍이 적어도 2개는 있습니다.

  • 매개 변수의 순서가 잘못될 여지가 있다.
  • 월과 일에 해당하는 숫자가 논리상 맞지 않을 수 있다.

소제목에서 알 수 있듯이, 새로운 타입을 들여와 인터페이스를 강화하면 상당수의 사용자 실수를 막을 수 있습니다. 월, 연, 일을 구분하는 간단한 Wrapper Type을 각각 만들고 이 타입을 Date 생성자 안에 둘 수 있을 것 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Day
{
  explicit Day(int d); // 사용자가 직접 형변환을 해주어야
    : val(d) {}        // 형변환이 가능하도록 하는 키워드 explicit
  int val;
};

struct Month
{
  explicit Month(int m);
    : val(m) {} 
  int val;
}

struct Year
{
  explicit Year(int y);
    : val(y) {} 
  int val;
}


추가로 각 타입의 값에 제약을 줄 수도 있습니다. 다음의 예제를 봅시다.

1
2
3
4
5
6
7
8
9
10
class Month
{
public:
  static Month Jan() { return Month(1) };
  static Month Feb() { return Month(2) };
...
...
  static Month Dec() { return Month(12) };
...
}

자 여기서 잠깐 질문!, static 비지역 정적 변수를 왜 사용하지 않았을까요?, 우리는 이미 답을 알고 있습니다. 비지역 정적 객체의 초기화는 번역 단위 밖에서는 순서를 보장할 수 없습니다. 기억이 나지 않으시면 항목4를 다시 한번 확인해보세요. ^_^

일관성 있는 인터페이스를 제공하라


예상 되는 사용자의 실수를 막는 방법으로는 타입에 제약을 부여하는 방법이 있습니다. const 붙이기가 바로 그것이지요. 항목3 에 정리가 잘 되어있습니다.

1
if ( a * b = c) ...

만약 위의 operator*의 반환 값이 const가 아니었다면요?(끔찍하군요) 여러분이 만든 operator*에 const 제한자를 붙이는 것은 더 나아가 STL 컨테이너와 기본 타입과의 일관성까지도 제공하는 셈이 되는 것이지요.

사용자의 암기력에 의존하지 마라


사용자가 인터페이스를 사용함에 있어서 지켜야할 규칙을 모두 암기하고 있다면 그것은 좋은 인터페이스 설계라고 할 수 없습니다. 사용자는 언제라도 규칙을 어길 가능성이 있습니다.

다음은 객체를 할당하고 포인터를 반환하는 팩터리 메서드 입니다.

1
Investment* createInvestment();

위의 함수를 사용할 때는, 자원 누출을 피하기 위해 반환 받은 포인터를 나중에라도 해제 해주어야 합니다. 당연히 사용자는 해제하는 것을 까먹을 수 있습니다. 어떻게 하는 것이 좋을까요? 우리는 스마트 포인터를 이용할 수 있습니다. 아래의 예를 봅시다.

1
std::shared_ptr<Investment> createInvestment();

이렇게 해두면 사용자는 shared_ptr을 이용해서 반환 값을 받을 수 밖에 없겠지요. 앞에서도 공부했듯이 shared_ptr에는 custom deleter를 지원합니다. 그렇다면 이번에는 이런 가정을 해봅시다.

사용자는 포인터에 대한 자원의 해제를 getRidOfInvestment라는 이름의 함수를 사용해야만 하는 상황이라면 어떨까요?(단순 메모리 해제가 아니라 무언가 작업을 더 해주어야만 하는 상황이겠지요)

자 다음의 코드를 보면 쉽게 이해하실 수 있을 겁니다.

1
2
3
4
5
6
7
8
std::shared_ptr<Investment> createInvestment()
{
  std::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
  retVal = ...;
  ...
  ...
  return retVal;
}

그 밖의 std::shared_ptr의 특징


앞서 저는 effective c++을 공부하면서, 현재의 modern c++과는 다른 내용에 대해서는 과감히 빼고 새로운 내용을 넣겠다고 했었습니다.

책에서는 std::tr1::shared_ptr에 대해 설명하고 있는데요. 저는 std::shared_ptr에 대해 설명하도록 하겠습니다. 먼저 std::shared_ptr은 cross-DLL problem에 대해 안전합니다. cross-DLL problem 즉 교차 DLL 문제는 2개 이상의 동적 링크 라이브러리를 사용하는 상황에서 다른 한쪽의 new를 사용했을 때, 나머지 한쪽에 정의된 delete를 사용하여 할당한 자원을 해제할 때 발생하는 런타임 에러를 지칭하는데요. std::shared_ptr을 사용하면 아주 손쉽게 해결이 가능합니다.

또 하나 책에서는 boost의 shared_ptr을 사용하면 약간의 오버헤드는 발생하지만 Thread-safety하게 사용할 수 있다고 하는데요. std::shared_ptr은 해당 기능을 제공하지는 않지만 std::atomic을 사용하여 Thread-safety하게 사용할 수 있습니다. std::atomic에 대해서는 따로 정리해보도록 하겠습니다.

End Note


  • 좋은 인터페이스는 제대로 쓰기엔 쉬우며, 엉터리로 쓰기엔 어렵습니다. 인터페이스를 만들 때는 이 특성을 지닐 수 있도록 고민하고 또 고민합시다.
  • 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 그리고 기본제공 타입과의 동작 호환성 유지하기가 있습니다.
  • 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산을 제한하기, 객체의 값에 대해 제약 걸기, 자원 관리 작업을 사용자 책임으로 놓지 않기가 있습니다.
  • shared_ptr은 Custom Deleter를 지원 합니다. 이 특징 때문에 shared_ptr은 교차 DLL 문제를 막아주며, 뮤텍스 등을 자동으로 잠금 해제하는 데 쓸 수 있습니다.

Reference


  • Effective C++ (Scott Meyers)

Updated:

Leave a comment