프로그래밍 공부
작성일
2024. 1. 22. 16:09
작성자
WDmil
728x90

4.4 C++디자인의 두 가지 원칙

C++에는 근간이 되는 디자인 원칙이 두 가지 있는데, 추상화재사용이다. 이 두가지 원칙은 너무나 중요해서 책의 테마로 삼는것 도 고려했을 정도 이다. 이 두 가지 원칙은 여러 문헌과 효율적인 C++프로그램 디자인에서 반복적으로 다루고 있다.

 

4.4.1 추상화

추상화(abstraction) 원칙은 비유를 통하면 이해하기 쉽다.

예를 들어 거의 모든 집에 하나씩 있는 텔레비전을 생각해보자. 어떻게 켜고 끄는지, 채널은 어떻게 바꾸고, 소리는 어떻게 조절하는지, 스피커나 DVD 플레이어 같은 오디오 장치는 어떻게 연결하는지 등등 텔레비전 사용 방법은 대부분의 사람이 알 고 있다. 하지만 텔레비전이 내부적으로 어떻게 작동하는지 설명할 수 있는 사람은 별로 없다.

 

공중을 떠다니는 신호를 어떻게 수신하고, 그것을 어떻게 화면과 소리로 변환하는지는 잘 모른다.

텔레비전이 어떻게 작동하는지는 모르지만 이용하는 데는 아무런 문제가 없다. 이것은 텔레비전이 내부 구현과 외부 인터페이스를 꺠끗하게 분리했기 때문이다. 우리는 전원버튼, 채널 전환기, 볼륨 제어기와 같은 인터페이스를 통해 텔레비전과 상호 연동한다.

 

텔레비전이 어떻게 작동하는지는 알지 못하고 알 필요도 없다. 텔레비전이 화면에 그림을 그리는 방법이 음극선을 이용하는지 외계인의 기술인지 상관하지 않는다. 왜나하면 그러한것 들은 인터페이스와 전혀 관계가 없기 때문이다.


4.4.1.1 추상화의 이점

추상화가 보여준 텔레비전 에에서의 장점은 소프트웨어에도 똑같이 적용된다. 주어진 코드는 그 구현 내용을 알지 못해도 사용할 수 있다. 단순한 예로 작성하려는 프로그램이 <cmath>에 선언된 sqrt()함수를 이용할 때 실제 제곱근 계산 알고리즘이 어떻게 되는지 알 필요는 없다. 사실 제곱근 알고리즘은 라이브러리 릴리스 버전마다 조금씩 바뀐다.

 

하지만 인터페이스 에 변화가 없는 한 sqrt()함수를 이용하는 프로그램은 바뀔 필요가 없다. 추상화 원칙은 클래스에도 바로 적용된다.

 

ostream클래스의 cout 객체가 출력될때 표준출력 장치로 다음과 같은 데이터를 출력한다고 해보자.

cout << "This call will display this line of thext" << endl;

위 라인에서는 cout의 문서에 설명된 인터페이스에 맞추어 입력 연산자(<<)를 문자 배열과 함께 사용했다.

이때 cout이 사용자 화면에 문자를 어떻게 그리는지는 알 필요 없다. 알아야 할 것은 인터페이스 뿐이다.

cout의 내부 구현 방식은 인터페이스가 그대로 이기만 하면 얼마든지 바꿀 수 있다.

 

4.4.1.2 추상화 디자인

함수와 클래스를 디자인할 때는 다른 프로그래머가 그 구현 내용을 알 필요가 없도록 해야한다.

구현 내용이 인터페이스 뒤로 숨겨진 추상화 디자인과 그러지 않은 디자인의 차이점을 이해하기 위해 채스 게임 프로그램을 생각해 보자.

 

체스판을 8x8 2차원 배열로 선언하고 각 항목에 체스 말의 포인터가 할당되도록 디자인했다면 체스판이 다음과 같이 선언된다.

ChessPiece* chessBoard[8][8];
...
ChessBoard[0][0] = new Rook();

 

그런데 이러한 방식으로는 추상화를 성립하지 못한다 모든 프로그래머가 체스판이 2차원 배열로 구현되어있다는 것을 알아야 한다. 만약 2차원 배열 대신 벡터 배열로 구현방식을 바꾸면 전체 프로그램에서 체스판을 이용하는 코드마다 수정이 필요해진다.

인터페이스와 구현이 분리되지 않은것 이다.

 

체스판을 클래스로 만들면 더 좋아진다. 클래스 정의는 인터페이스로 개방되고 상세 구현 내용은 숨겨진다.

class ChessBoard
{
	public:
    	// 생성 소멸자 생략
    	void setPieceat(ChessPiece* piece, int x, int y);
        ChessPiece& getPieceAt(int x, int y);
        bool isEmpty(int x, int y) const;
    private:
    // 데이터 맴버 생략
};

 

ChessBoard 클래스의 인터페이스는 내부 구현이 어떠해야 하는지 아무것도 강제하지 않는다.

체스판이 2차원 배열로 구현되어 있을 수 있지만, 인터페이스로부터는 그러한 구현 방식에 대한 힌트조차 얻을 수 없다.

따라서 체스판의 구현 방식이 바뀌어도 인터페이스는 바뀌어야 할 이유가 없다. 더욱이 내부 구현 차원에서 추가적인 기능들( 예를 들면 체스말이 경계를 벗어났는지 검사하는 등) 이 인터페이스의 정의와는 독립적으로 존재할 수도 있다.

 

이 구현에서 인터페이스 때문에 추가적인 노력이 들어간 부분은 하나밖에 없다. getPieceAt()함수는 참조형 변수를 리턴하고 있기 때문에 내부 구현 코드가 벡터와 같은 컬렉션 클래스에 포인터나 스마트 포인터를 저장하는 대신 직접 객체를 저장하는 것을 피하는것이 좋다.

 

컬랙션 클래스에 객체를 바로 저장하면 해결하기 힘든 기괴한 에일리어싱 문제에 부딛힐 수 있다.

 

에를들어 ChessBoard클래스를 이용하는 외부 모듈이 getPieceAt()함수로부터 받은 참조형 변수를 자체적으로 저장한다면, ChessBoard 클래스가 내부의 컬렉션 객체를 새로 생성했을 때 외부에서 저장한 참조형 변수는 더는 유효한 객체를 가리키지 않게 된다. 컬렉션 클래스에 포인터나 스마트 포인터를 저장하면 이러한 참조형 변수의 무효화 문제(reference-invalidation) 을 피할 수 있다.

 

 

728x90