상세 컨텐츠

본문 제목

항목 18) 좋은 인터페이스 설계: 제대로 쓰기에는 쉽게, 엉터리로 쓰기에는 어렵게

C++/Effective C++

by DeaKyungLee 2021. 12. 5. 18:12

본문

인터페이스를 사용하는 사용자의 실수를 설계 단계에서 미리 방지하자.

→ 좋은 인터페이스 설계

 

Wrapper

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

위의 클래스는 문법적으로 아무런 문제가 없다.

하지만 사용하는 입장에서 실수 하기 딱 좋은 코드이기도 하다.

...
Date d(30, 3, 1995); // 3,30을 넣었어야 함.
Date d(3, 40, 1995); // 40은 말이 안됨.

위와 같은 상황을 방지하는 간단한 방법은 Wrapper 타입을 이용하는 것이다.

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(30, 3, 1995); // 타입이 틀렸습니다!
Date d(3, 40, 1995); // 타입이 틀렸습니다.
Date d(Month(3), Day(30), Year(1995)); // 타입이 맞았습니다!

다시 말하자면, 각 인자에 대한 구조체를 따로 정의해서 사용하자는 것이다.

그리고 해당 구조체 내부에서 따로 값 검사를 진행하는 것도 괜찮다.

struct Month{ 
	explicit Month(int m) :
	{
		if ( 0 < m && 13 > m) 	// 0월, 13월은 존재할 수 없다
		{
			val = m;
		}
	} 
	int val;
 
 
 };
...

 

Smart Pointer ( shared_ptr )

좋은 인터페이스 설계의 또 다른 예시를 보자

Investment* createInvestment();		// (1) 설계자
 
 
std::tr1::shared_ptr<Investment> pInv(createInvestment());		// (2) 사용자 (설계자가 예상한 사용 방식)

위의 팩토리 함수의 설계자는 사용자가 스마트 포인터를 사용하리라 생각했다.

하지만 (2) 와 같은 방식의 코드를 사용자가 깜빡했다면?

포인터 삭제를 깜빡하거나, 똑같은 포인터를 두 번 삭제하는 등의 연쇄적인 문제가 발생 할 수 있다.

그렇다면 설계 자체에서 스마트 포인터를 반환하면 어떨까?

std::tr1::shared_ptr<investment> createInvestment();

스마트 포인터 사용을 깜빡해서 발생하는 문제는 발생하지 않을 것이다.

또 다른 가정으로,

만약 해당 객체에 대한 삭제 함수를 따로 제공한다면 어떨까?

즉, 스마트 포인터로 감싸진 객체에 대한 삭제 함수를 설계자가 미리 만들어 두는 것이다.

getRidOfInvestment()
{
	...
	...
 
 
	delete ...
}

여러가지 문제를 미리 예방하고 더 깔끔한 형식처럼 보이지만, 이것만으로는 오히려 사용자의 실수를 더 유발할 수 있다.

만약 이런 상황처럼 사용자가 반드시 해당 함수를 사용해서 객체를 삭제하도록 하고 싶다면, shared ptr의 또 다른 기능을 활용하면 된다.

shared_ptr 생성시에 두 번째 인자로 삭제자를 받는다.

즉, 해당 자원에 대한 카운트가 0이 되어서 삭제되는 시점에 호출될 함수를 사용자가 정의할 수 있다는 의미이다.

이것을 정리하자면 createInvestment 함수는 아래와 같이 변경되는 것이 바람직하다.

std::tr1::shared_ptr<investment> createInvestment()
{
    // 0 부분에 포인터가 들어가야 하기 때문에 static_cast로 캐스팅
    std::tr1::shared_ptr<investment> 
        retVal(static_cast<investment*>(0), getRidOfInvestment);	// 0 이 아니라 실제 객체를 통해 바로 초기화하는 것이 바람직하다.
    retVal = ...; 													// 이유는 항목 26 에서 다룬다.
    return retVal;
}

 

Cross DLL Problem

shared_ptr 의 또 다른 장점은 서로 다른 DLL 내부에서도 자신이 가리키는 객체에 대한 고유성을 자동으로 지킨다는 것이다.

즉, shared_ptr 을 통해 관리되는 자원은 서로 다른 DLL 에서 같은 이름의 자원이 존재하더라도

자원 삭제 시점에는 선언 시점에 할당한 자원을 제대로 삭제한다는 말이다.

 

Things to Remember

  • 좋은 인터페이스는 제대로 쓰기 쉬고, 엉터리로 사용하기에 어렵다.
  • 올바른 인터페이스 사용을 이끄는 방법은, 인터페이스 사이의 일관성 유지 및 기본 제공 타입과의 동작 호환성 유지가 있다.
  • 사용자의 실수를 방지하는 방법으로는 (1) 새로운 타입 만들기 ( Wrapper ), (2) 타입에 대한 연산 제한 하기 ( const operator * ), (3) 객체의 값에 대해 제약 걸기 , (4) 자원 관리 작업을 사용자 책임으로만 두지 않기 ( 스마트 포인터 반환 ) 가 있다.
  • shared_ptr 은 사용자 정의 삭제자를 지원하며, Cross DLL Problem 문제를 방지한다. 또한 뮤텍스 자동 잠금에도 사용할 수 있다. ( 항목 14 )

관련글 더보기

댓글 영역