friend는 단순한 문법 요소를 넘어섰다
이제 friend는 C++ 언어 내에서 단지 "private 멤버에 접근하게 해주는 키워드"를 넘어서, 고도로 제어된 신뢰 관계 설정 수단으로 자리 잡고 있다. 시간이 지날수록 그 사용은 더 엄격히 규정되고, 오히려 필요성이 커졌다.
C++98: 기본적인 접근 권한 부여
초기 C++에서는 friend가 매우 단순했다. 클래스 외부의 함수나 클래스에 대해 명시적으로 private/protected 멤버 접근 권한을 부여하는 역할만 수행했다.
class Database {
std::string secret_key;
friend void initialize_db(Database& db);
};
이 시절에는 템플릿도 제약이 많았고, 모듈 같은 개념은 존재하지 않았다. friend는 오직 헤더 파일 범위 내에서 동작하는 문법적 도구였다.
C++11: 템플릿과의 융합으로 진화
C++11은 friend의 활용 가능성을 크게 확장시켰다. 특히 템플릿 친구 선언(template friendship)이 강화되면서 제네릭 라이브러리 설계에 핵심 요소가 되었다.
template <typename T>
class Container {
T* data;
size_t count;
template <typename U>
friend class Container; // 모든 Container 특수화가 서로 friend
};
또한 다음과 같이 특정 함수 템플릿의 인스턴스를 친구로 지정하는 것도 가능해졌다.
template <typename T>
void debug_dump(const Container<T>& c); // 선언
template <typename T>
class Container {
friend void debug_dump<>(const Container<T>&); // 해당 인스턴스만 friend
};
이러한 기능은 STL 계열의 컨테이너와 알고리즘 분리 설계 철학을 실현하는 데 없어서는 안 될 요소가 되었다.
이동 의미론과의 통합
이동 생성자와 이동 대입 연산자가 등장하면서, 성능 최적화를 위해 friend가 필수적이게 되었다. 대표적인 예로 swap 구현이 있다.
class ResourceHolder {
void* ptr;
size_t size;
friend void swap(ResourceHolder& a, ResourceHolder& b) noexcept {
using std::swap;
swap(a.ptr, b.ptr);
swap(a.size, b.size);
}
};
이처럼 내부 자원을 직접 교환해야 하는 상황에서, 캡슐화를 유지하면서도 최적의 성능을 내기 위해서는 friend 외에 마땅한 수단이 없다.
C++17: 인라인 비멤버 함수 패턴의 확산
클래스 내부에 직접 정의되는 friend 함수는 암시적으로 인라인이 되며, ADL(Argument-Dependent Lookup)에도 참여한다. 이는 연산자 오버로딩에 이상적인 패턴이 되었다.
class Vector3 {
float x, y, z;
public:
Vector3(float x, float y, float z) : x(x), y(y), z(z) {}
friend Vector3 operator+(Vector3 lhs, const Vector3& rhs) {
lhs.x += rhs.x;
lhs.y += rhs.y;
lhs.z += rhs.z;
return lhs;
}
friend bool operator==(const Vector3& a, const Vector3& b) {
return a.x == b.x && a.y == b.y && a.z == b.z;
}
};
이 방식은 헤더 전파 문제를 줄이고, 논리적으로 관련된 함수들을 자연스럽게 묶어준다.
C++20: 모듈(Module) 환경下的 정교한 제어
모듈이 도입되면서 friend의 가시성 범위가 근본적으로 변화했다. 이제 친구 관계는 모듈 내부로 한정될 수 있다.
export module Graphics;
class Renderer {
int context_id;
friend class RenderPassBuilder; // 모듈 외부에선 보이지 않음
};
class RenderPassBuilder { /* ... */ }; // export하지 않음
외부 소비자는 Renderer의 인터페이스만 사용할 수 있고, 내부 협력 클래스는 전혀 노출되지 않는다. friend는 더 이상 "전역 범위의 특권 부여"가 아니라, 모듈 경계 내부의 신뢰 도메인 정의 수단이 된 것이다.
컨셉트(Concepts)와의 결합
C++20의 컨셉트를 활용하면, 조건부로 친구 관계를 설정할 수 있다.
template <typename T>
concept Loggable = requires(const T& t) {
std::cout << t;
};
class SecureData {
std::string content;
template <Loggable T>
friend void log_debug(const T& value); // 로깅 가능한 타입만 친구
};
이렇게 하면 단순한 이름 기반 접근 허용을 넘어, 행위적 요구사항을 충족하는 경우에만 허용하는 정교한 제어가 가능하다.
C++23 및 이후: 여전히 중심에 있는 friend
최신 표준 라이브러리에서도 ranges, views, coroutines 등의 내부 구현에는 friend가 활발히 사용된다. 예를 들어, 커스텀 할당기와 컨테이너 간의 저수준 상호작용은 종종 friend 없이는 불가능하다.
현대 C++은 "강한 캡슐화 + 제로 오버헤드 + 고성능"을 동시에 추구하는데, friend는 이 세 가지 목표를 조화시키는 핵심 매개체다.
설계 철학의 변화
과거에는 friend를 "캡슐화 위반"으로 보는 시각도 있었지만, 지금은 다르다. 대신 다음과 같은 인식이 일반화되었다:
friend는 캡슐화를 파괴하는 것이 아니라,
추상화 내부의 협력 구성요소들 사이의 신뢰 영역(trusted domain)을 정의하는 도구다.
복잡한 추상화는 하나의 클래스로 구성되지 않는다. 여러 클래스, 함수 템플릿, 타입 트레잇들이 함께 작동해야 한다. friend는 이러한 내부 구성요소들이 안전하고 효율적으로 협업할 수 있도록 해준다.
최신 C++에서의 모범 사례
- 먼저 일반 비멤버 함수를 고려하라
- private 접근이 필요하면
friend를 사용하라 - 친구 관계는 동일한 논리적 모듈 또는 컴포넌트 내에 한정하라
- 간단한 함수는 클래스 내에서 인라인 정의하라
- 모듈 외부로의 과도한 노출은 피하라
결론: friend는 여전히 살아있다
시간이 지날수록 friend는 약화되지 않았다. 오히려 언어의 정교함과 함께 진화하며, 다음과 같은 핵심 역할을 수행하고 있다:
- 제네릭 프로그래밍의 기반이 됨
- 값 의미론(value semantics) 설계의 핵심 도구
- 모듈 내부의 안전한 협업을 위한 다리 역할
이제 friend는 단순한 예약어가 아니라, C++의 zero-cost abstraction 철학을 실현하기 위한 필수적인 언어 메커니즘이다.