본문 바로가기
Language/Golang

Golang (Go언어) 구조체(Structure)

by HeeWorld 2024. 11. 5.

Tucker의 Go 언어 프로그래밍 책과 유튜브를 통해 학습 중입니다.

 

Golang 마스코트 Gopher(고퍼)

 

구조체(Structure)

- 구조체는 여러 필드를 묶어서 사용하는 타입

- 여러 필드를 묶어서 하나의 구조체를 만듦

- 다른 타입의 값들을 변수 하나로 묶어주는 기능

 

 

구조체 선언

type 타입명 struct {
  필드형 타입
  ...
  필드형 타입
}

 

- type: type 키워드를 적어서 새로운 사용자 정의 타입을 정의

- 타입명: 타입명의 첫 번째 글자가 대문자이면 패키지 외부로 공개되는 타입

- struct: 타입 종류인 struct를 기재

- { 필트형 타입 ... }: 중괄호 {} 안에 이 구조체에 속한 필드들을 적으며, 각 필드는 필드명과 타입을 기재

 

예를 들어 이름, 반, 번호, 성적 등으로 학생 데이터를 만들 때 각각을 변수로 선언하는 것보다 학생이라는 구조체로 묶으면 더 쉽게 다룰 수 있음

학생 구조

tpye Student struct {
   Name     string
   Class    int
   No       int
   Score    float64
}

 

→ Student로 구조체를 정의 하였으며, Student 타입을 int나 float64 같은 내장 타입 처럼 선언해 사용할 수 있음

var a Student

 

→ Student 타입 a 변수를 선언하고, a는 Student 필드인 Name, Class, No, Score 같은 필드들을 포함

→ a에 속한 각 필드에는 a.Name처럼 a 뒤에 점. 을 찍어서 접근할 수 있음

 

package main

import "fmt"

type House struct {
	Address string
	Size    int
	Price   float64
	Type    string
}

func main() {
	var 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)
	fmt.Println("타입:", house.Type)
}

// 결과

주소: 서울시 강동구
크기: 28평
가격: 9.80억 원
타입: 아파트

 

 

구조체 변수 초기화

- 초깃값 생략, 모든 필드 초기화, 일부 필드 초기화 방법이 있음

 

* 초깃값 생략

- 초깃값을 생략하면 모든 필드가 기본 값으로 초기화 됨

var house House

 

→ string 타입의 기본 값은 빈 문자열 ""이고, int는 0, float64는 0.0이기에 아래와 같이 초기화 됨.

- Address: ""

- Size: 0

- Price: 0.0

- Type: ""

 

* 모든 필드 초기화

- 모든 필드값을 중괄호 사이에 넣어서 초기화 하며, 모든 필드가 순서대로 초기화 됨

var house House = House{ "서울시 강동구", 28, 9.80, "아파트" }

 

→ 첫 필드는 Address이며, "서울시 강동구"가 입력되고 나머지 필드 순서와 입력한 순서에 맞춰 일대일 매칭되어 초기화 됨.

- Address: "서울시 강동구"

- Size: 28

- Price: 9.80

- Type: "아파트"

 

아래와 같이 여러 줄에 걸쳐서 초기화할 수 있음

var house House = House {
  "서울시 강동구",
  28,
  9.80,
  "아파트",  // 여러 줄로 초기화할 때는 제일 마지막 값 뒤에 꼭 쉼표를 달아야 함.
 }

 

* 일부 필드 초기화

- 일부 필드값만 초기화할 때는 '필드명: 필드값' 형식으로 초기화하며, 초기화되지 않은 나머지 변수에는 기본값이 할당

var house House = House{ Size: 28, Type: "아파트" }

 

- Address: ""

- Size: 28

- Price: 0.0

- Type: "아파트"

 

아래와 같이 여러 줄에 걸쳐서 초기화할 수 있음

var house House = House {
  Size: 28,
  Type: "아파트",  // 여러 줄로 초기화할 때는 제일 마지막 값 뒤에 꼭 쉼표를 달아야 함.
}

 

 

구조체를 포함하는 구조체

- 구조체의 필드로 다른 구조체를 포함할 수 있음

- 일반적인 내장 타입처럼 포함하는 방법과 포함된 필드 방식이 있음

type User struct {      // 일반 고객용 구조체
  Name string
  ID   string
  Age  int
}

type VIPUser struct {   // VIP 고객용 구조체
  UserInfo  User
  VIPLevel  int
  Price     int
}

 

→ 위와 같이 일반 고객 정보를 나타내는 User 구조체와 VIP 고객 정보를 나타내는 VIPUser 구조체가 있음.

- VIP고객도 고객이므로 이름, ID, 연령 정보를 입력할 변수를 각각 선언하지 않고 이미 만들어 사용하는 일반 고객 정보용 User 구조체를 사용할 수 있음

   = VIP가 일반 User를 포함하고 있다(구조체가 구조체를 포함하고 있음)

package main

import "fmt"

type User struct {
	Name string
	ID   string
	Age  int
}

type VIPUser struct {
	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,            // 마지막에 쉼표
	)
}

// 결과

유저: 송하나 ID: hana 나이: 23
VIP 유저: 화랑 ID: hwarang 나이: 40 VIP레벨: 3  VIP 가격: 250만 원

 

vip.UserInfo.Name

→ Name 필드는 vip 변수의 UserInfo 필드 안에 속하기 때문에 vip.UserInfo.Name으로 접근해야 함

(vip는 VIPUser structure에 포함된 것 중에 UserInfo에 포함된 Name)

 

 

포함된 필드 방식(Embedded Field)

→ 다른 언어에서는 embedded struct라고 씀

- 구조체에서 다른 구조체를 필드로 포함할 때 필드명을 생략하면 .을 한 번만 찍어 접근할 수 있음

type User struct {
  Name  string
  ID    string
  Age   int
  Level int
}

type VIPUser struct {
  User
  Price int
  Level int
}

 

→ VIPUser의 User에 타입만 적고 필드명이 없는 것(구조체만 가능함) = embedded field (내장된 필드)

package main

import "fmt"

type User struct {
	Name string
	ID   string
	Age  int
}

type VIPUser struct {
	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,
	)
}


// 결과

유저: 송하나 ID: hana 나이: 23
VIP 유저: 화랑 ID: hwarang 나이: 40 VIP레벨: 3  VIP 가격: 250만 원

 

✓ 만약, VIPUser에도 Name이라는 필드가 있다면 출력을 vip.Name으로 하면 VIPUser의 Name 값과 User의 Name 중 VIPUser의 Name이 출력됨.

   → VIPUser의 structure에 우선순위가 있음, 만약 User의 Name명을 출력하고 싶다면 타입명을 사용하면 됨 = vip.User.Name

 

 

구조체 크기

- 구조체 변수가 선언되면 컴퓨터는 구조체 필드를 모두 담을 수 있는 메모리 공간을 할당함

- 구조체의 사이즈는 모든 필드 사이즈를 다 더한 것(sum)

type User struct {
  Age  intr
  Score float64
}
var user User

 

→ 컴퓨터는 구조체 타입의 선언을 보고 해당 구조체의 사이즈를 알 수 있음

   = 구조체 사이즈는 모든 필드의 사이즈를 더한 값이므로, int 타입은 8바이트, float64 타입은 8바이트이기에 user의 크기는 16바이트가 됨

→ 메모리에 총 16바이트 사이즈를 할당함, 첫 번째 필드가 Age 두 번째 필드가 Score

→ Go에서는 Type이 나오면 Size가 나오고, 그 Type의 변수를 할당하면 그 Size 만큼 할당하게 됨

구조체 크기

 

구조체 복사

- 구조체 변숫값을 다른 구조체에 대입하면 모든 필드값이 복사됨

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}

	student2 := student

	PrintStudent(student2)
}

// 결과 값

나이:15 번호:23 점수:88.20

 

- PrintStudent() 함수는 Student 타입을 인수로 받기 때문에, student2의 모든 필드 값이 PrintStudent() 함수 내 s 인수로 복사 됨

 

→ 값은 같지만 서로 다른 메모리 공간을 가지고 있음

→ Go 내부에서는 필드 각각이 아닌 구조체 전체를 한 번에 복사함, 대입 연산자가 우변 값을 좌변 메모리 공간에 복사할 때

    '복사되는 크기'는 '타입 크기'와 같음. 구조체 크기는 모든 필드를 포함하므로 구조체 전체 필드가 복사되는 것

구조체 값 복사

 

 

필드 배치 순서에 따른 구조체 크기 변화 & 메모리 정렬

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()함수는 해당 변수의 메모리 공간 크기를 반환함 (해당 변수가 차지하고 있는 메모리 공간의 크기)

→ int32는 4바이트, float64는 8바이트로 합치면 12가 되어야 하지만 16이 나온 이유? 패딩=메모리정렬(Memory Alignment)

   = Go 컴파일러가 메모리 정렬을 한 후에 저장하기 때문에 이런 문제가 발생됨

 

메모리 정렬(Memory Alignment)

- 컴퓨터가 데이터에 효과적으로 접근하고자 메모리를 일정 크기 간격으로 정렬하는 것

 

✓ 레지스터는 실제 연산에 사용되는 데이터가 저장되는 곳으로, 레지스터 크기가 8바이트라는 것은 한 번 연산에 8바이트 크기를 연산할 수 있다는 것

    따라서, 데이터가 레지스터 크기와 똑같은 크기로 정렬되어 있으면 효율적으로 데이터를 읽어올 수 있음

 

예를 들어, 64비트 컴퓨터에 int64데이터의 시작 주소가 100번지일 경우 100은 8의 배수가 아니기 때문에 레지스터 크기(8)에 맞게 정렬되어 있지 않음.

이런 경우 데이터를 메모리에서 읽어올 때 성능을 손해보기 때문에 처음부터 플그램 언어에서 데이터를 만들 때 8의 배수인 메모리 주소에 데이터를 할당함.

이 경우 100번지가 아니라 8의 배수인 104번지에 할당 됨.

레지스터 크기인 8바이트에 맞춰 메모리 주소를 정렬

type User struct {
  Age  int32
  Score  float64
}

var user User

 

해당 예제에서 Age는 4바이트, Score는 8바이트 인데, User 구조체 변수 user의 시작 주소가 240번지이면 Age의 시작 주소 역시 240번지가 됨.

Age는 4바이트 공간을 차지하기 때문에 바로 붙여서 Score를 할당하면 Score의 시작 주소는 244번지가 되는데, 244는 8의 배수가 아니라 성능 손해를 봄.

그래서 프로그램 언어에서 User 구조체를 할당할 때 Age와 Score의 사이는 4바이트 만큼 띄워서 할당함.

이렇게 메모리 정렬을 위해 필드 사이에 공간을 띄우는 것을 메모리 패딩(Memory Padding)이라고 함.

4바이트 변수의 시작 주소는 4의 배수로 맞추고 2바이트 변수의 시작 주소는 2의 배수로 맞춰서 패딩함 = 이렇게 맞추는 게 컴퓨터 내부에서 처리하기에 더 효율적이기 때문

 

메모리 패딩을 고려한 필드 배치 방법

- 메모리 패딩으로 인해 생길 수 있는 문제 = 메모리 낭비

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

 

- User 구조체는 1바이트 필드 3개와 8바이트 필드 2개로 구성되어 19바이트 크기를 차지하지만, 실제 구조체 크기는 메모리 패딩으로 40바이트가 됨

메모리 낭비

 

✓ 8바이트 보다 작은 필드는 8바이트 크기(단위)를 고려하여 몰아서 배치하여 아래와 같이 구조체 필드 순서를 조정

     = 작은 바이트부터 큰 바이트로 배치

package main

import (
	"fmt"
	"unsafe"
)

type User struct {
	A int8 // 1바이트
	C int8 // 1바이트
	E int8 // 1바이트
	B int  // 8바이트
	D int  // 8바이트
}

func main() {
	user := User{1, 2, 3, 4, 5}
	fmt.Println(unsafe.Sizeof(user))
}

// 결과

24

 

→ 구조체 크기가 24바이트로 줄어, 40바이트 보다 16바이트 절약함

메모리 공간 절약

 

 

구조체의 역할

→ 결합도(의존성)은 낮게 응집도는 높게

- 프로그래밍 역사는 객체 간 결합도(객체 간 의존관계)는 낮추고 연관있는 데이터 간 응집도를 올리는 방향으로 흘러옴

   = 지금까지 배운 함수, 배열, 구조체 모두 응집도를 증가시키는 역할을 함.

 

✓ 함수는 관련 코드 블록을 묶어 응집도를 높이고 재사용성을 증가시킴

✓ 배열은 같은 타입의 데이터들을 묶어 응집도를 높임

✓ 구조체는 관련된 데이터들을 묶어서 응집도를 높이고 재사용성을 증가시킴

 

결합도(Coupling)와 응집도(Cohesion)
결합도는 모듈간 상호 의존 관계를 형성해서 서로 강하게 결합되어있는 정도를 나타내는 용어로 의존성이라고 말하기도 함
응집도는 모듈의 완성도를 말하는 것으로 모듈 내부의 모든 기능이 단일 목적에 충실하게 모여 있는지를 나타냄

'Language > Golang' 카테고리의 다른 글

Golang (Go언어) 문자열(String)  (4) 2024.11.08
Golang (Go언어) 포인터(Pointer)  (3) 2024.11.07
Golang (Go언어) 배열(Array)  (0) 2024.11.02
Golang (Go언어) for문 (반복문)  (4) 2024.10.31
Golang (Go언어) Switch문 (조건문)  (4) 2024.10.31