본문으로 건너뛰기

안동민 개발노트

본문 시작

프렌드 함수와 프렌드 클래스

friend로 특정 외부 함수나 클래스에 내부 접근을 허용하는 방법과 캡슐화를 지키는 제한적 사용 원칙을 익힙니다.

이전 절 ‘연산자 오버로딩 기초’에서 friend 키워드를 간단히 사용했습니다.

이번 절에서는 friend 문법 자체를 독립적으로 다룹니다.

friend는 캡슐화를 일부 우회하는 문법이므로, 정확한 용도와 한계를 알고 사용하는 것이 중요합니다.

HTML 다이어그램: /docs/cpp/ch9/ch9-7/1.html

friend란 무엇인가?

friend클래스 외부의 특정 함수 또는 클래스에게 해당 클래스의 private/protected 멤버 접근 권한을 부여하는 키워드입니다.

즉, “이 함수(또는 이 클래스)만 특별히 내부 구현에 접근하도록 허용”하는 선언입니다.


프렌드 함수 (friend function)

프렌드 함수는 클래스의 멤버 함수는 아니지만, 멤버처럼 내부 데이터에 접근할 수 있습니다.

프렌드 함수 기본 예시
#include <iostream>

class Box {
private:
    int width;
    int height;

public:
    Box(int w, int h) : width(w), height(h) {}

    // area 함수에 private 접근 권한 부여
    friend int area(const Box& b);
};

int area(const Box& b) {
    return b.width * b.height; // private 멤버 접근 가능
}

int main() {
    Box b(3, 4);
    std::cout << area(b) << "\n"; // 12
}

프렌드 함수는 객체를 호출 주체(this)로 갖지 않으므로, 좌측 피연산자가 클래스 객체가 아닌 경우(예: std::cout << obj)에 특히 유용합니다.


프렌드 클래스 (friend class)

특정 클래스 전체에 접근 권한을 주고 싶을 때는 friend class를 사용합니다.

프렌드 클래스 예시
#include <iostream>
#include <string>

class SecretVault {
private:
    std::string code{"A1B2C3"};

    friend class VaultInspector; // VaultInspector는 private 접근 가능
};

class VaultInspector {
public:
    void printCode(const SecretVault& vault) const {
        std::cout << "Vault code: " << vault.code << "\n";
    }
};

int main() {
    SecretVault vault;
    VaultInspector inspector;
    inspector.printCode(vault);
}

프렌드 클래스는 그 클래스의 모든 멤버 함수에 접근 권한을 줍니다.

그래서 권한 범위가 넓습니다.

특정 검사 함수 하나만 내부 접근이 필요하다면 friend void VaultInspector::check(const SecretVault&)처럼 특정 멤버 함수만 여는 설계도 검토할 수 있습니다.


operator<<와 프렌드의 관계

operator<<는 보통 다음 이유로 프렌드 함수로 선언됩니다.

  • 좌측 피연산자가 std::ostream이므로 멤버 함수로 만들기 부적합
  • 객체 내부 상태를 출력하려면 private 접근이 필요한 경우가 많음

다만 출력 연산자가 항상 friend여야 하는 것은 아닙니다.

공개 관찰자 함수로 충분하면 비프렌드 operator<<를 만들 수 있고, 클래스가 출력 정책을 직접 제공해야 한다면 print(std::ostream&) constto_string() 같은 공개 멤버 함수로 분리할 수도 있습니다.

operator<< 프렌드 패턴
#include <iostream>

class Point {
private:
    int x, y;

public:
    Point(int _x, int _y) : x(_x), y(_y) {}
    friend std::ostream& operator<<(std::ostream& os, const Point& p);
};

std::ostream& operator<<(std::ostream& os, const Point& p) {
    os << "(" << p.x << ", " << p.y << ")";
    return os;
}

이 패턴은 ‘연산자 오버로딩 기초’와 함께 이해하면 더 명확합니다.

다형성 계층(기반/파생 클래스)에서 출력 연산자를 설계할 때도 같은 원칙이 적용됩니다.

operator<<의 피연산자를 값(Base)으로 받으면 객체 슬라이싱이 발생할 수 있으므로, 보통 const Base&를 사용합니다.

단, 참조만으로 파생 타입별 출력이 자동으로 바뀌지는 않습니다.

실제 파생 타입의 출력 형식까지 다르게 하려면 기반 클래스에 virtual print(std::ostream&) const 같은 가상 함수가 필요합니다.

이 개념은 10장의 ‘다형성과 가상 함수’와 함께 보면 설계 의도가 더 분명해집니다.

HTML 다이어그램: /docs/cpp/ch9/ch9-7/2.html

중요한 특성: 상속/전이되지 않음

friend 관계는 직관과 달리 전이되지 않습니다.

  • A가 B의 friend라고 해서, B의 friend인 C가 A의 friend가 되지는 않습니다.
  • 기반 클래스의 friend가 파생 클래스의 private 멤버까지 접근할 수 있는 것은 아닙니다.
  • friend로 지정된 클래스의 파생 클래스도 권한을 자동 상속받지 않습니다.
  • friend 선언은 양방향이 아닙니다.

즉, 정확히 선언한 대상에만 권한이 부여됩니다.


friend 사용 시 주의사항

캡슐화 약화

무분별한 friend는 클래스 내부 구현을 외부에 과도하게 노출합니다.

결합도 증가

friend 대상과의 의존성이 강해져 리팩터링이 어려워질 수 있습니다.

최소 권한 원칙

가능하면 friend class보다 friend function처럼 좁은 범위를 우선 고려합니다.

HTML 다이어그램: /docs/cpp/ch9/ch9-7/3.html

대체 설계는 없는가?

항상 friend가 정답은 아닙니다.

  • 읽기 전용 정보라면 불변식을 깨지 않는 의미 있는 관찰자 함수 제공
  • 조작이 필요하면 공개 인터페이스(멤버 함수)로 의도 명시
  • 출력 전용이라면 필요한 정보만 직렬화 함수로 제공

friend는 “공개 API로 표현하기 어려운 제한된 경우”에 선택하는 것이 보통 더 안전합니다.


friend 사용 원칙 요약

friend를 선택하기 전에는 공개 인터페이스로 해결할 수 있는지, 권한 범위가 너무 넓지 않은지 먼저 확인해야 합니다.

아래 결정 카드는 그 판단 순서를 정리한 것입니다.

HTML 다이어그램: /docs/cpp/ch9/ch9-7/4.html
  • friend는 특정 외부 함수/클래스에 내부 접근 권한을 부여한다.
  • 연산자 오버로딩(operator<<)에서 매우 자주 사용된다.
  • 상속/전이/양방향이 아니므로 선언 대상은 명확하지만, friend class는 대상 내부의 접근 범위가 넓다.
  • 강력하지만 캡슐화를 약화시키므로 최소 범위로 제한해 사용해야 한다.

friend는 무조건 피해야 할 문법은 아니지만, 사용할 때마다 권한을 여는 이유와 범위를 문장으로 설명할 수 있어야 합니다.