프로그래밍C++ 소프트웨어 아키텍트

C++에서 집합(aggregation)과 구성(composition)은 무엇인가? 이 둘은 어떻게 다르고 어떤 접근 방식을 언제 사용해야 하나?

Hintsage AI 어시스턴트로 면접 통과

답변.

C++ 프로그래밍에서 객체를 결합하는 두 가지 방법인 집합과 구성을 자주 사용합니다. 이 개념들은 클래스 간의 서로 다른 관계를 반영하며, 관련 객체의 생명 주기와 파괴 책임에 영향을 미칩니다.

질문 역사:

객체 지향 설계에서 객체 간의 의존성을 구분하는 것은 항상 중요했습니다. 객체 지향 언어(Smalltalk, C++, Java)가 등장하면서 "부분 - 전체" 관계를 모델링하는 방법에 대한 질문이 생겼습니다. C++에서는 수동 메모리 관리와 객체의 생명 주기 때문에 이는 특히 중요해졌습니다.

문제:

집합과 구성 간의 잘못된 선택은 메모리 누수, 리소스 중복, 객체 파괴 오류를 초래합니다. 또한 이 개념들이 혼동되는 경우가 종종 있습니다.

해결책:

  • 구성 — 객체가 부분-객체를 소유하며 그 생성/파괴에 대한 책임을 지는 관계입니다. C++에서는 일반적으로 값으로서의 멤버 클래스 또는 unique_ptr를 통해 표현됩니다.
  • 집합 — 더 약한 관계로, 부분-객체가 "전체" 외부에 존재하며 그 생명 주기에 대한 책임이 소유자에게 있지 않습니다. 일반적으로 비소유 참조(포인터/레퍼런스)를 통해 구현됩니다.

코드 예시:

// 구성: class Engine {}; class Car { Engine engine; // Engine은 Car과 함께 생성되고 파괴됨 }; // 집합: class Person {}; class Team { std::vector<Person*> members; // Person 객체를 가리키며 소유하지 않음 };

주요 특징:

  • 구성 — 강한 연결(part-of), 소유
  • 집합 — 약한 연결(uses), 소유하지 않음
  • 구성은 메모리 관리를 자동화하고, 집합은 소유자에 대한 주의와 합의가 필요합니다.

함정 질문.

클래스 멤버에 객체에 대한 포인터가 있으면 항상 집합인가?

아니요! 만약 클래스가 이 포인터를 소유하고 있다면(예: std::unique_ptr를 통해), 여전히 구성입니다. 연결 유형은 필드의 유형이 아니라 생명 주기에 대한 책임에 따라 결정됩니다.

class House { std::unique_ptr<Room> room; // 구성, House는 Room을 소유함 };

구성이 참조 또는 raw 포인터를 통해 구현될 수 있는가?

가능합니다 — 그러나 단지 소유자가 생성하고 파괴하는 경우에만, 링크 또는 포인터가 최적화를 위해 사용되는 경우에 가능합니다. 그러나 소유권을 명확히 표현하기 위해 값이나 스마트 포인터를 사용하는 것이 훨씬 좋습니다.

구성에서 파트 객체가 소유자 외부에서 생성되어 전달된다면 어떻게 될까?

이 경우 구성의 불변성이 깨질 위험이 발생합니다: 외부에서 생성된 객체가 소유자에게 전달되면 소유자가 삭제하고 다른 곳에 링크가 남아있다면, dangling pointer가 발생할 수 있습니다. 프로젝트에서 소유권 및 파괴 책임을 엄격하게 정의해야 합니다.

일반적인 오류 및 안티 패턴

  • 집합과 구성 개념을 혼합하는 것(예: 필요 없는 raw 포인터를 보관하고 이를 소유자의 소멸자에서 삭제하려고 시도하기)
  • 엄격한 생명 주기가 필요한 곳에 집합 사용
  • 비소유 포인터를 해제하지 않기

실제 사례

부정적인 사례

한 팀은 모든 내부 객체를 raw 포인터로 컨테이너에 저장하고 소멸자에서 수동으로 삭제하기로 결정했습니다. 모든 것이 잘 작동하다가 소유권 체계가 변경되었습니다. 결과적으로 포인터가 두 번 해제되어 충돌이 발생했습니다.

장점:

  • 특정 경우에 대한 아키텍처의 유연성(예: 객체 간의 유동적인 관계)

단점:

  • 메모리 관리와 관련된 높은 오류 위험
  • 유지보수 어려움

긍정적인 사례

다른 팀은 실제 소유 관계에 대해 std::unique_ptr로 전환하고, 비소유는 임시 참조 형태로만 사용했습니다. 이는 아키텍처를 명확하게 표현했습니다.

장점:

  • 투명하고 이해하기 쉬운 소유 관계
  • 메모리 누수나 이중 해제 없음

단점:

  • 순환 구성이 항상 가능하지 않음
  • 때때로 객체 간의 통신 프로토콜을 수정해야 함