Go Struct 이해하기: 정의, 사용법, 익명 필드 및 중첩
Grace Collins
Solutions Engineer · Leapcell

Go의 타입 시스템은 단순성과 효율성을 위해 설계되었으며, 데이터 집계를 위한 핵심에는 struct
가 있습니다. struct
는 0개 이상의 다른 타입의 명명된 필드를 단일 논리적 단위로 그룹화하는 복합 데이터 타입입니다. Go에서 사용자 정의 데이터 타입을 정의하는 주요 방법이며, 객체 지향 언어의 클래스와 유사하지만 (Go는 순수한 OOP는 아님) 상속 계층 구조는 없습니다. 이 글에서는 Go에서의 struct 정의, 실용적 사용법, 익명 필드의 고유한 개념, struct 중첩의 힘에 대해 자세히 알아보겠습니다.
Struct 정의 및 초기화
Struct는 type
키워드, struct 이름, 그리고 중괄호 안에 필드를 포함하는 struct
키워드를 사용하여 정의됩니다. 각 필드에는 이름과 타입이 있습니다.
간단한 예시로 Person
struct를 정의해 보겠습니다.
package main import "fmt" // Person은 개인 정보를 담는 struct를 정의합니다. type Person struct { Name string Age int IsAdult bool } func main() { // 여러 가지 방법으로 struct 초기화: // 1. 제로 값 초기화: // 모든 필드는 해당 제로 값(예: "", 0, false)으로 초기화됩니다. var p1 Person fmt.Println("p1:", p1) // 출력: p1: { 0 false} // 2. 필드별 할당: p1.Name = "Alice" p1.Age = 30 p1.IsAdult = true fmt.Println("p1 업데이트:", p1) // 출력: p1 업데이트: {Alice 30 true} // 3. struct 리터럴 사용 (순서대로): // Struct 정의에 선언된 순서대로 필드가 있어야 합니다. p2 := Person{"Bob", 25, false} fmt.Println("p2:", p2) // 출력: p2: {Bob 25 false} // 4. struct 리터럴 사용 (이름 지정): // 필드 재정렬에 더 읽기 쉽고 견고하기 때문에 가장 일반적이고 권장되는 방법입니다. p3 := Person{ Name: "Charlie", Age: 40, IsAdult: true, } fmt.Println("p3:", p3) // 출력: p3: {Charlie 40 true} // 5. struct 포인터 생성: p4 := &Person{Name: "Diana", Age: 22, IsAdult: true} fmt.Println("p4 (포인터):", p4) // 출력: p4 (포인터): &{Diana 22 true} fmt.Println("p4.Name:", p4.Name) // Go는 struct 필드에 대해 자동으로 포인터를 역참조합니다 fmt.Println("(*p4).Name:", (*p4).Name) // 명시적 역참조도 작동합니다 }
필드 접근 및 수정
Struct(또는 struct 포인터)의 필드에는 점(.
) 연산자를 사용하여 접근합니다. 수정은 간단한 할당으로 이루어집니다.
package main import "fmt" type Car struct { Make string Model string Year int Color string } func main() { myCar := Car{ Make: "Toyota", Model: "Camry", Year: 2020, Color: "Silver", } fmt.Println("내 차:", myCar) fmt.Println("제조사:", myCar.Make) fmt.Println("연식:", myCar.Year) // 필드 수정 myCar.Color = "Blue" fmt.Println("새 색상:", myCar.Color) // Go에서 struct는 복사되는 값 타입입니다. // 하나의 struct를 다른 struct에 할당하면 복사가 이루어집니다. anotherCar := myCar // `anotherCar`는 `myCar`의 별도의 복사본입니다. anotherCar.Year = 2023 fmt.Println("내 차 연식:", myCar.Year) // 출력: 내 차 연식: 2020 fmt.Println("다른 차 연식:", anotherCar.Year) // 출력: 다른 차 연식: 2023 // 기본 데이터를 공유하려면 포인터를 사용하세요. carPtr := &myCar carPtr.Year = 2024 // 이는 원본 `myCar`를 수정합니다. fmt.Println("내 차 연식 (ptr 업데이트 후):", myCar.Year) // 출력: 내 차 연식 (ptr 업데이트 후): 2024 }
익명 필드 (임베딩 필드)
Go는 "익명 필드" 또는 "임베딩 필드"라고 하는 강력한 기능을 제공합니다. 필드에 이름을 지정하는 대신 타입만 지정합니다. 이렇게 하면 익명 타입의 필드가 포함된 struct로 암시적으로 임베딩되어 최상위 레벨로 승격됩니다. 이 메커니즘은 Go가 상속보다 구성을 달성하는 방식이며, 일종의 "타입 승격"을 제공합니다.
package main import "fmt" type Engine struct { Type string Horsepower int } type Wheels struct { Count int Size int } // Car (익명 필드 포함) type ModernCar struct { Make string Model string Engine // Engine 타입의 익명 필드 Wheels // Wheels 타입의 익명 필드 Price float64 } func main() { myModernCar := ModernCar{ Make: "Tesla", Model: "Model 3", Engine: Engine{ // 임베딩된 Engine 필드 초기화 Type: "Electric", Horsepower: 450, }, Wheels: Wheels{ // 임베딩된 Wheels 필드 초기화 Count: 4, Size: 19, }, Price: 55000.00, } fmt.Println("내 현대차:", myModernCar) // 임베딩된 필드 직접 접근: fmt.Println("엔진 타입:", myModernCar.Type) // Engine.Type 직접 접근 fmt.Println("엔진 마력:", myModernCar.Horsepower) // Engine.Horsepower 직접 접근 fmt.Println("바퀴 개수:", myModernCar.Count) // Wheels.Count 직접 접근 fmt.Println("바퀴 크기:", myModernCar.Size) // Wheels.Size 직접 접근 // 필요한 경우 원래 복합 타입을 통해 접근할 수도 있습니다: fmt.Println("전체 엔진:", myModernCar.Engine) fmt.Println("전체 바퀴:", myModernCar.Wheels) // 임베딩된 타입 또는 임베딩된 타입과 포함된 struct 간에 필드 이름 충돌이 발생하면, // 임베딩 struct에 직접 선언된 필드가 우선합니다. // 두 임베딩 타입 간의 충돌 시 필드를 명시해야 합니다. type Dimensions struct { Width float64 Height float64 } type SpecificCar struct { Make string Dimensions // 임베딩된 struct Weight float64 } type Garage struct { Name string Dimensions // 임베딩된 struct, SpecificCar.Dimensions와 충돌 Location string } // 예시 1: 임베딩된 필드 접근 sc := SpecificCar{ Make: "Sedan", Dimensions: Dimensions{ Width: 1.8, Height: 1.5, }, Weight: 1500, } fmt.Println("SpecificCar 너비:", sc.Width) // Dimensions.Width 접근 // 예시 2: 이름 충돌 (이 간단한 예제에서는 직접 표시되지 않지만 Go의 규칙이 적용됨) // `type Car struct { Color string; Vehicle }`와 `Vehicle`에도 `Color string` 필드가 있다면, // `Car.Color`는 `Vehicle.Color`가 아닌 `Car`의 직접적인 `Color`를 참조할 것입니다. // `Vehicle.Color`에 액세스하려면 `Car.Vehicle.Color`를 사용해야 합니다. // `ModernCar` 예제에서 `Engine`과 `Wheels` 모두 `ID`라는 필드를 가지고 있다면, // `myModernCar.Engine.ID`와 `myModernCar.Wheels.ID`로 접근해야 합니다. }
익명 필드는 특히 인터페이스를 구성(인터페이스 임베딩)하고 재사용 가능한 구성 요소를 구축하는 데 강력합니다.
Struct 중첩
Struct 중첩은 다른 struct를 네임드 필드로 포함하는 관행을 말합니다. 임베딩된 타입의 필드가 승격되는 익명 필드와 달리, 네임드 중첩에서는 네임드 내부 struct 필드를 통해 명시적으로 필드에 접근합니다. 이는 복잡한 데이터를 구성하고 이름 충돌을 방지하는 데 유용합니다.
package main import "fmt" type Manufacturer struct { Name string Country string } type EngineDetails struct { FuelType string Cylinders int Horsepower int } type Vehicle struct { ID string Manufacturer Manufacturer // Manufacturer는 중첩된 struct입니다 Engine EngineDetails // EngineDetails는 중첩된 struct입니다 Price float64 } func main() { // 중첩된 struct를 사용하여 Vehicle 인스턴스 생성 myVehicle := Vehicle{ ID: "V1001", Manufacturer: Manufacturer{ // 중첩된 Manufacturer struct 초기화 Name: "Honda", Country: "Japan", }, Engine: EngineDetails{ // 중첩된 EngineDetails struct 초기화 FuelType: "Gasoline", Cylinders: 4, Horsepower: 150, }, Price: 25000.00, } fmt.Println("차량 ID:", myVehicle.ID) fmt.Println("제조사 이름:", myVehicle.Manufacturer.Name) // 중첩된 필드 접근 fmt.Println("제조사 국가:", myVehicle.Manufacturer.Country) // 중첩된 필드 접근 fmt.Println("엔진 연료 타입:", myVehicle.Engine.FuelType) // 중첩된 필드 접근 fmt.Println("엔진 마력:", myVehicle.Engine.Horsepower) // 중첩된 필드 접근 // 중첩된 필드 수정 myVehicle.Engine.Horsepower = 160 fmt.Println("새 엔진 마력:", myVehicle.Engine.Horsepower) // 전체 중첩 struct도 할당할 수 있습니다. myVehicle.Manufacturer = Manufacturer{Name: "Toyota", Country: "Japan"} fmt.Println("새 제조사 이름:", myVehicle.Manufacturer.Name) }
익명 필드와 명명된 중첩 선택하기
익명 필드와 명명된 중첩 간의 선택은 설정하려는 의미론적 관계에 따라 달라집니다.
- 익명 필드 (임베딩): 외부 struct가 임베딩된 struct "종류" 또는 "속성"을 가지고 있고 필드를 최상위 레벨로 승격시키고 싶을 때 사용합니다. 이는 더 강력하고 통합된 관계를 의미하며, 코드 재사용이나 인터페이스 충족에 자주 사용됩니다. 기능을 or 속성을 혼합하는 것으로 생각하세요.
- 명명된 중첩: 외부 struct가 그 자체로 struct인 고유한 구성 요소 또는 부분을 "가질" 때 사용합니다. 이는 명확한 포함 관계를 의미하며 구성 요소는 해당 이름으로 명시적으로 액세스됩니다. 복잡한 계층적 데이터 구조를 모델링하는 데 이상적입니다.
Struct 태그
Go struct에는 필드와 연결된 "태그"도 있을 수 있습니다. 이는 메타데이터 목적으로 리플렉션에 사용되며, 가장 일반적으로 JSON 또는 XML과 같은 형식으로 직렬화/역직렬화하거나 유효성 검사 라이브러리에 사용됩니다.
package main import ( "encoding/json" "fmt" ) type User struct { ID int `json:"id"` // JSON 키 이름 지정 Username string `json:"username,omitempty"` // JSON 키 지정 및 비어 있으면 생략 Email string `json:"email"` Password string `json:"-"` // "-" 태그는 JSON 직렬화 동안 이 필드를 무시합니다. CreatedAt string `json:"created_at,string"` // JSON의 문자열로 처리 } func main() { u := User{ ID: 1, Username: "gopher", Email: "gopher@example.com", Password: "supersecret", // 이 필드는 JSON에서 무시됩니다. CreatedAt: "2023-10-27T10:00:00Z", } // struct를 JSON으로 마샬링 jsonData, err := json.MarshalIndent(u, "", " ") if err != nil { fmt.Println("JSON 마샬링 오류:", err) return } fmt.Println("JSON 출력:\n", string(jsonData)) // 출력: // JSON 출력: // { // "id": 1, // "username": "gopher", // "email": "gopher@example.com", // "created_at": "2023-10-27T10:00:00Z" // } // JSON을 다시 struct로 언마샬링 jsonString := `{"id":2, "username":"anon", "email":"anon@example.com", "password":"abcd", "created_at":"2023-10-28T11:00:00Z"}` var u2 User err = json.Unmarshal([]byte(jsonString), &u2) if err != nil { fmt.Println("JSON 언마샬링 오류:", err) return } fmt.Println("\n언마샬링된 User 2:", u2) fmt.Println("User 2 비밀번호 (무시되었으므로 제로값이어야 함):",