『Tucker의 Go 언어 프로그래밍』 스터디 요약 노트 : 13강 구조체

2021. 5. 28. 23:32개발/Golang

728x90
반응형

구조체는 여러 필드를 묶어서 사용하는 타입입니다. 구조체를 통하여 연관된 여러 데이터를 하나의 이름으로 묶을수 있습니다. 그리고 개별 데이터보다는 관련 데이터가 묶인 객체 단위로 코딩할 수 있게 도와줍니다. 그리고 여러값을 손쉽게 다른 함수로 전달 할 수 있습니다.  구조체에 대해 한번 정리해보겠습니다.

 

 

1. 선언 및 기본 사용

- 여러 필드를 묶어서 하나의 구조체(structure)를 만듭니다.

- 배열이 같은 타입 값들을 변수 하나로 묶어줬던 것과 달리 구조체는 다른 타입의 값들을 변수 하나로 묶어주는 기능입니다.

 

type 타입명 struct {
	필드명 타입
    ...
    필드명 타입
}
  • type : 새로운 사용자 정의 타입을 정의할 것임을 알림
  • 타입명 : 타입명의 첫 번째 글자가 대문자이면 패키지 외부로 공개되는 타입입니다. ( 16강 패키지 참조 )
  • struct : 타입종류인 struct를 적습니다.

 

 

구조체 정의 예)

type Student struct {
	Name string
    Class int
    no	int
    Score float64
}
  • Student 구조체를 정의 했습니다. 이제 Student 타입을 int나 float64 같은 내장 타입처럼 선언해서 사용할 수 있습니다.
  • var a Student 변수를 선언하면 a에 속한 각 필드에는 a.Name 처럼 a 뒤에 점 .을 찍어서 접근할 수 있습니다.

 

< 예제 >

 

package main

import "fmt"

type House struct {		// House 구조체 정의
	Address string
    Size	int
    Price 	float64
    Type	string
}

func main() {
	var house House		// Houst 구조체 변수
    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)
}

결과값

  • House 구조체를 정의합니다. House 구조체는 주소,크기,가격 등의 부동산 정보를 가진 구조체 입니다.
  • House 구조체의 변수 house를 선언합니다. 
  • 가격을 출력할 때 소수점 이하 두번째 자리까지 출력하도록 합니다. (5장 'fmt패키지를 이용한 텍스트 입출력 참고)

 

2. 구조체 변수 초기화

 

1) 초깃값 생략

var house House
  • 초깃값을 생략하면 모든 필드가 기본값으로 초기화됩니다.
  • string 타입의 기본값은 빈 문자열은 ""이고, int 0이고 float64 0.0이기 때문에 변수 house의 필드는 초기화 됩니다.
    • Address: ""
    • Size: 0
    • Price: 0.0
    • Type: ""

 

2) 모든 필드 초기화

 

var house House = House{ "서울시 강동구",28,9.80,아파트" }
  • 첫 필드는 Address입니다. "서울시 강동구"가 입력됩니다. 나머지 필드도 순서와 입력한 순서에 맞춰 1:1 매칭되어 초기화 됩니다.
    • Address: "서울시 강동구"
    • Size: 28
    • Price: 9.80
    • Type: "아파트"
  • 아래와 같이 여러줄에 걸쳐서 초기화도 가능합니다.
var house House = House {
    "서울시 강동구",
    28,
    9.80,
    "아파트",	// 여러줄로 초기화 할때는 제일 마지막 값 뒤에는 꼭 쉼표를 달아주세요.
 }

 

 

3) 일부 필드 초기화

 

var house House = House { Size: 80, Type: "아파트" }
  • 일부 필드값만 초기화할 때는 '필드명 : 필드값' 형식으로 초기화 합니다. 초기화 되지 않은 나머지 변수에는 기본값이 할당 됩니다.
    • Address:""
    • Size: 28
    • Price: 0.0
    • Type: "아파트"
  • 여러줄에 걸처서 초기화도 가능합니다.
var house House = House {
	Size = 28,
    Type: "아파트",	// 여러줄 초기화시 마지막줄엔 무조건 쉼표 ,를 달아주세요.
}

 

 

3. 구조체를 포함하는 구조체

구조체의 필드로 다른 구조체를 포함할 수 있습니다. 일반적인 내장 타입처럼 포함하는 방법과 포함된 필드 방식이 있습니다.

 

1) 내장 타입처럼 포함하는 방식

 

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

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

 

  • VIPUser 고객도 일반 고객과 같이 고객이므로 이름, 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,
    }
    
    fmt.Printf("유저:%s ID:%s 나이: %d\n", user.Name,user.ID,user.Age)
    fmt.Printf("VIP 유저 :%s ID:%s 나이: %d VIPLevel: %d Price: %d\n",
            vip.UserInfo.Name,	// UserInfo 안의 Name
            vip.UserInfo.ID,	// UserInfo 안의 ID
            vip.UserInfo.Age,	// UserInfo 안의 Age
            vip.VIPLevel,
            vip.Price,
    )
}

결과값

  • User와 VIPUser를 초기화 합니다.
  • VIPUser의 UserInfo는 User구조체로 접근하여 값을 만들었습니다.

 

2) 포함된 필드 방식 (embedded field)

 

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 VIPLevel: %d Price: %d\n",
            vip.Name,	// . 하나로 접근함
            vip.ID,	
            vip.Age,	
            vip.VIPLevel,
            vip.Price,
    )
}

결과값

  • VIPUser 구조체 필드에서 UserInfo User가 User 구조체로 바꾸면서 UserInfo를 필드 이름을 생략했습니다.
  • vip.UserInfo.Name에서 vip.Name으로 . 하나로 접근 하였습니다.
  • 구조체 안에 포함된 다른 구조체의 필드명을 생략하는 경우를 '포함된 필드'라고 부릅니다. 포함된 필드를 이용하면 .을 두번 찍을 필요 없이 한 번만으로 바로 접근할 수 있어서 편리합니다.

 

4. 구조체 크기

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

그렇다면 메모리 크기는 어떻게 알 수 있을까요? 크기를 구하는 방법을 알아봅시다.

 

type User struct {
    Age int
    Score	float64
}
  • User구조체의 변수 var user User를 선언하고 Age와 Score 필드를 지정하여 메모리 공간을 찾아 할당합니다.
  • Age는 int로 8바이트, Score는 float64로 8바이트 입니다. 총 16바이트가 필요합니다. 그렇기에 user의 구조체의 크기는 16바이트가 됩니다. ( 구조체 User로 16바이트 )

1) 구조체 값 복사

 

package main

import "fmt"

type Student struct {	// 필드명 Student가 대문자이므로 외부로 공개됩니다.
	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)	// 함수 호출시에도 구조체가 복사됩니다.
 }

결과값

  • Student의 구조체는 대문자로 시작했으므로 외부로 공개되는 필드입니다. ( 16장 '패키지'에서 자세히 설명 )
  • student의 모든 필드값을 student2로 복사합니다. Age,No,Score 모든 필드값이 복사됩니다.
  • PrintStudnet() 함수는 Student 타입을 인수로 받기 때문에 역시 마찬가지로 student2의 모든 필드 값이 PrintStudent() 함수내 s 인수로 복사됩니다

   ※ Go 내부의 구조체를 복사할 때는 한번에 복사합니다. 그리고 우변의 값이 좌변 메모리 공간에 복사할 때는 '복사되는 크기는  '타입 크기'와 같습니다.

 

2) 필드 배치 순서에 따른 구조체 크기 변화

 

package main

import (
	"fmt"
    "unsafe"
)

type User struct {
	Age int32
    Score float64
}

func main() {
	user := User{ 23, 77.2 }
    fmt.Println(unsafe.Sizeof(user))
}

결과값

  • Age값을 int32로 지정했고 값은 4바이트이고 Score는 8바이트입니다. 값은 원래 12입니다.
  • 근데 결과값은 12가아닌 16이 나왔습니다. 그 이유는 '메모리 정렬' 때문입니다.

3) 메모리 정렬이란 ?

  • 컴퓨터가 데이터에 효과적으로 접근하고자 메모리를 일정 크기 간격으로 정렬하는것을 말한다.
  • 컴퓨터는 실제 연산에 사용되는 데이터가 저장되는 레지스터를 사용합니다. 
  • 레지스터 크기가 4바이트인 컴퓨터를 32비트 컴퓨터라고하고, 8바이트 컴퓨터를 64비트 컴퓨터라고 합니다. 레지스터 크기가 8바이트라는 것은 한번에 연산 8바이트 크기를 연산할수 있다는 이야기입니다. 따라서 데이터가 레지스터크기의 똑같은 크기로 정렬되어 있으면 더욱 효율적으로 데이터를 읽어올 수 있습니다. ( 8바이트로 딱딱끊어서 가져옴 )

 

< 예시 >

package main

import (
	"fmt"
	"unsafe"
    )
    
    type User struct {
    	A int8	// 1
        B int	// 8
        C int8	// 1
        D int	// 8
        E int8	// 1
        } // 19
        
func main() {
	user := User{1,2,3,4,5}
   	fmt.Println(unsafe.Sizeof(user))
 }

 

 

  • 예제의 사이즈는 19바이트인데 size를 본 결과 40바이트가 나왔습니다. A,C,E는 1바이트인데 컴퓨터가 내부에서 처리하기에 효율적이기 위해 8바이트로 끊었고 그 결과  A,C,E에서 7바이트씩 패딩이 일어나 총 40바이트의 사이즈가 나오게 되었습니다.
  • 그렇다면 사이즈를 줄이기위해 어떻게 해야할까요? 밑의 예제를 통해 보도록하겠습니다.

 

 

< 예제 >

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))
 }

  • A,C,E 바이트가 작기에 몰아서 써줬더니 위의 그림처럼 5바이트의 패딩만이 발생하였습니다.
  • 총 24바이트로 16바이트가 줄었습니다.

 

※ 요즘 같은 데스크탑은 메모리가 많기때문에 패딩을 신경쓸필요가 없습니다. 그러나 매우 메모리 공간이 작은 임베디드 하드웨어에서 돌아가는 프로그램이라면 패딩을 고려하는것이 좋습니다.

 

 

 

5. 구조체의 역활

  • low coupling : 관련도 낮은것, 떨어져야 할것들은 떨어져있어야 한다.
  • high cohesion : 관련도 높은것, 붙어야할 것은 붙어야 한다. 
  • 구조체는 응집도를 높여준다. 그리고 객체지향프로그래밍에 기반이 된다. ( Go는 Class가 없음 )
728x90
반응형