설계 및 선언 🔐

안녕하세요! 두두코딩 입니다 ✋
오늘은 좋은 인터페이스 만들기에 대해 알아보겠습니다.

이전 포스팅을 참고하면 내용이해가 훨씬 수월합니다!

🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!

궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇

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

사용자 쪽에서 외워서 사용하도록 만들면 사용자가 잘못 사용하기 쉬워진다.

// factory 함수라 가정
Investment* createInvestment();

위의 함수를 사용할 때, 자원 누출을 피하기 위해 createInvestment에서 얻은 포인터를 나중에 삭제해야한다. 하지만 위와 같이 함수를 구성할 경우 사용자가 실수로 자원을 해제하지 않아 자원 누수가 발생할 수 있거나 똑같은 포인터에 대해 delete가 두 번일어 날 수 있다.

이전 포스팅을 통해 auto_ptr 또는 tr1::shared_ptr를 할당 받아 해당 포인터를 직접 삭제하는 것이 아니라 스마트포인터를 통해 삭제하도록 구성했다. 하지만 이런 스마트포인터를 만든다는 것을 잊어먹고 프로그램을 완성하게 되면 어떻게 될까? 고치기가 힘들어진다. 따라서, 인터페이스를 만들때 부터 스마트포인터를 반환하도록 해 사용자에게 사용하도록 지시하는 것이 좋은 인터페이스 설계방법이다.

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

위와 같이, 원시포인터를 반환받는 것이 아니라, 스마트포인터를 반환받도록 구성해두면 사용자가 깜빡하고, 스마트포인터를 안만들어 자원 누수를 하는 케이스를 배제시킬 수 있다.

사실 tr1::shared_ptr를 반환하는 구조는 자원 누수를 막는 것 뿐 아니라 상당수의 사용자 실수를 사전 봉쇄 할 수 있는 장점이 있다. 본인이 인터페이스 설계자라면 적극 기용하도록 하자!🙄

tr1::shared_ptr 특징

이야기가 나온 김에, tr1::shared_ptr의 몇 가지 특징을 알아보자. 특징을 알아보면서, 실제 인터페이스를 구현할 때, 왜 해당 스마트 포인터를 이용하면 좋은지 살펴보도록 하자.

우리가 앞서 이야기 했듯, shared_ptr을 이용할 경우 자원해제 관련하여 발생하는 문제를 사전에 막을 수 있다. 또한, 삭제자를 지정할 수 있어, 꼭 delete만 사용하는 것이 아닌 delete를 커스텀 할 수 있다는 장점이 있다. 예를들어, createInvsetment를 통해 Investment*를 직접 delete를 통해 삭제하지 않고, getRidOfInvestment라는 함수를 만들어 share_ptr의 삭제자로 지정할 경우, 자원해제 지정된 함수를 호출하도록 변경할 수 있다.

해당 방법을 사용할 때, 스마트 포인터를 반환할 때, 삭제자가 지정된 스마트 포인터를 반환해야한다는 점이다. 그렇지 않을 경우, 일반적으로 사용하는 delete를 호출할 수도 있기 때문이다.

아래와 같이, 함수 내에서 shared_ptr를 선언하고, 삭제자가 지정된 해당 값을 반환하도록 해야한다.

std::tr1::shared_ptr<Investment> createInvestment() {
	std::tr1::shared_ptr<Investment>
		pInv (0, getRidOfInvestment);

	pInv = ...;

	return pInv;
}

위의코드를 수행할 경우 선언부분에서 컴파일 에러가 발생한다. tr1::shared_ptr에서 첫번째 인자는 “포인터”를 전달받도록 해야한다. 0 일 경우 int형 타입이다. 물론 암시적으로 null 포인터로 전달받을 수 있지만 shared_ptr 같은 경우 암시적 형 변환이 불가능 하기 때문에 Investment* 타입을 전달해야한다. 이 문제를 해결하기 위해서는 static_cast를 통해 형변환으로 해당 값을 Investment* 타입으로 변환해주어야한다. satic_cast 같은 경우 후에 좀 더 자세히 다루도록 하자.

std::tr1::shared_ptr<Investment> createInvestment() {
	std::tr1::shared_ptr<Investment> pInv (static_cast<Investment*>(0),
																				 getRidOfInvestment);

	pInv = ...;

	return pInv;
}

위의 코드와 같이 작성하게 되면 컴파일 에러 없이 정상동작하는 것을 확인할 수 있다.

tr1::shared_ptr을 사용하는 특징 중 가장 좋은 특징은 교차 DLL 문제를 막을 수 있다는 점이다.

객체 생성 시, 어떤 DLL을 활용해 new를 사용할 경우 기존에 사용하던 DLL의 delete가 아닌 다른 DLL의 delete를 사용하는 경우를 교차 DLL 문제라고 한다. 우리가 tr1::shared_ptr를 활용하면, 해당 문제 없이 new를 처음 사용했던 DLL에서 무조건 delete를 사용하도록 구현되어져 있다. (즉, tr1::shared_ptr를 적극 사용하라.)

Tip DLL이란 Dynamically Linked Library의 줄임말로, 동적 링크 라이브러리라고 부른다. 우리가 일반적으로 Library를 사용하기 위해 lib 파일들을 소스 코드내 추가하는 것이 아니라, 메모리 용량을 줄이기 위해 위치를 알려주고 해당 위치에서 library에 접근하도록 한다.

To Sum Up

👉 좋은 인터페이스는 제대로 쓰기에 쉬우며 엉터리로 쓰기에 어렵게 만드는 것이다.

👉 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 기본제공 타입과의 동작 호환성 유지하기가 존재한다.

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

👉 tr1::shared_ptr은 사용자 정의 삭제자를 지원한다. 해당 특징 때문에 교차 DLL 문제를 막을 수 있다.