본문으로 건너뛰기
Previous
Next
C++ friend 키워드 | 'Friend' 가이드 | 핵심 개념과 실전 활용

C++ friend 키워드 | 'Friend' 가이드 | 핵심 개념과 실전 활용

C++ friend 키워드 | 'Friend' 가이드 | 핵심 개념과 실전 활용

이 글의 핵심

C++ 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++ friend 키워드 | ‘Friend’ 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ friend 키워드 | ‘Friend’ 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, friend, access, 접근제어, OOP 등으로 검색하시면 이 글이 도움이 됩니다.