[Effective C++ 정리 #18] 좋은 인터페이스 설계의 핵심
이 글은 『Effective C++』를 읽고 개인적으로 공부한 내용을 정리한 기록입니다.
저는 컴퓨터공학을 전공하지 않았으며, 프로그래밍을 공부하는 과정에서의 이해와 생각을 정리하기 위해 글을 작성하고 있습니다.
따라서 내용 중 일부에 오류나 부정확한 설명이 있을 수 있으며, 피드백은 언제든지 환영합니다. 확인 후 수정하도록 하겠습니다.
전문적인 해설이 아닌 개인적 시선에서의 정리임을 참고하고 읽어주시면 감사하겠습니다.
[Effective C++ 정리 #18] 인터페이스는 잘못 쓰기 어렵게 설계해야 합니다
C++는 다양한 인터페이스로 가득합니다. 함수 인터페이스, 클래스 인터페이스, 템플릿 인터페이스까지, 이 모든 인터페이스는 클라이언트가 프로그램과 상호작용하는 접점입니다. 따라서 **인터페이스 설계는 개발자가 코드의 사용 방법을 직접 제시하는 일**이며, 그만큼 실수 가능성을 줄이는 방향으로 설계되어야 합니다.
본 아이템 #18의 핵심 메시지는 명확합니다. “인터페이스는 올바르게 사용하기 쉽게 만들고, 잘못 사용하기 어렵게 만들어야 한다.”
잘못된 사용을 유도하는 Date 클래스의 예
다음과 같은 Date 클래스가 있다고 가정합니다:
class Date {
public:
Date(int month, int day, int year);
};
이 생성자를 사용하는 코드에서 다음과 같은 실수는 매우 쉽게 발생합니다:
Date d(30, 3, 1995); // 잘못된 순서! month와 day가 바뀌었음
Date d(3, 40, 1995); // 잘못된 일(day)! 40일은 없음
이런 실수는 코드 상으로는 유효하지만 논리적으로는 잘못된 값입니다. 사용자가 이런 실수를 저지른다면 이는 **인터페이스 설계가 불충분했기 때문**입니다.
새로운 타입을 활용!
다음처럼 Day
, Month
, Year
라는 별도의 타입을 만들어 실수를 방지할 수 있습니다:
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(Month(3), Day(30), Year(1995)); // ✅ 올바른 사용
Date d(Day(30), Month(3), Year(1995)); // ❌ 컴파일 에러
이처럼 타입을 도입하는 것만으로도 **컴파일 타임에서 실수를 방지**할 수 있습니다.
유효한 값만 허용하도록 제한
예를 들어 월(month)은 1~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); // 외부에서 임의로 생성 못하게 함
int val;
};
이렇게 하면 사용자는 반드시 Month::Mar()
같은 명시적 표현을 사용해야 하며, 임의의 숫자를 넘기는 실수를 원천적으로 막을 수 있습니다.
const, 타입 일관성 등 추가 방어 기법
다음은 흔히 발생할 수 있는 실수입니다:
if (a * b = c) ... // = 대신 ==을 쓰려던 실수
이런 오류는 operator*
의 반환 타입에 const
를 붙이면 막을 수 있습니다. C++은 const int = ...
같은 코드는 허용하지 않기 때문입니다.
또한 내 타입이 int
나 double
같은 내장 타입처럼 동작하게 설계하면 사용자가 익숙한 방식으로 다룰 수 있어 실수 가능성이 줄어듭니다.
스마트 포인터 반환으로 자원 관리 실수 방어
다음은 이전 아이템에서 사용했던 예시입니다:
Investment* createInvestment(); // 호출자가 delete 필요
이 경우 클라이언트는 다음 두 가지 실수를 할 수 있습니다:
delete
를 깜빡해서 메모리 누수 발생- 중복
delete
로 인해 프로그램 크래시 발생
더 나은 인터페이스는 스마트 포인터를 반환하는 것입니다:
std::shared_ptr<Investment> createInvestment();
이렇게 하면 클라이언트는 자원 해제 책임에서 해방되고, 스마트 포인터가 예외 상황에도 안전하게 자원을 해제합니다.
사용자 정의 deleter로 커스터마이징도 가능!
shared_ptr
는 자원을 해제할 때 호출할 사용자 정의 함수를 지정할 수 있습니다:
void getRidOfInvestment(Investment* p);
std::shared_ptr<Investment> p(
new Investment, getRidOfInvestment
);
이렇게 하면 클라이언트는 실수로 delete
를 호출할 일이 없고, 지정한 방식으로 자원이 관리됩니다.
또한 이 방식은 **cross-DLL 문제**도 방지할 수 있습니다. 한 DLL에서 생성한 객체를 다른 DLL에서 delete
하면 문제가 발생할 수 있는데, shared_ptr
는 생성된 DLL의 delete 연산을 사용하도록 보장합니다.
핵심 요약
- 인터페이스는 올바르게 쓰기 쉬워야 하고, 잘못 쓰기는 어려워야 합니다.
- 타입 시스템, const 제한자, 값 제한 등 다양한 기법을 활용합니다.
- 클라이언트가 해야 할 일을 최소화하는 것이 좋은 인터페이스입니다.
shared_ptr
는 자동 해제와 사용자 정의 deleter 지원으로 대표적인 좋은 인터페이스 예 입니다.
감사합니다.