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의 종류:
- friend 함수: 특정 함수가 private 멤버에 접근
- friend 클래스: 특정 클래스의 모든 멤버 함수가 접근
- 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 |