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

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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)과 같이 잘못 적는 경우를 막고 싶다면?

  1. enum을 사용한다. (⚠️ 타입 안정성 약함. 항목 2 참조)
  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 팩토리 함수를 통해, 생성된 객체의 포인터를 가져왔었다.

여기서 생길 수 있는 실수는 두 가지 이상이 있다.

  1. 포인터 삭제를 깜빡할 수 있다.
  2. 같은 포인터에 대해 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 짝을 신경 쓰지 않아도 된다.



🧐 정리

  1. 좋은 인터페이스는 제대로 쓰기 쉽고, 엉터리로 쓰기 어렵다.

    → 이를 생각하며 인터페이스를 만들도록 하자.

  2. 인터페이스의 올바른 사용을 위한 방법으로

    → 인터페이스의 일관성 지키기,

    → 기본제공 타입과의 동작 호환성 유지가 있다.

  3. 사용자 실수를 방지하는 방법으로

    → 새로운 타입 만들기,

    → 타입에 대한 연산 제한,

    → 객체 값에 제약 걸기,

    → 자원 관리 작업을 사용자 책임으로 놓지 않기가 있다.

  4. shared_ptr은 사용자 정의 삭제자를 지원한다.

    → 이 특징으로 교차 DLL 문제를 막고,

    → 뮤텍스 자동 잠금 해제에 쓸 수 있다. (항목 14 참고)

Cpp 카테고리 내 다른 글 보러가기

댓글남기기