C++ Aggregate Initialization | '집합체 초기화' 가이드
이 글의 핵심
집합체(aggregate)는 조건을 만족하는 구조체·배열로, 중괄호로 멤버를 직접 채웁니다. C++20 지정 초기화(designated initializers), 기본·값·리스트 초기화와의 차이, 구조체 API 설계 시 흔한 실수를 다룹니다.
집합체 초기화란?
집합체(aggregate) 는 사용자 정의 생성자·비공개 멤버·가상 등이 없는 구조체·배열이며, 중괄호 {}로 한 번에 초기화할 수 있습니다. 리스트 초기화, 값 초기화, 지정 초기화와 함께 보면 초기화 규칙을 정리하기 좋습니다.
// 타입 정의
struct Point {
int x;
int y;
};
Point p = {10, 20}; // 집합체 초기화
왜 필요한가?:
- 간결성: 생성자 없이 멤버를 직접 초기화
- 안전성: 모든 멤버를 명시적으로 초기화
- 가독성: 구조체 정의와 초기화가 명확
// ❌ 생성자 방식: 코드가 길어짐
// 타입 정의
struct Point {
int x, y;
Point(int x_, int y_) : x(x_), y(y_) {}
};
Point p(10, 20);
// ✅ 집합체 초기화: 간결
struct Point {
int x, y;
};
Point p = {10, 20};
집합체 조건
// ✅ 집합체
struct Aggregate {
int x;
double y;
};
// ❌ 비집합체
struct NonAggregate {
int x;
private:
int y; // 비공개 멤버
};
집합체 조건 상세:
C++17/C++20 기준으로 다음 조건을 모두 만족해야 집합체입니다:
- 배열 또는 클래스 타입 (구조체, 클래스, 공용체)
- 사용자 정의 생성자 없음 (컴파일러 생성 생성자는 OK)
- 비공개/보호 멤버 없음 (모든 멤버가
public) - 가상 함수 없음
- 가상/비공개/보호 기반 클래스 없음 (C++17부터 public 상속은 OK)
// ✅ 집합체 예시
struct Aggregate1 {
int x;
double y;
};
struct Aggregate2 : Aggregate1 { // C++17: public 상속 OK
int z;
};
// ❌ 비집합체 예시
struct NonAggregate1 {
int x;
NonAggregate1() = default; // 사용자 정의 생성자
};
struct NonAggregate2 {
int x;
private:
int y; // 비공개 멤버
};
struct NonAggregate3 {
virtual void func() {} // 가상 함수
};
집합체 판별 방법:
#include <type_traits>
struct Point { int x, y; };
static_assert(std::is_aggregate_v<Point>); // true
// 사용 예시
template<typename T>
void initIfAggregate(T& obj) {
if constexpr (std::is_aggregate_v<T>) {
obj = {}; // 집합체 초기화
}
}
실전 예시
예시 1: 구조체 초기화
struct Person {
std::string name;
int age;
double height;
};
// 집합체 초기화
Person p1 = {"Alice", 30, 165.5};
Person p2{"Bob", 25, 175.0};
// 일부 생략
Person p3 = {"Charlie", 35}; // height는 0.0
Person p4 = {"David"}; // age는 0, height는 0.0
예시 2: 배열 초기화
C/C++ 예제 코드입니다.
int arr1[5] = {1, 2, 3, 4, 5};
int arr2[5] = {1, 2}; // 나머지는 0
// 2차원 배열
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
예시 3: 중첩 구조체
struct Address {
std::string city;
int zipCode;
};
struct Person {
std::string name;
Address address;
};
Person p = {
"Alice",
{"Seoul", 12345}
};
std::cout << p.name << std::endl;
std::cout << p.address.city << std::endl;
예시 4: C++20 지정 초기화
struct Config {
int width = 800;
int height = 600;
bool fullscreen = false;
};
// C++20: 지정 초기화
Config cfg = {
.width = 1920,
.height = 1080,
.fullscreen = true
};
다른 초기화 방식과 비교 (집합체 관점)
집합체 초기화는 보통 중괄호 리스트로 이루어지며, 리스트 초기화·값 초기화와 겹칩니다.
| 문법 | 의미 (집합체 S) |
|---|---|
S a; | 기본 초기화 — 지역이면 멤버가 쓰레기일 수 있음 |
S a{}; | 값 초기화 — 멤버를 0·빈 값으로 맞추는 쪽(타입에 따라 0 초기화 포함) |
S a = {1, 2}; | 집합체 초기화(복사 리스트 형태) |
S a{1, 2}; | 집합체 + 리스트 초기화 |
비집합체(사용자 정의 생성자가 있는 클래스)는 S a{...}가 생성자 호출로 가므로, “집합체 초기화”라는 용어는 엄격히는 집합체 타입에만 해당합니다.
구조체 초기화 실전 패턴
- 한 줄에 의미 부여: API 응답·설정값을
struct로 묶고return { ok, msg };처럼 쓰면 필드 이름이 문서 역할을 합니다. - 기본 멤버 초기화 +
{}: C++11 이후 멤버에= 0또는= {}를 두고, 전체는Config c{}로 통일하면 “어떤 필드도 빠뜨리지 않았는가”를 코드 리뷰하기 쉽습니다. - 중첩: 바깥 구조체 초기화 시 안쪽은 또 다른 집합체로
{...}를 한 단계 더 두면 됩니다(이미 예시 3).
C++20 지정 초기화(designated initializers) 심화
C++20에서는 C99 스타일의 .멤버 = 값 문법이 집합체 초기화에 들어옵니다. 선언 순서와 동일한 순서로만 써야 하며, 건너뛰면 생략된 멤버는 값 초기화됩니다.
struct Point { int x; int y; int z = 0; };
Point p = {.x = 1, .y = 2}; // z는 기본값 0 또는 값 초기화 규칙
// Point q = {.y = 2, .x = 1}; // 순서 어긋남 → C++20에서 ill-formed
장점
- 필드 의미가 이름으로 드러나 큰
struct에서 실수(인자 순서 바꿈)가 줄어듭니다. - 리스트 초기화와 같이 쓰면 가독성이 좋아집니다.
주의
- C와의 호환: C에서는 지정 초기화가 더 유연한 부분이 있었으나, C++20은 위에서 말한 순서 제약이 엄격합니다.
- 비집합체에서는 집합 규칙 밖이므로, 생성자를 쓰는 타입은 생성자 인자로 설계하는 편이 맞습니다.
기본값
struct Data {
int x = 10; // 기본값
int y = 20;
int z = 30;
};
Data d1 = {}; // {10, 20, 30}
Data d2 = {100}; // {100, 20, 30}
Data d3 = {100, 200}; // {100, 200, 30}
기본값 동작 원리:
집합체 초기화 시 일부 멤버를 생략하면, 생략된 멤버는:
- 멤버 기본값이 있으면 그 값 사용
- 멤버 기본값이 없으면 값 초기화 (0, nullptr, false 등)
// 타입 정의
struct Config {
int width = 800; // 기본값 있음
int height = 600; // 기본값 있음
bool fullscreen; // 기본값 없음
};
Config c1 = {}; // {800, 600, false} (모두 기본값)
Config c2 = {1920}; // {1920, 600, false} (width만 지정)
Config c3 = {1920, 1080}; // {1920, 1080, false}
Config c4 = {1920, 1080, true}; // {1920, 1080, true} (모두 지정)
실무 활용: 옵셔널 파라미터:
struct HttpRequest {
std::string url;
std::string method = "GET"; // 기본값
int timeout = 30; // 기본값
bool followRedirects = true; // 기본값
};
// 필수 파라미터만 지정
HttpRequest req1 = {"https://example.com"};
// {url: "https://example.com", method: "GET", timeout: 30, followRedirects: true}
// 일부 파라미터 지정
HttpRequest req2 = {"https://example.com", "POST", 60};
// {url: "https://example.com", method: "POST", timeout: 60, followRedirects: true}
자주 발생하는 문제
문제 1: 순서
struct Point {
int x;
int y;
};
// ❌ 순서 바뀜
// Point p = {.y = 20, .x = 10}; // C++20에서 에러
// ✅ 순서 유지
Point p = {.x = 10, .y = 20};
문제 2: 생성자 있는 클래스
// ❌ 비집합체
struct NonAggregate {
int x;
NonAggregate(int v) : x(v) {}
};
// NonAggregate obj = {10}; // 에러
// ✅ 생성자 호출
NonAggregate obj(10);
문제 3: 상속
// C++17: 상속된 집합체도 가능
struct Base {
int x;
};
struct Derived : Base {
int y;
};
Derived d = {{10}, 20}; // Base{10}, y=20
문제 4: 배열 크기
C/C++ 예제 코드입니다.
// ✅ 크기 명시
int arr1[5] = {1, 2, 3};
// ✅ 크기 추론
int arr2[] = {1, 2, 3}; // 크기 3
// ❌ 초과
// int arr3[2] = {1, 2, 3}; // 에러
초기화 방식 비교
struct Point {
int x, y;
};
// 집합체 초기화
Point p1 = {10, 20};
Point p2{10, 20};
// 생성자 호출 (생성자 있으면)
Point p3(10, 20);
// 기본 초기화
Point p4; // x, y는 쓰레기 값
초기화 방법 비교표:
| 방법 | 문법 | 집합체 | 비집합체 | 특징 |
|---|---|---|---|---|
| 집합체 초기화 | Point p = {10, 20}; | ✅ | ❌ | 멤버 직접 초기화 |
| 리스트 초기화 | Point p{10, 20}; | ✅ | ✅ | 좁히기 방지 |
| 직접 초기화 | Point p(10, 20); | ❌ | ✅ | 생성자 호출 |
| 기본 초기화 | Point p; | ⚠️ | ⚠️ | 쓰레기 값 가능 |
| 값 초기화 | Point p{}; | ✅ | ✅ | 0 또는 기본값 |
실무 권장:
// ✅ 집합체: 중괄호 초기화
struct Config {
int width, height;
};
Config cfg = {800, 600};
// ✅ 비집합체: 생성자 호출
class Widget {
int value_;
public:
Widget(int v) : value_(v) {}
};
Widget w(10);
// ✅ 안전한 초기화: 값 초기화
Config cfg2{}; // {0, 0}
실무 패턴
패턴 1: 설정 구조체
struct ServerConfig {
std::string host = "localhost";
int port = 8080;
int maxConnections = 100;
bool enableLogging = true;
};
// 기본값 사용
ServerConfig cfg1 = {};
// 일부만 변경
ServerConfig cfg2 = {"0.0.0.0", 3000};
// C++20: 지정 초기화
ServerConfig cfg3 = {
.host = "192.168.1.1",
.port = 9000
};
패턴 2: 반환값 최적화
struct Result {
bool success;
std::string message;
int errorCode;
};
Result processData(const std::string& data) {
if (data.empty()) {
return {false, "Empty data", 1};
}
// 처리 로직
return {true, "Success", 0};
}
패턴 3: 테스트 데이터
struct TestCase {
std::string input;
std::string expected;
bool shouldPass;
};
std::vector<TestCase> tests = {
{"hello", "HELLO", true},
{"world", "WORLD", true},
{"", "", false}
};
흔한 실수와 함정 (추가)
std::string이 있는 집합체를{0}만으로…
멤버가std::string이면 전부 0으로 두려면 각 멤버 규칙에 맞는 값 초기화가 필요합니다.S{}가 일반적으로 안전합니다.memset으로 0 채우기는 절대 하지 마세요.- 지정 초기화와 순서
선언 순서와 다르게 쓰면 C++20에서 에러입니다. 리팩터링으로 멤버 순서를 바꾸면 지정 초기화 목록도 함께 고쳐야 합니다. - 집합체가 아니게 된 뒤
virtual이나 사용자 정의 생성자를 추가하면 갑자기T a = {1,2,3}가 깨지거나 생성자 경로로 바뀝니다. 공개 API의struct는 변경 시 영향 범위를 넓게 봐야 합니다.
FAQ
Q1: 집합체 조건은 무엇인가요?
A:
- 모든 멤버가
public - 사용자 정의 생성자 없음
- 가상 함수 없음
- C++17부터 public 상속 허용
Q2: 일부 멤버를 생략할 수 있나요?
A: 가능합니다. 생략된 멤버는 멤버 기본값 또는 값 초기화(0, nullptr 등)됩니다.
struct Point { int x, y, z = 100; };
Point p = {10}; // {10, 0, 100}
Q3: 초기화 순서는?
A: 선언 순서대로 초기화됩니다. C++20 지정 초기화도 선언 순서를 따라야 합니다.
Q4: C++20에서 어떤 변화가 있나요?
A: 지정 초기화(designated initializers) 지원이 추가되었습니다.
struct Point { int x, y; };
Point p = {.x = 10, .y = 20}; // C++20
Q5: 배열 크기는 어떻게 결정되나요?
A:
- 명시적:
int arr[5] = {1, 2, 3};(크기 5) - 추론:
int arr[] = {1, 2, 3};(크기 3)
Q6: 중첩 구조체는 어떻게 초기화하나요?
A: 중괄호를 중첩하여 초기화합니다.
struct Inner { int a, b; };
struct Outer { Inner inner; int c; };
Outer o = {{10, 20}, 30}; // inner={10, 20}, c=30
Q7: 집합체 초기화 학습 리소스는?
A:
- “C++ Primer” by Lippman, Lajoie, Moo
- “Effective C++” by Scott Meyers
- cppreference.com - Aggregate initialization
관련 글: 리스트 초기화, 값 초기화, 지정 초기화, 기본 초기화.
한 줄 요약: 집합체 초기화는 중괄호로 구조체나 배열의 멤버를 직접 초기화하는 간결한 방법입니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ List Initialization | “리스트 초기화” 가이드
- C++ Value Initialization | “값 초기화” 가이드
- C++ Designated Initializers | “지정 초기화” 가이드
- C++ Default Initialization | “기본 초기화” 가이드
관련 글
- C++ Aggregate Initialization 완벽 가이드 | 집합 초기화
- C++20 Designated Initializers 완벽 가이드 | 명확한 구조체 초기화
- C++ call_once |
- C++ 배열 vs vector |
- C++ struct vs class |
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ Aggregate Initialization | ‘집합체 초기화’ 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(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++ Aggregate Initialization | ‘집합체 초기화’ 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 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 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
이 글에서 다루는 키워드 (관련 검색어)
C++, aggregate, initialization, struct, array 등으로 검색하시면 이 글이 도움이 됩니다.