[Golang] 구조체 (Structure)
구조체란?
배열이 같은 타입의 값들을 변수 하나로 묶어줬던 것과는 달리
구조체는 다른 타입의 값들을 변수 하나로 묶어주는 기능이다.
선언 및 기본 사용
형식
type 타입명 struct {
필드명 타입
...
필드명 타입
}
- type 키워드를 적어서 새로운 사용자 정의 타입을 정의한다.
- 타입명의 첫 번째 글자가 대문자면 패키지 외부로 공개되는 타입이다.
예제 1. Student 구조체
type Student struct {
Name string
Class int
No int
Score float64
}
- Student 구조체를 정의했고 이제 int나 float64 같은 내장 타입처럼 선언하여 사용할 수 있다.
Student 타입 구조체 변수 선언
var a Student
- a는 Student의 필드들인 Name, Class, No, Score 같은 필드들을 포함한다.
- 각 필드는 a.Name 처럼 점 . 을 찍어서 접근할 수 있다.
예제 2.
package main
import "fmt"
type House struct { // ❶ House 구조체를 정의합니다.
Address string
Size int
Price float64
Type string
}
func main() {
var house House // ❷ House 구조체 변수를 선언합니다.
house.Address = "서울시 강동구 ..." // ❸ 각 필드값을 초기화합니다.
house.Size = 28
house.Price = 9.8
house.Type = "아파트"
fmt.Println("주소:", house.Address) // ❹ 필드값을 출력합니다.
fmt.Printf("크기 %d평\\n", house.Size)
fmt.Printf("가격: %.2f억원\\n", house.Price) // ➎ 소수점 2자리까지 출력합니다.
fmt.Println("타입:", house.Type)
}
구조체 변수 초기화
1️⃣ 초깃값 생략
var house House
- 초깃값을 생략하면 모든 필드가 기본값으로 초기화 된다.
2️⃣ 모든 필드 초기화
var house House = House{ "서울시 강동구", 28, 9.80, "아파트" }
- 모든 필드값을 중괄호 사이에 넣어서 초기화한다.
여러 줄에 걸쳐서 초기화할 수 있다.
var house House = House{
"서울시 강동구",
28,
9.80,
"아파트", // 여러 줄로 초기화할 때는 젤 마지막 값에 꼭 쉼표를 붙이자.
}
3️⃣ 일부 필드 초기화
var house House = House{ Size: 29, Type: "아파트"}
- ‘필드명: 필드값’ 형식으로 초기화 할 수 있다.
- 초기화되지 않은 나머지 필드는 역시 타입의 기본값이 할당된다.
당연히 여러줄로 초기화 가능
var house House = House {
Size: 29,
Type: "아파트",
}
구조체를 포함하는 구조체
구조체의 필드로 다른 구조체를 포함할 수 있다.
1️⃣ 내장 타입처럼 포함하는 방식
예를 들어 아래와 같은 구조체가 있다고 해보자.
type User struct {
Name string
ID string
Age int
}
type VIPUser struct {
UserInfo User
VIPLevel int
Price int
}
- VIP 고객도 고객이므로 이름, ID, 연령정보를 입력할 변수를 각각 선언하지 않고 이미 만들어 놓은 User 구조체를 활용하는 방법이 깔끔하다.
그러면 아래처럼 구조체 안에 내장 타입으로 포함시킬 수 있다.
package main
import "fmt"
type User struct { // 일반 고객용 구조체
Name string
ID string
Age int
}
type VIPUser struct { // VIP 고객용 구조체
UserInfo User // 구조체 안에 구조체
VIPLevel int
Price int
}
func main() {
user := User{"송하나", "hana", 23}
vip := VIPUser{
User{"화랑", "hwarang", 40},
3,
250, // 여러 줄로 초기화할 때는 제일 마지막 값 뒤에 꼭 쉼표를 달아주세요.
} // ❶ User를 포함한 VIPUser 구조체 변수를 초기화합니다.
fmt.Printf("유저: %s ID: %s 나이 %d\\n", user.Name, user.ID, user.Age)
fmt.Printf("VIP 유저: %s ID: %s 나이 %d VIP 레벨: %d VIP 가격: %d만원\\n",
vip.UserInfo.Name, // ❷ UserInfo 안의 Name
vip.UserInfo.ID, // ❸ UserInfo 안의 ID
vip.UserInfo.Age,
vip.VIPLevel, // ❹ VIPUser의 VIPLevel
vip.Price, // ➎ 마지막에 쉼표
)
}
2️⃣ 포함된 필드 방식
내장 타입으로 접근하려면 vip.UserInfo.Name 과 같이 두 단계에 걸쳐서 접근해야 한다.
package main
import "fmt"
type User struct { // 일반 고객용 구조체
Name string
ID string
Age int
}
type VIPUser struct { // VIP 고객용 구조체
User // ❶ 필드명 생략
VIPLevel int
Price int
}
func main() {
user := User{"송하나", "hana", 23}
vip := VIPUser{
User{"화랑", "hwarang", 40},
3,
250,
}
fmt.Printf("유저: %s ID: %s 나이 %d\\n", user.Name, user.ID, user.Age)
fmt.Printf("VIP 유저: %s ID: %s 나이 %d VIP 레벨: %d VIP 가격: %d만원\\n",
vip.Name, // ❷ . 하나로 접근할 수 있습니다.
vip.ID,
vip.Age,
vip.VIPLevel,
vip.Price, // 여러 줄로 초기화할 때는 제일 마지막 값 뒤에 꼭 쉼표를 달아주세요.
)
}
- 구조체에서 다른 구조체를 필드로 포함할 때 필드명을 생략하면 .을 한번만 찍어 접근할 수 있다.
❗ 필드 중복
만약 포함된 필드안에 속한 필드명과 포함된 상위 구조체의 필드명이 서로 겹치면 어떻게 될까?
package main
import "fmt"
type User struct {
Name string
ID string
Age int
Level int // ❶ User의 Level 필드
}
type VIPUser struct {
User // ❷ Level 필드를 갖는 구조체
Price int
Level int // ❸ VIPUser의 Level 필드
}
func main() {
user := User{"송하나", "hana", 23, 10}
vip := VIPUser{
User{"화랑", "hwarang", 40, 10},
250,
3, // 여러 줄로 초기화할 때는 제일 마지막 값 뒤에 꼭 쉼표를 달아주세요.
}
fmt.Printf("유저: %s ID: %s 나이 %d\\n", user.Name, user.ID, user.Age)
fmt.Printf("VIP 유저: %s ID: %s 나이 %d VIP 레벨: %d 유저 레벨:%d\\n",
vip.Name,
vip.ID,
vip.Age,
vip.Level, // ➍ VIPUser의 Level
vip.User.Level, // ➎ 포함된 구조체명을 쓰고 접근
)
}
- 이름이 겹칠 경우 현재 변수타입에 해당하는 구조체의 필드에 접근한다.
- User 구조체의 중복된 필드인 Level에 접근하려면 vip.User.Level 과 같이 접근해야 한다.
구조체 크기
아래와 같은 구조체 User가 정의되어 있다고 치자.
type Student struct {
Age int
Score float64
}
- int 타입은 64비트 = 8바이트, float64 타입은 64비트 = 8바이트이다. 즉 구조체 User는 16바이트가 된다.
1️⃣ 구조체 값 복사
package main
import "fmt"
type Student struct {
Age int // ❶ 대문자로 시작하는 필드는 외부로 공개됩니다.
No int
Score float64
}
func PrintStudent(s Student) {
fmt.Printf("나이:%d 번호:%d 점수:%.2f\\n", s.Age, s.No, s.Score)
}
func main() {
var student = Student{15, 23, 88.2}
// ❷ student 구조체 모든 필드가 student2 로 복사됩니다.
student2 := student
PrintStudent(student2) // ❸ 함수 호출시에도 구조체가 복사됩니다.
}
2️⃣ 필드 배치 순서에 따른 구조체 크기 변화
package main
import (
"fmt"
"unsafe"
)
type User struct {
Age int32 // ❶ 4바이트
Score float64 // 8바이트
}
func main() {
user := User{23, 77.2}
fmt.Println(unsafe.Sizeof(user))
}
---
16
- 변수의 메모리 공간 크기를 반환하는 함수인 unsafe.Sizeof() 를 사용하여 확인결과 4바이트 + 8바이트 해서 12바이트가 나오지 않고 16바이트가 반환되었다.
- 이는 메모리 정렬 때문에 그렇다.
❗메모리 정렬
메모리 정렬이란 컴퓨터가 데이터에 효과적으로 접근하고자 메모리를 일정 크기 간격으로 정렬하는 것을 말한다.
레지스터 크기가 4바이트인 컴퓨터를 32비트 컴퓨터라고 부르고 8바이트인 컴퓨터를 64비트 컴퓨터라고 부른다.
*레지스터 : 실제 연산에 사용되는 데이터가 저장되는 곳
만약,
64비트 컴퓨터에서 int64 데이터의 시작 주소가 10번지 일 경우 10은 8의 배수가 아니기 때문에 처음부터 프로그램 언어에서 데이터를 만들 때 8의 배수인 메모리 주소에 데이터를 할당하게 된다. (이 경우엔 10번지가 아니라 16번지에 할당됨.)
데이터가 레지스터 크기와 똑같은 크기로 정렬이 되어 있지 않으면 손해를 보게되므로
프로그램에서 데이터 할당 시 메모리 정렬이 된다.
💡 다시 돌아와서 정리하기
위의 예제를 다시 보면 Age는 4바이트이고 Score는 8바이트이다. User 구조체 변수인 user 변수의 시작 주소가 240번지라고 치면 Age의 시작 주소 역시 240번지이다.
Age는 4바이트의 공간을 차지하기 때문에 바로 붙여서 Score를 할당하면 Score의 시작주소는 244번지가 된다.
하지만 244번지는 8의 배수가 아니라서 4바이트 만큼 띄워서 8의 배수를 맞추고 메모리를 할당하게 된다.
이렇게 띄워서 메모리를 할당하는 것을 메모리 패딩 (Memory Padding)이라고 한다.
3️⃣ 메모리 패딩을 고려한 배치방법
package main
import (
"fmt"
"unsafe"
)
type User struct {
A int8 // 1바이트
B int // 8바이트
C int8 // 1바이트
D int // 8바이트
E int8 // 1바이트
}
func main() {
user := User{1, 2, 3, 4, 5}
fmt.Println(unsafe.Sizeof(user))
}
---
40
- 위의 예시에 나온 변수들이 어떻게 메모리가 할당 되었을까?
- 메모리 정렬에 의해 바로 아래 사진처럼 메모리가 할당 되었을 것이다.변수 A, C, E에 7바이트씩 Memory Padding이 됨.
이에 대한 해결방법은 크기가 큰 변수를 제일 먼저 배치하면 될 것이다. 다시 필드 순서를 재조정 해보자면..
package main
import (
"fmt"
"unsafe"
)
type User struct {
B int // 8바이트
D int // 8바이트
A int8 // 1바이트
C int8 // 1바이트
E int8 // 1바이트
}
func main() {
user := User{1, 2, 3, 4, 5}
fmt.Println(unsafe.Sizeof(user))
}
---
24
- 메모리 크기 반환값이 기존 40에서 24로 줄었다.
💡 메모리 용량이 충분하다면 패딩으로 인한 메모리 낭비를 크게 걱정하지 않아도 된다.
(임베디드 하드웨어의 프로그램이라면 패딩을 고려해야 함.)
구조체는 중요하다. (엄격, 근엄, 진지)
프로그래밍 역사는 그 동안 객체 간의 결합도를 낮추고 응집도를 올리는 방향으로 흘러왔다.
지금까지 배운 함수, 배열, 구조체 모두 응집도를 증가시키는 역할을 한다.
- 함수(Function)는 관련 코드 블록을 묶어서 응집도와 재사용성을 증가시킴
- 배열(Array)은 같은 타입의 데이터들을 묶어서 응집도를 증가시킴
- 구조체(Structure)는 관련된 데이터들을 묶어서 응집도와 재사용성을 증가시킴
구조체가 등장함으로써 개발자는 개별 데이터에 집중하지 않고 보다 큰 범위에서 생각할 수 있게 되었다.
- ex) 쇼핑몰을 제작한다고 했을 때 상품에 대한 상품 가격이나 상품명과 같은 개별 데이터들은 나중에 추가가 가능함.
그러면서 자연스럽게 코딩의 중심이 구조체간의 관계나 상호작용 중심으로 변화하게 되었음.
*그 연장선으로 인터페이스라는 개념이 추가되어 객체지향 프로그래밍으로 발전함.