본문 바로가기
C++/Effective C++

항목 18 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자

by 멍청이 개발자 2024. 1. 20.
반응형

자 우선 인터페이스란 무엇일까? 인터페이스는 사실 우리가 코드로 작성하여 응용할 수 있는것은 모두 인터페이스다. 함수, 클래스, 템플릿 등 왠만한 것은 다 인터페이스 라고 보면 된다. 그렇다면 이제 항목 18의 제목대로 제대로 쓰기엔 쉽고 엉터리로 쓰기엔 어려운 인터페이스는 어떻게 만들 수 있을까? 우선 날짜를 나타내는 어떤 클래스에 넣을 생성자를 설계하고 있다고 가정하자.

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

자 이 클래스는 별 문제가 없어 보인다. 하지만 매개변수의 전달 순서가 잘못될 여지가 열려있다.

Date d(20, 1, 2024); // 1, 20, 2024 여야하는데 20, 1을 넣어버렸다!

또한 월, 일, 연도와 상관 없는 숫자가 들어갈 가능성도 있다.

Date d(1, 40, 2024); // 1, 20, 2024 여야하는데 1, 40을 넣어버렸다!

그래서 아예 월, 일, 연도를 나타내는 새로운 타입을 만들어 인터페이스를 강화하면 상당수의 사용자 실수를 막을 수 있다. 

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 d(20, 1, 2024); // 에러! 타입이 틀림. explicit으로 선언했기때문에 암시적 변환 불가
Date d(Day(20), Month(1), Year(2024)); // 에러! 타입이 틀림
Date d(Month(1), Day(20), Year(2024)); // 타입이 맞으므로 정상 작동

자 이렇게만 해도 확실히 엉터리로 쓰기에는 쉽지 않다. 여기서 한 가지 더 추가를 하자면 월(月)은 12가지 밖에 되지 않으므로 별도의 클래스를 만들어 정적함수로 정해진 값을 반환하도록 설계할 수도 있다.

class Month
{
public:
    static Month Jan() {return Month(1);}
    static Month Feb() {return Month(2);}
    ...
    static Month Dec() {return Month(12);}
    ...
 private:
     explicit Month(int m); // Month에 사용자가 직접 값을 넣어 생성하는 것을 막음
 };
 
 Date d(Month::Jan(), Day(20), Year(2024));

여기서 enum을 쓰지 굳이 왜 멍청하게 저렇게 하느냐 라고 할 수도 있는데 우리가 항목2에서 배웠다시피 enum은 int처럼 쓰일 가능성이 있기 때문에 하지 않는것이 좋다고 한다. 

 

자, 다음으로 사용자의 실수를 막는 다른 방법으로는 어떤 타입에 제약을 부여해 그 타입을 통해 할 수 있는 일을 묶어 버리는 방버이 있다. 대표적으로 const를 붙이는 것이다. 이에 대해서는 항목3에서 잘 설명해 놨으니 넘어가도록 하자. 

 

다음으로는 인터페이스를 설계할때는 일관성 있도록 하자는 것이다. STL 자료구조의 반복자 iterator를 살펴보면 내부적인 구현은 다르지만 모두 begin, end 등과 같이 항상 같은 이름의 함수로 구성되어 STL 자료구조 중 하나만 쓸 줄 알아도 다른 자료구조를 사용하는데 큰 어려움이 없다. 만약 STL 자료구조마다 서로 다른 이름의 함수로 iterator가 구현 되어있었다면 우리는 실수하기가 더 쉬울것이다. 이렇듯이 우리도 항상 인터페이스를 설계할 때는 사용자가 최대한 외워야 할것을 줄이도록 하는것이 좋다. 

 

다음으로 우리가 항목 13에서 봤던 팩토리 함수를 가져와봤다.

Investment* createInvestment();

우리는 이 함수의 반환 값을 auto_ptr이나 shared_ptr과 같은 스마트 포인터에 넣어서 관리하도록 배웠다. 하지만 사용자가 아예 스마트 포인터를 사용해야한다는 사실 조차 잊어버리면 어떻게 할까? 그래서 애초부터 이 팩토리 함수가 스마트 포인터를 반환하도록 만들면 이런 불상사를 막을 수 있다.

std::shared_ptr<Investment> createInvestment();

또한 항목 14에서 보았듯이 shared_ptr은 삭제자 지정이 가능하다고 했다. 그래서 delete로 삭제해서는 안되는 자원을 shared_ptr로 반환할때 전용 삭제자를 붙여서 반환하면 이러한 문제들을 원천부터 막아버릴 수 있어서 매우 좋다.

std::shared_ptr<Investment> createInvestment()
{
    return std::shared_ptr<Investment>(new Stock, investmentDeleter); // Stock은 Investment에서 파생된 클래스
}

자, 이번 항목은 인터페이스 설계에 있어서 정말 중요한 내용이었다. 꼭 명심하고 나중에 프로그램을 설계할 때 써먹도록 하자.

반응형