[Effective C++] 항목 18 : 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자

수민이슈·2023년 3월 22일
0

Effective C++

목록 보기
18/30
post-thumbnail

스콧 마이어스의 Effective C++을 읽고 개인 공부 목적으로 요약 작성한 글입니다!

💡 인터페이스는 제대로 쓰기에 쉬우며 엉터리로 쓰기에 어렵다는 특성을 유지하도록 설계하자!
💡 인터페이스의 올바른 사용을 이끄는 방법으로는,
인터페이스 사이의 일관성 잡아주기, 기본제공 타입과의 동작 호환성 유지하기 등이 있다!
💡 사용자의 실수를 방지하는 방법으로는
새로운 타입 만들기, 타입에 대한 연산 제한하기, 객체의 값에 제약 걸기, 자원 관리 작업을 사용자 책임으로 놓지 않기 등이 있다.
💡 tr1::shared_ptr는 사용자 정의 삭제자를 지원한다.
이 때문에 tr1::shared_ptr는 교차 DLL 문제를 막아주며, 뮤텍스 등을 자동으로 잠금해제하는데 쓸 수 있다!


🖊️ 소프트웨어 설계

: 소프트웨어가 내가 원하는 동작을 하도록 틀을 짜는 방법.

제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게

이 지침을 기반으로
정확성, 효율, 캡슐화, 유지보수성, 확장성, 규약 준수를 더해서
설계해야 한다.


🖊️ 사용자의 실수를 잡아주자

함수, 클래스, 템플릿이 다 ~ 인터페이스다!

코드를 잘못 작성했을 경우, 잘못됐다고 알려줘야 하고
생각한대로 동작하지 않는다면, 그 코드는 컴파일되지 않아야 한다.
따라서
어떤 코드가 컴파일된다면, 원하는 대로 동작해야 한다.

예시 상황

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

이런 상황이 있다고 했을 때, 오류가 발생할 여지가 많다.

Date d(30, 3, 2023);				// 매개변수의 전달 순서가 잘못된 경우
Date d(3, 40, 2023);				// 유효범위를 벗어난 매개변수가 들어온 경우

해결방법 1 : Wrapper 클래스로 타입 에러 잡기

struct Day {
	explicit Day(int d) : val(d) {};
    
    int val;
};

struct Month {
	explicit Month(int m) : 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(30, 3, 2023);							// Error
Date d(Day(30), Month(3), Year(2023));			// Errpr
Date d(Month(3), Day(30), Year(2023));			// OK

이렇게 해주면 사용자가 신경써서 넣어줘야 하니까
매개변수의 타입 에러 (순서 등)를 잡을 수 있다.

근데 아직 2023년 14월 41일의 문제는 잡지 못했다

해결방법 2: 타입 값에 제약 걸기

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);
};

Date d(Month::Mar(), Day(30), Year(1995));

enum을 써서 유효 범위를 지정해도 되긴 하지만 안정성 측면에서는 그닥 비추다.

이렇게 하면 1~12월로
Month 타입의 객체에 유효 범위를 지정해줄 수 있다.

Month값이 새로 생성되지 않도록 생성자는 private으로 지정해줬다.

비지역 정적 객체에 대한 초기화 순서는 정해져 있지 않기 때문에 (항목 4)
함수를 사용해서 Month를 표현했다 (객체가 아닌 점)


🖊️ 일관성있는 인터페이스를 제공하자

사용자 정의 타입은 기본 제공 타입과 유사하게 동작하게 하자

STL 컨테이너의 인터페이스를 보면
.size() 멤버함수만 넣으면 컨테이너의 원소의 개수를 리턴해준다
어떤 컨테이너든 간에 통용되는 멤버 함수다

사용자가 쉽게 사용할 수 있는 인터페이스가 굿이다
그래서 일관성을 갖도록 설계하는게 중요하다.

예시 상황

Investment* createInvestment();

이 함수는 자원관리할 때 계 ~ 속 예시로 썼던 함순데
Investment 타입의 객체를 동적할당하고 그 포인터를 반환하는 함수다.
그러면 동적 할당을 했으니까 그 포인터를 언젠가는 꼬옥,, 해제해줘야 한다.

근데 사용자는 실수를 할 수 있잖아?

  • delete를 하지 않는 경우
  • 똑같은 포인터에 대해 delete를 두 번 이상 적용하는 경우

첫 번째에는 메모리 누수가 발생하고
두 번째에는 댕글링 포인터 문제가 발생하겠지

그래서
createInvestment()의 반환값을 스마트포인터에 저장하고, 스마트포인터에게 삭제 작업을 떠넘기자고 했다.
근데
스마트포인터를 사용해야 한다는 것도 까먹어버리면?

해결방법 : 팩토리 함수가 스마트 포인터를 반환하도록 하자

그래서,,
그냥
ㅋㅋ
처음부터 createInvestment() 함수가 스마트포인터를 반환하게 하자

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

어떤디.
이렇게 하면 무조건 동적할당한 객체의 포인터는 shared_ptr의 타입으로 반환해줄거고
Investment 타입의 객체가 불필요해지면 스마트포인터가 알아서 삭제해줄거다.
그래서 메모리 누수 걱정은 안해도 될 것만 같다.

shared_ptr는 삭제자를 지원한다

그니까
shared_ptr는 생성 시점에
자원을 해제할 때. 그니까 참조 카운트가 0이 될 때 호출해줄 함수를 지정할 수 있다.

그래서
shared_ptr는 nullptr를 물게 하고, 삭제자로 getRidOfInvestment를 갖도록 해보자

std::tr1::shared_ptr<Investment> createInvestment() {
	std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
    
    retVal = ...;
    return retVal;
}

retVal을 생성할 때

첫 번째 인자는 포인터로 관리할 실제 포인터인데,
null을 물게 하고 싶은데 Investment* 타입의 포인터를 넣어줘야 하니까
static_cast로 형변환해서 집어넣는다.
두 번째 인자는 삭제자이다.

근데
retVal로 관리할 실제 객체의 포인터를 결정하는 시점이 retVal을 생성하는 시점보다 앞설 수 있다면,
실제 객체의 포인터를 바로 retVal의 생성자에 넘겨버리는게 낫다.
지금은 retVal을 null로 초기화하고 대입하는 방식이다.

shared_ptr는 교차 DLL 문제를 방지한다.

shared_ptr는 포인터별 삭제자를 자동으로 쓰므로, 교차 DLL 문제를 방지해준다.

그게 뭐냐면

객체를 생성할 때 어떤 DLL(동적 링크 라이브러리 : dynamically linked library)의 new를 썼는데
그 객체를 삭제할 때는 이전 DLL과 다른 DLL의 delete를 쓰는 경우이다.
그니까
new/delete가 다른 DLL에서 실행되면 런타임 에러가 나는 문제를 말한다.

예를 들어서

std::tr1::shared_ptr<Investment> createInvestment() {
	return std::tr1::shared_ptr<Investment>(new Stock);
}

이 함수가 반환하는 shared_ptr는 다른 DLL들 사이에서 뒹굴어도 교차 DLL 문제는 신경안써도 된다.
Stock 객체를 가리키는 shared_ptr는 Stock 객체의 참조 카운트가 0이 될 때
어떤 DLL의 delete로 삭제해야 되는지 알고있당.


그니까 어쨋든...

tr1::shared_ptr를 사용하면 사용자가 저지를 수 있는 실수들을 미연에 방지할 수 있으니까
웬만하면 shared_ptr를 사용하도록 하자.

shared_ptr를 구현한 것들 중 좀 흔히 쓰이는게 boost 라이브러리이다


😊 느낀점

와우
이거 실환가
인터페이스 설계로 넘어오니까 좀 어질어질하다
어렵긴하다
여러 번 봐야겠당.

0개의 댓글