어떤 인터페이스를 어떻게 써 봤는데 결과 코드가 사용자가 생각한 대로 동작하지 않는다면, 그 코드는 컴파일 되지 않아야 맞습니다. 거꾸로 생각해서, 어떤 코드가 컴파일 된다면 그 코드는 사용자가 원하는 대로 동작해야할 것입니다.

 

'제대로 쓰기에 쉽고, 엉터리로 쓰기에 어려운' 인터페이스를 개발 하려면?

우선 사용자가 저지를 만한 실수의 종류를 머리에 넣어 두고 있어야 합니다.  

 

1
2
3
4
5
6
7
8
9
10
11
12
class Date
 
{
 
public :
 
    Date( int month, int day, int year );
 
}; 
 
Date d( 30, 3, 1995 ); // 3, 30 이어야 하는데 30, 3을 넣음.
Date d( 3, 40, 1995 ); // 3, 30 이어야 하는데 3, 40을 넣음. 

 

위의 코드는 어찌보면, 문제될게 없어보이지만, 사용자가 쉽게 실수를 저지를수 있습니다.

위의 주석대로 매개변수의 전달 순서가 잘못될 여지가 있습니다.

 

위의 문제는 간단한 랩퍼 타입을 각각 일, 월, 연을 만들고 이 타입을 Date 생성자 안에 두면, 어느정도 해결 됩니다.

 

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 ) 
        : day( d ) 
    {} 
    int day; 
}; 
 
struct Month 
    explicit Month( int m ) 
        : month( m ) 
    {} 
    int month; 
};
 
struct Year 
    explicit Year( int y ) 
        : year( y ) 
    {} 
    int year;
}; 
 
class Date 
public : 
    Date( const Month& m, const Day& m, const Year& y ); 
};
 
Date d( 30, 3, 1995 );                   // 에러. 타입이 틀림.
Date d( Day(30), Month(3), Year(1995) ); // 에러. 타입이 틀림. 
Date d( Month(3), Day(30), Year(1995) ); // OK

 

위와 같이 적절한 타입만 제대로 준비되어 있으면, 각타입의 값에 제약을 가하더라도 괜찮은 경우가 생기게 됩니다.

 

예를 들어,

월이 가질 수 잇는 유효한 값은 12개이므로, Month 구조체 타입에 이 사실을 제약조건으로 부여할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 ); // Month 값이 새로 생성되지 않도록 명시호출 생성자가 private 멤버이다. 
 
    ... 
 
};  
 
Date d( Month::Jan(), Day(30), Year(1995) );

 

 

사용자 실수로 예상되는 것중에 또 하나는 다음과 같은 경우가 있다

 

if( a * b = c ) ... // '='가 '=='가 되어야 맞다

 
우리는 항목 3에 잘 설명해논, operator*의 반환 타입을 const로 한정함으로써 사용자가 사용자정의 타입에 대해 위와같은 실수를 저지르지 않도록 할 수 있었습니다.

 

 

기본적으로 우리는 int등의 타입이 어떻게 동작하는지 그 성질을 이미 다 알고 있기 때문에, 사용자를 위해 만드는 타입도 웬만하면 이들과 똑같이 동작하게 만드는 센스를 갖추어야 합니다.

 

이것은 일관성 있는 인터페이스를 제공하기 위해서입니다.

STL 컨테이너의 인터페이스는 전반적으로 완벽하진 않지만, 일관성을 갖고 있으며, 이때문에 사용하는데 큰 부담이 없습니다.

ex ) STL컨테이너 size() 멤버함수를 개방해 놓음

 

사용자 쪽에서는 뭔가를 외워야 제대로 쓸 수 있는 인터페이스는 잘못 쓰기 쉽습니다.

 

예를 들어,

이전 항목 13 에서 봤던 createInvestment() 팩토리 함수의 경우, 클래스 포인터를 반환한다.

함수에서 얻어낸 포인터는 동적 할당된 자원이기 때문에, 종료 전에 반드시 메모리를 해제해야한다.

사용자는 다음과 같은 실수를 저지를 수 있습니다.

 

1) 포인터 삭제를 잊어버렸다!

2) 같은 자원에 delete를 두번 해버렸다!!

 

이전 항목 13 에서는 이 문제를 해결하기 위해 스마트 포인터를 이용했다.

그러나 사용자는 스마트 포인터를 사용해야 한다는 사실을 모르면 어떻게 할까요?

 

그 해결책,

애초부터 팩토리 함수가 스마트 포인터를 반환하게 만드는 것입니다.

 

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

 

위와같은 방법으로 구현되면, 사용자는 무조건 해당 타입에 맞게 받아줘야합니다. 그렇지 않으면 에러가 납니다.

게다가 std::tr1::shared_ptr은 생성 시점에 자원 해제 함수(일명 삭제자)를 직접 엮을 수 있는 기능을 갖고 있기 때문에, 자원해제에 관련된 상당수의 사용자 실수를 사전 봉쇄할 수 있습니다.

 

또하나, std::tr1::shared_ptr은 '교차 DLL 문제'를 방지해준다.

 

서로 다른 DLL 사이에서 new와 delete를 했을 경우, 꼬여서 런타임 에러가 발생하는 것을 막아준다는 것입니다.

왜냐하면 기본 삭제자가 무조건 동일 DLL에서 이 짝을 수행할 수 있게 설계되어있기 때문이고, std::tr1::shared_ptr은 생성시에 그해당 DLL을 붙잡고 있기 때문입니다.

 

참고로, tr1::shared_ptr을 구현한 제품 중 가장 흔히 쓰이는 것은 Boost 라이브러리 입니다. 부스트의 shared_ptr은 일단 크기가 원시 포인터의 두배이고, 느리며, 내부 관리용 동적 메모리 까지 추가로 들지만, 사용자의 실수는 눈에 띄게 줄어들것입니다.

 

이것 만은 잊지 말자!

◆ 좋은 인터페이스는 제대로 쓰기에 쉬우며 엉터리로 쓰기에 어렵습니다. 인터페이스를 만들때는 이 특성을 지닐 수 있도록 고민하고 또 고민합시다.

◆ 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 그리고 기본제공 타입과의 동작 호환성 유지하기가 있습니다.

◆ 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산을 제한하기, 객체의 값에 대해 제약 걸기, 자원 관리 작업을 사용자 책임으로 놓지 않기가 있습니다.

◆ std::tr1::shared_ptr은 사용자 정의 삭제자를 지원합니다. 이 특징 때문에 std::tr1::shared_ptr은 교차 DLL 문제를 막아주며, 뮤텍스 등을 자동으로 잠금 해제하는데 쓸수 있습니다.


+ Recent posts