[Effective C++] 18. 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자
제대로 쓰기엔 쉽고 엉터리로 쓰기엔 어려운 인터페이스를 개발하려면 우선 사용자가 저지를 만한 실수의 종류를 머리에 넣어두고 있어야 한다.
사용자 타입 시스템을 활용한 인터페이스
아래 예는 날짜를 나타내는 어떤 클래스에 넣을 생성자를 설계하는 과정이다.
class Date
{
public:
Date(int month, int day, int year);
...
};
문제점
1. 매개변수의 전달 순서가 잘못될 여지가 있다.
Date d(30, 3, 2022); // 일과 월을 바꿔서 입력함
2. 월과 일에 해당하는 숫자가 잘못된 숫자일 수 있다.
Date d(3, 40, 2022); 3월 40일은 없다.
해결방안
1. 새로운 타입을 들여와 인터페이스를 강화하여 사용자의 실수를 막는다.
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(3, 30, 2022); // 타입이 틀려서 에러 발생
Date d(Day(3), Month(30), Year(2022)); // 타입이 틀려서 에러 발생
Date d(Month(3), Day(30), Year(2022)); // 정상 컴파일
2. 새로운 타입에 제약을 넣어서 오류를 방지한다.
class Month
{
public:
static Month Jan() {return Month(1);}
...
static Month Dec() {return Month(12);}
private:
explicit Month(int m);
};
Date d(Month::Mar(), Day(30), Year(2022));
여기서 정적 객체를 사용해서 하지 않고 함수로 return 하는 이유는 '비지역 정적 객체들의 초기화 순서는 정해지지 않는다'는 특성 때문이다.(항목 4)
Const를 활용한 인터페이스
어떤 타입이 제약을 부여하여 그 타입을 통해 할 수 있는 일을 묶어버리면, 예상되는 사용자 실수를 막을 수 있다.
제약 부여 방법으로 흔히 쓰이는 예는 const 붙이기이다. 항목 3에서 설명했듯이 operator*의 반환 타입을 const로 한정함으로써 사용자가 사용자 정의 타입에 대해 다음과 같은 실수를 저지르지 않도록 할 수 있다.
if((a*b)=c) // 비교하려 했으나 대입이 됨.
그리고 일관성 있는 인터페이스를 제공하기 위해 별 다른 이유가 없다면 사용자 정의 타입은 기본 제공 타입처럼 동작하게 만들어야 한다.
제대로 쓰기에 괜찮은 인터페이스를 만들어 주는 중요한 요인이 일관성이기 때문이다.
사용자 쪽에서 뭔가를 외워야 제대로 쓸 수 있는 인터페이스는 잘 못 쓰기 쉽고 언제라도 잊어버릴 수 있다.
스마트 포인터를 활용한 인터페이스
항목 13에서 썼던 팩토리 함수를 보자.
Investment* createInvestment();
이렇게 사용할 경우 자원 누출을 피하기 위해 createInvestment()에서 얻어낸 포인터를 나중에라도 삭제해야 한다.
문제점
1. 사용자가 포인터 삭제를 잊을 수 있다.
2. 똑같은 포인터에 대해 delete가 두 번 이상 적용될 수 있다.
3. 자원관리를 위해 스마트 포인터에 createInvestment에서 얻은 포인터를 저장시키려고 했지만 저장하는 것을 잊을 수 있다.
해결방안
shared_ptr<Investment> createInvestment();
애초에 팩토리 함수가 스마트 포인터를 반환하게 하자. 이렇게 해두면 함수의 반환 값을 shared_ptr에 넣어둘 수밖에 없을뿐더러 나중에 Investment객체가 필요 없어졌을 때 객체를 삭제하는 것에 사용자가 신경 쓸 필요가 없어진다.
그리고 shared_ptr을 반환하는 구조는 자원 해제에 관련된 상당수의 사용자 실수를 사전에 봉쇄할 수 있고 생성 시점에 자원 해제 함수(삭제자)를 직접 엮을 수 있는 기능을 가지고 있어 인터페이스 설계자에게 좋다.
shared_ptr 삭제자 활용하기
위에서 더 나아가서 createInvestment를 통해 얻은 Investment* 포인터를 직접 삭제하지 않게 하고 getRidOfInvestment라는 함수에 포인터를 넘겨 삭제하도록 하는 기능을 만들었다.
class Investment
{
public:
...
};
Investment* createInvestment()
{
return new Investment;
}
void getRidOfInvestment(Investment* obj)
{
delete obj;
}
void Func()
{
Investment* pInvest = createInvestment();
...
getRidOfInvestment(pInvest);
}
문제점
1. 사용자가 getRidOfInvestment를 사용하지 않고 직접 delete 할 수 있다.
2. delete를 하고 getRidOfInvestment도 호출하는 일이 발생할 수 있다.
해결방안
shared_ptr<Investment> createInvestment()
{
// 방법1
shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
// 방법 2
shared_ptr<Investment> retVal(new Investment, getRidOfInvestment);
retVal = ...;
return retVal;
}
1. 포인터를 null ptr로 초기화하고 나중에 대입하는 방식
2. 실제 객체 포인터를 바로 생성자에 넘기는 방법
만약, retVal로 관리할 실제 객체의 포인터를 결정하는 시점이 retVal을 생성하는 시점보다 앞설 수 있으면 2번의 방법을 채택하는 게 좋다.(항목 26 참고)
shared_ptr의 교차 DLL문제 방지에 대하여
shared_ptr의 좋은 특징으로는 교차 DLL문제를 방지한다는 점도 있는데, 교차 DLL문제란 어떤 DLL에서 객체를 생성하고, 다른 DLL에서 소멸하는 문제를 말한다.
보통 객체의 생성과 삭제가 서로 다른 DLL에서 호출될 때 발생하는데, shared_ptr은 생성된 DLL과 동일한 DLL에서 delete를 사용하도록 삭제자가 만들어져 있어서 방지가 가능하다.
요약
- 좋은 인터페이스는 제대로 쓰기에 쉬우며 엉터리로 쓰기에 어렵다. 인터페이스를 만들 때는 이 특성을 지닐 수 있도록 고민을 많이 하자.
- 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 기본 제공 타입과의 동작 호환성 유지하기가 있다.
- 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산을 제한하기, 객체의 값에 대해 제약 걸기, 자원 관리 작업을 사용자 책임으로 놓지 않기가 있다.
- shared_ptr은 사용자 정의 삭제자를 지원한다. 이 특징 때문에 shared_ptr은 교차 DLL문제를 막아주며, 뮤텍스 등을 자동으로 잠금 해제하는 데 쓸 수 있다.
'Books > Effective C++' 카테고리의 다른 글
[Effective C++] 20. 값에 의한 전달보다는 상수객체 참조자에 의한 전달 방식을 택하는 편이 대개 낫다 (0) | 2022.05.29 |
---|---|
[Effective C++] 19. 클래스 설계는 타입 설계와 똑같이 취급하자 (0) | 2022.05.29 |
[Effective C++] 17. new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자 (0) | 2022.05.28 |
[Effective C++] 16. new 및 delete를 사용할 때는 형태를 반드시 맞추자 (0) | 2022.05.25 |
[Effective C++] 15. 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자 (0) | 2022.05.22 |
댓글
이 글 공유하기
다른 글
-
[Effective C++] 20. 값에 의한 전달보다는 상수객체 참조자에 의한 전달 방식을 택하는 편이 대개 낫다
[Effective C++] 20. 값에 의한 전달보다는 상수객체 참조자에 의한 전달 방식을 택하는 편이 대개 낫다
2022.05.29 -
[Effective C++] 19. 클래스 설계는 타입 설계와 똑같이 취급하자
[Effective C++] 19. 클래스 설계는 타입 설계와 똑같이 취급하자
2022.05.29 -
[Effective C++] 17. new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자
[Effective C++] 17. new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자
2022.05.28 -
[Effective C++] 16. new 및 delete를 사용할 때는 형태를 반드시 맞추자
[Effective C++] 16. new 및 delete를 사용할 때는 형태를 반드시 맞추자
2022.05.25