C++ friend 키워드 | "Friend" 가이드

C++ friend 키워드 | "Friend" 가이드

이 글의 핵심

friend 키워드는 다른 클래스나 함수가 private 또는 protected 멤버에 접근할 수 있도록 허용합니다. 이는 캡슐화를 유지하면서도 특정 외부 함수나 클래스에게 제한적인 접근 권한을 부여하는 메커니즘입니다.

friend란?

friend 키워드는 다른 클래스나 함수가 private 또는 protected 멤버에 접근할 수 있도록 허용합니다. 이는 캡슐화를 유지하면서도 특정 외부 함수나 클래스에게 제한적인 접근 권한을 부여하는 메커니즘입니다.

class Box {
private:
    int width;
    
public:
    Box(int w) : width(w) {}
    
    // friend 함수 선언
    friend void printWidth(const Box& box);
};

// friend 함수 정의
void printWidth(const Box& box) {
    cout << "Width: " << box.width << endl;  // private 접근 가능
}

int main() {
    Box box(10);
    printWidth(box);  // Width: 10
}

왜 필요한가?:

  • 연산자 오버로딩: operator<<, operator+ 등을 비멤버 함수로 구현
  • 헬퍼 함수: 클래스와 밀접하게 관련된 유틸리티 함수
  • 클래스 간 협력: 두 클래스가 서로의 내부 구현을 알아야 할 때
  • 캡슐화 유지: public getter/setter 없이 선택적 접근
// ❌ public getter: 모든 코드가 접근 가능
class Box {
public:
    int getWidth() const { return width; }
private:
    int width;
};

// ✅ friend: 특정 함수만 접근 가능
class Box {
private:
    int width;
    friend void printWidth(const Box& box);
};

friend의 종류:

  1. friend 함수: 특정 함수가 private 멤버에 접근
  2. friend 클래스: 특정 클래스의 모든 멤버 함수가 접근
  3. friend 멤버 함수: 특정 클래스의 특정 멤버 함수만 접근
class A {
private:
    int data;
    
    // friend 함수
    friend void func(const A& a);
    
    // friend 클래스
    friend class B;
    
    // friend 멤버 함수
    friend void C::process(const A& a);
};

friend 함수

class Point {
private:
    int x, y;
    
public:
    Point(int x, int y) : x(x), y(y) {}
    
    // friend 함수
    friend double distance(const Point& p1, const Point& p2);
    friend void print(const Point& p);
};

double distance(const Point& p1, const Point& p2) {
    int dx = p1.x - p2.x;
    int dy = p1.y - p2.y;
    return sqrt(dx * dx + dy * dy);
}

void print(const Point& p) {
    cout << "(" << p.x << ", " << p.y << ")" << endl;
}

int main() {
    Point p1(0, 0);
    Point p2(3, 4);
    
    cout << "Distance: " << distance(p1, p2) << endl;  // 5
    print(p1);  // (0, 0)
}

friend 클래스

class Engine {
private:
    int horsepower;
    
public:
    Engine(int hp) : horsepower(hp) {}
    
    // Car 클래스를 friend로 선언
    friend class Car;
};

class Car {
private:
    Engine engine;
    string model;
    
public:
    Car(const string& m, int hp) : model(m), engine(hp) {}
    
    void printInfo() {
        cout << "Model: " << model << endl;
        cout << "Horsepower: " << engine.horsepower << endl;  // private 접근
    }
};

int main() {
    Car car("Tesla", 450);
    car.printInfo();
}

연산자 오버로딩

class Complex {
private:
    double real, imag;
    
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // friend 연산자
    friend Complex operator+(const Complex& c1, const Complex& c2);
    friend ostream& operator<<(ostream& os, const Complex& c);
};

Complex operator+(const Complex& c1, const Complex& c2) {
    return Complex(c1.real + c2.real, c1.imag + c2.imag);
}

ostream& operator<<(ostream& os, const Complex& c) {
    os << c.real;
    if (c.imag >= 0) {
        os << "+" << c.imag << "i";
    } else {
        os << c.imag << "i";
    }
    return os;
}

int main() {
    Complex c1(3, 4);
    Complex c2(1, 2);
    Complex c3 = c1 + c2;
    
    cout << c3 << endl;  // 4+6i
}

실전 예시

예시 1: 행렬

class Matrix {
private:
    vector<vector<int>> data;
    int rows, cols;
    
public:
    Matrix(int r, int c) : rows(r), cols(c), data(r, vector<int>(c, 0)) {}
    
    friend Matrix operator*(const Matrix& m1, const Matrix& m2);
    friend ostream& operator<<(ostream& os, const Matrix& m);
};

Matrix operator*(const Matrix& m1, const Matrix& m2) {
    if (m1.cols != m2.rows) {
        throw invalid_argument("행렬 곱셈 불가");
    }
    
    Matrix result(m1.rows, m2.cols);
    
    for (int i = 0; i < m1.rows; i++) {
        for (int j = 0; j < m2.cols; j++) {
            for (int k = 0; k < m1.cols; k++) {
                result.data[i][j] += m1.data[i][k] * m2.data[k][j];
            }
        }
    }
    
    return result;
}

ostream& operator<<(ostream& os, const Matrix& m) {
    for (int i = 0; i < m.rows; i++) {
        for (int j = 0; j < m.cols; j++) {
            os << m.data[i][j] << " ";
        }
        os << endl;
    }
    return os;
}

예시 2: 분수

class Fraction {
private:
    int numerator;
    int denominator;
    
    int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
    
    void simplify() {
        int g = gcd(abs(numerator), abs(denominator));
        numerator /= g;
        denominator /= g;
    }
    
public:
    Fraction(int n = 0, int d = 1) : numerator(n), denominator(d) {
        if (d == 0) {
            throw invalid_argument("분모는 0이 될 수 없음");
        }
        simplify();
    }
    
    friend Fraction operator+(const Fraction& f1, const Fraction& f2);
    friend Fraction operator*(const Fraction& f1, const Fraction& f2);
    friend bool operator==(const Fraction& f1, const Fraction& f2);
    friend ostream& operator<<(ostream& os, const Fraction& f);
};

Fraction operator+(const Fraction& f1, const Fraction& f2) {
    int n = f1.numerator * f2.denominator + f2.numerator * f1.denominator;
    int d = f1.denominator * f2.denominator;
    return Fraction(n, d);
}

Fraction operator*(const Fraction& f1, const Fraction& f2) {
    return Fraction(f1.numerator * f2.numerator, 
                   f1.denominator * f2.denominator);
}

bool operator==(const Fraction& f1, const Fraction& f2) {
    return f1.numerator == f2.numerator && 
           f1.denominator == f2.denominator;
}

ostream& operator<<(ostream& os, const Fraction& f) {
    os << f.numerator << "/" << f.denominator;
    return os;
}

int main() {
    Fraction f1(1, 2);
    Fraction f2(1, 3);
    
    cout << f1 + f2 << endl;  // 5/6
    cout << f1 * f2 << endl;  // 1/6
}

예시 3: 스트림 연산자

class Person {
private:
    string name;
    int age;
    
public:
    Person(const string& n, int a) : name(n), age(a) {}
    
    friend ostream& operator<<(ostream& os, const Person& p);
    friend istream& operator>>(istream& is, Person& p);
};

ostream& operator<<(ostream& os, const Person& p) {
    os << "Name: " << p.name << ", Age: " << p.age;
    return os;
}

istream& operator>>(istream& is, Person& p) {
    cout << "이름: ";
    is >> p.name;
    cout << "나이: ";
    is >> p.age;
    return is;
}

int main() {
    Person p1("Alice", 30);
    cout << p1 << endl;
    
    Person p2("", 0);
    cin >> p2;
    cout << p2 << endl;
}

friend vs getter/setter

// friend 사용
class Point {
private:
    int x, y;
    
public:
    Point(int x, int y) : x(x), y(y) {}
    
    friend double distance(const Point& p1, const Point& p2);
};

double distance(const Point& p1, const Point& p2) {
    int dx = p1.x - p2.x;  // 직접 접근
    int dy = p1.y - p2.y;
    return sqrt(dx * dx + dy * dy);
}

// getter 사용
class Point {
private:
    int x, y;
    
public:
    Point(int x, int y) : x(x), y(y) {}
    
    int getX() const { return x; }
    int getY() const { return y; }
};

double distance(const Point& p1, const Point& p2) {
    int dx = p1.getX() - p2.getX();  // getter 사용
    int dy = p1.getY() - p2.getY();
    return sqrt(dx * dx + dy * dy);
}

자주 발생하는 문제

문제 1: friend 남용

// ❌ 캡슐화 파괴
class BadClass {
private:
    int data;
    
public:
    friend class A;
    friend class B;
    friend class C;
    // 너무 많은 friend
};

// ✅ 최소한의 friend
class GoodClass {
private:
    int data;
    
public:
    int getData() const { return data; }
    void setData(int d) { data = d; }
};

문제 2: 양방향 friend

// ❌ 순환 의존
class A {
    friend class B;
};

class B {
    friend class A;
};

// ✅ 필요한 경우만 friend

문제 3: friend 상속 안됨

class Base {
private:
    int data;
    
public:
    friend void func(const Base& b);
};

class Derived : public Base {
    // func은 Derived의 friend 아님
};

실무 패턴

패턴 1: 팩토리 함수

class DatabaseConnection {
private:
    std::string connectionString_;
    bool connected_ = false;
    
    // private 생성자
    DatabaseConnection(const std::string& connStr) 
        : connectionString_(connStr) {}
    
public:
    // friend 팩토리 함수
    friend DatabaseConnection createConnection(const std::string& host, int port);
    
    void connect() {
        connected_ = true;
        std::cout << "연결됨: " << connectionString_ << '\n';
    }
};

// 팩토리 함수가 private 생성자 호출
DatabaseConnection createConnection(const std::string& host, int port) {
    std::string connStr = host + ":" + std::to_string(port);
    return DatabaseConnection(connStr);
}

// 사용
auto conn = createConnection("localhost", 5432);
conn.connect();

패턴 2: 비교 연산자

class Date {
private:
    int year_, month_, day_;
    
public:
    Date(int y, int m, int d) : year_(y), month_(m), day_(d) {}
    
    // friend 비교 연산자
    friend bool operator==(const Date& lhs, const Date& rhs);
    friend bool operator<(const Date& lhs, const Date& rhs);
};

bool operator==(const Date& lhs, const Date& rhs) {
    return lhs.year_ == rhs.year_ &&
           lhs.month_ == rhs.month_ &&
           lhs.day_ == rhs.day_;
}

bool operator<(const Date& lhs, const Date& rhs) {
    if (lhs.year_ != rhs.year_) return lhs.year_ < rhs.year_;
    if (lhs.month_ != rhs.month_) return lhs.month_ < rhs.month_;
    return lhs.day_ < rhs.day_;
}

// 사용
Date d1(2026, 3, 12);
Date d2(2026, 3, 13);
if (d1 < d2) {
    std::cout << "d1이 더 이른 날짜\n";
}

패턴 3: 테스트 헬퍼

class BankAccount {
private:
    double balance_;
    std::string accountNumber_;
    
public:
    BankAccount(const std::string& accNum, double initialBalance)
        : accountNumber_(accNum), balance_(initialBalance) {}
    
    void deposit(double amount) {
        balance_ += amount;
    }
    
    // 테스트 헬퍼를 friend로 선언
    friend class BankAccountTest;
};

// 테스트 클래스
class BankAccountTest {
public:
    static void verifyBalance(const BankAccount& account, double expected) {
        if (account.balance_ == expected) {
            std::cout << "테스트 통과\n";
        } else {
            std::cout << "테스트 실패: " << account.balance_ << " != " << expected << '\n';
        }
    }
};

// 사용
BankAccount acc("123456", 1000.0);
acc.deposit(500.0);
BankAccountTest::verifyBalance(acc, 1500.0);

friend 함수 vs friend 클래스: 역할 정리

구분friend 함수friend 클래스
범위선언된 그 자유 함수 하나(또는 오버로드 집합의 일부)만 접근해당 클래스의 모든 멤버 함수가 grantor의 비공개 멤버에 접근
ODR·네임스페이스클래스 안에 선언해도 클래스 멤버가 아님 — 네임스페이스 스코프에서 정의friend 클래스 자체는 여전히 별도 타입; “전체 허용”이라 변경 영향이 큼
유지보수필요한 함수만 좁게 열 수 있어 변경 범위가 작음한 번 열면 그 클래스 전체가 내부에 의존 — 결합도 상승

friend 멤버 함수(friend void Other::f(const X&);)는 “특정 타입의 특정 메서드만” 열 때 쓰며, Other 선언이 앞에 있어야 하는 등 선언 순서를 맞춰야 합니다.

연산자 오버로딩에서의 활용 (심화)

  • 대칭 이항 연산: operator+(T,T)를 비멤버로 두면 좌측·우측 변환 규칙이 자연스럽습니다. private 멤버를 읽으려면 friend 또는 public 접근자가 필요합니다.
  • 스트림 연산자 operator<<, operator>>: 첫 번째 인자가 std::ostream&이므로 멤버로 넣기 어렵고, 비멤버 + friend 선언이 관례입니다.
  • 멤버 vs 비멤버: operator+=는 보통 멤버, operator+는 비멤버 friend 조합이 흔합니다. 일관된 네이밍·예외 보장도 friend 본문 안에서 처리합니다.
class Vec {
    double x, y;
    friend Vec operator+(Vec a, Vec b) {
        return {a.x + b.x, a.y + b.y};
    }
};

캡슐화와의 관계

friend는 “private를 없애는 것”이 아니라, 검증된 동료에게만 문을 여는 것에 가깝습니다.

  • 장점: 불필요한 public getter 남발을 줄이고, 불변 조건(invariant)을 깨지 않는 경로만 열 수 있습니다.
  • 단점: friend 본문은 클래스 내부 표현에 결합됩니다. grantee 코드가 바뀌면 friend 구현도 같이 수정되는 경우가 많습니다.

Effective C++ 계열에서 말하듯, 비멤버 비friend 함수가 가능하면 그쪽이 의존성이 가장 적습니다. friend는 “정말 private가 필요할 때만” 선택합니다.

실전 라이브러리·프레임워크 패턴

  • 단위 테스트: 테스트 픽스처나 friend class FooTest로 불변식만 검증하고, 프로덕션에서는 숨깁니다(남용 시 테스트 전용 로직이 프로덕션 헤더에 노출되므로 팀 규칙이 필요합니다).
  • 모듈 내부 협력자: 같은 컴포넌트 안의 detail 네임스페이스 함수만 friend로 두고, 외부 API는 좁게 유지합니다.
  • 직렬화: boost::serialization 스타일에서 serialize를 friend로 두는 패턴이 과거에 흔했습니다. 최근에는 리플렉션·코드젠 대안도 검토합니다.

남용 시 신호와 대안

  • 신호: friend 선언이 수십 줄로 늘어남, 서로 다른 팀 모듈이 서로 friend, “편해서” public API를 안 만들고 전부 friend.
  • 대안: PIMPL로 구현 세부 숨기기, 무명 네임스페이스 + 팩토리, 인터페이스 클래스로 접근 지점 최소화, C++20 모듈로 구현 단위 캡슐화.

FAQ

Q1: friend는 언제 사용해야 하나요?

A:

  • 연산자 오버로딩: operator<<, operator+ 등을 비멤버 함수로 구현
  • 헬퍼 함수: 클래스와 밀접하게 관련된 유틸리티 함수
  • 밀접한 클래스 간 협력: 두 클래스가 서로의 내부 구현을 알아야 할 때
  • 팩토리 함수: private 생성자를 호출하는 팩토리 패턴
// 연산자 오버로딩
friend std::ostream& operator<<(std::ostream& os, const MyClass& obj);

// 헬퍼 함수
friend double distance(const Point& p1, const Point& p2);

Q2: friend는 캡슐화를 파괴하나요?

A: 과도한 사용은 문제입니다. friend는 캡슐화를 제한적으로 완화하는 메커니즘이므로, 필요한 경우에만 최소한으로 사용해야 합니다.

// ❌ 남용: 너무 많은 friend
class BadClass {
private:
    int data;
    friend class A;
    friend class B;
    friend class C;
    friend class D;
};

// ✅ 적절한 사용: 필요한 경우만
class GoodClass {
private:
    int data;
    friend std::ostream& operator<<(std::ostream& os, const GoodClass& obj);
};

Q3: friend 함수는 멤버 함수인가요?

A: 아니요. friend 함수는 클래스의 멤버 함수가 아니라 독립적인 함수입니다. 클래스 내부에 선언되지만, 클래스 스코프에 속하지 않습니다.

class MyClass {
    friend void func(const MyClass& obj);  // 멤버 함수 아님
};

// 독립적인 함수
void func(const MyClass& obj) {
    // obj의 private 멤버 접근 가능
}

Q4: friend는 상속되나요?

A: 아니요. friend 관계는 상속되지 않습니다. 파생 클래스는 기반 클래스의 friend를 자동으로 상속하지 않습니다.

class Base {
private:
    int data;
    friend void func(const Base& b);
};

class Derived : public Base {
    // func은 Derived의 friend가 아님
};

void func(const Base& b) {
    // b.data 접근 가능
}

void func(const Derived& d) {
    // d.data 접근 불가 (Base의 private)
}

Q5: friend vs public getter/setter?

A:

  • friend: 선택적 접근 (특정 함수/클래스만)
  • public getter/setter: 모든 코드가 접근 가능
// friend: 특정 함수만 접근
class Box {
private:
    int width;
    friend void printWidth(const Box& box);
};

// public: 모든 코드가 접근
class Box {
public:
    int getWidth() const { return width; }
private:
    int width;
};

선택 기준:

  • 특정 함수/클래스만 접근해야 하면: friend
  • 모든 코드가 접근해야 하면: public getter/setter

Q6: friend 함수와 멤버 함수 중 어느 것을 사용해야 하나요?

A:

  • 멤버 함수: 객체의 상태를 변경하거나 객체에 밀접하게 관련된 경우
  • friend 함수: 두 객체를 대칭적으로 다루거나 연산자 오버로딩
// 멤버 함수: 객체 상태 변경
class Point {
    void move(int dx, int dy) { x += dx; y += dy; }
};

// friend 함수: 대칭적 연산
class Point {
    friend double distance(const Point& p1, const Point& p2);
};

Q7: friend 학습 리소스는?

A:

  • “Effective C++” (3rd Edition) by Scott Meyers (Item 23: Prefer non-member non-friend functions)
  • cppreference.com - Friend declaration
  • “C++ Primer” (5th Edition) by Stanley Lippman

관련 글: Operator Overloading, Access Control.

한 줄 요약: friend는 특정 함수나 클래스가 private 멤버에 접근할 수 있도록 허용하는 메커니즘입니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 상속과 다형성 | “virtual 함수” 완벽 가이드
  • C++ static 멤버 | “Static Members” 가이드
  • C++ 디자인 패턴 | “싱글톤/팩토리/옵저버” 실전 가이드

관련 글

  • C++ 시리즈 전체 보기
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ ADL |
  • C++ Aggregate Initialization |