728x90

18. 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자

제목과 같이 제대로 쓰기엔 쉽고, 엉터리로 사용하기 어렵게 하려면 사용자가 저지를 실수에 미리 예상해야 합니다. 그리고 그 실수에는 컴파일되지 않게 해야 합니다.

생일을 나타내는 클래스가 있습니다. 이 클래스는 생성할 때 년, 월, 일을 전달받습니다.

#include <iostream>
using namespace std;

class Birth {
public:
  Birth(int year, int month, int day);
};

int main()
{
  Birth myBirth(1996,9,21);
}

하지만 사용자의 실수 혹은 여러 요인에 따라 순서를 다르게 넣을 수 있습니다. 아래의 경우 컴파일은 정상적으로 되지만 실제 동작은 설계자가 의도하지 않은 대로 흘러갈 것입니다.

#include <iostream>
using namespace std;

class Birth {
public:
  Birth(int year, int month, int day);
};

int main()
{
  Birth myBirth(1996,9,21);
  Birth wrongBth(9,21,1996);
}

이런 실수를 방지하기 위해 할 수 있는 방법은 넘겨주는 타입을 클래스 랩퍼로 명시해 컴파일 단에서 오류를 내 실수를 방지하는 것입니다.

다른 방법으로는 const 붙이기 방법이 있습니다. 예전 예제에서 봤던 operator* 예제가 이에 해당합니다.

class Test { };

const Test operator*(const Test& lhs, const Test& rhs);

int main()
{
    Test a,b,c;
    
    if ( a * b = c ) {		// 원래 비교 연산자를 사용하려 했지만 대입이 되어 버린 상황
    }
}

인터페이스를 일관성 있게 제공해야 한다는 점을 가장 잘 이해하기 위해서는 STL을 생각하면 편합니다. STL의 경우 size 함수 하나로 원소의 개수를 알 수 있습니다.

인터페이스를 사용자가 호출 후 어떤 동작을 해주지 않으면 문제가 될 수 있는 설계는 좋지 않습니다. 예전 예제에서 했던 팩토리 함수가 그 예입니다. create함수를 통해 동적 할당한 포인터 객체를 넘기게 되는데 이는 나중에 사용자가 해제해줘야 하는 번거로움이 있었습니다. 아니면 이 생성된 객체를 스마트 포인터에 넘기곤 했습니다. 이런 번거로움 없이 바로 스마트 포인터를 넘긴다면 사용자는 더 편하지 않을까요?

19. 클래스 설계는 타입 설계와 똑같이 취급하자

c++에서 클래스를 설계하는 것은 하나의 타입을 설계하는 것과 다르지 않습니다. 그리고 타입 설계자에 맞게 함수, 연산자를 오버로드 해야하며 메모리 관리 그리고 초기화 및 정상 종료 등을 처리해야 합니다. 효과적인 클래스를 설계하기 위해서는 아래와 같은 고려사항들이 존재합니다.

  • 새로 정의한 타입에 대한 객체 생성 및 소멸에 대하여
  • 객체 초기화 및 대입에 대하여
  • 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에는 어떤 의미를 줄지에 대하여
  • 새로운 타입이 가질 수 있는 적절한 값의 범위와 제약에 대하여
  • 기존 클래스 상속에 대하여
  • 타입 변환에 대하여
  • 연산자 오버로드 및 멤버 함수에 대하여
  • 컴파일러가 기본으로 생성하는 함수의 접근성에 대하여
  • 접근 권한에 대하여
  • 선언되지 않은 인터페이스에 대하여
  • 일반적이며 꼭 필요한지에 대하여

20. 값에 의한 전달보다는 상수 객체 참조자에 의한 전달 방식을 택하는 편이 대개 낫다

값에 의한 전달은 인자로 전달된 객체의 사본을 통해 초기화되고 호출한 쪽은 반환한 값의 사본을 전달 받습니다. 결국 클래스의 멤버 변수 및 상속 관계에 따라 여러 번의 생성자와 소멸자들이 호출되게 됩니다.

#include <iostream>

class Human {
private:
  std::string sName;
  std::string sAddress;
};
class Student : public Human {};

bool checkStudent(Student s);

위 연산 과정을 줄이기 위해서는 참조자를 통해 전달 그리고 const를 통해 전달된 객체의 불변성을 보장하게 합니다.

#include <iostream>

class Human {
private:
  std::string sName;
  std::string sAddress;
};
class Student : public Human {};

bool checkStudent(const Student& s);

그리고 값에 대한 전달에 가장 큰 문제 중 하나는 함수 내에서 객체를 생성하여 처리하기 때문에 상속 관계에 있는 클래스의 기본 클래스를 인자로 전달 받는 경우 복사 손실 문제가 일어날 수 있습니다.

#include <iostream>

class Human {
private:
  std::string sName;
  std::string sAddress;
};
class Student : public Human {};

bool checkStudent(Human s);

int main()
{
  Student s;
  checkStudent(s);
}

위와 같이 인자로 기본 클래스를 전달받고 호출부분에 파생 클래스를 넘겨주더라도 함수 안에서 기본 클래스를 통해 생성되며 그에 해당하는 멤버 변수들과 함수가 호출되게 됩니다. 하지만 상수 참조자를 통해 전달하면 새로 생성하는 과정이 없기 때문에 위와 같은 문제를 방지할 수 있습니다.

하지만 무조건적인 참조자를 통한 전달을 선택하는 것이 아닙니다. STL 반복자, 함수 객체 타입, 기본 제공 타입의 경우에는 값에 의한 전달을 하는 것이 더욱 효율적입니다. 이처럼 상황에 맞는 전달 방식을 사용하는 것이 중요합니다.

728x90

+ Recent posts