본문 바로가기
프로그래밍/Golang

Golang 캡슐화, 임베딩

by slowin 2024. 8. 9.

개요

Go 언어는 간결하고 효율적인 설계를 중시하며, 객체 지향 프로그래밍 패러다임을 직접적으로 지원하지 않습니다. 그러나 캡슐화(encapsulation)와 임베딩(embedding) 같은 개념을 통해 객체 지향 프로그래밍의 핵심 개념들을 구현할 수 있습니다.

1. 캡슐화 (Encapsulation)

캡슐화는 객체의 데이터를 외부로부터 숨기고, 그 데이터에 접근하거나 조작하는 방법을 제공하는 개념입니다. Go에서는 캡슐화를 구조체와 메서드를 통해 구현할 수 있습니다.

1.1 접근 제어

Golang에서는 대소문자를 사용하여 접근 제어를 수행합니다:

  • 대문자로 시작하는 필드나 메서드: 외부에서 접근 가능 (public)
  • 소문자로 시작하는 필드나 메서드: 같은 패키지 내에서만 접근 가능 (private)

1.2 구조체 (Struct)

Go에서는 struct를 사용하여 데이터를 캡슐화합니다. 구조체는 관련된 데이터를 그룹화하여 하나의 단위로 만들 수 있게 해줍니다.

// 생략...

type Person struct {
    Name string  // 외부에서 접근 가능
    age  int     // 같은 패키지 내에서만 접근 가능
}

func (p *Person) SetAge(age int) {
    if age > 0 {
        p.age = age
    }
}

func (p Person) GetAge() int {
    return p.age
}

func main() {
    person := Person{Name: "Alice"}
    person.SetAge(30)
    fmt.Printf("%s is %d years old\n", person.Name, person.GetAge())
}

이 예제에서 age 필드는 private이므로 직접 접근할 수 없고, SetAge와 GetAge 메서드를 통해서만 접근 및 수정이 가능합니다.

1.3 완전한 캡슐화

앞서 설명한 예제에서 Name 필드가 public으로 선언되어 있어 외부에서 직접 접근 및 수정이 가능했습니다. 이는 완전한 캡슐화를 위반하는 것으로, 데이터의 무결성을 해칠 수 있는 잠재적 위험이 있습니다.

문제점

type Person struct {
    Name string  // 외부에서 접근 가능
    age  int     // 같은 패키지 내에서만 접근 가능
}

func main() {
    person := Person{Name: "Alice"}
    person.Name = ""  // 유효하지 않은 이름으로 직접 수정 가능
}

이 코드에서 Name 필드는 외부에서 직접 수정이 가능하므로, 빈 문자열이나 유효하지 않은 이름으로 설정될 수 있습니다.

3.2 캡슐화 개선

더 나은 캡슐화를 위해 모든 필드를 private으로 만들고, getter와 setter 메서드를 통해 접근하도록 개선할 수 있습니다.

type Person struct {
    name string
    age  int
}

func NewPerson(name string) Person {
    return Person{name: name}
}

func (p *Person) SetName(name string) error {
    if name == "" {
        return errors.New("name cannot be empty")
    }
    p.name = name
    return nil
}

func (p Person) GetName() string {
    return p.name
}

func (p *Person) SetAge(age int) error {
    if age < 0 {
        return errors.New("age cannot be negative")
    }
    p.age = age
    return nil
}

func (p Person) GetAge() int {
    return p.age
}

func main() {
    person := NewPerson("Alice")
    
    err := person.SetName("")
    if err != nil {
        fmt.Println("Error:", err)
    }
    
    fmt.Println("Name:", person.GetName())
}

캡슐화 정리

  • 데이터 무결성: 유효하지 않은 데이터가 설정되는 것을 방지합니다.
  • 유연성: 내부 구현을 변경하더라도 외부 인터페이스는 그대로 유지할 수 있습니다.
  • 디버깅 용이성: 데이터 변경 지점을 명확히 알 수 있어 디버깅이 쉬워집니다.

2. 임베딩 (Embedding)

Golang Embedding(임베딩)은 다른 구조체를 현재의 구조체 내에 포함시키는 것을 의미합니다. 임베딩된 구조체는 필드와 메서드를 현재 구조체에서 직접 사용할 수 있으며, 이는 마치 상속처럼 동작하지만, Go에서는 명시적인 상속 개념이 없기 때문에 임베딩으로 이 기능을 구현합니다.

2.1 기본 임베딩

type Address struct {
    Street  string
    City    string
    Country string
}

type Employee struct {
    Name    string
    Address // 임베딩된 구조체
}

func main() {
    emp := Employee{
        Name: "Bob",
        Address: Address{
            Street:  "123 Main St",
            City:    "Anytown",
            Country: "USA",
        },
    }

    fmt.Println(emp.Name)     // 직접 접근
    fmt.Println(emp.Street)   // 임베딩된 필드에 직접 접근
}

이 예제에서 Employee 구조체는 Address 구조체를 임베딩하고 있습니다. 이를 통해 Employee의 인스턴스에서 Address의 필드에 직접 접근할 수 있습니다.

2.2 메서드 임베딩

임베딩된 구조체의 메서드도 외부 구조체에서 사용할 수 있습니다.

type Printer struct{}

func (p Printer) Print() {
    fmt.Println("Printing...")
}

type Scanner struct{}

func (s Scanner) Scan() {
    fmt.Println("Scanning...")
}

type PrinterScanner struct {
    Printer
    Scanner
}

func main() {
    ps := PrinterScanner{}
    ps.Print() // Printer의 메서드 호출
    ps.Scan()  // Scanner의 메서드 호출
}

이 예제에서 PrinterScanner 구조체는 Printer와 Scanner를 임베딩하여 두 구조체의 메서드를 모두 사용할 수 있습니다.

2.3 주의할 점

필드 충돌

임베딩된 구조체와 임베딩을 받는 구조체가 동일한 이름의 필드를 가지는 경우, 필드 충돌이 발생할 수 있습니다. 이 경우, Go는 가장 바깥 구조체의 필드를 우선적으로 사용합니다. 필요한 경우, 명시적으로 필드명을 지정해야 합니다.

type Address struct {
    City string
}

type Person struct {
    City string
    Address
}

func main() {
    p := Person{City: "Los Angeles", Address: Address{City: "San Francisco"}}
    fmt.Println(p.City)          // Los Angeles 출력
    fmt.Println(p.Address.City)  // San Francisco 출력
}