[C++] 항목 18: 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 4. 설계 및 선언
👉🏻 항목 18: 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자
우리는 인터페이스를 만들 때, 사용자가 실수할 가능성을 막아두어야 한다.
💥 실수가 생기는 경우를 알아보자
1. 매개변수의 전달
class Date {
public:
Date(int month, int day, int year);
...
};
int main() {
Date d(30, 3 1995); // day, month, year 순서로 써버림!
Date d(3, 40, 1995); // day가 40이라니
}
→ 위 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& month, const Day& day, const Year& year);
...
};
int main() {
Date d(30, 3, 1995); // 에러
Date d(Day(30), Month(3), Year(1995)); // 에러
Date(Month(3), Day(30), Year(1995)); // 정상
Date(Month(3), Day(40), Year(1995)); // 흠..
}
→ 이 정도로만 해도 순서를 잘못 적는 등, 실수를 막는데 충분하다.
❓ 그러나 Day(40)과 같이 잘못 적는 경우를 막고 싶다면?
- enum을 사용한다. (⚠️ 타입 안정성 약함. 항목 2 참조)
- Year, Month, Day의 집합을 미리 정의한다.
📌 아래는 2번을 적용한 코드
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); // private, 밖에서 사용하지 못하게 함
...
};
int main() {
Date d(Month::Mar(), Day(30), Year(1995));
}
→ 월을 나타내는 데 객체가 아닌 함수를 쓰고 있다.
→ 비지역 정적 객체들의 초기화는 문제가 생길 수 있기 때문이다.
→ 이에 대해선 항목 4를 읽어보자.
🚫 반환 값 제약
if(a * b = c) // 비교하려 했는데, 대입 시도
→ 웬만해선 사용자 정의 타입은 기본제공 타입처럼 동작하게 만들자.
→ 항목 3을 읽어보자.
이렇게까지 어긋나는 동작을 피하는 이유는,
일관성 있는 인터페이스를 제공하기 위해서이다.
예를 들어:
- STL 컨테이너(vector, set, …) →
size()
함수 제공 - 자바:
- 배열 →
length
프로퍼티 - String →
length()
메소드 - List →
size()
메소드
- 배열 →
→ 이런 것들이 일관성이 떨어지며, 사용하기 불편해진다.
💧 자원 누출 예방
Investment* createInvestment();
항목 13에서 우리는 createInvestment
팩토리 함수를 통해, 생성된 객체의 포인터를 가져왔었다.
여기서 생길 수 있는 실수는 두 가지 이상이 있다.
- 포인터 삭제를 깜빡할 수 있다.
- 같은 포인터에 대해
delete
를 두 번 이상 적용할 수 있다.
✅ 해결책: 팩토리 함수의 반환 값을 스마트 포인터로 만든다
shared_ptr<Investment> createInvestment();
→ 팩토리 함수의 리턴 값을 shared_ptr로 두면 사용자가 신경 쓸 필요도 없을 뿐 아니라
→ 삭제자도 지정해 줄 수 있는 이점이 있다.
🔍 잠깐 shared_ptr에 대해서 더 자세히 알아보자
1. null 값과 사용자 정의 삭제자를 가진 shared_ptr
shared_ptr<Investment> pInv(0, getRidOfInvestment); // 컴파일 실패
→ 우리는 null 값을 가지며, 사용자 정의 삭제자를 가진 shared_ptr을 생성하고자 했다.
→ 그러나 0
은 포인터가 아닌, int 값이기에 컴파일이 되지 않는다.
shared_ptr<Investment> pInv(static_cast<Investment*>(0), getRidOfInvestment);
→ 0
을 Investment* 타입의 포인터로 형 변환시켜주면 해결할 수 있다.
객체의 포인터가 결정되는 시점이 늦어지는 경우엔 아래처럼 쓴다:
shared_ptr<Investment> createInvestment() {
shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
retVal = ...; // retVal이 실제 객체를 가리키도록 한다.
return retVal;
}
→ 물론, 객체의 포인터가 먼저 생성된다면, retVal의 생성자에 바로 넘겨주는 게 더 낫다.
2. 교차 DLL 문제를 신경 쓰지 않아도 된다
교차 DLL 문제가 생기는 경우:
new
를 사용할 때는 A의 동적 링크 라이브러리-
delete
를 쓸 때는 B의 동적 링크 라이브러리→ 이런 경우 런타임 에러가 난다.
→ 그러나 shared_ptr에서는 동적 할당된 값에 대해 new/delete 짝을 신경 쓰지 않아도 된다.
🧐 정리
-
좋은 인터페이스는 제대로 쓰기 쉽고, 엉터리로 쓰기 어렵다.
→ 이를 생각하며 인터페이스를 만들도록 하자.
-
인터페이스의 올바른 사용을 위한 방법으로
→ 인터페이스의 일관성 지키기,
→ 기본제공 타입과의 동작 호환성 유지가 있다.
-
사용자 실수를 방지하는 방법으로
→ 새로운 타입 만들기,
→ 타입에 대한 연산 제한,
→ 객체 값에 제약 걸기,
→ 자원 관리 작업을 사용자 책임으로 놓지 않기가 있다.
-
shared_ptr은 사용자 정의 삭제자를 지원한다.
→ 이 특징으로 교차 DLL 문제를 막고,
→ 뮤텍스 자동 잠금 해제에 쓸 수 있다. (항목 14 참고)
댓글남기기