제 15 장: 친구(Friends)

지금까지 언급된 모든 예제에서 비밀(private) 멤버는 오직 자신의 클래스 멤버에서만 접근할 수 있음을 보았다. 좋은 일이다. 캡슐화와 데이터 은닉을 강제하기 때문이다. 기능을 클래스 안에 캡슐화해 넣어서 여러 책임이 노출되는 것을 방지한다. 데이터를 감춤으로써 클래스의 데이터 정합성을 증진하고 소프트웨어의 다른 부분이 클래스에 속한 데이터에 의존해 구현되지 못하도록 방지한다.

이번의 (아주) 짧은 장을 통하여 friend 키워드와 그 사용법에 깔린 원리를 소개한다. friend 키워드가 있으면 함수는 클래스의 비밀(private) 멤버에 접근할 수 있다. 그렇다고 해서 데이터 은닉이라는 원칙을 포기한다는 뜻은 아니다.

이 장은 클래스 사이의 친구 관계는 다루지 않는다. 클래스 사이에 친구 관계가 자연스러운 상황은 제 17장과 제 21장에 다루며 그런 상황은 함수 사이의 친구 관계를 다루는 방법을 자연스럽게 확장하면 된다.

친구 사이를 선언하려면 (즉, friend 키워드를 사용하려면) 정의가 잘된 개념적 이유가 충분해야 한다. 전통적으로 클래스 개념의 정의는 보통 다음과 같다.

클래스는 데이터 집합과 거기에 작용하는 함수들을 모은 데이터 구조이다.

11장에서 보았듯이 연산자 함수는 클래스 인터페이스 밖에 정의할 필요가 있다. 피연산자들에 대하여 승격을 사용할 수 있고 또는 직접 통제하에 있지 않은 기존 클래스의 편의기능들을 확장할 수 있기 때문이다. 위의 전통적인 클래스 개념의 정의에 따르면 클래스 인터페이스 안에 정의하지 않더라도 그 함수들은 그 클래스에 속해 있다고 간주해야 한다. 다시 말해, 언어 구문이 허용했다면 틀림없이 그 함수들은 클래스 인터페이스 안에 정의되었을 것이다.

그런 함수를 두 가지 방식으로 구현할 수 있다. 하나는 공개 멤버 함수를 사용하여 구현하는 것이다. 이 접근법은 11.2절에 사용했다.

또다른 접근법은 클래스 개념의 정의를 적용하는 것이다. 그런 함수들이 실제로 그 클래스에 속해 있다고 서술하면 그들에게는 객체의 데이터 멤버에 직접 접근을 허용해야 한다. 이런 목적은 friend 키워드를 사용하여 달성한다.

일반 원칙으로서 같은 파일에 클래스 인터페이스로 선언되어 있으면 클래스 객체의 데이터에 작용하는 모든 함수들은 그 클래스에 속해 있으며 그 클래스의 데이터 멤버에 직접 접근할 수 있다.

15.1: 친구 함수

11.2절에서 Person 클래스의 삽입 연산자는 다음과 같이 구현되었다 (9.3절).
    ostream &operator<<(ostream &out, Person const &person)
    {
        return
            out <<
                "Name:    " << person.name() <<  ", "
                "Address: " << person.address() <<  ", "
                "Phone:   " << person.phone();
    }
이제 Person 객체를 스트림 안으로 삽입할 수 있다.

그렇지만 이 구현은 세 개의 멤버 함수를 호출하기를 요구한다. 이것은 비효율의 근원으로 볼 수 있다. 개선하는 방법은 Person::insertInto 멤버를 구현하고 operator<<가 그 함수를 호출하도록 만드는 것이다. 이 두 함수는 다음과 같이 정의할 수 있다.

    std::ostream &operator<<(std::ostream &out, Person const &person)
    {
        return person.insertInto(out);
    }
    std::ostream &Person::insertInto(std::ostream &out)
    {
        return
            out << "Name:    " << d_name << ", "
                   "Address: " << d_address << ", "
                   "Phone:   " << d_phone;
    }
insertInto는 멤버 함수이므로 객체의 데이터 멤버에 직접 접근한다. 그래서 personout 안으로 삽입할 때 멤버 함수를 따로 더 호출하지 않아도 된다.

다음 단계는 insertInto가 오직 operator<<를 위해서만 정의되어 있고 그리고 operator<<Person의 클래스 인터페이스를 포함하고 있는 헤더 파일에 선언되어 있으므로 Person 클래스에 속한 함수로 간주해야 한다는 사실을 깨닫는 것이다. 그러므로 insertIntooperator<<를 친구로 선언하면 없어도 된다.

친구 함수는 클래스 인터페이스에 친구로 선언해야 한다. 이 친구 선언멤버 함수가 아니므로 클래스의 privateprotected 그리고 public 구역에 영향을 받지 않는다. 친구 선언은 클래스 인터페이스 어디에든 배치할 수 있다. 그러나 관례적으로 친구 선언은 클래스 인터페이스 윗쪽에 놓인다. Person 클래스는 다음과 같이 추출 연산자와 삽입 연산자에 friend를 선언하면서 시작한다.

    class Person
    {
        friend std::ostream &operator<<(std::ostream &out, Person &pd);
        friend std::istream &operator>>(std::istream &in, Person &pd);

        // 이전에 보인 인터페이스 (데이터와 함수)
    };
이제 삽입 연산자는 직접 Person 객체의 데이터 멤버에 접근이 가능하다.
    std::ostream &operator<<(std::ostream &out, Person const &person)
    {
        return
            cout << "Name:    " << person.d_name << ", "
                    "Address: " << person.d_address << ", "
                    "Phone:   " << person.d_phone;
    }
친구 선언은 진짜 선언이다. 클래스에 친구 선언이 있으면 이 친구 함수는 다시 클래스 인터페이스 아래에 선언할 필요가 없다. 이것은 또한 클래스 설계자의 의도를 확실하게 알려준다. 친구 함수가 클래스에 선언되어 있으므로 그 클래스에 속해 있다고 간주할 수 있다.

15.2: 확장 친구 선언

C++확장 친구 선언추가되었다. class 키워드가 없어도 클래스를 친구로 선언할 수 있다. 예를 들어,
    class Friend;                   // 클래스 선언
    typedef Friend FriendType;      // 그에 대한 typedef 
    using FName = Friend;           // using 선언

    class Class1
    {
        friend Friend;              // FriendType 그리고 FName: 역시 OK
    };
C++11 표준 이전에서 친구 선언은 friend class Friend와 같이 명시적으로 class를 요구한다.

컴파일러가 아직 친구의 이름을 보지 못했다면 class를 명시적으로 사용해야 한다. 예를 들어

    class Class1
    {
        // friend Unseen;           // 컴파일 실패: Unseen이 무슨 유형인지 모름.
        friend class Unseen;        // OK
    };
22.10절에 확장 친구 선언을 클래스 템플릿에 사용하는 법을 다룬다.