Go 언어에 클래스가 없다고? 걱정 마세요! 구조체와 인터페이스로 다 됩니다!

Go 언어에 클래스가 없다고? 걱정 마세요! 구조체와 인터페이스로 다 됩니다!

들어가며: "Go에는 클래스가 없다는데, 그럼 객체지향은 어떻게...?"

자바(Java)나 C++ 같은 객체지향 프로그래밍 언어에 익숙한 개발자들이 Go 언어를 처음 접할 때 가장 많이 하는 질문 중 하나가 바로 "Go에는 클래스가 없나요?" 일 겁니다.

맞습니다! Go 언어에는 우리가 흔히 알고 있는 '클래스(Class)'라는 개념이 없습니다.

이 사실에 살짝 당황하셨을 수도 있는데요.

하지만 걱정하지 마세요! Go 언어는 클래스 없이도 데이터를 구조화하고 행동을 정의하는 아주 스마트한 방법들을 가지고 있답니다.

이번 글에서는 Go 언어가 왜 클래스를 사용하지 않는지, 그리고 클래스 대신 어떤 멋진 기능들을 제공하는지 쉽고 재미있게 알아보겠습니다.

Go 언어의 독특한 매력에 푹 빠질 준비, 되셨나요?

왜 Go에는 클래스가 없을까요? 단순함과 명료함을 향한 선택!

Go 언어가 클래스를 도입하지 않은 것은 우연이 아닙니다.

이는 Go 언어의 핵심 철학인 '단순함(Simplicity)'과 '명료함(Clarity)'을 지키기 위한 의도적인 설계인데요.

복잡한 클래스 상속 계층 구조 대신, Go 언어는 더 직관적이고 이해하기 쉬운 방법으로 객체지향 프로그래밍의 장점을 취하고자 했습니다.

그럼 클래스 대신 무엇을 사용할 수 있을까요? 바로 '구조체(Struct)'와 '메소드(Method)', 그리고 '인터페이스(Interface)'입니다!

데이터 묶음의 달인, 구조체(Struct) 다시 보기!

앞선 글에서도 잠깐 등장했지만, Go 언어에서 데이터를 묶어 관리하는 가장 기본적인 단위는 바로 '구조체(Struct)'입니다.

구조체를 사용하면 서로 관련된 여러 데이터 필드들을 하나의 단위로 깔끔하게 정리할 수 있는데요.

예를 들어 '사람'이라는 정보를 표현하고 싶다면, 이렇게 구조체를 만들 수 있습니다.

package main

import "fmt"

// 사람 정보를 담는 Person 구조체 정의
type Person struct {
    Name string // 이름 필드 (문자열)
    Age  int    // 나이 필드 (정수)
}

func main() {
    // Person 구조체 변수 생성
    p := Person{Name: "고길동", Age: 45}
    fmt.Println(p.Name, "님은", p.Age, "살입니다.")
}



이처럼 Person 구조체는 NameAge라는 두 개의 정보를 하나로 묶어 관리합니다.

마치 클래스의 멤버 변수와 비슷한 역할을 하는 것이죠.

구조체에 행동을 부여하다! 메소드(Method)의 등장!

"데이터만 있으면 무슨 재미인가요? 행동도 있어야죠!" 라고 생각하셨다면, 정확합니다!

Go 언어는 클래스가 없지만, 특정 타입(구조체를 포함해서)에 함수를 연결하여 마치 클래스의 메소드처럼 동작하게 만들 수 있습니다.

이것이 바로 '메소드'인데요.

Person 구조체에 인사하는 기능을 추가해 볼까요?

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

// Person 타입에 연결된 Greet 메소드 정의
// (p Person) 부분을 '리시버(Receiver)'라고 부릅니다.
func (p Person) Greet() string {
    return fmt.Sprintf("안녕하세요! 제 이름은 %s이고, %d살입니다.", p.Name, p.Age)
}

func main() {
    person1 := Person{Name: "둘리", Age: 8}
    // Person 타입의 변수를 통해 Greet 메소드 호출
    fmt.Println(person1.Greet())
}



여기서 Greet 함수는 Person 타입의 '리시버(Receiver)' p를 가지고 있습니다.

덕분에 person1.Greet()처럼 Person 구조체 변수를 통해 직접 호출할 수 있게 되는 것이죠.

마치 다른 언어에서 person1.greet()와 같이 객체의 메소드를 호출하는 것과 똑같습니다.

행동의 약속, 인터페이스(Interface)로 유연성을 더하다!

Go 언어의 인터페이스는 특정 데이터 구조를 강요하지 않으면서 '행동'만을 정의하는 아주 강력한 도구입니다.

인터페이스는 "이러이러한 메소드들을 가지고 있어야 해!"라는 일종의 '약속' 또는 '규약'이라고 생각할 수 있는데요.

어떤 타입이든 이 약속(인터페이스에 정의된 모든 메소드)을 지키기만 하면, 해당 인터페이스 타입으로 취급될 수 있습니다.

예를 들어, '인사하는 기능'을 가진 모든 것을 Greeter라고 부르기로 약속(인터페이스 정의)해 보겠습니다.

package main

import "fmt"

// Greeter 인터페이스 정의: Greet() string 메소드를 가져야 함
type Greeter interface {
    Greet() string
}

type Person struct {
    Name string
    Age  int
}

// Person 타입은 Greet() 메소드를 구현하므로 Greeter 인터페이스를 만족합니다.
func (p Person) Greet() string {
    return fmt.Sprintf("사람: 안녕! 내 이름은 %s.", p.Name)
}

type Dog struct {
    Name string
}

// Dog 타입도 Greet() 메소드를 구현하므로 Greeter 인터페이스를 만족합니다.
func (d Dog) Greet() string {
    return fmt.Sprintf("강아지: 멍멍! 내 이름은 %s.", d.Name)
}

// Greeter 인터페이스 타입을 인자로 받는 함수
func SayHello(g Greeter) {
    fmt.Println(g.Greet())
}

func main() {
    p := Person{Name: "희동이"}
    d := Dog{Name: "바둑이"}

    // Person 타입과 Dog 타입 모두 SayHello 함수의 인자로 전달 가능
    SayHello(p)
    SayHello(d)
}



PersonDog 타입은 서로 다른 구조를 가졌지만, 둘 다 Greet() 메소드를 가지고 있기 때문에 Greeter 인터페이스를 만족합니다.

그래서 SayHello 함수는 Person 타입의 변수와 Dog 타입의 변수 모두를 아무 문제 없이 인자로 받을 수 있는 것이죠.

이것이 바로 인터페이스를 통한 다형성의 마법입니다!

상속 대신 조합(Composition)을 선택한 Go!

앞서 Go 언어는 상속 대신 '조합'을 선호한다고 말씀드렸는데요.

이는 한 구조체가 다른 구조체를 필드로 '포함(embedding)'함으로써 코드 재사용성을 높이는 방식입니다.

마치 레고 블록처럼 기존 구조체를 가져와 새로운 구조체의 부품으로 사용하는 것이죠.

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) Greet() {
    fmt.Printf("안녕, 내 이름은 %s, 나이는 %d살이야.\n", p.Name, p.Age)
}

// Employee 구조체는 Person 구조체를 포함(임베딩)합니다.
type Employee struct {
    Person   // Person 구조체의 필드와 메소드가 Employee로 승격됩니다.
    Position string // 직책 필드 추가
}

func main() {
    emp := Employee{
        Person:   Person{Name: "마이콜", Age: 32},
        Position: "가수",
    }

    // Employee를 통해 Person의 필드에 직접 접근
    fmt.Println(emp.Name, "의 직책은", emp.Position, "입니다.")
    // Employee를 통해 Person의 메소드 직접 호출
    emp.Greet()
}



Employee 구조체는 Person 구조체를 그대로 품고 있어서, Person의 필드(Name, Age)와 메소드(Greet)를 마치 자신의 것처럼 사용할 수 있습니다.

복잡한 상속 관계없이도 필요한 기능을 효과적으로 재사용할 수 있는 아주 깔끔한 방법입니다.

마무리하며: Go 언어, 클래스 없이도 충분히 강력해요!

비록 Go 언어에는 전통적인 의미의 클래스가 없지만, 구조체, 메소드, 그리고 인터페이스라는 강력한 도구들을 통해 객체지향 프로그래밍의 핵심적인 기능들을 훌륭하게 구현할 수 있습니다.

특히 상속 대신 조합을 강조하는 Go의 철학은 코드를 더욱 단순하고 명료하며 유지보수하기 쉽게 만들어줍니다.

처음에는 클래스가 없다는 사실에 조금 어색할 수 있지만, Go 언어만의 방식을 이해하고 활용하다 보면 그 매력에 푹 빠지게 될 것입니다!

자주 묻는 질문 (FAQs)

Q1: 왜 Go 언어에는 자바나 C++처럼 클래스가 없나요?

Go 언어는 단순함을 매우 중요하게 생각합니다.

그래서 복잡한 클래스 기반 상속 대신, 구조체와 인터페이스를 사용하는 방식을 선택했습니다.

이게 코드를 더 깔끔하고 이해하기 쉽게 만들어준다고 믿기 때문이죠.

Q2: Go 언어에서 구조체에 메소드를 어떻게 연결하나요?

구조체 타입의 '리시버(receiver)'를 갖는 함수를 정의하면 됩니다.

이렇게 하면 그 함수는 해당 구조체와 연결된 메소드처럼 동작하게 됩니다.

func (p Person) Greet() { ... } 와 같은 형태로요!

Q3: Go 언어에서 상속 대신 사용하는 방법은 무엇인가요?

Go 언어는 '조합(composition)'을 사용합니다.

한 구조체가 다른 구조체를 필드로 포함(embedding) 시키면, 포함된 구조체의 필드와 메소드를 마치 자신의 것처럼 사용할 수 있게 됩니다.

이게 바로 Go 스타일의 코드 재사용 방법입니다.