제목: “이번 주의 팁 #218: FTADLE로 확장 지점 설계하기”
원문 게시일: 2023년 1월 19일
작성자: Andy Soffer
빠른 링크: abseil.io/tips/218
확장 지점 설계하기
가정해 봅시다. 여러분은 sketchy
라는 라이브러리를 개발 중이고, 이 라이브러리는 캔버스에 그림을 그리는 기능을 제공합니다. 이미 점, 선, 텍스트 같은 기본 도형을 그리는 기능을 제공하지만, 사용자가 자신만의 타입을 그리는 방식을 정의할 수 있도록 확장 지점(extension point)을 설계하려고 합니다.
확장 지점 설계 목표
C++는 확장 지점을 정의할 수 있는 다양한 메커니즘을 제공하지만, 각 방식마다 장단점이 있습니다. 확장 지점을 설계할 때 고려해야 할 중요한 요소는 다음과 같습니다:
- 가독성: 엔지니어가 라이브러리와 확장의 관계를 얼마나 쉽게 이해할 수 있는가?
- 유지보수성: 라이브러리 및 사용자 요구사항이 변할 때 확장 지점을 얼마나 쉽게 변경할 수 있는가?
- 의존성 관리: 확장 지점이 사용자 바이너리에 불필요하게 라이브러리를 링크하도록 요구하는가?
- ODR(One Definition Rule) 위반 방지: 확장 지점이 프로그램의 여러 부분에서 모순된 정의를 갖게 되는 위험을 피할 수 있는가?
FTADLE: 훌륭한 이름을 가진 좋은 패턴
확장 지점을 정의할 때는 FTADLE(Friend Template Argument Dependent Lookup Extension) 패턴을 사용하는 것이 좋습니다. FTADLE 패턴은 가독성, 유지보수성, 의존성 관리, ODR 위반 방지 측면에서 뛰어난 성능을 제공합니다. 이 패턴은 ADL(Argument Dependent Lookup)이라는 기능을 적극적으로 활용합니다. ADL에 대한 자세한 설명은 Tip #49를 참고하세요.
FTADLE 설계 단계
- 확장 지점의 이름을 정하고 프로젝트의 네임스페이스를 접두사로 붙입니다. 예를 들어,
sketchy
프로젝트에서 “그리기” 확장을 위해SketchyDraw
라는 이름을 사용합니다. - 확장 지점에서 사용될 타입을 설계합니다. 여기서는 사용자가 도형을 그리는 데 사용할
sketchy::Canvas
입니다. - 기능을 오버로드 집합(overload set)으로 구현합니다. 기본 지원 타입(
sketchy::Point
,sketchy::Line
)을 처리하는 비템플릿 함수와 확장 지점(SketchyDraw
)을 호출하는 템플릿 함수를 함께 만듭니다.
FTADLE 구현 예제
namespace sketchy {
// 기본 타입인 Point를 그립니다.
void Draw(Canvas& c, const Point& p);
// 기본 타입인 Line을 그립니다.
void Draw(Canvas& c, const Line& l);
// 확장 지점을 호출하는 템플릿 함수
template <typename T>
void Draw(Canvas& c, const T& value) {
SketchyDraw(c, value); // ADL을 통해 적절한 오버로드를 찾습니다.
}
} // namespace sketchy
사용자 정의 타입 확장
사용자는 자신만의 타입을 확장하려면 friend 함수 템플릿을 추가하고 SketchyDraw
를 구현하면 됩니다. 예를 들어 Triangle
타입을 확장하는 경우:
class Triangle {
public:
explicit Triangle(Point a, Point b, Point c) : a_(a), b_(b), c_(c) {}
template <typename SC>
friend void SketchyDraw(SC& canvas, const Triangle& triangle) {
sketchy::Draw(canvas, sketchy::Line(triangle.a_, triangle.b_));
sketchy::Draw(canvas, sketchy::Line(triangle.b_, triangle.c_));
sketchy::Draw(canvas, sketchy::Line(triangle.c_, triangle.a_));
}
private:
Point a_, b_, c_;
};
// 사용 예제
void DrawTriangles(sketchy::Canvas& canvas, absl::Span<const Triangle> triangles) {
for (const Triangle& triangle : triangles) {
sketchy::Draw(canvas, triangle);
}
}
피해야 할 설계 패턴
가상 함수
가상 함수와 클래스 계층 구조는 매우 경직되어 있어 변경이 어렵습니다. 기본 클래스와 파생 클래스는 항상 동시에 업데이트되어야 합니다. 또한, 가상 함수는 사용자에게 불필요한 의존성을 강요합니다.
멤버 함수 검사
템플릿 메타프로그래밍을 사용하면 특정 멤버 함수의 존재 여부를 확인할 수 있지만, 이름 충돌이 발생할 수 있습니다:
template <typename Image>
void DisplayImage(const Image& image) {
image.draw();
}
class Cowboy {
public:
void draw(); // 권총을 뽑는 함수
};
Cowboy c;
DisplayImage(c); // 의도하지 않은 동작
FTADLE 패턴은 프로젝트 네임스페이스를 접두사로 사용해 이러한 충돌을 방지합니다.
템플릿 특수화
템플릿 특수화는 ODR(One Definition Rule) 위반 위험이 큽니다. 동일한 타입에 대해 다른 정의를 제공하거나 중복 정의될 경우 프로그램은 정의되지 않은 동작(UB)을 초래합니다.
결론
FTADLE 패턴은 가독성, 유지보수성, ODR 위반 방지 측면에서 뛰어나며, 의존성 문제를 해결합니다. 확장 지점을 설계해야 한다면 FTADLE을 적극적으로 사용해 보세요.