느리지만 꾸준히, 코딩

Golang 캡슐화, 임베딩 본문

프로그래밍/Golang

Golang 캡슐화, 임베딩

slowin 2024. 8. 9. 17:00

개요

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 출력
}