관리 메뉴

기억을 위한 기록들

[CPP-effective] 4-1 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자. 본문

C & CPP/Effective C++

[CPP-effective] 4-1 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자.

에드윈H 2021. 4. 9. 17:55

인터페이스는 만리장성을 쌓는 접선 수단입니다.

 

이상적으로 어떤 인터페이스를 어떻게 써 봤는데 결과 코드가 사용자가 생각한 대로 동작하지 않는다면 그 코드는 컴파일되지 않아야 되는 게 맞다. 반대로 어떤 코드가 컴파일이 되면 사용자가 원하는 대로 동작해야 하는 것이다.

 

'제대로 쓰기엔 쉽고 엉터리로 쓰기에 어려운' 인터페이스를 개발하려면 우선 사용자가 저지를만한 실수의 종류를 미리 생각해봐야 한다. 예를 들어 날짜를 나타내는 어떤 클래스에 넣을 생성자를 설계하고 있다고 가정하자.

 

class Date{
public:
     Date(int month, int day, int year);
     //...
};

 

별 문제가 없을 것 같지만, 매개변수의 순서가 잘못될 여지가 있다.

 

Date d1(21, 4, 2021);  //월 일 년 순서였는데, 일 월 년 순으로 넣었다....21월은 없으니까

 

어이없으면서도 발생할만한 문제이다.

 

새로운 타입을 들여와 인터페이스를 강화하면 상당수 사용자 실수를 막을 수 있게 된다.

 

바로 타입 시스템으로 간단한 래퍼(wrapper) 타입을 각각 만들고 이 타입을 Date 생성자 안에 둘 수 있다.

 

struct Day {
  explicit Day(int d)
     : val(d)
  {}
  
  int val;
};


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

struct Year {
  explicit Year(int y)
     : val(y)
  {}
  
  int val;
};
class Date{
public:
     Date(const Month& m,const Day& d,const Year& y);
     //...
};

이렇게 변경해주게 되면

 

Date d1(21, 4, 2021);  //에러!
Date d1(Day(21), Month(4), Year(2021));  //에러!
Date d1(Month(4), Day(21), Year(2021));  // 성공!

 

타입을 적절히 새로 준비해 두기만 해도 인터페이스 사용 에러를 막는 데는 꽤나 충분해진다.

 

더 나아가 각 타입의 값에 제약을 가하더라도 괜찮은 경우가 생기게 된다.

예를 들어 월의 경우 1월부터 12월까지만 존재하는데 이 이상은 불가능하게 된다. 월 표시 방법으로 enum을 넣는 방법이 있긴 한데, 타입 안전성은 그리 믿음직하지 못하다. 때로는 int처럼 쓸 수 있긴 하지만, 타입 안전성이 신경 쓰인다면 유효한 Month의 집합을 미리 정의해 두어도 괜찮다.

 

class Month{  //단순 정보의 구조체보다 클래스로 변경 
public:
    static Month Jan() { return Month(1); }; //1월부터
    static Month Feb() { return Month(2); }; //2월..
    //..
    //..
    static Month Dec() { return Month(12); }; //12월 까지
    //..
private:
    explicit Month(int m); //Month 객체가 새로 생성되지 않도록 명시호출 생성자는 private 멤버
};
Date d1(Month::Aug(), Day(19), Year(2021));  // 2021년 8월 19일 성공!

 

기본제공 타입과 쓸데없이 어긋나는 동작을 피하는 실질적인 이유는 일관성 있는 인터페이스를 제공하기 위해서이다.

 

괜찮은 인터페이스를 만들어 주는 요인 중에 일관성만큼 똑 부러지는 것은 별로 없으며, 괜찮은 인터페이스를 더 나쁘게 만들어 버리는 요인 중에 비일관성을 따라오는 것이 거의 없다.

 

STL컨테이너의 인터페이스는 전반적으로 일관성을 갖고 있으며, 이 때문에 사용하는데 큰 부담이 없다.

 

예로 모든 STL 컨테이너는 size란 멤버 함수를 갖고 있다. 이 함수는 어떤 컨테이너에 들어 있는 원소의 개수를 알려준다.

std::vector<int > v;
//.. 

v.size();


std::queue<int > q;
//..

q.size();

 

 

사용자 쪽에서 뭔가를 외워야 제대로 쓸 수 있는 인터페이스는 잘못 쓰기 쉽다. 언제라도 잊을 수 있기 때문이다.

 

 

정리

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