[Golang] struct(구조체)

2021-10-22 hit count image

Golang에서 struct(구조체)를 정의하고 사용하는 방법에 대해서 알아보도록 하겠습니다.

개요

이번 블로그 포스트에서는 Golang에서 struct(구조체)를 사용하여 변수를 선언하고 사용하는 방법에 대해서 살펴보려고 합니다. 이 블로그 포스트에서 소개하는 코드는 다음 링크를 통해 확인하실 수 있습니다.

구조체

Golang에서 구조체(struct)는 여러 타입의 필드를 묶어서 제공할 수 있는 타입입니다.

구조체의 역할

구조체는 코드의 결합도, 의존성을 낮게 만들고 응집도를 높게 만드는 역할을 합니다.

Low coupling, high cohesion

  • 함수는 관련 코드 블록을 묶어서 응집도를 높이고 재사용성을 증가시킵니다.
  • 배열은 같은 타입의 데이터들을 묶어서 응집도를 높입니다.
  • 구조체는 관련 데이터들을 묶어서 응집도를 높이고 재사용성을 증가시킵니다.

이런 구조체는 객체 지향 프로그래밍(OOP - Object Oriented Programming)에 기반이 됩니다.

구조체 정의

Golang에서 구조체(struct)는 다음과 같이 정의할 수 있습니다.

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

구조체도 함수와 마찬가지로 필드명을 사용하여 외부에 노출시킬 필드(Public)와 노출 시키지 않을 필드(Private)를 설정할 수 있습니다. 필드명이 대문자로 시작하면 외부에서 사용이 가능하지만, 소문자로 시작하는 경우에는 외부에서 사용이 불가능하다.

이를 확인하기 위해 main.go 파일을 생성하고 다음과 같이 수정합니다.

package main

import "fmt"

type Student struct {
  Name  string
  Class int
  No    int
}

func main() {
  var s Student
  s.Name = "Tom"
  s.Class = 1
  s.No = 1

  fmt.Println(s)
  fmt.Printf("%v\n", s)
  fmt.Printf("Name: %s, Class: %d, No: %d\n", s.Name, s.Class, s.No)
}

이렇게 수정한 프로그램을 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
{Tom 1 1}
{Tom 1 1}
Name: Tom, Class: 1, No: 1

초기화

다음과 같이 구조체를 초기화하지 않고 변수를 선언한 경우, 모든 필드값은 필드 타입의 기본값으로 설정됩니다.

type Student struct {
  Name  string
  Class int
  No    int
}

var s Student;

구조체로 변수를 생성할 때, 필드 순서로 초기값을 대입할 수 있습니다.

var s1 Student = Student{"Tom", 1, 2}
var s2 Student = Student{
  "John",
  1,
  3,
}

또는 다음과 같이 필드명을 지정하여 초기화할 수 있다.

var a Student = Student{ Name: "Deku", Class: 1, No: 3 };

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

type Student struct {
  Name  string
  Class int
  No    int
}

func main() {
  var s Student
  fmt.Println(s)

  var s1 Student = Student{"Tom", 1, 2}
  var s2 Student = Student{
    "John",
    1,
    3,
  }
  fmt.Println(s1)
  fmt.Println(s2)

  var s3 Student = Student{Name: "Deku", Class: 1, No: 3}
  fmt.Println(s3)
}

이렇게 수정한 코드를 실행하면, 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
{ 0 0}
{Tom 1 2}
{John 1 3}
{Deku 1 3}

구조체를 포함한 구조체

Golang에서는 다음과 같이 구조체가 다른 구조체를 포함할 수 있습니다. Golang에서는 이를 Nested struct라고 부릅니다.

type ClassInfo struct {
  Class int
  No int
}

type Student struct {
  Class ClassInfo
  Name string
}

이렇게 구조체가 포함된 구조체는 다음과 같이 초기화를 할 수 있습니다.

var s Student = Student{
  Class: ClassInfo{Class: 1, No: 1},
  Name:  "John",
}

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

type ClassInfo struct {
  Class int
  No    int
}

type Student struct {
  Class ClassInfo
  Name  string
}

func main() {
  var s Student = Student{
    Class: ClassInfo{Class: 1, No: 1},
    Name:  "John",
  }

  fmt.Println(s.Class.Class)
  fmt.Println(s.Class.No)
  fmt.Println(s.Name)
}

이렇게 수정한 코드를 실행하면, 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
1
1
John

Embedded field

Golang에서는 구조체가 다른 구조체를 포함할 수 있으며, 다음과 같이 필드명을 사용하지 않고 구조체를 포함 시킬 수 있습니다. 이를 Golang에서는 Embedded field라고 부릅니다.

type ClassInfo struct {
  Class int
  No int
}

type Student struct {
  ClassInfo
  Name string
}

이렇게 선언된 Embedded Field는 다음과 같이 초기화할 수 있습니다.

var s Student = Student{
  ClassInfo: ClassInfo{Class: 1, No: 1},
  Name:      "John",
}

이렇게 사용된 Embedded Field는 다음과 같이 직접 접근하여 사용할 수 있습니다.

fmt.Println(s.Class)
fmt.Println(s.No)
fmt.Println(s.Name)

Embedded Field는 다음과 같이 현재 구조체의 필드명과 중복되는 필드를 사용할 수 있습니다.

type ClassInfo struct {
  Class int
  No int
}

type Student struct {
  ClassInfo
  Name string
  No int
}

이렇게 중복된 필드명은 다음과 같이 초기화할 수 있습니다.

var s1 DupStudent = DupStudent{
  ClassInfo: ClassInfo{Class: 1, No: 1},
  Name:      "John",
  No:        10,
}

필드명이 중복되었기 때문에 다음과 같이 중복된 필드를 직접 접근하여 사용하면, 현재 구조체의 값이 반환되게 됩니다.

fmt.Println(s1.No) // 10

그럼, 앞에서와 같이 Embedded Field의 필드값에 접근하고 싶다면, 다음과 같이 사용할 수 있습니다.

fmt.Println(s1.ClassInfo.No) // 1

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

type ClassInfo struct {
  Class int
  No    int
}

type Student struct {
  ClassInfo
  Name string
}

type DupStudent struct {
  ClassInfo
  Name string
  No   int
}

func main() {
  var s Student = Student{
    ClassInfo: ClassInfo{Class: 1, No: 1},
    Name:      "John",
  }

  fmt.Println(s.Class)
  fmt.Println(s.No)
  fmt.Println(s.Name)

  var s1 DupStudent = DupStudent{
    ClassInfo: ClassInfo{Class: 1, No: 1},
    Name:      "John",
    No:        10,
  }

  fmt.Println(s1.Class)
  fmt.Println(s1.No)
  fmt.Println(s1.Name)
  fmt.Println(s1.ClassInfo.No)
}

이렇게 수정한 파일을 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
1
1
John
1
10
John
1

메모리 정렬

Golang에서는 보통 프로그래밍할 때에는 메모리를 크게 신경 쓸 필요가 없습니다. 하자만, 만약 여러분이 메모리가 작은 디바이스나 메모리 효율을 생각하며 프로그램을 작성해야 한다면, 구조체의 메모리 정렬(Memory Alignment)을 알아둘 필요가 있다.

s := Student{"John", 1}
var str string = "John"
var i int = 1

fmt.Println(unsafe.Sizeof(s))
fmt.Println(unsafe.Sizeof(str))
fmt.Println(unsafe.Sizeof(i))

Golang은 CPU에서 계산하기 편하게 하기 위해, 구조체를 8의 배수로 정렬하여 메모리에 저장합니다.

24
16
8

만약 필드의 타입이 8의 배수보다 작은 경우, Golang은 빈 공간(Memory Padding)을 추가하여 8의 배수로 만들어 저장하게 됩니다.

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

위와 같은 구조체는 변수의 타입만 고려한다면, 19 바이트만이 사용되야 하지만, 구조체는 8의 배수로 정렬이 되기 때문에 A, C, E에 빈 공간을 추가하여 8 바이트로 계산하므로 실제로 사용되는 메모리는 40 바이트가 됩니다.

이렇게 빈 공간(Memory Padding)에 추가로 낭비되는 메모리가 많아지는 것을 막기 위해 다음과 같이 작은 메모리를 먼저 선언하여 메모리 정렬을 수행할 수 있습니다.

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

역시 19 바이트가 실제로 사용되는 메모리이지만, A, C, E가 함께 선언되었기 때문에 3바이트의 실제 공간과 5바이트의 빈 공간을 추가하여 8바이트로 계산되게 됩니다. 따라서 사용되는 메모리는 24바이트가 됩니다.

이처럼 Golang은 CPU의 계산을 효율적으로 하기 위해 8배수 정렬을 하며, 이로 인한 메모리 낭비를 최소화하기 위해서는 위와 같이 메모리 정렬을 수행하는 것이 좋습니다.

이를 확인하기 위해 main.go 파일을 열고 다음과 같이 수정합니다.

package main

import (
  "fmt"
  "unsafe"
)

type Student struct {
  Name  string
  Class int
}

type Memory struct {
  A int8
  B int
  C int8
  D int
  E int8
}

type MemoryAlignment struct {
  A int8
  C int8
  E int8
  B int
  D int
}

func main() {
  s := Student{"John", 1}
  var str string = "John"
  var i int = 1

  fmt.Println(unsafe.Sizeof(s))
  fmt.Println(unsafe.Sizeof(str))
  fmt.Println(unsafe.Sizeof(i))

  m := Memory{1, 2, 3, 4, 5}
  fmt.Println(unsafe.Sizeof(m))

  ma := MemoryAlignment{1, 2, 3, 4, 5}
  fmt.Println(unsafe.Sizeof(ma))
}

이렇게 수정한 코드를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
24
16
8
40
24

완료

이것으로 Golang에서 구조체(struct)를 정의하고 사용하는 방법에 대해서 알아보았습니다. 또한, 구조체의 메모리 사용에 대해서 알아보았고, 메모리 정렬을 통해 좀 더 효율적으로 메모리를 사용할 수 있음을 알게 되었습니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

책 홍보

스무디 한 잔 마시며 끝내는 React Native 책을 출판한지 벌써 2년이 다되었네요.
이번에도 좋은 기회가 있어서 스무디 한 잔 마시며 끝내는 리액트 + TDD 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.

스무디 한 잔 마시며 끝내는 React Native, 비제이퍼블릭
스무디 한 잔 마시며 끝내는 리액트 + TDD, 비제이퍼블릭
Posts