2016년 2월 25일 목요일



2.1 문제 도메인 모델
API는 문제에 대한 논리적인 추상화를 제공하고 문제를 해결할 수 있어야 한다.

사용자에게 세세한 구현 코드보다는 동작 방식을 제공한다. 때문에 API가 논리적인 인터페이스를 갖추었는지 검토해야 한다.

예로 주소록을 설계할 때 간단한 기능을 생각하고, 초기 설계는 UML을 통해서 시각화 한다. 이 다이어그램은 문제 도메인의 논리적인 추상화를 제공한다.

API는 문제 도메인의 핵심 객체를 모델링해야 한다. 이를 객체지향 설계(객체 모델링)이라고 하며, 이유는 객체의 계층 구조를 설명하기 때문이다.

객체 모델링의 목표는 객체들을 식별하고 각 객체들이 제공해야 하는 동작과 서로의 연결 관계를 확인하는 것이다.



2.2 구체적인 구현 숨기기

물리적 은닉은 private으로 코드를 선언해서 클라이언트가 코드를 사용할 수 업게 만드는 것으로 공개된 인터페이스(.h)로부터 분리된 파일(.cpp)에 상세한 내부 로직을 구현하는 것이다.

인라인으로 구현된 함수는 실제 API개발에서는 간단하고 명확한 설명을 위해 최대한 자제해야 한다.

논리적 은닉은 API의 특정 요소에 접근하지 못하도록 언어의 기능을 활용하는 것으로, API의 구체적인 로직이 외부로 노출되지 않도록 C++의 protected와 private기능을 활용하는 것이다.

Tip. 캡슐화는 API의 기반이 되는 구현 코드에서 공개 인터페이스를 분리하는 프로세스다.



2.2.3 멤버 변수 감추기

데이터 멤버가 API의 논리적 인터페이스 중 일부라면 멤버 변수가 간접적으로 노출되도록 게터와 세터 메서드를 제공해야 한다.

게터/세터 사용시 장점
유효성 체크( 클래스의 내부의 상태 값 체크 ),
지연된 평가( 계산 비용을 고려하여 값의 연산을 지연시킬 필요가 있다. ),
캐싱( 자주 사용되는 값을 저장하여 재 연산없이 바로 리턴 ),
추가연산( 필요시 추가 연산 기능 )
알림( 특정 모듈에게 값 변경시 전달 )
디버깅( 접근 및 변수를 체크하기 위해 assert문 등 사용 ),
동기화, 훌륭한 접근 제어, 바뀌지 않는 관계 유지

Tip. 클래스의 데이터 멤버는 항상 public이나 protected가 아닌 private으로 선언해야 한다.


2.2.4 메서드 구현 숨기기

정보 은닉을 사용한 프로그램은 사용하지 않은 프로그램 보다 4배나 코드 변경이 쉬웠다.

Tip. 절대로 비상수 포인터나 참조를 private 데이터 멤버로 리턴하지 않는다. 이는 캡슐화를 어기는 행위이다.

Pimple 관용법 .cpp파일에 있는 구조체나 클래스 코드안에 private 멤버 변수를 격리 시킨다. .h파일은 구현 클래스의 불투명 포인터를 담고 있는다.



2.3 작게 완성하기

Tip. 오컴의 면도날 "불확실에 대한 가설은 간결해야 한다."

API는 최대한 단순해야 한다. 노출하는 클래스의 수, public 멤버, 이해하기 쉬워야 한다. API는 사용자들의 머릿속에 금방 떠올릴 수 있어야 한다.


2.3.2 가상함수의 추가는 신중하게

상속은 강력하지만 함정이 있다.
베이스 클라이언트 개선시, 의도치 않은 방법으로 동작할 수 있다. API를 잘못된 방법으로 혹은 오류가 많이 발생하도록 확장 시킬 수 있다. 함수 오버라이드는 내부적으로 클래스 통합을 방해한다.

가상함수 사용시 문제점
런타임 시 vtable 탐색을 통한 오버헤드, 사용할 수록 vtable을 가리키는 포인터의 크기로 객체 크기도 증가, 가상함수는 인라인이 될 수 없다. 가상함수를 오버로드 하는 것은 어렵다.

Tip. 타당한 이유나 부득이한 상황이 아니라면 함수를 오버라이드 할 수 없게 만든다.

cf) 원시성 : 하나의 메서드를 효과적으로 구현하기 위해서 클래스의 세부 내용에 접근하는 특성

래퍼 클래스를 핵심 API와 분리시키는 일. 대신 외부로 노출시킬 핵심 API의 특정 기능들을 위해 별도의 클래스를 만들어야 한다. OpenGL에서는 OpenGL Utility Library 혹은 GLU라고 부르는 유틸리티 라이브러리를 제공한다.

개발한 API는 사용하기 쉬운 인터페이스를 통해 기본 기능을 제공해야 하고 고급 기능은 분리된 계층으로 나눠 제공해야 한다는 것을 의미한다.


2.4.2 잘못 사용하기에도 어렵게

- 코드의 가독성을 높이기 위해서는 Boolean 타입 보다는 enum 타입을 사용하라.
- 함수에 같은 타입의 파라미터를 여러 개 사용하지 말자.
- 함수의 이름과 파라미터의 순서를 일관성 있게 유지하자.


2.4.4 수직적인

API 설계에 있어서 직교성은 영향을 미치지 않는 메서드를 의미한다. 어떤 속성에 값을 할당하는 메서드 호출은 오직 그 속성의 값만을 변화시킬 뿐 외부에서 접근하는 다른 속성의 값을 변화시키지는 않는다.

수직적인 API란 다른 코드에 영향을 미치지 않는 함수를 말한다.
수직적인 API 설계시 중복 제거와 독립성 증가를 고려하자!



2.4.5 견고한 자원 할당

잘못된 포인터나 참조문제를 해결하기 위해 shared_ptr, weak_ptr, scoped_ptr등을 사용하자.
만약 객체의 메모리 해제를 클라이언트가 담당해야 한다면 스마트포인터를 사용해서 동적으로 할당된 객체를 리턴하라.

Tip. 자원의 할당과 해제를 객체의 생성과 소멸처럼 생각하라.

절대로 특정 플랫폼을 명시하는 #if 또는 #ifdef 문을 API의 public 헤더 파일에서 사용하지 않는다. 이 조건 문은 API의 구체적인 내용을 외부로 노출하고 플랫폼별로 API가 다르게 행동하도록 만든다.



2.5 느슨한 연결
컴포넌트를 작은 단위로 구성하면 할수록 더 쉽게 연결 관계를 느슨하게 만들 수 있다.


2.5.1 이름만 사용한 연결
실제로 #include문을사용해서 클래스의 전체 선언을 참조할 필요가 없다면 전방 선언을 사용하라.

2.5.2 클래스 줄이기
연관 관계를 줄이기 위해서 멤버 함수 대신 비멤버, 비 프렌드 함수를 사용하라.


매니저 클래스는 여러 개의 하위 수준 클래스를 캡슐화함으로써 의존성을 줄일 수 있다.


2.5.5 콜백과 옵저버, 알림
어떤 이벤트가 발생했을 때 이를 다른 클래스에 알리는 작업.

고려해야할 사항
 - 재진입성(Reentrancy) : 재진입하려는 행동을 허용하고 미리 예쌍해서 상태를 일관성 있게 유지 하도록 구현
 - 수명 관리 : 같은 이벤트에 대해 동일한 클라이언트 코드를 여러 번 호출하지 않도록 중복된 클라이언트 등록을 제거 할 수 있는 기능이 필요
 - 이벤트 순서 : API는 클라이언트에게 콜백이나 알림을 보낼 때 그 순서를 명확히 해야 한다.


콜백

콜백은 모듈 A안에 있는 함수를 가리키는 포인터로서, 모듈 A는 모듈B로 전달된 후 B가 적정한 시기에 모듈 A에 있는 함수를 실행시킬 수 있게 한다. 모듈 B는 A에 대해 아는것이 없으며 A와 관련된 어떤 include나 링크를 가지지 않는다.
콜백은 대규모 프로젝트에서 순환 의존고리를 제거하는 매우 유용한 방법이다.
가콜백 함수와 자주 사용되는 클로저(closure)가 있다. 클로저는 모듈 A가 B에게 전달하는 코드 조각으로 모듈 B는 A의 콜백 함수를 호출하는 함수 안에서 클로저를 사용한다.

class ModuleB
{
public :
   typedef void (*Callbacktype)(const std::string &name, void* data);
   void SetCallback(CallbackType cb, void *data);
private :
   CallbackType mCallback;
   void *mClosure;
};

//call
if( mCallback )
   (*mCallback)("Hello World", mClosure);

cf) Boost Lib에서 boost::bind라는 기능 제공


옵저버

객체가 의존하는 객체 목록을 관리하는 소프트웨어 디자인 패턴으로 변경사항이 발생하면 관리되는 객체의 메서드를 호출해서 변경을 알린다.


알림

가장 널리 사용되는 방법은 신호(signal)와 슬롯(slot)이다
신호와 슬롯이라는 개념은 버튼 클릭이나 타이머 이벤트 같이 관련 메서드에 모든 이벤트를 전달하기 위해 만들어진 방법이다.

cf) Boost Lib의 boost::signals와 boost::singlan2등 방법 참고

class MySlot
{
public :
   void operator()() const
   {
      std::out << "MySlot called!" << std::endl;
   }
};

MySlot slot; //생성

boost::signal<void ()> signal; //파라미터 없이 신호 생성

signal.connect(slot); //기호에 슬롯 연결

signal(); //신호를 보내고 모든 슬롯을 호출


2.6 안정화와 문서화, 테스트

좋은 API라면 사용자가 이 API를 통해서 어떤 것들을 할 수 있고 각각의 API가 어떤 행동을 하며 어떻게 하면 최적화를해서 사용할 수 있고 어떤 조건에서 오류가 발생하는지를 명확히 이해할 수 있게끔 구체적인 문서를 제공해야 한다.
또한 새로운 변경이 발생하더라도 기존의 유즈 케이스에는 영향이 미치지 않도록 API 구현 코드를 사용해서 확장된 테스트 자동화 테스트 프로세스를 갖추어야 한다.

0 개의 댓글:

댓글 쓰기