Post

Go 구조체와 메서드 바인딩

Go의 구조체(Struct)와 메서드(Method)를 Java 클래스와 비교하며 이해하고, 값/포인터 수신자의 개념을 실습으로 알아봅니다.

Go 구조체와 메서드 바인딩

Go 구조체와 메서드 바인딩

Java에서 모든 것을 class로 설계하는 데 익숙한 우리에게 Go의 struct는 조금 다르게 다가온다.

Java 개발자의 시선으로 Go의 객체지향적인 설계 방법 살펴보기 위해서 RPG 게임의 기본 단위인 ‘게임 캐릭터’를 직접 만들어보려고 한다.

이 글에서는 Go의 구조체와 메서드 개념을 확실히 이해하는 것이 목표이다.

1. 구조체(Struct): 데이터의 뼈대

Go에서 구조체(Struct)는 여러 데이터 필드를 묶어 관리하는 사용자 정의 타입

Java의 클래스에서 메서드는 제외하고, 데이터 필드만 남겨둔 형태와 매우 유사함.

즉, 순수한 데이터의 명세서라고 할 수 있음.

Go 구조체 vs Java 클래스 (데이터 관점)

구분Go structJava class
목적관련 데이터 필드의 묶음데이터(필드)와 행위(메서드)의 묶음
선언type 이름 struct { ... }class 이름 { ... }
특징데이터 정의에만 집중데이터와 메서드를 함께 정의
예시캐릭터의 이름, 레벨, HP 등캐릭터의 속성 + 걷기, 공격하기 등

실습 (1): 게임 캐릭터 구조체 정의하기

먼저, 우리만의 캐릭터를 표현할 데이터 뼈대를 만들어 봄.

실습 파일: 06-구조체와-메서드/01-구조체-정의/main.go

아래 코드는 GameCharacter라는 구조체를 정의하고, 이 구조체를 사용해 character1character2라는 두 개의 인스턴스(객체)를 생성하는 과정을 보여줍니다. character1은 변수 선언 후 필드에 값을 할당하는 방식을, character2는 선언과 동시에 값을 초기화하는 리터럴 방식을 사용합니다.

sequenceDiagram
    participant main as "main()"
    main->>main: var character1 GameCharacter
    main->>main: character1.Name = "Hossam"
    main->>main: ... (필드 값 할당)
    main->>main: character2 := GameCharacter{...} (리터럴 초기화)
    main->>main: fmt.Println(character1)
    main->>main: fmt.Println(character2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// E:\hossam-blog\_posts\03.코딩\golang\06-구조체와-메서드\01-구조체-정의\main.go

// main 패키지를 선언함. 이 파일이 실행 가능한 프로그램임을 나타냄.
package main

// fmt 패키지를 임포트함. 콘솔 입출력을 위해 사용됨.
import "fmt"

// GameCharacter 구조체를 정의함.
// Java의 클래스와 유사하게 관련 데이터(필드)를 묶는 역할을 함.
// 하지만 Java 클래스와 달리 메서드를 포함하지 않고 순수하게 데이터만 정의함.
type GameCharacter struct {
    Name  string // 캐릭터의 이름을 저장하는 필드
    Level int    // 캐릭터의 레벨을 저장하는 필드
    HP    int    // 캐릭터의 체력을 저장하는 필드
    MP    int    // 캐릭터의 마나를 저장하는 필드
}

// 프로그램의 시작점인 main 함수임.
func main() {
    // GameCharacter 구조체의 인스턴스(객체)를 생성함.
    // var 키워드를 사용하여 'character1'이라는 이름의 변수를 선언하고, GameCharacter 타입으로 지정함.
    var character1 GameCharacter

    // 필드에 값을 할당함. .(점) 연산자를 사용하여 각 필드에 접근함.
    character1.Name = "Hossam"
    character1.Level = 99
    character1.HP = 8000
    character1.MP = 12000

    // 구조체 인스턴스를 초기화하는 또 다른 방법 (리터럴 초기화)
    // 선언과 동시에 중괄호 {} 안에 필드와 값을 직접 명시하여 인스턴스를 생성함.
    character2 := GameCharacter{
        Name:  "Alex",
        Level: 50,
        HP:    5000,
        MP:    7000,
    }

    // 생성된 구조체 인스턴스의 정보를 출력함.
    // Println 함수는 구조체를 출력할 때 필드와 값을 보기 좋게 표시해줌.
    fmt.Println("캐릭터 1 정보:", character1)
    fmt.Println("캐릭터 2 정보:", character2)

    // 특정 필드에 접근하여 값을 개별적으로 출력할 수도 있음.
    fmt.Printf("캐릭터 2의 이름: %s, 레벨: %d\n", character2.Name, character2.Level)
}

위 코드는 GameCharacter라는 설계도를 만들고, 이 설계도를 바탕으로 character1character2라는 실제 캐릭터 인스턴스를 생성하는 과정을 보여줌. Java에서 new 키워드로 객체를 생성하는 것과 개념적으로 동일함.


2. 메서드(Method): 데이터에 생명을 불어넣다

구조체가 데이터의 뼈대라면, 메서드(Method)는 그 뼈대에 붙는 살과 근육임. 즉, 특정 타입(구조체 등)에 종속되어 동작하는 함수를 의미함. Java 클래스 내부에 메서드를 선언하는 것과 달리, Go는 구조체 외부에 func (수신자) 메서드명() 형태로 선언하여 구조체와 연결(바인딩)함.

이때 등장하는 수신자(Receiver) 개념이 Java 개발자에게는 가장 중요함. 수신자는 메서드가 어떤 데이터와 연결되는지를 알려주는 매개변수임.

값 수신자 vs 포인터 수신자

구분값 수신자 (Value Receiver)포인터 수신자 (Pointer Receiver)
선언func (c GameCharacter) ...func (c *GameCharacter) ...
동작 원리메서드 호출 시 데이터의 복사본이 전달됨메서드 호출 시 데이터의 원본 주소(포인터)가 전달됨
Java 비유원시 타입을 메서드 인자로 넘기는 것과 유사객체를 메서드 인자로 넘겨 this로 필드를 변경하는 것과 유사
주 사용처원본 데이터를 수정할 필요가 없을 때 (e.g., 정보 출력)원본 데이터를 수정해야 할 때 (e.g., 상태 변경)

실습 (2): 캐릭터에 메서드 추가하기

이제 우리 캐릭터가 스스로 정보를 표시하고, 레벨업도 할 수 있도록 메서드를 추가해 봄.

실습 파일: 06-구조체와-메서드/02-메서드-바인딩/main.go

이 코드는 GameCharacter 구조체에 DisplayInfoLevelUp 두 메서드를 추가합니다. DisplayInfo는 원본을 수정할 필요가 없으므로 값 수신자를 사용하고, LevelUp은 레벨과 체력을 직접 변경해야 하므로 포인터 수신자를 사용합니다. main 함수에서 LevelUp을 호출하면 character의 원본 데이터가 변경되고, 이는 이후 DisplayInfo 호출 시 변경된 상태로 출력되는 것을 통해 확인할 수 있습니다.

sequenceDiagram
    participant main as "main()"
    participant character as "character: GameCharacter"

    main->>character: character := GameCharacter{...}
    main->>character: character.DisplayInfo() 호출
    note right of character: 값 수신자: character의 복사본으로 정보 출력

    main->>character: character.LevelUp() 호출
    note right of character: 포인터 수신자: character의 원본 데이터 수정 (Level, HP 증가)

    main->>character: character.DisplayInfo() 호출
    note right of character: 변경된 정보(Level, HP)가 출력됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// E:\hossam-blog\_posts\03.코딩\golang\06-구조체와-메서드\02-메서드-바인딩\main.go

// main 패키지를 선언함.
package main

// fmt 패키지를 임포트함.
import "fmt"

// GameCharacter 구조체를 정의함.
type GameCharacter struct {
    Name  string // 캐릭터 이름
    Level int    // 레벨
    HP    int    // 체력
    MP    int    // 마나
}

// '값 수신자(Value Receiver)' 메서드
// (c GameCharacter)는 이 메서드가 GameCharacter 타입에 속해 있음을 나타냄.
// 'c'는 메서드 내에서 구조체 인스턴스의 복사본에 접근할 때 사용하는 변수임.
// 원본 데이터를 수정하지 않고 단순히 정보를 보여주는 경우에 값 수신자를 사용함.
func (c GameCharacter) DisplayInfo() {
    fmt.Printf("이름: %s, 레벨: %d, HP: %d, MP: %d\n", c.Name, c.Level, c.HP, c.MP)
}

// '포인터 수신자(Pointer Receiver)' 메서드
// (c *GameCharacter)는 메서드가 구조체의 포인터에 바인딩됨을 의미함.
// Java에서 객체의 메서드가 'this'를 통해 원본 객체를 수정하는 것과 유사함.
// 메서드 내에서 구조체의 원본 필드 값을 변경해야 할 때 포인터 수신자를 사용함.
func (c *GameCharacter) LevelUp() {
    c.Level++ // c가 가리키는 원본 인스턴스의 Level 필드를 1 증가시킴.
    c.HP += 100 // 원본 인스턴스의 HP를 100 증가시킴.
    fmt.Printf("%s가 레벨업! (현재 레벨: %d)\n", c.Name, c.Level)
}

// 프로그램의 시작점인 main 함수임.
func main() {
    // GameCharacter 인스턴스를 생성하고 초기화함.
    character := GameCharacter{
        Name:  "Hossam",
        Level: 99,
        HP:    8000,
        MP:    12000,
    }

    // DisplayInfo 메서드를 호출함.
    // 이 메서드는 '값 수신자'이므로 character 인스턴스의 복사본을 사용하여 정보를 출력함.
    fmt.Println("--- 초기 상태 ---")
    character.DisplayInfo()

    // LevelUp 메서드를 호출함.
    // 이 메서드는 '포인터 수신자'이므로 character 인스턴스의 원본 데이터를 직접 수정함.
    fmt.Println("\n--- 레벨업 진행 ---")
    character.LevelUp()

    // 레벨업 후 변경된 상태를 확인하기 위해 다시 DisplayInfo 메서드를 호출함.
    // Level과 HP가 변경된 것을 확인할 수 있음.
    fmt.Println("\n--- 레벨업 후 상태 ---")
    character.DisplayInfo()
}

DisplayInfo는 캐릭터의 상태를 바꾸지 않고 보여주기만 하므로 값 수신자로 충분함. 하지만 LevelUp은 캐릭터의 레벨과 체력을 직접 수정해야 하므로, 원본에 접근할 수 있는 포인터 수신자를 사용해야만 함.


3. 생성자 함수 패턴: 캐릭터 공장을 만들어보자

Java에서는 클래스에 생성자(Constructor)를 정의하여 객체 초기화 로직을 구현함. Go에는 생성자가 없지만, 생성자 함수(Constructor Function) 패턴을 사용하여 동일한 효과를 얻을 수 있음.

Go 생성자 함수 vs Java 생성자

구분Go 생성자 함수Java 생성자
정의 방식func NewTypeName() *TypeNamepublic ClassName() { ... }
특징관례적으로 New로 시작하는 일반 함수클래스 이름과 동일한 특수 메서드
반환값보통 포인터를 반환반환값 없음 (객체 자동 생성)
유연성다양한 생성자 함수 정의 가능오버로딩으로 다양한 생성자 정의

실습 (3): 캐릭터 생성자 함수 만들기

실습 파일: 06-구조체와-메서드/03-생성자-함수/main.go

이 코드는 다양한 생성자 함수를 통해 캐릭터를 만드는 방법을 보여줍니다. NewGameCharacter는 기본 캐릭터를, NewWarriorNewMage는 특정 직업에 맞는 능력치를 가진 캐릭터를 생성하는 팩토리 역할을 합니다. main 함수에서는 이 함수들을 호출하여 여러 종류의 캐릭터를 손쉽게 생성하고, 각 캐릭터의 LevelUp 메서드를 호출하여 직업별로 스탯이 다르게 오르는 것을 확인합니다.

sequenceDiagram
    participant main as "main()"
    participant NewGameCharacter as "NewGameCharacter()"
    participant NewWarrior as "NewWarrior()"
    participant NewMage as "NewMage()"
    participant warrior as "warrior: *GameCharacter"
    participant mage as "mage: *GameCharacter"

    main->>+NewGameCharacter: NewGameCharacter("Alice", "궁수")
    NewGameCharacter-->>-main: &GameCharacter{...}

    main->>+NewWarrior: NewWarrior("Charlie")
    NewWarrior-->>-main: warrior 포인터

    main->>+NewMage: NewMage("Diana")
    NewMage-->>-main: mage 포인터

    main->>warrior: warrior.LevelUp()
    note right of warrior: 전사 스탯에 맞게 레벨업
    main->>mage: mage.LevelUp()
    note right of mage: 마법사 스탯에 맞게 레벨업
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// E:\hossam-blog\_posts\03.코딩\golang\06-구조체와-메서드\03-생성자-함수\main.go

package main

import "fmt"

// GameCharacter 구조체 정의
type GameCharacter struct {
    Name  string
    Level int
    HP    int
    MP    int
    Class string // 직업 필드 추가
}

// 기본 캐릭터 생성자 함수
// Go의 관례에 따라 'New' + 타입명으로 함수명을 정함
// 포인터를 반환하여 메모리 효율성을 높임
func NewGameCharacter(name, class string) *GameCharacter {
    return &GameCharacter{
        Name:  name,
        Level: 1,    // 기본 레벨 1로 시작
        HP:    100,  // 기본 체력 100
        MP:    50,   // 기본 마나 50
        Class: class,
    }
}

// 고급 캐릭터 생성자 함수 (커스텀 스탯)
// Java의 생성자 오버로딩과 유사한 효과
func NewAdvancedCharacter(name, class string, level, hp, mp int) *GameCharacter {
    return &GameCharacter{
        Name:  name,
        Level: level,
        HP:    hp,
        MP:    mp,
        Class: class,
    }
}

// 팩토리 패턴: 직업별 캐릭터 생성
func NewWarrior(name string) *GameCharacter {
    return &GameCharacter{
        Name:  name,
        Level: 1,
        HP:    150,  // 전사는 체력이 높음
        MP:    30,   // 마나는 낮음
        Class: "전사",
    }
}

func NewMage(name string) *GameCharacter {
    return &GameCharacter{
        Name:  name,
        Level: 1,
        HP:    80,   // 마법사는 체력이 낮음
        MP:    120,  // 마나는 높음
        Class: "마법사",
    }
}

// 캐릭터 정보 표시 메서드 (값 수신자)
func (c GameCharacter) DisplayInfo() {
    fmt.Printf("이름: %s, 직업: %s, 레벨: %d, HP: %d, MP: %d\n",
               c.Name, c.Class, c.Level, c.HP, c.MP)
}

// 레벨업 메서드 (포인터 수신자)
func (c *GameCharacter) LevelUp() {
    c.Level++
    // 직업별로 다른 스탯 증가
    switch c.Class {
    case "전사":
        c.HP += 20
        c.MP += 5
    case "마법사":
        c.HP += 10
        c.MP += 15
    default:
        c.HP += 15
        c.MP += 10
    }
    fmt.Printf("%s(%s)가 레벨업! (현재 레벨: %d)\n", c.Name, c.Class, c.Level)
}

func main() {
    fmt.Println("=== 다양한 캐릭터 생성 방법 ===")

    // 1. 기본 생성자 사용
    character1 := NewGameCharacter("Alice", "궁수")
    fmt.Println("1. 기본 생성자로 생성:")
    character1.DisplayInfo()

    // 2. 고급 생성자 사용
    character2 := NewAdvancedCharacter("Bob", "도적", 10, 200, 80)
    fmt.Println("\n2. 고급 생성자로 생성:")
    character2.DisplayInfo()

    // 3. 팩토리 패턴 사용
    warrior := NewWarrior("Charlie")
    mage := NewMage("Diana")

    fmt.Println("\n3. 팩토리 패턴으로 생성:")
    warrior.DisplayInfo()
    mage.DisplayInfo()

    fmt.Println("\n=== 레벨업 테스트 ===")
    warrior.LevelUp()
    mage.LevelUp()

    fmt.Println("\n레벨업 후:")
    warrior.DisplayInfo()
    mage.DisplayInfo()
}

4. 구조체 임베딩: Go식 상속

Java의 상속(extends)과 달리, Go는 구조체 임베딩(Struct Embedding)을 통해 코드 재사용을 구현함. 이는 “has-a” 관계가 아닌 “is-a” 관계를 표현하는 Go만의 독특한 방식임.

Go 임베딩 vs Java 상속

구분Go 임베딩Java 상속
키워드별도 키워드 없음 (익명 필드)extends
메서드 호출자동으로 승격(promotion)super.method() 또는 직접 호출
다중 상속지원 (여러 구조체 임베딩 가능)미지원 (단일 상속만)
오버라이딩메서드 재정의로 구현@Override 어노테이션

실습 (4): 임베딩을 활용한 캐릭터 계층 구조

실습 파일: 06-구조체와-메서드/04-구조체-임베딩/main.go

이 코드는 BaseCharacterWarriorMage 구조체에 임베딩하여 코드 재사용성을 높이는 방법을 보여줍니다. WarriorMageBaseCharacter의 필드와 메서드(DisplayBasicInfo)를 직접 자신의 것처럼 사용할 수 있습니다. 또한, 각 직업 구조체는 LevelUp 메서드를 자신에게 맞게 재정의(오버라이딩)하여, 부모의 LevelUp을 호출하면서 자신만의 추가 스탯을 올리는 확장된 기능을 수행합니다.

sequenceDiagram
    participant main as "main()"
    participant warrior as "warrior: *Warrior"
    participant mage as "mage: *Mage"
    participant BaseCharacter as "BaseCharacter (임베딩됨)"

    main->>main: warrior := NewWarrior("Thor")
    main->>main: mage := NewMage("Gandalf")

    main->>warrior: warrior.DisplayBasicInfo()
    note right of warrior: BaseCharacter의 메서드가<br/>자동으로 호출됨 (승격)

    main->>mage: mage.DisplayBasicInfo()
    note right of mage: BaseCharacter의 메서드가<br/>자동으로 호출됨 (승격)

    main->>warrior: warrior.Attack()
    note right of warrior: Warrior 고유 메서드 호출

    main->>mage: mage.CastSpell("파이어볼")
    note right of mage: Mage 고유 메서드 호출

    main->>warrior: warrior.LevelUp()
    activate warrior
    warrior->>BaseCharacter: w.BaseCharacter.LevelUp() 호출
    activate BaseCharacter
    BaseCharacter-->>warrior: 기본 레벨업 완료
    deactivate BaseCharacter
    warrior->>warrior: 전사 추가 스탯 적용
    warrior-->>main: return
    deactivate warrior
    note right of warrior: 메서드 오버라이딩

    main->>mage: mage.LevelUp()
    activate mage
    mage->>BaseCharacter: m.BaseCharacter.LevelUp() 호출
    activate BaseCharacter
    BaseCharacter-->>mage: 기본 레벨업 완료
    deactivate BaseCharacter
    mage->>mage: 마법사 추가 스탯 적용
    mage-->>main: return
    deactivate mage
    note right of mage: 메서드 오버라이딩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// E:\hossam-blog\_posts\03.코딩\golang\06-구조체와-메서드\04-구조체-임베딩\main.go

package main

import "fmt"

// 기본 캐릭터 구조체 (부모 역할)
type BaseCharacter struct {
    Name  string
    Level int
    HP    int
    MP    int
}

// 기본 캐릭터 메서드들
func (bc *BaseCharacter) DisplayBasicInfo() {
    fmt.Printf("이름: %s, 레벨: %d, HP: %d, MP: %d\n",
               bc.Name, bc.Level, bc.HP, bc.MP)
}

func (bc *BaseCharacter) LevelUp() {
    bc.Level++
    bc.HP += 10
    bc.MP += 5
    fmt.Printf("%s가 레벨업! (레벨: %d)\n", bc.Name, bc.Level)
}

// 전사 구조체 (임베딩을 통한 "상속")
type Warrior struct {
    BaseCharacter        // 익명 필드로 임베딩
    Strength      int    // 전사만의 고유 필드
    Armor         int    // 방어력
}

// 전사의 고유 메서드
func (w *Warrior) Attack() {
    damage := w.Strength * 2
    fmt.Printf("%s가 검으로 공격! 데미지: %d\n", w.Name, damage)
}

// 메서드 오버라이딩 (BaseCharacter의 LevelUp을 재정의)
func (w *Warrior) LevelUp() {
    w.BaseCharacter.LevelUp() // 부모의 메서드 호출
    w.Strength += 3           // 전사만의 추가 스탯 증가
    w.Armor += 2
    fmt.Printf("전사 스탯 증가: 힘 +3, 방어력 +2\n")
}

// 마법사 구조체
type Mage struct {
    BaseCharacter        // 임베딩
    Intelligence  int    // 지능
    ManaRegen     int    // 마나 재생력
}

// 마법사의 고유 메서드
func (m *Mage) CastSpell(spellName string) {
    manaCost := 20
    if m.MP >= manaCost {
        m.MP -= manaCost
        damage := m.Intelligence * 3
        fmt.Printf("%s가 %s 마법 시전! 데미지: %d (남은 MP: %d)\n",
                   m.Name, spellName, damage, m.MP)
    } else {
        fmt.Printf("%s의 마나가 부족합니다!\n", m.Name)
    }
}

// 마법사의 레벨업 오버라이딩
func (m *Mage) LevelUp() {
    m.BaseCharacter.LevelUp()
    m.Intelligence += 4
    m.ManaRegen += 2
    fmt.Printf("마법사 스탯 증가: 지능 +4, 마나재생 +2\n")
}

// 전사 생성자
func NewWarrior(name string) *Warrior {
    return &Warrior{
        BaseCharacter: BaseCharacter{
            Name:  name,
            Level: 1,
            HP:    120,
            MP:    30,
        },
        Strength: 15,
        Armor:    10,
    }
}

// 마법사 생성자
func NewMage(name string) *Mage {
    return &Mage{
        BaseCharacter: BaseCharacter{
            Name:  name,
            Level: 1,
            HP:    80,
            MP:    100,
        },
        Intelligence: 20,
        ManaRegen:    5,
    }
}

func main() {
    fmt.Println("=== 구조체 임베딩 테스트 ===")

    // 캐릭터 생성
    warrior := NewWarrior("Thor")
    mage := NewMage("Gandalf")

    fmt.Println("초기 상태:")
    warrior.DisplayBasicInfo() // 임베딩된 메서드 자동 사용 가능
    mage.DisplayBasicInfo()

    fmt.Println("\n=== 고유 스킬 사용 ===")
    warrior.Attack()
    mage.CastSpell("파이어볼")

    fmt.Println("\n=== 레벨업 (오버라이딩된 메서드) ===")
    warrior.LevelUp()
    mage.LevelUp()

    fmt.Println("\n레벨업 후 상태:")
    warrior.DisplayBasicInfo()
    mage.DisplayBasicInfo()

    // 임베딩된 필드에 직접 접근도 가능
    fmt.Printf("\n전사의 힘: %d, 방어력: %d\n", warrior.Strength, warrior.Armor)
    fmt.Printf("마법사의 지능: %d, 마나재생: %d\n", mage.Intelligence, mage.ManaRegen)
}

5. 메서드 체이닝: 매끄러운 호출 연결

Java의 Builder 패턴이나 jQuery의 메서드 체이닝처럼, Go에서도 메서드가 수신자 자신을 반환하도록 하여 연쇄 호출을 구현할 수 있음.

실습 (5): 메서드 체이닝으로 캐릭터 설정하기

실습 파일: 06-구조체와-메서드/05-메서드-체이닝/main.go

이 코드는 빌더 패턴을 사용하여 메서드 체이닝을 구현합니다. NewCharacterBuilder로 빌더를 생성한 후, SetName, SetClass 등 여러 설정 메서드를 연속적으로 호출합니다. 각 설정 메서드는 자기 자신(*CharacterBuilder)을 반환하므로, 점(.)을 찍어 계속해서 다른 메서드를 호출할 수 있습니다. 마지막으로 Build 메서드를 호출하여 최종 설정이 완료된 GameCharacter 객체를 얻습니다.

sequenceDiagram
    participant main as "main()"
    participant builder as "builder: *CharacterBuilder"

    main->>builder: NewCharacterBuilder()
    builder->>builder: .SetName("Arthur")
    builder->>builder: .SetClass("기사")
    builder->>builder: .SetLevel(25)
    builder->>builder: .SetStats(300, 100)
    builder->>builder: .AddSkill("검술")
    builder->>builder: .AddSkill("방패막기")
    builder->>builder: .AddSkill("돌진")
    builder->>main: .Build()
    note right of main: 최종 GameCharacter 객체 반환
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// E:\hossam-blog\_posts\03.코딩\golang\06-구조체와-메서드\05-메서드-체이닝\main.go

package main

import "fmt"

// CharacterBuilder 구조체
type CharacterBuilder struct {
    character *GameCharacter
}

// GameCharacter 구조체 (기존 정의 재사용)
type GameCharacter struct {
    Name   string
    Level  int
    HP     int
    MP     int
    Class  string
    Skills []string
}

// 새로운 빌더 생성
func NewCharacterBuilder() *CharacterBuilder {
    return &CharacterBuilder{
        character: &GameCharacter{
            Level:  1,
            HP:     100,
            MP:     50,
            Skills: make([]string, 0),
        },
    }
}

// 체이닝 메서드들 (모두 *CharacterBuilder를 반환)
func (cb *CharacterBuilder) SetName(name string) *CharacterBuilder {
    cb.character.Name = name
    return cb // 자기 자신을 반환하여 체이닝 가능
}

func (cb *CharacterBuilder) SetClass(class string) *CharacterBuilder {
    cb.character.Class = class
    return cb
}

func (cb *CharacterBuilder) SetLevel(level int) *CharacterBuilder {
    cb.character.Level = level
    return cb
}

func (cb *CharacterBuilder) SetStats(hp, mp int) *CharacterBuilder {
    cb.character.HP = hp
    cb.character.MP = mp
    return cb
}

func (cb *CharacterBuilder) AddSkill(skill string) *CharacterBuilder {
    cb.character.Skills = append(cb.character.Skills, skill)
    return cb
}

// 최종 캐릭터 반환
func (cb *CharacterBuilder) Build() *GameCharacter {
    return cb.character
}

// 캐릭터 정보 출력 메서드
func (gc *GameCharacter) DisplayFullInfo() {
    fmt.Printf("=== %s (%s) ===\n", gc.Name, gc.Class)
    fmt.Printf("레벨: %d, HP: %d, MP: %d\n", gc.Level, gc.HP, gc.MP)
    fmt.Printf("스킬: %v\n", gc.Skills)
    fmt.Println()
}

func main() {
    fmt.Println("=== 메서드 체이닝으로 캐릭터 생성 ===")

    // 메서드 체이닝을 사용한 캐릭터 생성
    warrior := NewCharacterBuilder().
        SetName("Arthur").
        SetClass("기사").
        SetLevel(25).
        SetStats(300, 100).
        AddSkill("검술").
        AddSkill("방패막기").
        AddSkill("돌진").
        Build()

    mage := NewCharacterBuilder().
        SetName("Merlin").
        SetClass("대마법사").
        SetLevel(30).
        SetStats(200, 400).
        AddSkill("파이어볼").
        AddSkill("아이스스피어").
        AddSkill("라이트닝").
        AddSkill("힐링").
        Build()

    // 결과 출력
    warrior.DisplayFullInfo()
    mage.DisplayFullInfo()
}

6. 수신자 선택 가이드라인

실무에서 값 수신자와 포인터 수신자 중 어떤 것을 선택할지 고민될 때 참고할 수 있는 가이드라인:

포인터 수신자를 사용해야 하는 경우

  1. 메서드가 수신자를 수정하는 경우
  2. 구조체가 큰 경우 (복사 비용 절약)
  3. 일관성 유지 (다른 메서드가 포인터 수신자를 사용하는 경우)

값 수신자를 사용해도 되는 경우

  1. 수신자를 수정하지 않는 경우
  2. 구조체가 작은 경우 (기본 타입, 작은 구조체)
  3. 불변성이 중요한 경우

실습 (6): 성능 비교 테스트

실습 파일: 06-구조체와-메서드/06-성능-비교/main.go

이 코드는 동일한 작업을 수행하는 값 수신자 메서드(ProcessWithValue)와 포인터 수신자 메서드(ProcessWithPointer)의 실행 시간을 비교합니다. LargeStruct는 큰 배열을 포함하고 있어 복사 비용이 많이 듭니다. 루프를 반복하며 각 메서드를 호출했을 때, 값 수신자는 매번 구조체 전체를 복사하므로 시간이 더 오래 걸리는 반면, 포인터 수신자는 주소값만 전달하므로 훨씬 빠른 성능을 보입니다.

sequenceDiagram
    participant main as "main()"
    participant large as "large: LargeStruct"

    main->>main: 큰 구조체(large) 생성
    main->>main: 값 수신자 메서드 10만회 호출 시작
    loop 100,000 times
        main->>large: large.ProcessWithValue()
        note right of large: 호출 시마다 large 구조체 전체가 복사됨
    end
    main->>main: 시간 측정 종료 (valueTime)

    main->>main: 포인터 수신자 메서드 10만회 호출 시작
    loop 100,000 times
        main->>large: large.ProcessWithPointer()
        note right of large: 호출 시마다 large 구조체의 주소(포인터)만 전달됨
    end
    main->>main: 시간 측정 종료 (pointerTime)

    main->>main: valueTime과 pointerTime 비교 결과 출력
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// E:\hossam-blog\_posts\03.코딩\golang\06-구조체와-메서드\06-성능-비교\main.go

package main

import (
    "fmt"
    "time"
)

// 큰 구조체 예제
type LargeStruct struct {
    Data [1000]int
    Name string
    ID   int
}

// 값 수신자 메서드
func (ls LargeStruct) ProcessWithValue() int {
    sum := 0
    for _, v := range ls.Data {
        sum += v
    }
    return sum
}

// 포인터 수신자 메서드
func (ls *LargeStruct) ProcessWithPointer() int {
    sum := 0
    for _, v := range ls.Data {
        sum += v
    }
    return sum
}

func main() {
    // 큰 구조체 인스턴스 생성
    large := LargeStruct{
        Name: "TestStruct",
        ID:   1,
    }

    // 배열에 데이터 채우기
    for i := 0; i < 1000; i++ {
        large.Data[i] = i
    }

    iterations := 100000

    // 값 수신자 성능 측정
    start := time.Now()
    for i := 0; i < iterations; i++ {
        large.ProcessWithValue()
    }
    valueTime := time.Since(start)

    // 포인터 수신자 성능 측정
    start = time.Now()
    for i := 0; i < iterations; i++ {
        large.ProcessWithPointer()
    }
    pointerTime := time.Since(start)

    fmt.Printf("=== 성능 비교 결과 (%d회 반복) ===\n", iterations)
    fmt.Printf("값 수신자 시간: %v\n", valueTime)
    fmt.Printf("포인터 수신자 시간: %v\n", pointerTime)
    fmt.Printf("성능 차이: %.2fx\n", float64(valueTime)/float64(pointerTime))
}

정리: Go 구조체와 메서드의 핵심

Java 개발자가 기억해야 할 Go의 특징

  1. 구조체는 데이터 컨테이너: Java 클래스의 필드 부분만 담당
  2. 메서드는 외부 바인딩: 구조체 외부에서 연결하는 방식
  3. 수신자가 핵심: 값 vs 포인터 선택이 성능과 동작을 결정
  4. 임베딩으로 재사용: 상속 대신 구조체 조합으로 코드 재사용
  5. 생성자 함수 패턴: New 접두사 함수로 초기화 로직 구현
This post is licensed under CC BY 4.0 by the author.