제대로 쓰기엔 쉽고 엉터리로 쓰기에 어려운 인터페이스를 개발하려면 우선 사용자가 저지를 만한 실수의

종류를 머리에 넣어두고 있어야 한다.

 

예시 1

 

아래 예는 날짜를 나타내는 어떤 클래스에 넣을 생성자를 설계하는 과정이다.

 

1
2
3
4
5
6
class Date
{
public:
    Date(int month, int day, int year);
    ...
};
cs

 

문제점

 

 - 매개 변수의 전달 순서가 잘못될 여지가 있다.

1
Date d(303 , 2017); //월에 30이라는 값이 들어가짐
cs

 

 - 월과 일에 해당하는 숫자가 잘못된 숫자일 수도 있다.

1
Date d(340 , 2017); //3월 40일이라는 값이 들어가짐.
cs

 

 

개선 1-1

 

새로운 타입을 들여와 인터페이스를 강화하면 사용자의 실수를 막을 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct Day
{
    explicit Day(int d) : val(d) {    }
 
    int val;
};
 
struct Month
{
    explicit Month(int dm) : 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(3302017);                    //타입이 틀려서 에러 발생.
 
Date d(Day(30), Month(3), Year(2017));  //타입이 틀려서 에러 발생.
 
Date d(Month(3), Day(30), Year(2017));  //정상 컴파일.
cs

 

 

개선 1-2

 

'월'이 가질 수 있는 유효한 값을 검사해야 하는데 enum을 사용해도 되지만 타입 안정성은 그리 믿음직하지 못하다.

타입 안정성이 신경 쓰인다면 유효한 Month의 집합을 미리 정의해 두어도 좋다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Month
{
public:
    static Month Jan() { return Month(1);  }
    static Month Feb() { return Month(2);  }
    static Month Mar() { return Month(2);  }
    ...
    static Month Dec() { return Month(12); }
 
private:
    explicit Month(int m);    //Month 값이 외부에서 생성되지 못하도록
                              //생성자를 private으로 선언.
};
 
Date d(Month::Mar(), Day(30), Year(2017));
cs

 

여기서 귀찮게 하나 하나 함수로 return하는 식으로 프로그램이 되어 있다.

아래와 같이 편하게 정적 객체로 사용할 수도 있는데.. 그러지 않은 이유는....

 

1
2
3
4
5
6
7
8
9
class Month
{
public:
    static int Jan = 1;
    static int Feb = 2;
    static int Mar = 3;
    ...
    static int Dec = 12;
};
cs

 

항목4에서 배운 '비지역 정적 객체들의 초기화 순서는 정해지지 않는다'는 특성 때문이다.

 

 

예시 2

 

예상되는 사용자 실수를 막는 다른 방법으로는 어떤 타입이 제약을 부여하여 그 타입을 통해 할 수 있는 일들을

묶어 버리는 방법이 있다.

제약 부여 방법으로 아주 흔히 쓰이는 예가 'const 붙이기'이다. 항목3에서 설명했듯이 operator*의 반환 타입을

const로 한정함으로서 사용자가 사용자 정의 타입에 대해 다음과 같은 실수를 저지르지 않도록 할 수 있었다.

 

1
if ( (a * b) = c ) // 비교하려고 했으나 a*b 에 c를 대입해 버린 코드.
cs

 

여기서 하고자 하는 얘기는 '별다른 이유가 없다면 사용자 정의 타입은 기본 제공 타입처럼 동작하게 만들어라'

라는 것이다. int 등의 타입 정도는 사용자들이 그 성질을 이미 다 알고 있기 때문에, 사용자를 위해 만드는 타입도

웬만하면 이들과 똑같이 동작하게 만들라는 것이다.

 

기본제공 타입과 쓸데없이 어긋나는 동작을 피하는 실질적인 이유는 일관성 있는 인터페이스를 제공하기 위해서이다.

제대로 쓰기에 괜찮은 인터페이스를 만들어주는 요인 중에 중요한 것은 일관성이다.

또한 불편한 인터페이스를 만들어주는 요인 중에 비일관성을 따라오는 것도 거의 없다.

 

사용자 쪽에서 뭔가를 외워야 제대로 쓸 수 있는 인터페이스는 잘 못 쓰기 쉽고 언제라도 잊어버릴 수 있다.

 

 

예시 3

 

항목 13에서 썼던 팩토리 함수를 한 번 보자.

 

1
Investment* createInvestment();
cs

 

이렇게 사용하면 자원 누출을 피하기 위해 createInvestment에서 얻어낸 포인터를 나중에라도 삭제해야 한다.

 

문제점 

 

 - 사용자가 포인터 삭제를 잊을 수 있다.

 - 똑같은 포인터에 대해 delete가 두 번 이상 적용될 수 있다.

 - 스마트 포인터에 createInvestment에서 얻은 포인터를 저장시키는 것을 잊을 수 있다.

 

개선

 

1
std::tr1::shared_ptr<Investment> createInvestment();
cs

 

애초에 팩토리 함수가 스마트 포인터를 반환하게 만들자.

이렇게 해 두면, 함수의 반환 값은 tr1::shared_ptr에 넣어둘 수 밖에 없을 뿐더러, 나중에 Investment 객체가

필요 없어졌을 때 객체를 삭제하는 것에 사용자가 신경쓸 필요가 없어진다.

 

tr1::shared_ptr을 반환하는 구조는 자원 해제에 관련된 상당수의 사용자 실수를 사전 봉쇄할 수 있고 생성 시점에

자원 해제 함수(삭제자)를 직접 엮을 수 있는 기능을 갖고 있기 때문에 설계자에게 좋다.

 

 

예시 4

 

createInvestment를 통해 얻은 Investment* 포인터를 직접 삭제하지 않게 하고 getRidOfInvestment라는 함수에

포인터를 넘겨 삭제하도록 하는 기능을 준비했다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Invest
{
public:
    ...
};
 
Invest* createInvest())
{
    return new Invest;
}
 
void getRidOfInvestment(Invest* obj)    //Invest 객체 삭제 전용 함수.
{
    delete obj;
}
 
void Fucn()
{
    Invest* pInvest = createInvest();
 
    ...        //pInvest 사용.
 
    getRidOfInvestment(pInvest);    //pInvest 객체 삭제.
}
cs

 

문제점

 

 - 사용자가 getRidOfInvestment를 사용하지 않고 직접 delete를 할 수 있다.

 - 실수로 delete도 하고 getRidOfInvestment도 호출하는 일이 발생할 수 있다.

 

개선

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Invest
{
public:
    ...
};
 
void getRidOfInvestment(Invest* obj) //삭제자.
{
    delete obj;
}
 
std::tr1::shared_ptr<Invest> createInvest()
{
    std::tr1::shared_ptr<Invest> ptr(new Invest, getRidOfInvestment); //초기화 포인터, 삭제자.
 
    return ptr;
}
 
 
int _tmain(int argc, _TCHAR* argv[])
{
    std::tr1::shared_ptr<Invest> pInvest = createInvest();
 
    return 0;
}
 
cs

 

createInvesetment 함수를 삭제자가 묶인 tr1::shared_ptr을 반환하도록 구현하자.

그러면 알아서 pInvest가 가리키는 객체가 0이 되면 삭제자를 호출하여 객체를 삭제할 것이다.

그런데 그냥 위에처럼 쓸거면 삭제자 자체를 만들 필요는 없다. 삭제자가 없어도 저 기능을 알아서 하기 때문이다.

 

삭제자를 별도로 사용하는 경우는 만약 일반 지역 변수의 주소를 스마트 포인터로 가리킬 때, 일반 지역 변수는 new로 생성된

것이 아니기 때문에 소멸 시 delete를 하면 안되므로 delete를 하지 않는 삭제자를 별도로 만들어주기 위해서 사용한다.

또는 배열을 스마트 포인터로 가리킬 경우에 delete[]를 사용해주기 위한 삭제자를 별도로 만들어줄 때 사용한다.

 

즉, 삭제자가 명시된 스마트 포인터는 스마트 포인터의 소멸자를 호출하지 않고 명시된 삭제자를 호출한다.

삭제자가 없는 일반 스마트 포인터는 소멸자가 호출되어 자신이 가리키던 객체의 Heap memory를 delete 시켜준다.

 

그리고 삭제자 함수를 사용할 때, 삭제자 함수는 스마트 포인터가 가리키는 포인터형의 파라미터를 반드시 가지고 있어야한다.

 

 

예시 5

 

tr1::shared_ptr는 '교차 DLL 문제'가 생기는 경우에도 이런 문제를 미연에 방지해주는 역할을 해준다.

'교차 DLL 문제'가 생기는 경우는 어떤 DLL에서 객체를 생성하고 이 객체를 다른 DLL에서 소멸시킬 때 이다.

즉, 객체가 생성(new)되고 삭제(delete)되는 것이 서로 다른 DLL에서 이뤄지면 안된다는 것이다.

 

그런데 tr1::shared_ptr을 사용하면 이 문제를 피할 수 있다. tr1::shared_ptr은 생성된 DLL과 동일한 DLL에서 delete를

사용하도록 삭제자가 만들어져 있기 때문이다.

 

1
2
3
4
5
6
7
8
9
10
class stock : Invest
{
    ...
};
 
std::tr1::shared_ptr<Invest> createInvest()
{
    return std::tr1::shared_ptr<Invest>(new stock);
}
 
cs

 

createInvest가 반환하는 shared_ptr은 다른 DLL들 사이에 이리저리 넘겨지더라도 교차 DLL 문제를 걱정하지 않아도 된다는

뜻이다.

 

4줄 요약

 

 - 좋은 인터페이스는 제대로 쓰기에 쉬우며 엉터로리로 쓰기에 어렵다. 인터페이스를 만들 때는 이 특성을 지닐 수 있도록

   고민해야 한다.

 

 - 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 그리고 기본 제공 타입과의 동작

   호환성 유지하기이다.

 

 - 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산을 제한하기, 객체의 값에 대해 제약 걸기,

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

 

 - tr1::shared_ptr은 사용자 정의 삭제자를 지원한다. 이 특징 때문에 tr1::shared_ptr은 교차 DLL 문제를 막아주며,

   뮤텍스 등을 자동으로 잠금 해제하는데 사용할 수 있다.

 

+ Recent posts