Skip to content

Latest commit

 

History

History
698 lines (516 loc) · 20 KB

55-70.md

File metadata and controls

698 lines (516 loc) · 20 KB

55-70

임베딩

임베딩이 아닌 필드로써 선언

프로그램에서 사용할 user 를 정의한다.

type user struct {
    name  string
    email string
}

이벤트가 발생했음을 사용자들에게 알려주는 기능을 하는 notify 메서드를 구현한다.

func (u *user) notify() {
    fmt.Printf("Sending user email To %s<%s>\n", u.name, u.email)
}

admin 은 특정 권한을 가진 관리자를 의미하는데, person user 는 임베딩이 아니다. 단지 person 이라는 필드를 user 라는 타입으로 정의하고 생성한 것이다.

type admin struct {
    person user // 임베딩이 아니다.
    level  string
}

구조체 리터럴로 admin 사용자를 생성한다. admin 을 구성하는 person 필드 또한 구조체 타입이기 때문에 초기화를 위해 또 다른 리터럴을 사용하였다.

func main() {
    ad := admin{
        person: user{
            name:  "Hoanh An",
            email: "hoanhan101@gmail.com",
        },
        level: "superuser",
    }

admin 타입의 값은 person 필드를 이용해 notify 를 호출할 수 있다.

    ad.person.notify()
Sending user email To Hoanh An<hoanhan101@gmail.com>

임베딩 타입

프로그램에서 사용할 user 를 정의한다.

type user struct {
    name  string
    email string
}

이벤트가 발생했음을 사용자들에게 알려주는 기능을 하는 notify 메서드를 구현한다.

func (u *user) notify() {
    fmt.Printf("Sending user email To %s<%s>\n", u.name, u.email)
}

admin 은 특정 권한을 가진 관리자이다. 이번에는 person 필드를 사용하지 않고, admin 타입 내부에 user 타입의 값을 임베딩 해본다. admin 은 외부 타입(outer type)이 되고, user 는 내부 타입(inner type)이 되는 inner-type-outer-type 관계이다.

내부 타입 승격(Inner type promotion)

Go 의 임베딩은 내부 타입 승격 이라는 특별한 메커니즘을 가지고 있다. 이 메커니즘은 내부 타입과 관련된 모든 것들이 외부 타입에서도 사용할 수 있도록 승격된다는 것을 의미한다. 즉, 아래와 같이 구성하면 내부 타입인 user와 관련해서 더 많은 의미를 내포할 수 있게 된다.

type admin struct {
    user  // 임베딩 타입
    level string
}

외부 타입인 admin 과 내부 타입인 user 를 생성해보자. 내부 타입 값인 user 가 필드처럼 보이지만 필드가 아니다. 필드처럼 타입명을 통해 내부 값에 접근할 수는 있다. user 의 구조체 또한 리터럴을 통해 내부 값을 초기화 할 수 있다.

func main() {
    ad := admin{
        user: user{
            name:  "Hoanh An",
            email: "hoanhan101@gmail.com",
        },
        level: "superuser",
    }

    // 내부 타입 메서드를 직접 사용 가능하다.
    ad.user.notify()
    ad.notify()
}

내부 타입 승격으로 외부 타입에서 notify 메서드를 바로 사용할 수 있고, 결과 역시 같다.

Sending user email To Hoanh An<hoanhan101@gmail.com>
Sending user email To Hoanh An<hoanhan101@gmail.com>

임베디드 타입과 인터페이스

notifier 인터페이스는 알림에 대한 행동(behavior)을 정의하고 있다.

type notifier interface {
    notify()
}

프로그램에서 사용할 user 를 정의한다.

type user struct {
    name  string
    email string
}

이벤트가 발생했음을 사용자들에게 알려주는 기능을 하는 notify 메서드를 포인터 리시버를 통해 구현한다.

func (u *user) notify() {
    fmt.Printf("Sending user email To %s<%s>\n", u.name, u.email)
}

admin 은 특정 권한을 가진 관리자를 의미한다.

type admin struct {
    user
    level string
}

func main() {
    // admin user 를 만든다.
    ad := admin{
        user: user{
            name:  "Hoanh An",
            email: "hoanhan101@gmail.com",
        },
        level: "superuser",
    }

관리자에게 알림을 보내보자.

내부 타입 승격에 의해 외부 타입에서도 내부 타입에서 사용하는 것과 같은 계약(contract)이 구현되어 있다면 이를 사용할 수 있기 때문에 단순히 외부 타입값의 주소만을 함수에 전달해주면 된다.

    sendNotification(&ad)

임베딩은 서브 타입 관계를 생성하지는 않는다. user는 여전히 user일 뿐이고 admin은 여전히 admin이다. 외부 타입이 사용할 수 있도록 내부 타입에서 사용하는 행동을 노출시켜 줄 뿐이다. 외부 타입에서도 내부 타입과 같은 인터페이스 혹은 같은 계약을 구현할 수 있다는 것이다.

이를 구현하기 위해 타입을 재사용할 수 있으며 이는 상태(state)를 혼합하거나 공유하지 않고 행동을 외부 타입까지 확장할 수 있게 된다.

아래처럼 sendNotificationnotifier 의 구현체를 받아서 알람을 보내는 다형성 함수이다.

func sendNotification(n notifier) {
    n.notify()
}
Sending user email To Hoanh An<hoanhan101@gmail.com>

동일한 인터페이스를 구현한 외부 타입 및 내부 타입

notifier 인터페이스는 알림에 대한 행동(behavior)을 정의하고 있다.

type notifier interface {
    notify()
}

프로그램에서 사용할 user 를 정의한다.

type user struct {
    name  string
    email string
}

이벤트가 발생했음을 사용자들에게 알려주는 기능을 하는 notify 메서드를 구현한다.

func (u *user) notify() {
    fmt.Printf("Sending user email To %s<%s>\n", u.name, u.email)
}

admin 은 특정 권한을 가진 관리자를 의미한다.

type admin struct {
    user
    level string
}

아래의 notify 메서드는 유저가 아닌 관리자에게 특정 이벤트를 알려준다. 이제 두 가지의 notifier 인터페이스를 구현하였다. 하나는 내부 타입, 다른 하나는 외부 타입으로 구현하였다. 외부 타입에서 인터페이스를 구현하면 내부 타입 승격 메커니즘이 발생하지는 않는다. 내부 타입에 의해서 승격된 것들을 외부 타입이 덮어쓰는 것이다.

func (a *admin) notify() {
    fmt.Printf("Sending admin email To %s<%s>\n", a.name, a.email)
}

admin 사용자를 만들어보자.

func main() {
    ad := admin{
        user: user{
            name:  "Hoanh An",
            email: "hoanhan101@gmail.com",
        },
        level: "superuser",
    }

알람을 관리자에게 전송해보자. 내부 타입에서 구현한 인터페이스 구현체는 외부타입으로 승격되지 않는다.

    sendNotification(&ad)

내부 타입으로 직접적으로 접근해서 메서드를 사용할 수는 있다.

    ad.user.notify()

하지만 아래 처럼 사용했을 경우에는 내부 타입 승격이 일어나지 않고, 외부 타입에서 구현한 notify 를 사용한다.

   ad.notify()
Sending admin email To Hoanh An<hoanhan101@gmail.com>
Sending user email To Hoanh An<hoanhan101@gmail.com>
Sending admin email To Hoanh An<hoanhan101@gmail.com>

내보내기(Exporting)

가이드라인

패키지는 자체적으로 사용 가능한 코드의 단위다. 패키지에 속해있는 모든 것들은 다른 패키지들에서 접근할 수 있도록 내보낸(exported) 형태로 만들어져 있거나 다른 패키지들에서 접근할 수 없도록 내보내지 않은 형태로 만들어져 있다.

내보내기 식별자(Exported identifier)

counters 패키지는 경고 카운터에 관한 기능을 제공한다.

package counters

AlertCounter 는 내보낸(exported) 형태의 정수형 카운터 알람이다. 첫번째 단어가 대문자인 경우 이는 내보낸 형태라 정의할 수 있다.

type AlertCounter int

alertCounter 는 내보내지 않은(unexported) 형태의 정수형 카운터 알람이다. 첫번째 단어가 소문자인 경우 이는 내보내지 않은 형태라 정의할 수 있다. 카운터 패키지 내부가 아닌 다른 패키지에서 접근한 경우 alertCounter 에 접근할 수 없다.

type alertCounter int

아래처럼 main 패키지에서 counters 패키지를 불러온다.

package main

import (
    "fmt"

    "github.com/hoanhan101/counters"
)

내보낸 형태의 변수를 생성하고 10 으로 초기화 할 수 있다.

counter := counters.AlertCounter(10)

아래처럼 내보내지 않은 형태의 변수를 생성하고 10으로 초기화 할 수 없다.

counter := counters.alertCounter(10)

만약 내보내지 않은 형태의 변수를 사용한다면 아래와 같은 컴파일 에러가 발생하게 된다.

  • cannot refer to unexported name counters.alertCounter
  • undefined: counters.alertCounter

내보내지 않은 식별자 값에 접근

counters 패키지는 경고 카운터에 관한 기능을 제공한다.

package counters

``

alertCounter 는 내보내지 않은(unexported) 형태의 정수형 카운터 알람이다.

type alertCounter int

내보내지 않은 형태의 값을 초기화하고 생성하는 역할을 하는 New 함수를 통해 내보내기 함수를 선언할 수 있다. 아래의 New 함수는 내보내지 않은 형태의 alertCounter 값을 반환하고 있다.

func New(value int) alertCounter {
    return alertCounter(value)
}

내보내기 혹은 내보내지 않기는 비공개, 공개 메커니즘과 같은 것이 아니라 식별자 그 자체이기 때문에 위와 같이 사용했을 경우에 코드는 컴파일 될 수 있다. 하지만 이 방식은 캡슐화를 사용하지 않기 때문에 이런 방식으로 사용하지 말고 내보내기 방식을 사용하는 것이 낫다.

내보내지 않은 식별자 값에 접근해보자.

package main

import (
    "fmt"

    "github.com/hoanhan101/counters"
)

counters 패키지에서 내보내고 있는 New 함수를 통해 내보내지 않은 형태의 변수를 생성한다.

func main() {
    counter := counters.New(10)
    fmt.Printf("Counter: %d\n", counter)
}
Counter: 10

내보낸 형태 구조체 내의 내보내지 않은 필드

users 패키지는 사용자 관리에 관한 기능을 제공한다.

package users

내보낸 형태의 User 는 사용자에 대한 정보를 의미한다. UserNameID 의 2개의 내보낸 필드와 password 의 내보내지 않은 1개의 필드를 정의하고 있다.

type User struct {
    Name string
    ID   int

    password string
}
package main

import (
    "fmt"

    "github.com/hoanhan101/users"
)

구조체 리터럴로 users 패키지에 있는 User 타입을 생성한다. 여기서 password 는 내보낸 형태가 아니기 때문에 컴파일 에러가 발생한다.

func​ ​main​() {
    u := users.User{
        Name: ​"Hoanh"​,
        ID: ​101​,
        password: ​"xxxx"​,
    }
    fmt.Printf(​"User: %#v\n"​, u)
}
unknown field 'password' in struct literal of type users.User

내보내지 않은 형태를 임베딩하고 있는 내보낸 형식

users 패키지는 사용자 관리에 관한 기능을 제공한다.

package users

내보내지 않은 형태의 user 는 사용자에 대한 정보를 의미하며, 2개의 내보낸 필드를 정의한다.

type user struct {
    Name string
    ID   int
}

내보낸 형태의 Manager 는 관리자에 대한 정보를 의미하며, 내보내지 않은 형태로 임베딩된 필드 user 를 정의한다.

type Manager struct {
    Title string

    user
}
package main

import (
    "fmt"

    "github.com/hoanhan101/users"
)

user 패키지에 있는 Manager 타입의 값을 생성한다. Manager 타입 값을 생성할 때는 오직 내보낸 필드인 Title 만을 초기화 할 수 있고, 내보내지 않은 형태로 임베딩된 user 타입에는 바로 접근할 수 없다.

func main() {
    u := users.Manager{
        Title: "Dev Manager",
    }

그러나 manager 값을 초기화하고 난 이후에는 내보내지 않은 형태를 내보낸 필드로써 접근 할 수 있다.

However, once we have the manager value, the exported fields from that unexported type are accessible.

    u.Name = "Hoanh"
    u.ID = 101
    fmt.Printf("User: %#v\n", u)
}
User: users.Manager{Title:"Dev Manager", user:users.user{Name:"Hoanh", ID:101}}

다시 한번 이야기하지만 이 방식을 사용하는 것보다는 user 를 내보내는 것이 더 좋은 방식이므로 이를 사용하는 것이 낫다.

소프트웨어 설계

조합

그룹핑 형식

상태에 의한 그룹핑

이번 파트는 OOP 패턴의 유형계층에 대한 예시이다. 이는 Go 에서 자주 사용되는 방식은 아니다. Go 는 서브타이핑이라는 개념이 없기 때문이다. Go 에서 모든 타입은 고유하며 기본 타입, 파생된 타입이라는 개념은 존재하지 않는다. 즉, 이 패턴은 Go 프로그래밍에서는 좋은 설계 원칙이 아니다.

Animal 은 동물에 대한 기본 속성을 정의하고 있다.

type Animal struct {
    Name     string
    IsMammal bool
}

Speak 는 동물들이 말하는 방식에 관한 일반적인 행동을 정의하고 있다. 스스로 말할 수 없는 동물 때문에 이는 무의미한 메서드일 수 있다. Speak 는 실제로 모든 동물이 가진 특성은 아니다.

func (a *Animal) Speak() {
    fmt.Println("UGH!",
        "My name is", a.Name,
        ", it is", a.IsMammal,
        "I am a mammal")
}

DogAnimal 과 관련된 모든것과 PackFactor 라는 Dog 만이 가진 속성을 정의하고 있다.

type Dog struct {
    Animal
    PackFactor int
}

Speak 는 개가 말하는 방식을 의미한다.

func (d *Dog) Speak() {
    fmt.Println("Woof!",
        "My name is", d.Name,
        ", it is", d.IsMammal,
        "I am a mammal with a pack factor of", d.PackFactor)
}

CatAnimal 과 관련된 모든것과 ClimbFactor 라는 Cat 만이 가진 속성을 정의하고 있다.

type Cat struct {
    Animal
    ClimbFactor int
}

Speak 는 고양이가 말하는 방식을 의미한다.

func (c *Cat) Speak() {
    fmt.Println("Meow!",
        "My name is", c.Name,
        ", it is", c.IsMammal,
        "I am a mammal with a climb factor of", c.ClimbFactor)
}

여기까지는 괜찮을 지도 모른다. 하지만 이 코드는 컴파일되지 않는다. Animals 란 요소를 바탕으로 CatDog 를 그루핑했기 때문이다. 즉, Go 는 서브타이핑 개념이 없는데도 불구하고 서브타이핑을 사용했다. Go 는 공통된 DNA('구조체 내 공통된 필드'에 대한 비유)를 바탕으로 그룹핑하는 것을 권장하지 않는다. 누구인지에만 초점을 맞춘다면 그룹화하는데 큰 제한이 있기 때문에 공통된 DNA를 바탕으로 API 를 설계한다는 생각을 그만해야 한다. 서브타이핑은 다양성에 한계가 있다. 그룹화 가능하도록 서브셋을 작게 구성하면 그 형식과 관련된 것밖에 설계할 수 없지만, 행동에 초점을 맞추면 전체적인 형태로 넓혀서 설계할 수 있다.

Animal 부분을 초기화한 후 Dog 속성을 정의해서 Dog 를 생성한다.

animals := []Animal{
    Dog{
        Animal: Animal{
            Name: "Fido",
            IsMammal: true,
        },
        PackFactor: 5,
    },

Animal 부분을 초기화한 후 Cat 속성을 정의해서 Cat 를 생성한다.

    Cat{
        Animal: Animal{
            Name: "Milo",
            IsMammal: true,
        },
        ClimbFactor: 4,
    },
}

Animal 들이 말하도록 한다.

    for _, animal := range animals {
        animal.Speak()
    }
}

위 방식이 냄새나는 코드인 이유:

  • Animal 타입은 재사용 가능한 추상화된 계층을 제공한다.
  • 이 프로그램은 Animal 타입으로 값을 만들거나 단독으로 사용할 필요가 전혀 없다.
  • Animal 타입의 Speak 메서드 구현은 일반화이다.
  • Animal 타입에서 정의한 Speak 메서드는 절대 호출되어지지 않는다.

행동에 의한 그루핑

이번 파트는 구성과 인터페이스를 활용한 예시이며 이는 Go 에서 사용되면 좋은 방식이다.

공통된 상태가 아닌 공통된 행동으로 그룹화하는 이 패턴은 Go 프로그램에서 좋은 설계 원칙이다. Go 의 뛰어난 특성중 하나는 미리 구성할 필요가 없다는 점이다. 컴파일러는 컴파일 타임에 인터페이스와 행동을 자동으로 식별한다. 이는 현재 또는 미래에 작성한 인터페이스와 호환될 수 있는 코드를 지금 작성할 수 있다는 의미이다. 또한 컴파일러가 즉석에서 행동을 식별하기 때문에 선언된 위치도 중요하지 않다. 대신 어떤 행동을 해야 하는지를 고려해야 한다.

Speaker 는 그룹이 일원이 되기 위해 따라야 할 공통된 행동을 정의하고 있다. Speaker 는 구체적인 타입에 대한 계약이다. Animal 타입은 제거한다.

type Speaker interface {
    Speak()
}

DogDog 가 필요한 모든 것을 정의하고 있다.

type Dog struct {
    Name       string
    IsMammal   bool
    PackFactor int
}

Speak 는 개가 말하는 방식을 의미한다. 말하는 방식이 정의된 Dog 는 구체적인 타입인 Speaker 그룹의 일원이 된다.

func (d Dog) Speak() {
    fmt.Println("Woof!",
        "My name is", d.Name,
        ", it is", d.IsMammal,
        "I am a mammal with a pack factor of", d.PackFactor)
}

CatCat이 필요한 모든 것을 정의하고 있다. 복사 붙여넣기를 하면 약간의 시간이 걸릴지도 모르지만, 대부분의 경우 디커플링은 코드 재사용보다 더 나은 방식이다.

type Cat struct {
    Name        string
    IsMammal    bool
    ClimbFactor int
}

Speak 는 고양이가 말하는 방식을 의미한다. 말하는 방식이 정의된 Cat 는 구체적인 타입인 Speaker 그룹의 일원이 된다.

func (c Cat) Speak() {
    fmt.Println("Meow!",
        "My name is", c.Name,
        ", it is", c.IsMammal,
        "I am a mammal with a climb factor of", c.ClimbFactor)
}

말하는 방식이 정의된 동물들을 생성해보자.

func main() {
    speakers := []Speaker{

Dog 의 속성을 초기화해서 Dog 를 생성한다.

        Dog{
            Name:       "Fido",
            IsMammal:   true,
            PackFactor: 5,
        },

Cat 의 속성을 초기화해서 Cat 을 생성한다.

        Cat{
            Name:       "Milo",
            IsMammal:   true,
            ClimbFactor: 4,
        },
    }

Speaker 들이 말하도록 한다.

Have the Speakers speak.

    for _, spkr := range speakers {
        spkr.Speak()
    }
Woof! My name is Fido , it is true I am a mammal with a pack factor of 5
Meow! My name is Milo , it is true I am a mammal with a climb factor of 4

타입 선언에 대한 지침:

  • 새롭거나 유일한 것을 대표하는 타입을 선언한다. 가독성을 위해 별칭(alias)을 생성하지 않는다.
  • 모든 타입의 값이 자체적으로 생성되거나 사용되었는지 확인한다.
  • 상태가 아니라 행동을 위한 타입을 임베드 해야 한다. 행동에 대해 고려하지 않는다면 미래에 유지보수하기 어려운 설계가 될 수 있다.
  • 특정 타입이 이미 있는 타입을 위한 별칭이거나 추상화하고 있다면 의문을 가져야 한다.
  • 특정 타입이 유일한 목적이 공통 상태를 공유하는 것이라면 의문을 가져야 한다.