프렌드 함수와 프렌드 클래스
friend로 특정 외부 함수나 클래스에 내부 접근을 허용하는 방법과 캡슐화를 지키는 제한적 사용 원칙을 익힙니다.
접근 지정자에서 private 멤버는 그 클래스의 멤버 함수와, 클래스가 허락한 friend에서만 쓸 수 있다고 했습니다. 출력 연산자처럼 클래스의 멤버로 만들 수 없는 함수가 내부 데이터를 써야 할 때 이 friend를 씁니다. friend 선언은 클래스가 지정한 특정 함수나 클래스에게 private과 protected 멤버의 접근을 허락합니다.
friend는 캡슐화에 일부러 낸 예외이므로, 어디까지 열리는지와 언제 쓸지를 정확히 알아야 합니다. 이 글은 프렌드 함수와 프렌드 클래스의 문법, 출력 연산자와의 관계, 권한이 전해지지 않는 규칙, 그리고 friend 대신 쓸 수 있는 설계를 다룹니다.
C++ · FRIEND ACCESS
friend는 지정한 함수나 클래스에 비공개 접근 권한을 준다
본문 Box가 area를 friend로 선언하면 비멤버 area는 width와 height를 읽어 12를 계산합니다. 접근 권한과 객체 상태의 올바른 사용은 별도로 설계해야 합니다.
일반 호출자 → Box
공개 API를 사용합니다. private 멤버 이름을 직접 사용할 수 없습니다.
friend area → Box
지정된 area는 비멤버 함수지만 width=3, height=4를 읽어 12를 계산합니다. 암시적 this는 없습니다.
friend 함수 · 좁은 권한 지정
특정 함수만 지정하면 그 함수에 private/protected 접근을 허용합니다. 함수는 여전히 비멤버입니다.
public API로 구현할 수 있다면 friend가 필요하지 않을 수 있습니다.
friend 클래스 · 권한 범위 확인
본문 SecretVault가 VaultInspector를 friend로 지정하면 그 클래스의 멤버에 접근 권한을 줍니다.
friend 관계는 대칭·전이·상속되지 않습니다. 한 클래스 전체에 주는 권한과 특정 함수에 주는 권한을 구분합니다.
friend는 접근 제어의 예외를 명시하는 도구입니다. private나 friend 선언 자체가 불변식 유지 또는 메모리 안전을 자동으로 보장하지는 않습니다.
프렌드 함수
클래스 안에 friend와 함께 함수의 선언을 적으면, 그 함수는 클래스의 멤버가 아니지만 private 멤버를 쓸 수 있습니다. 아래 Box 안의 friend int area(const Box& b);가 그 선언이고, 함수의 정의는 클래스 밖에 보통 함수처럼 따로 있습니다. 멤버가 아니므로 정의 앞에는 Box::가 붙지 않고, friend도 클래스 안의 선언에만 적습니다.
#include <iostream>
class Box {
private:
int width;
int height;
public:
Box(int w, int h) : width(w), height(h) {}
friend int area(const Box& b);
};
int area(const Box& b) {
return b.width * b.height;
}
int main() {
Box b(3, 4);
std::cout << area(b) << '\n';
}12위 그림처럼 일반 호출자는 공개 멤버로만 Box를 다룰 수 있지만, friend로 지정된 area는 width와 height를 직접 읽어 12를 계산합니다. area는 여전히 멤버가 아닌 보통 함수이므로 this가 없고, 다룰 객체를 매개변수로 받아 area(b)처럼 호출합니다. friend 선언을 클래스의 public, private 가운데 어디에 두어도 뜻은 같습니다.
프렌드 클래스
클래스 전체에 접근을 허락하려면 friend class를 씁니다. 그러면 그 클래스의 모든 멤버 함수가 private 멤버를 쓸 수 있습니다.
#include <iostream>
#include <string>
class SecretVault {
private:
std::string code{"A1B2C3"};
friend class VaultInspector;
};
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);
}vault code: A1B2C3프렌드 클래스는 그 클래스의 지금 있는 멤버 함수와 나중에 추가할 멤버 함수 모두에 권한을 주므로 범위가 넓습니다. 함수 하나만 접근이 필요하다면 friend void VaultInspector::printCode(const SecretVault&) const;처럼 특정 멤버 함수만 지정할 수 있습니다. 다만 이렇게 하려면 SecretVault보다 앞에 VaultInspector의 클래스 정의가 있어야 합니다.
출력 연산자와 friend
연산자 오버로딩 기초에서 본 것처럼 operator<<는 왼쪽 피연산자가 스트림이어서 멤버로 만들 수 없습니다. 출력할 내용이 공개된 함수로 충분하면 보통 함수로 정의하면 되지만, private 멤버를 출력해야 한다면 friend로 지정합니다.
#include <iostream>
class Point {
private:
int x;
int y;
public:
Point(int x0, int y0) : x(x0), y(y0) {}
friend std::ostream& operator<<(std::ostream& os, const Point& p) {
return os << '(' << p.x << ", " << p.y << ')';
}
};
int main() {
Point p(1, 2);
std::cout << p << '\n';
}(1, 2)이 예제는 friend 함수의 본문을 클래스 안에 바로 정의했습니다. 이렇게 클래스 안에서 정의한 프렌드 함수는 보통의 이름 찾기로는 보이지 않고, 인자에 Point가 있을 때 인자 의존 조회로만 찾아집니다. 그래서 그 클래스와 관련된 호출에서만 후보가 되어, 다른 타입의 << 호출에 끼어들지 않는다는 장점이 있어 흔히 쓰는 방식입니다.
상속 관계의 클래스를 출력할 때는 operator<<가 기반 클래스를 const Base&로 받아야 객체가 잘리지 않습니다. 하지만 참조로 받는다고 파생 클래스마다 다른 형식으로 출력되지는 않으므로, 기반 클래스에 가상 함수 virtual void print(std::ostream&) const를 두고 operator<<가 그것을 부르게 합니다. 가상 함수는 다형성과 가상 함수에서 다룹니다.
권한은 전해지지 않습니다
friend는 선언한 대상에게만 권한을 주며, 다른 곳으로 퍼지지 않습니다.
| 관계 | 권한이 생기는가 |
|---|---|
A가 B를 friend로 지정 | B는 A의 private을 쓸 수 있지만, A가 B의 private을 쓸 수 있는 것은 아님 |
A가 B를, B가 C를 friend로 지정 | C는 A의 private을 쓸 수 없음 |
A가 B를 friend로 지정하고, D가 B를 상속 | D의 멤버 함수는 A의 private을 쓸 수 없음 |
A가 B를 friend로 지정하고, E가 A를 상속 | B는 E가 새로 선언한 private을 쓸 수 없음 |
friend는 양방향이 아니고, 이어지지 않고, 상속되지 않습니다. 그래서 권한이 정확히 선언한 대상에만 있다는 점은 분명하지만, friend class를 쓰면 그 대상 안에서는 모든 멤버 함수에 권한이 열린다는 점을 함께 기억해야 합니다.
friend를 쓰기 전에
friend가 많아질수록 클래스의 내부 구현에 의존하는 코드가 늘어나, 멤버의 이름이나 표현을 바꿀 때 고쳐야 할 곳이 클래스 밖으로 퍼집니다. 그래서 먼저 공개된 함수로 해결할 수 있는지를 봅니다.
읽기만 필요하다면 객체의 규칙을 깨지 않는 조회 함수를 공개하고, 바꾸는 일이 필요하다면 그 의도를 이름으로 드러내는 멤버 함수를 공개합니다. 출력이 목적이라면 print(std::ostream&) const 같은 공개 멤버 함수를 두고 operator<<가 그것을 부르게 할 수도 있습니다. 이런 방법으로 표현하기 어려운 좁은 경우에만, friend class보다 friend 함수처럼 범위가 작은 쪽을 골라 씁니다.
정리
friend는 클래스가 지정한 함수나 클래스에게private과protected멤버의 접근을 허락합니다.- 프렌드 함수는 멤버가 아니어서
this가 없고, 다룰 객체를 매개변수로 받습니다. - 프렌드 클래스는 그 클래스의 모든 멤버 함수에 권한을 주므로, 필요하면 특정 멤버 함수만 지정합니다.
- 클래스 안에서 정의한 프렌드 함수는 인자 의존 조회로만 찾아지며,
operator<<에 흔히 씁니다. friend관계는 양방향이 아니고, 이어지지 않으며, 상속되지 않습니다.- 공개 함수로 해결할 수 있는지 먼저 보고,
friend는 가장 좁은 범위로 씁니다.
연습 문제
1. friend 선언을 지우면
friend_function.cpp에서 Box 안의 friend 선언만 지우면 빌드는 어떻게 되나요? area가 const Box&로 받는다는 점이 접근을 허락해 주나요?
해설 보기
area가 b.width와 b.height라는 private 멤버를 직접 쓰므로 컴파일 오류가 납니다. const 참조는 객체를 읽기만 하겠다는 경로를 정할 뿐, private 멤버의 접근 권한을 주지는 않습니다. 접근 권한은 클래스의 friend 선언에서만 생깁니다.
2. 이어지지 않는 권한
SecretVault가 VaultInspector를 friend로 지정하고, VaultInspector가 다시 Helper를 friend로 지정했습니다. Helper의 멤버 함수에서 vault.code를 읽을 수 있나요?
해설 보기
읽을 수 없습니다. Helper가 얻은 것은 VaultInspector의 private 멤버를 쓸 권한이고, SecretVault는 Helper를 지정한 적이 없습니다. friend 관계는 이어지지 않으므로, Helper가 vault.code를 쓰려면 SecretVault가 직접 Helper를 friend로 지정해야 합니다.
3. 프렌드 클래스를 상속받은 클래스
class SubInspector : public VaultInspector를 만들고, SubInspector의 멤버 함수에서 vault.code를 읽으면 컴파일되나요? SubInspector 객체로 물려받은 printCode를 부르는 것은 어떤가요?
해설 보기
SubInspector의 멤버 함수에서 vault.code를 직접 쓰는 것은 컴파일 오류입니다. friend 관계는 상속되지 않기 때문입니다. 반면 물려받은 printCode를 부르는 것은 괜찮습니다. 접근이 일어나는 곳은 VaultInspector의 멤버 함수인 printCode 안이고, 그 함수에는 권한이 있기 때문입니다.
이것으로 9장의 객체 지향 프로그래밍 기초를 마칩니다. 다음 장의 상속의 개념과 구현에서는 기존 클래스를 물려받아 새 클래스를 만드는 상속과, 기반 클래스와 파생 클래스의 생성과 소멸 순서를 다룹니다.