[추상 팩토리 메서드 패턴]

 다양한 구성 요소 별로 '객체의 집합'을 생성해야 할 때 유용하다. 이 패턴을 사용하여 상황에 알맞은 객체를 생성할 수 있다.

 

지난번에 본 생성패턴 이 가지고 있는 기본적인 형태이다. 기억하고 넘어가자.

추상팩토리는 말 그대로 객체의 집합을 생성할 때 사용한다.

 

이번 예제는 파스타 를 들어보기로 하자. 지난번 배운 팩토리 메서드 패턴과 함께 조합해서 사용해 보자.

파스타의 핵심 이 되는 면부터 살펴보자.

면을 위한 팩토리 메서드를 적용하는 다이어그램을 먼저 확인해 본다면 

누들팩토리 와 그 구현체로 생산자와 실제 제품 들과의 관계를 분리시켰다

noodle의 중간 abstract를 unimport 구조체가 되어 외부 패키지에서는 접근이 불가능하지만 NoodleFactory를 제공하는 제공자가 있다면 접근가능하도록 설계하였다.

package noodle

type NoodleFacotry interface {
	CreateNoodle(name string) Noodle
}

type noodleFactory struct{}

func (k *noodleFactory) CreateNoodle(name string) Noodle {
	switch name {
	case "Fusilli":
		return &Fusilli{
			noodle{name: "Korean Fusilli"},
		}
	case "Penne":
		return &Penne{
			noodle{name: "Korean Penne"},
		}
	case "Linguine":
		return &Linguine{
			noodle{name: "Korean Linguine"},
		}
	default:
		return &Spaghetti{
			noodle{name: "Korean Spaghetti"},
		}
	}
}

func NewNoodleFactory() NoodleFacotry {
	return &noodleFactory{}
}

누들 패키지에서는 NewNoodleFacotry를 통해 하나의 팩토리만 던져주게 된다. 현재는 하나의 값 만 반환하게 되지만 switch-case를 적용해 특정 팩토리 생산의 명령을 주입받는 방법도 존재한다. 

package noodle

import (
	"blogEx/abstoractFactory"
	"fmt"
)

type Noodle interface {
	abstoractFactory.Vegi
	Boiling()
}

type noodle struct {
	name string
}

func (n *noodle) Boiling() {
	fmt.Println("면 을 삶는 중입니다..")
}

func (n *noodle) String() string {
	return fmt.Sprintf("파스타 면 은 %s", n.name)
}

동일 패키지에 Noodle 인터페이스에 대한 정의와 구현체를 작성해 주었다. 구현체 내부에는 추가적인 인터페이스를 인자로 받아서

이 구현체를 상속하는 하위 구조체에서 구현을 강제하도록 만들었다. 

package noodle

type Fusilli struct {
	noodle
}

func (f *Fusilli) IsVegi() bool {
	return false
}

type Linguine struct {
	noodle
}

func (f *Linguine) IsVegi() bool {
	return false
}

type Spaghetti struct {
	noodle
}

func (f *Spaghetti) IsVegi() bool {
	return false
}

type Penne struct {
	noodle
}

func (f *Penne) IsVegi() bool {
	return true
}

동일 패키지 의 하위 제품의 구현을 작성했다. 기본적인 상속과, 더불어 IsVegi()의 인터페이스를 각각 구현해 주었다.

팩토리 메서드 지난 글(https://guiwoo.tistory.com/category/Go%20Dive/Design%20Pattern)

 

우리 는 파스타를 만드는 데 있어 이런 재료만 필요한 것 이 아닌 추가적인 재료가 필요하다. 마저 확인해 보자. 위와 동일한 구조를 가진 소스이다.

이렇게 동일한 구성을 가지고 Vegitables와 topping 도 선언해 주자.

 

이렇게 생성된 팩토리 들을 각각 의 방식으로 사용할 수도 있지만 보다 클라이언트의 특정 행동에 범위를 제한하기 위해 우리는 집합체를 생성하게 된다면? 클라이언트는 이 집합체를 얻음으로써, 각각의 팩토리를 가져와 객체의 구성을 만들수 있다는 장점이 있다. 아래 그림을 보자.

각각의 팩토리 를 조합하는 파스타 키트를 운용하는 것이다. 각각의 팩토리 인터페이스를 반환해 주는 방식이다 이렇게 했을 때 장점이 무엇인가?

파스타 키트는 다양한 팩토리를 주입받을 수 있으며 심지어 각각의 팩토리에서 또한 다양한 방식으로 팩토리 제공이 가능하다.

누들 팩토리에서 이탈리안 누들을 줄수도 있고, 한국 누들 을 줄수도 있는 등 팩토리를 갈아 끼울 수 있다는 장점 이 존재한다.

코드를 확인해 보자.

type PastaKit interface {
	Noodles() noodle.NoodleFacotry
	Sources() source.SourceFactory
	Vegetables() vegitable.VegetableFactory
	Toppings() topping.ToppingFactory
}

type GuiwooPastKit struct{}

func (p *GuiwooPastKit) Noodles() noodle.NoodleFacotry {
	return noodle.NewNoodleFactory()
}
func (p *GuiwooPastKit) Sources() source.SourceFactory {
	return source.NewSourceFactory()
}
func (p *GuiwooPastKit) Vegetables() vegitable.VegetableFactory {
	return vegitable.NewVegetableFactory()
}
func (p *GuiwooPastKit) Toppings() topping.ToppingFactory {
	return topping.NewToppingFactory()
}

func NewGuiwooPastaKit() PastaKit {
	return &GuiwooPastKit{}
}

파스타 키트에서는 추가적인 팩토리 메서드 적용이 아닌 생성패턴 의 기본 골격만 적용하였다. 인터페이스 와 그것에 대한 구현체

이렇게 선언된 파스타 키트를 이용해 클라이언트는 어떻게 사용할까?

func main() {
	var kit pastakit.PastaKit = pastakit.NewGuiwooPastaKit()
	noodle := kit.Noodles().CreateNoodle("Fusilli")
	source := kit.Sources().CreateSource("Cream")
	topping := kit.Toppings().CreateTopping("Chicken")
	vegetables := kit.Vegetables().CreateVegetable("Paprika")

	fmt.Println("pasta 끝")

	noodle.Boiling()
	fmt.Println(noodle)
	source.Prepare()
	fmt.Println(source)
	topping.PutOn()
	fmt.Println(topping)
	vegetables.Chop()
	fmt.Println(vegetables)
}

파스타 키트를 받아서 정말 다양한 파스타 의 종류 를 만들어 낼수있다. 

만약 원산지가 바뀌어 버렸다면? 그냥 팩토리 내부 구현체 만 추가적으로 작성해서 넣어주면 원산지가 바뀐 파스타 키트 를 클라이언트에서는 지속적으로 사용이 가능하다.


다시 말해 클라이언트는 파스타 키트 의 원산지 재료가 무엇이든 지 간에 궁금하지 않다. 제공되는 파스타 키트에서는 누들 소스 토핑 그리고 야채들이 제공되어야 하기 때문에 정말 코드 를 호출 하는 호출 부와 제공 하는 프로덕트 부 의 느슨한 결합이 완성된 형태라고 볼수 있다.

 

이렇게 느슨한 결합이 주는 장점으로는 테스트 코드의 작성 즉 파스타 키트 에서 생산되는 각각의 팩토리에 대해 테스트하기 쉬우며( 목 객체를 만들기 매우 편하다.), 변화되는 코드에 있어 손쉽게 교체 및 추가할 수 있다는 점이 있다. 

그러면 장점만 있는가? 위의 단순한 코드 제공에만 무려 14개의 파일과 5개의 디렉터리로 나누어져 있다. 다시 말해 복잡하다. 이해하기 어려울 수 있으며 코드의 양이 정말 많아진다는 단점이 존재한다.

 

그래서 그러면 팩토리 메서드 패턴 이랑 추상 팩토리랑 도대체 무엇이 다른가 에 의한 의문점이 남을수 있다.

팩토리 메소드 패턴 은? 상속을 통한 객체 생성과 오직 하나의 제품을 생산하는 쪽에 키워드가 맞춰져 있고,

추상 팩토리는? 객체의 구성을 통해 필요 객체 생성을 만들어 낼 때 즉 제품의 구성 쪽에 키워드가 맞춰져 있다.

 

그렇다면 이런 다른 키워드를 포워딩하고 있는데 언제 사용해야 하는가?

제품군을 만들어야 한다면? 추상팩토리

클라이언트 코드와 인스턴스의 생성 클래스를 분리시켜야 한다면? 팩토리 메서드 패턴을 활용하면 된다.


통합 구조


통합코드

· Noodle Package

더보기
package noodle
### 팩토리 ####
type NoodleFacotry interface {
	CreateNoodle(name string) Noodle
}

type noodleFactory struct{}

func (k *noodleFactory) CreateNoodle(name string) Noodle {
	switch name {
	case "Fusilli":
		return &Fusilli{
			noodle{name: "Korean Fusilli"},
		}
	case "Penne":
		return &Penne{
			noodle{name: "Korean Penne"},
		}
	case "Linguine":
		return &Linguine{
			noodle{name: "Korean Linguine"},
		}
	default:
		return &Spaghetti{
			noodle{name: "Korean Spaghetti"},
		}
	}
}

func NewNoodleFactory() NoodleFacotry {
	return &noodleFactory{}
}
### 하위 상속 구조체 ###
package noodle

type Fusilli struct {
	noodle
}

func (f *Fusilli) IsVegi() bool {
	return false
}

type Linguine struct {
	noodle
}

func (f *Linguine) IsVegi() bool {
	return false
}

type Spaghetti struct {
	noodle
}

func (f *Spaghetti) IsVegi() bool {
	return false
}

type Penne struct {
	noodle
}

func (f *Penne) IsVegi() bool {
	return true
}
### 누들 구현체 ###
package noodle

import (
	"blogEx/abstoractFactory"
	"fmt"
)

type Noodle interface {
	abstoractFactory.Vegi
	Boiling()
}

type noodle struct {
	name string
}

func (n *noodle) Boiling() {
	fmt.Println("면 을 삶는 중입니다..")
}

func (n *noodle) String() string {
	return fmt.Sprintf("파스타 면 은 %s", n.name)
}

·SourcePackage

더보기
package source

type SourceFactory interface {
	CreateSource(name string) Source
}

type sourceFactory struct{}

func (t *sourceFactory) CreateSource(name string) Source {
	switch name {
	case "Tomato":
		return &Tomato{
			source{name: "Lagu source"},
		}
	case "Cream":
		return &Cream{
			source{name: "Creamy milk"},
		}
	case "Arabiata":
		return &Arabiata{
			source{name: "Arabiata"},
		}
	default:
		return &Oil{
			source{name: "Olive Oil"},
		}
	}
}

func NewSourceFactory() SourceFactory {
	return &sourceFactory{}
}

### source ###
package source

import (
	"blogEx/abstoractFactory"
	"fmt"
)

type Source interface {
	abstoractFactory.Vegi
	Prepare()
}

type source struct {
	name string
}

func (s *source) Prepare() {
	fmt.Println(s.name, "준비 완료")
}

func (s *source) String() string {
	return fmt.Sprintf("소스 는 %s 입니다.", s.name)
}

### 하위 상속 구조체 ###
package source

type Tomato struct {
	source
}

func (t *Tomato) IsVegi() bool {
	return false
}

type Cream struct {
	source
}

func (c *Cream) IsVegi() bool {
	return false
}

type Oil struct {
	source
}

func (o *Oil) IsVegi() bool {
	return true
}

type Arabiata struct {
	source
}

func (a *Arabiata) IsVegi() bool {
	return true
}

· Topping

더보기
package topping

type ToppingFactory interface {
	CreateTopping(name string) Topping
}

type toppingFactory struct{}

func (t *toppingFactory) CreateTopping(name string) Topping {
	switch name {
	case "Chicken":
		return &Chicken{
			topping{name: "chicken from South Korea"},
		}
	case "Beef":
		return &Beef{
			topping{name: "beef from Australia"},
		}
	case "Pork":
		return &Pork{
			topping{name: "pork from USA"},
		}
	default:
		return &Scampi{
			topping{name: "Scampi from Norway"},
		}
	}
}

func NewToppingFactory() ToppingFactory {
	return &toppingFactory{}
}

### topping ###
package topping

import (
	"fmt"
)

type Topping interface {
	PutOn()
}

type topping struct {
	name string
}

func (t *topping) PutOn() {
	fmt.Println(t.name, "Putting on the pasta")
}

func (t *topping) String() string {
	return fmt.Sprintf("토핑은 %s 입니다.", t.name)
}

### 하위 상속 구조체 ###
package topping

type Pork struct {
	topping
}

type Beef struct {
	topping
}

type Scampi struct {
	topping
}

type Chicken struct {
	topping
}

· Vegetable

더보기
package vegitable

type VegetableFactory interface {
	CreateVegetable(name string) Vegetable
}

type vegetableFactory struct{}

func (j *vegetableFactory) CreateVegetable(name string) Vegetable {
	switch name {
	case "SpringOnion":
		return &SpringOnion{
			vegetable{name: "Japanese Onion"},
		}
	case "Onion":
		return &Onion{
			vegetable{name: "Japanese Onion"},
		}
	case "Paprika":
		return &Paprika{
			vegetable{name: "Japanese Paprika"},
		}
	default:
		return &Garlic{
			vegetable{name: "Japanese Garlic"},
		}
	}
}

func NewVegetableFactory() VegetableFactory {
	return &vegetableFactory{}
}

### vegetable ###
package vegitable

import (
	"fmt"
)

type Vegetable interface {
	Chop()
	Slice()
}

type vegetable struct {
	name string
}

func (v *vegetable) Chop() {
	fmt.Printf("%s chopped", v.name)
}

func (v *vegetable) Slice() {
	fmt.Printf("%s sliced", v.name)
}


### 하위 상속 구조체 ###
package vegitable

type SpringOnion struct {
	vegetable
}

type Onion struct {
	vegetable
}

type Paprika struct {
	vegetable
}

type Garlic struct {
	vegetable
}

·  Pastakit

더보기
package pastakit

import (
	"blogEx/abstoractFactory/noodle"
	"blogEx/abstoractFactory/source"
	"blogEx/abstoractFactory/topping"
	"blogEx/abstoractFactory/vegitable"
)

type PastaKit interface {
	Noodles() noodle.NoodleFacotry
	Sources() source.SourceFactory
	Vegetables() vegitable.VegetableFactory
	Toppings() topping.ToppingFactory
}

type GuiwooPastKit struct{}

func (p *GuiwooPastKit) Noodles() noodle.NoodleFacotry {
	return noodle.NewNoodleFactory()
}
func (p *GuiwooPastKit) Sources() source.SourceFactory {
	return source.NewSourceFactory()
}
func (p *GuiwooPastKit) Vegetables() vegitable.VegetableFactory {
	return vegitable.NewVegetableFactory()
}
func (p *GuiwooPastKit) Toppings() topping.ToppingFactory {
	return topping.NewToppingFactory()
}

func NewGuiwooPastaKit() PastaKit {
	return &GuiwooPastKit{}
}

· ClientSide

더보기
package main

import (
	"blogEx/abstoractFactory/pastakit"
	"fmt"
)

func main() {
	var kit pastakit.PastaKit = pastakit.NewGuiwooPastaKit()
	noodle := kit.Noodles().CreateNoodle("Fusilli")
	source := kit.Sources().CreateSource("Cream")
	topping := kit.Toppings().CreateTopping("Chicken")
	vegetables := kit.Vegetables().CreateVegetable("Paprika")

	fmt.Println("pasta 끝")

	noodle.Boiling()
	fmt.Println(noodle)
	source.Prepare()
	fmt.Println(source)
	topping.PutOn()
	fmt.Println(topping)
	vegetables.Chop()
	fmt.Println(vegetables)
}

출처:

https://en.wikipedia.org/wiki/Abstract_factory_pattern

 

Abstract factory pattern - Wikipedia

From Wikipedia, the free encyclopedia Software design pattern The abstract factory pattern in software engineering is a design that provides a way to create families of related objects without imposing their concrete classes, by encapsulating a group of in

en.wikipedia.org

https://refactoring.guru/ko/design-patterns/abstract-factory

 

추상 팩토리 패턴

/ 디자인 패턴들 / 생성 패턴 추상 팩토리 패턴 다음 이름으로도 불립니다: Abstract Factory 의도 추상 팩토리는 관련 객체들의 구상 클래스들을 지정하지 않고도 관련 객체들의 모음을 생성할 수 있

refactoring.guru

https://sourcemaking.com/design_patterns/abstract_factory

 

Design Patterns and Refactoring

Design Patterns and Refactoring articles and guides. Design Patterns video tutorials for newbies. Simple descriptions and full source code examples in Java, C++, C#, PHP and Delphi.

sourcemaking.com

https://www.baeldung.com/java-abstract-factory-pattern

https://www.digitalocean.com/community/tutorials/abstract-factory-design-pattern-in-java

 

Abstract Factory Design Pattern in Java | DigitalOcean

 

www.digitalocean.com

 

최근 디자인 패턴을 공부하면서 Interface, Type에 대해 부족한 부분을 발견하고 이번 기회에 쉬는 날 정리해 보고자. 글을 작성한다.

GO TOUR 에서 작성된 글에 의하면? 

인터페이스 타입 은 특정 함수를 정의 하는 용도로 사용한다. Java에 익숙한 사람들이라면 아래 예제는 당연하고 이해가 간다.

type Sender interface {
	send()
}

type Bank struct {
	account Account
}

func (b *Bank) send() {
	fmt.Printf("%v 에서 돈을 보냅니다 .", b.account.ID)
}

type Account struct {
	ID      string
	Balance uint
}

func main() {
	var a Sender = &Bank{Account{"귀우 의 계좌", 123}}
	a.send()
}

Sender 인터페이스 를 선언 (er을 이용해 Interface를 정의한다 "GO effecitve") 

뱅크의 타입을 선언 했지만? Sender의 타입으로 받아서 샌더가 가지고 있는 함수를 사용할 수 있다.

 

당연하다 그렇다면 역으로도 가능한가 ? 저 특정 인터페이스에서 원하는 타입을 뽑아야 내하는 경우가 생긴다면 어떻게 할 것인가? 

가능하다.  어떻게 ?

bank := &Bank{Account{"귀우 의 계좌", 123}}
var a Sender = bank
a.send()

if x, ok := a.(*Bank); ok {
   x.send()
}

Interface로 선언된 타입은 최초 구현은 Bank의 포인터 타입이다.

추출하고자 하는 정확한 타입에 대해 위와 같이 추출해서 사용한다.

 

좋다 인터페이스 와 구현하는 구조체 간의 쌍방향 관계가 된다 는 사실을 알았으니 이제 타입 끼리 해보자.

 

동일한 인터페이스를 구현하는 2개 의 타입 간의 크로스 캐스팅이 가능한가?

type Email struct {
	sender   string
	receiver string
	address  string
}

func (e *Email) send() {
	fmt.Printf("%v 에서 %v 읭 %v 로 보냅니다. ", e.sender, e.receiver, e.address)
}

func main() {
	var a Sender = email
	if v, ok := a.(*Email); ok {
		v.send()
	}
	if v, ok := a.(*Bank); ok {
		v.send()
	}
 }

 

 

이 경우 콘솔에서는 메일에 해당하는 샌더를 실행시켜 준다. 즉 2번째 if 문이 false로 타입추출에 실패했다.

그래서 이게 뭐? 당연한 거 아닌가?

 

아래 예제를 보자.

type ExpressEmail struct {
	*Email
	price int
}

func main() {
	//bank := &Bank{Account{"귀우 의 계좌", 123}}
	email := &ExpressEmail{
		&Email{"a", "b", "13472 Mortgatan6 Saltjobaden"},
		10000000,
	}
	var a Sender = email
	fmt.Println(reflect.TypeOf(a))
	if v, ok := a.(*Email); ok {
		v.send()
	}
	if v, ok := a.(*ExpressEmail); ok {
		v.superFast()
		v.send()
	}
}

ExpressEmail로 인터페이스를 받아서 Email을 뽑으려고 하면 실패한다.  왜? ExpresEmail 구현체 안의 Email 도 하나의 고유한 타입이기 때문에 위와 같이 타입 추출이 불가능하다. (반면 자바는 가능하다.)

 

여기서 언어의 차이가 드러난다. 객체 지향과, 그렇지 않은 그래서 클래스의 상속에 따른 구조와 임베디드 타입에 의한 새로운 타입의 차이 가 있기 때문에 위와 같은 동일한 로직에서 서로 다른 결과를 얻게 된다.

 

반면 인터페이스 안에 인터페이스로 정의된다면 그건 타입캐스팅이 될까? 

type Sender interface {
	send()
}

type Receiver interface {
	receive()
}

type SendReceive interface {
	Sender
	Receiver
}

type CommonSendReceive struct{}

func (c *CommonSendReceive) send() {
	fmt.Println("Send Common Message")
}
func (c *CommonSendReceive) receive() {
	fmt.Println("Receive Common Message")
}

func main() {
	var sr SendReceive = &CommonSendReceive{}

	if v, ok := sr.(Sender); ok {
		v.send()
	}

	if v, ok := sr.(Receiver); ok {
		v.receive()
	}
}

놀랍게도 가능하다. 인터페이스 안에 임베디드 타입으로 구성된 인터페이스에 대해서는 타입 추출이 가능해진다.

 

왜 그럴까?

 

타입 추출은 컴파일러에게 인터페이스를 구현하는 것으로부터 내가 원하는 것처럼 뽑아달라고 부탁하는 것이다. 

이렇게 되면 인터페이스 값에 기반하지 않은 타입이라면 에러가 발생하게 된다.

그러나 인터페이스 유니온 즉 인터페이스끼리 결합된 인터페이스에서는 분리가 가능해진다. 

인터페이스의 결합을 코드로 풀면? 그냥 두 개의 메서드 합친 결과물이다. 그러니 당연히 필요한 부분의 분리가 가능해진다.

 

왜? 정확히는 컴파일러에게 내가 원하는 메서드를 어떤 걸 사용할 건데 그 타입으로 뽑아줘라고 부탁을 하는 것이기 때문이다.

 

아까 예제 ExpressEmail에서 email을 뽑아보자.

type Sender interface {
	send()
}

type Email interface {
	sendEmail()
}
type email struct{}

func (e *email) sendEmail() {
	fmt.Println("고유 이메일")
}

type ExpressEmail struct {
	Email
	price int
}

func (e *ExpressEmail) send() {
	fmt.Println("Send Express Email")
}

아까 와의 차이가 무엇인가? 인터페이스 타입을 인자로 가지냐 아니면 특정 타입이냐의 차이가 되고 그 결과는 타입 추출 의 가능 여부를 판별한다.

func main() {
	var a Sender = &ExpressEmail{
		&email{},
		500,
	}

	if v, ok := a.(*ExpressEmail); ok {
		v.send()
	}
	if v, ok := a.(Email); ok {
		v.sendEmail()
	}
}

위와 같은 코드는 전부 실행이 된다. 인터페이스의 강력함이 아닐수가 없다... 

심지어 인터페이스가 아닌 구조체를 넣게 되더라도 그게 특정 인터페이스를 구현하고 있다면 인터페이스 의 타입추출이 가능해진다.

func main() {
	var a Sender = &ExpressEmail{
		email{},
		500,
	}

	if v, ok := a.(*ExpressEmail); ok {
		v.send()
	}
	if v, ok := a.(Email); ok {
		v.sendEmail()
		fmt.Println(reflect.TypeOf(v))
		if e, ok := v.(*email); ok {
			fmt.Println(reflect.TypeOf(e))
		}
	}

}

이 코드에서 보면  email이라는 구조체는 Email 인터페이스를 구현하고 있고 우리는 그 안에 email 있다는 사실을 알고 역으로 풀고자 한다.

그러나 마지막 if 문에서 프린트 라인이 찍히지 않는다. 왜?

reflect를 이용해 타입 추출을 하더라도 저 타입은 아직도 ExpressEmail이다. 즉 cpu는 이렇게 알고 있다는 소리이다. 

아무리 우리가 밖에서 Email로 추론하고 값을 달라고 하더라도 cpu에서 지정된 오리지널 값에서 잘라서 준다는 의미이다.

 

아무래도 고에서 지원하는 상속과 embedded 타입 간의 간극을 잘 파악하여 사용하지 않는다면 오류를 남발하는 코드를 작성할 것이다.

'Go > Go Basic' 카테고리의 다른 글

Ulitmate-Go-02  (0) 2023.09.05
Ulitmate-Go-01 (string,메모리 패딩)  (3) 2023.08.19
Effective Go 04  (0) 2023.02.12
Effective Go 03  (0) 2023.02.10
Effective Go 02  (0) 2023.02.09

생성 패턴 이란?

소프트웨어 엔지니어링에서 생성 디자인 패턴은 개체 생성 메커니즘을 다루는 디자인 패턴으로 상황에 적합한 방식으로 개체를 생성하려고 합니다. 개체 생성의 기본 형태는 디자인 문제를 일으키거나 디자인에 복잡성을 더할 수 있습니다. 생성 디자인 패턴은 이 객체 생성을 어떻게든 제어함으로써 이 문제를 해결합니다. --wikipeda--

어떤 아이디어에 착안해서 패턴으로 자리 잡았는가?

2가지 의 메인 콘셉트가 있다. 하나는 시스템 이 사용하는 구체적인 클래스에 대해 캡슐화하는 것이고, 다른 하나는 이런 구체적인 클래스의 인스턴스 생성 결합되는 방식을 숨기는 것이다.

 

나아가 생성패턴 은 클래스와 객체 생성 패턴으로 분류될 수 있다. 객체 생성 패턴은 객체를 , 클래스 생성 패턴은 클래스 인스턴스화를 

처리한다. 

 

Common Structure

왼쪽 은 대부분의 생성 패턴이 공통적으로 가지고 있는 간단한 클래스 다이어그램이다.
다른 생성 패턴에는 추가 및 다른 참여 클래스가 필요하다.

 

간단 한 설명을 보자면

 

· Creator = 개체의 인터페이스를 선언하고, 반환값을 준다. 

· Concrete Creator = 개체 인터페이스를 구현하는 클래스이다.

 

이에 따라 잘 알려진 생성 디자인 패턴은 

추상 팩토리, 빌더, 팩토리 메서드, 프로토 타입, 싱글톤 패턴으로 나뉘고 이중 팩토리 메서드 패턴부터 보자.

 

[팩토리 메서드 패턴]

클래스 기반 프로그래밍에서 정확한 클래스를 지정하지 않고도 객체 생성 문제를 처리하는 방식이다. 생성자를 호출하는 대신

인터페이스에 지정되고 자식클래스에 의해 구현되거나 기본,파생 클래스 에 의해 선택적으로 구현되는 방식이다.

 

"객체 생성을 공장 클래스, 함수로 캡슐화 처리하여 대신 생성 하게 하는 생성 디자인 패턴이다."

"객체 생성에 필요한 과정을 템플릿처럼 미리 구성해 놓고 , 객체 생성에 관한 전, 후 처리를 통해 객체를 유연하게 만들 수 있다"

 

아래와 같은 문제에 직면하게 된다면? 팩토리 메서드 패턴을 적용해 보면 최적의 선택이 될 수 있다.

  • 서브 클래스가 인스턴스 화 할 클래스를 재정의하려면 어떻게 해야 할까?
  • 클래스는 어떻게 인스턴화를 하위 클래스에 위임할 수 있을까?

의 주된 문제를 해결하는 설루션이 팩토리 메서드 패턴이다. 

 

바로 가보자.

피자 가게를 운영한다고 가정해보자. 주문에 해당하는 코드를 아래와 같이 정의해 보자.

type Pizza struct{}

func (p *Pizza) prepare() {
	fmt.Println("Prepare")
}
func (p *Pizza) bake() {
	fmt.Println("Bake")
}
func (p *Pizza) cut() {
	fmt.Println("cut")
}
func (p *Pizza) box() {
	fmt.Println("boxing")
}

func orderPizza(t string) Pizza {
	var p Pizza	

	switch t {
	case "Cheese":
		// 치즈 피자가 p 가 된다.
	case "Hawaiian":
		// 하와이안 피자가 p 가 된다
	case "Margherita":
		// 마르게리타 피자가 p 가 된다.
	default:
		// 디폴트 피자 는 햄 피자가 p 가 된다.
	}
	
	p.prepare()
	p.bake()
	p.cut()
	p.box()

	return p
}

이 코드의 문제점 만약 새로운 피자가 추가되거나 삭제되거나 하게 된다면 기존 코드 의 변경이 필요하다. 즉 저 orderPizza 부분 이 문제가 된다는 소리이다.  Switch에 해당하는 부분을 캡슐화해보자.

더보기
package main

import "fmt"

type Pizza interface {
	prepare()
	bake()
	cut()
	box()
}

type pizza struct {
	name string
}

func (p *pizza) prepare() {
	fmt.Println("피자 소스, 토핑, 치즈 준비중 .....")
	fmt.Println("오븐 달구는중 .......")
	time.Sleep(time.Millisecond * 500)
}
func (p *pizza) bake() {
	fmt.Println("피자 굽는중 ....")
	time.Sleep(time.Second)
}
func (p *pizza) cut() {
	fmt.Println("피자 자르는중 .........")
}
func (p *pizza) box() {
	fmt.Printf("[%s] 박스에 넣기 완료\n", p.name)
}

type Cheese struct {
	pizza
}
type Hawaiian struct {
	pizza
}
type Margherita struct {
	pizza
}
type Ham struct {
	pizza
}

type FactoryPizza struct {
	p Pizza
}

func (f *FactoryPizza) createPizza(t string) Pizza {
	switch t {
	case "Cheese":
		// 치즈 피자가 p 가 된다.
		f.p = &Cheese{pizza{"치즈 피자"}}
	case "Hawaiian":
		// 하와이안 피자가 p 가 된다
		f.p = &Cheese{pizza{"하와이안 피자"}}
	case "Margherita":
		// 마르게리타 피자가 p 가 된다.
		f.p = &Cheese{pizza{"마르게리타 피자"}}
	default:
		// 디폴트 피자 는 햄 피자가 p 가 된다.
		f.p = &Cheese{pizza{"햄 피자"}}
	}
	return f.p
}

조금 많은 과정이 들어갔지만 하나씩 차근차근 살펴보자.

피자에 해당하는 구조체 들을 각각 선언했으며, 피자 인터페이스 와 중간 구현체 그리고 상속을 받는 구조이다.

이렇게 함에 따라 최하위 구현체들에게 생성의 책임을 위임할 수 있으며 중간구현체 가 있음으로 공통되는 부분을 제거해서

사용할 수 있다. 

 

이에 대해 클라이언트 코드 도 수정해 보자.

type PizzaStore struct {
	factory FactoryPizza
}

func (ps *PizzaStore) orderPizza(pizza string) Pizza {
	p := ps.factory.createPizza(pizza)

	p.prepare()
	p.bake()
	p.cut()
	p.box()
	return p
}

팩토리를 피자 스토어에 던져주고 그걸 이용해 피자만 생성해 주면 된다.

코드를 보면 희한한 감이 생긴다 그냥 스위치 부분을 아래 구조체에 그냥 넘긴 게 아닌가? 뭐가 다른지 모를 수도 있다.

만약 이 피자

팩토리를 사용하는 곳이 정말 많고 생성된 피자의 객체를 받아와서 설명하고, 가격을 알려주는 함수가 추가적으로 있다면 하위클래스 모든 곳에서 이에 해당하는 함수를 전부 구현해야 합니다.

 

가게를 확장해 보자. 새로운 지점과 서로 다른 피자를 제공하는 경우가 생긴다면?

단순하게 생각해 본다면 factoryzPizza를 여러 개 만들면 되지 않을까? 바로 가보자.

더보기

메인 타입

type PizzaStore interface {
	orderPizza(name string) Pizza //최상위 인터페이스 강제할 함수
}
type PizzaProducer interface {
	createPizza(name string) Pizza //인터페이스 구현체 에서 사용될 함수 를 강제할 부분
}

type pizzaStore struct {
	p  Pizza
	pp PizzaProducer // 최초 구현체 생성시 인자를 주입받기 위해 생성
}

func (p *pizzaStore) orderPizza(name string) Pizza {
	p.p = p.pp.createPizza(name)

	p.p.prepare()
	p.p.bake()
	p.p.cut()
	p.p.box()

	return p.p
}

 하위 구현체

type NyPizza struct {
	pizzaStore
}

func (n *NyPizza) createPizza(t string) Pizza {
	switch t {
	case "Cheese":
		// 치즈 피자가 p 가 된다.
		n.p = &Cheese{pizza{"뉴욕 스타일 치즈 피자"}}
	case "Hawaiian":
		// 하와이안 피자가 p 가 된다
		n.p = &Cheese{pizza{"뉴욕 스타일 하와이안 피자"}}
	case "Margherita":
		// 마르게리타 피자가 p 가 된다.
		n.p = &Cheese{pizza{"뉴욕 스타일 마르게리타 피자"}}
	default:
		// 디폴트 피자 는 햄 피자가 p 가 된다.
		n.p = &Cheese{pizza{"뉴욕 스타일 햄 피자"}}
	}
	return n.p
}

type LAPizza struct {
	pizzaStore
}

func (n *LAPizza) createPizza(t string) Pizza {
	switch t {
	case "Cheese":
		// 치즈 피자가 p 가 된다.
		n.p = &Cheese{pizza{"LA 스타일 치즈 피자"}}
	case "Hawaiian":
		// 하와이안 피자가 p 가 된다
		n.p = &Cheese{pizza{"LA 스타일 하와이안 피자"}}
	case "Margherita":
		// 마르게리타 피자가 p 가 된다.
		n.p = &Cheese{pizza{"LA 스타일 마르게리타 피자"}}
	default:
		// 디폴트 피자 는 햄 피자가 p 가 된다.
		n.p = &Cheese{pizza{"LA 스타일 햄 피자"}}
	}
	return n.p
}

최종 클라이언트 코드

func main() {
	a := &pizzaStore{pp: &NyPizza{}} // 피자 의 프로듀서 를 각각 주입 해준다.
	b := &pizzaStore{pp: &LAPizza{}} 

	a.orderPizza("Cheese")
	fmt.Println()
	b.orderPizza("Hawaiian")
}

이렇게 특정 피자 가게 의 주입을 넣어줄 수 있으면서 피자 생성이 가능해진다. 이렇게 생산된 피자의 제조 방법 은 모두 캡슐화되어 관리된다. 그림으로 보면 아래와 같다.

이렇게 병렬 적으로 구성된 생산자, 제품 클래스는 무한히 확장 가능한 형태가 된다.

 

"팩토리 메서드 패턴은 객체를 생성할 때 피룡한 인터페이스를 만듭니다."

"어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정합니다. "

 

크리에이터에 해당하는 부분은 실질적인 제품의 생산이 아닌 어떤 방식으로 만들어야 하는지에 대한 방식만 결정하고 있다.

이에 반해 제품에서는 실질적은 인스턴스의 반환 이 일어나고 있다.

다시 말해 사용하는 서브클래스에 따라 반환되는 객체 인스턴스가 결정된다.

 

마지막 클라이언트 호출부에서 고민이 생긴다.

함수형태로 반환해서 이름을 주입하는 방법 도 하나의 좋은 방법이 될 거 같아 공유하고자 한다.

type PizzaStore interface {
	orderPizza(p PizzaProducer) func(name string) Pizza
}

func main() {
	// 호출 /....
    var ps PizzaStore
   	pa.orderPizza(&NyPizza{})("cheese")
    
    a := &pizzaStore{pp: &NyPizza{}}
    a.orderPizza("cheese")
}

구현의 방식은 중요하지 않다. 중요한 부분은 생성하는 서브 클래스 의 생성자 주입이고 그거의 형태를 최초 생성 시 넣어주느냐

아니면 함수 호출 과정에 넣어주느냐의 차이이다.


단 이런 추상 팩토리 패턴은 가끔씩 안티패턴으로 불리기도 한다. 왜?

  1. 과도한 추상화: 추상화는 객체 생성에 대한 복잡성을 감추기 위한 목적으로 사용되어야 합니다. 그러나 과도한 추상화는 코드를 이해하기 어렵게 만들고 유지보수를 어렵게 만듭니다.
  2. 복잡성 증가: 팩토리 메서드 패턴은 객체 생성을 처리하는 인터페이스를 정의하기 때문에, 팩토리 클래스와 그 클래스의 서브 클래스 사이의 상호작용이 복잡해질 수 있습니다.
  3. 새로운 타입 추가의 어려움: 팩토리 메소드 패턴을 사용하면 새로운 객체 타입을 추가하는 것이 상대적으로 어려워집니다. 새로운 타입을 추가하려면 팩토리 클래스와 그 클래스의 서브 클래스를 수정해야 합니다.
  4. 성능 저하: 팩토리 메소드 패턴은 객체 생성 시간이 길어지는 경우 성능에 영향을 미칠 수 있습니다. 이는 팩토리 클래스가 객체를 생성하는 과정에서 오버헤드가 발생하기 때문입니다.

1번 관점에서 이유없는 추상화가 발생한다면 이런경우에 해당한다. 추상화 된 인터페이스에 대해 아무 행동이 없다면 ? 굳이 추상화를 거칠 필요가 없다. 다시말해 그냥 일반적으로 구현하는게 보다 직관적 이라는 소리 이다.

 

2번 은 구현하면서 상당히 많이 느꼇다. 실제적으로 인터페이스 팩토리 클래스 사이 에 어떤 연관 을 규정하고 구현 해야할지 상당히 애를 먹었다.

 

3번 의 관점에서 본다면 위에 구현된 예제는 안티패턴이 되어버린다. 왜? OCP를 어기고 있다. 새로운 타입 이 추가되면 기존코드를 고쳐야한다. 예를들어 Switch 문에서 새로운 뉴욕스타일의 피자가 추가된다면 ? 기존 코드를 고쳐야 한다. 이는 OCP 를 어기고 있다. 

그럼에도 팩토리 메서드  패턴이 사용되는 이유로는 기존 클래스 들과 의 연관관계 가 상당히 적어진다.

즉 개별적으로 동작한다는 소리이다. 

새로운 피자가 추가된다고 해서 기존 클래스에 어떤 연관관계 가 있는지 생각해 본다면 보다 명확해지지 않을까 싶다.
예를 들어 기존 뉴욕 치즈 피자에 새로운 스트링이 추가된다면 ? 그 부분만 수정한다고 해서 LA 피자가 바뀌는가 ? 라고 생각해보면 쉽게 답할수 있다.

 

4번 예를 들어 db 의 커넥션 팩토리 메소드 가 있다고 생각해보자. 그 커녁센 이 무조건 있어야 실행되는 어플리케이션 이라면 ? 그 커넥션 을 위해 계속적인 리트라이가 일어나게 된다면 ? 성능의 저하가 발생될수도 있다. 


다시 처음에 설명했던 생성패턴 은 2가지 콘셉트에 대해 살펴보자.

1. 하나는 시스템 이 사용하는 구체적인 클래스에 대해 캡슐화하는 것이고,

: orderPizza 안에 creaetPizza를 숨겨 클라이언트는 피자의 생성 방식을 알지 못한다.

2. 다른 하나는 이런 구체적인 클래스의 인스턴스 생성 결합되는 방식을 숨기는 것이다.

: 각각의 하위 구현체 들은 피자의 생성된다.

 

이로 인해 얻는 장점은?

객체의 생성을 제어하고, 단순화한다.

단순 한 줄이면 우리는 원하는 객체를 얻을 수가 있으며 심지어 다양한 객체를 생성할 수도 있는 힘이 생긴다.

그렇게 생성된 객체에 대해 우리는 특정 행동을 명시하고 제어할 수 있는 제어권 이 생긴다. 

 

또한 생성패턴이 적용되어. 캡슐화되어 있기 때문에 단위 테스트 하는 데 있어 용이해진다.

보이는가? 정말 낮은 단계에서부터 상위까지 이렇게 정말 쉽게 모든 생성자에 대해 손쉽게 생성할 수 있다. 

팩토리 메서드 패턴의 장점이 여기서 드러난다.

이렇게 생성된 객체 들에 대해 추가적인 행동에 대해 검증하면 된다.

정말 손쉽고 작은 단위로 모든 객체에 대해 이렇게 테스트가 쉽다는 것은 정말 큰 장점이라고 생각한다.

 

패턴은 너무 어렵다 ....

 

 

 

 

출처

Encapsulation and Testing

Go DesignPattern Udemy

ChatGpt 예제

고 디자인 패턴

헤드퍼스트 디자인패턴

위키피디아

 

Why Factory method pattern call as a anti-pattern

출처 : https://go.dev/doc/effective_go#concurrency

 

Effective Go - The Go Programming Language

Effective Go Introduction Go is a new language. Although it borrows ideas from existing languages, it has unusual properties that make effective Go programs different in character from programs written in its relatives. A straightforward translation of a C

go.dev

Concurrency

드디어 고 언어의 꽃인 동시성 프로그래밍 챕터에 왔다. 지나오면서 고 언어에서 oop 를 하기위해 제공되는 여러 키워드 들을 살펴봤고 문법 과 실행 방식에 대해 공부했지만 이번에는 그 결이 다른 동시성 프로그래밍 이다.

바로 읽어보자.

Share by communicating

고 에서는 공유된 자원이 채널 을 통해 전달되는 기존 동시성 프로그래밍 의 방법과 궤를 달리한다고 한다. 

오직 하나의 고루틴 에서만 주어진 시간 동안 값에 접근 할수있다. 이에 따른 데이터 레이스는 설계에 따라 발생할수 없다. 

왜 ? 데이터 레이스란 하나의 값에 동시다발적으로 두개 혹은 그이상의 동시쓰레드 가 접근하려고 할때 발생 되는데 현재 문서에서 말하는 오직 하나의 고루틴에서만 접근하기 때문에 설계적으로 발생할수 없다고 한다.

고 에서 는 변수 주변에 뮤텍스 락을 걸어 수행할수 있다 다만 채널 을 사용해 접근한다면 ? 보다 명확하고 올바르게 프로그램을 더 쉽게 작성할수 있다.

Goroutines

기존 용어의 다양한 전달에 따라 고루틴으로 부르고 있다.  고루틴은 단순한 모델을 가지고 잇는데. 동일한 주소 공간을 가지고 함수 와 고루틴 이 동시실행되는 것을 의미한다.

고루틴 은 가벼우며, 스택 에 할당되는 값보다 조금 더 비용이 소모된다. 스택 은 메모리 할당 의 비용이 저렴하며, 많은 할당 즉 데이터가 요구 되어진다면, 힙메모리를 이용하기도 한다.

응 ? 문장이 매우 난해하다. 기본적으로 이 베이스가 있어야 한다. 스택 > 힙 메모리 공간 보다 빠르다. 그러나 힙 처럼 다량 을 한번에 할당하기에 제한이 있다. 그렇기에 고루틴은 경량 쓰레드 라는 별명이 있다 왜 ? 스택에 할당되기에 가벼워야하며 빠르기때문에
(스택 은 Cpu 단일 메모리 로 접근이 가능하기에 빠르다 주로 함수호출, 함수 반환값 주소, 임시변수 등이 할당됨)

글을 마저 읽다보면 The effect is similar to the Unix shell's & notation for running a command in the background
유닉스 쉘의 & 백그라운드 실행과 유사한 기능을 한다고 한다. 

예시를 보자.

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  // Note the parentheses - must call the function.
}

우선 저 함수 선언 과 호출 부분 부터 눈에 가장 확실히 들어오는데 go 키워드를 앞에붙여주며 함수를 선언과 동시에 실행한다.

이건 Go 루틴의 일반적인 예시가 아니라고 한다 왜 ? 완료 신호를 보내줄수 없기 떄문에 이 고루틴 함수가 언제 끝나는지 모른다. 

이에 따라 채널을 활용한 방법에 대해 이야기 하는데 예제 몇개를 더 작성해보면서 연습해보자.

func main() {
	practice.F("Direct")

	go practice.F("GoRoutine")

	go func(msg string) {
		for i := 0; i < 3; i++ {
			fmt.Println(msg, ":", i)
		}
	}("anonymous")

	time.Sleep(time.Second)
	fmt.Println("Done")
}

Direct : 0
Direct : 1
Direct : 2
anonymous : 0
anonymous : 1
anonymous : 2
GoRoutine : 0
GoRoutine : 1
GoRoutine : 2
Done

이런 결과 값이 나온다. 그러나 여러번 실행하면 멀티쓰레드 답게 순서 보장없이 마구잡이로 나온다.

단 중간에 타임 슬립 이 있다 이게 왜 필요할까 ?

메인 펑션 또한 고루틴 함수이다. 이에 메인 펑션은 종료되면 ? 모든함수가 종료된다. 즉 저 고루틴 이 실행되는것을 기다려주지 않는다.

만약 저 중간 함수가 없다면 ? 메인 고루틴은 코드를 쭉읽어가면서 함수를 바로 종료한다.

그래서 이 위의 설명중 하나 완료신호 를 주는 방법 중 하나로 채널에 대해 언급하는 이유이다.

(이렇게 말고 sync.WaitGroup 을 이용해 저렇게 슬립을 안하고 이용하는 방법도 있다)

Channels

전에 make 함수를 이용해서 만들수 있는 타입 중에 채널이 언급된 적이있다. 즉 채널을 생성하기 위해서는 make 함수를 이용해 만들고 이에 대한 반환값은 ? 값벨류 이지만 값복사 가 아닌 레퍼런스 복사가 발생한다.

또한 채널의 버퍼사이즈를 설정해줄수 있다. 마치 슬라이스 사이즈를 설정하는것과 유사하다.

ci := make(chan int)            // unbuffered channel of integers
cj := make(chan int, 0)         // unbuffered channel of integers
cs := make(chan *os.File, 100)  // buffered channel of pointers to Files
func main() {
	c := make(chan int)
	go func() {
		fmt.Println("Go routine is Running")
		for i := 0; i < 10; i++ {
			c <- i * i
		}
	}()
	for i := 0; i < 10; i++ {
		fmt.Println("Value is : ", <-c)
	}
	fmt.Println("Function is done")
}

 

채널 에 데이터 를 집어넣고 그 갯수 만큼 뺴는 로직이다. 재미있는 부분이 있는데 <-c 채널에서 이렇게 데이터를 뽑는 부분에서 넣어주는 데이터 와 끝나는 데이터 가 일치하지 않으면 ? 채널을 받는 로직에서는 무한히 기다리면서 메인 고루틴 과의 데드락 을 야기한다.

언퍼버 채널 이라면 ? 송신자는 수신자가 데이터를 수신할떄 까지 기다리고, 수신자는 송신자가 데이터를 전달해줄때 까지 기다린다.

버퍼 채널 이라면 ? 송신자는 버퍼의 사이즈가 가득차기 전까지 계속 데이터를 보낸다. 만약 가득찼다면 수신자 중가 데이터를 원복할떄 까지 기다린이후 다시 재 진행 된다. 아래 예를 보자.

func main() {
	// Buffered channel with buffer size 2
	bufferedChannel := make(chan int, 5)
	go func() {
		for i := 1; i <= 5; i++ {
			bufferedChannel <- i
			fmt.Println("Sent value on buffered channel:", i)
		}
		close(bufferedChannel)
	}()
	for value := range bufferedChannel {
		fmt.Println("Received value from buffered channel:", value)
	}
	fmt.Println("Buffered channel is done.")

	// Unbuffered channel
	unbufferedChannel := make(chan int)
	go func() {
		for i := 1; i <= 5; i++ {
			unbufferedChannel <- i
			fmt.Println("Sent value on unbuffered channel:", i)
		}
		close(unbufferedChannel)
	}()
	for value := range unbufferedChannel {
		fmt.Println("Received value from unbuffered channel:", value)
	}
	fmt.Println("Unbuffered channel is done.")
}

Sent value on buffered channel: 1
Sent value on buffered channel: 2
Sent value on buffered channel: 3
Sent value on buffered channel: 4
Sent value on buffered channel: 5
Received value from buffered channel: 1
Received value from buffered channel: 2
Received value from buffered channel: 3
Received value from buffered channel: 4
Received value from buffered channel: 5
Buffered channel is done.
Sent value on unbuffered channel: 1
Received value from unbuffered channel: 1
Received value from unbuffered channel: 2
Sent value on unbuffered channel: 2
Sent value on unbuffered channel: 3
Received value from unbuffered channel: 3
Received value from unbuffered channel: 4
Sent value on unbuffered channel: 4
Sent value on unbuffered channel: 5
Received value from unbuffered channel: 5
Unbuffered channel is done.

 

 

보는 바와 같이 버퍼 와 언버퍼 간의 차이가 존재한다. 따라서 이런 두종류 의 버퍼에 대해서 선택할떄 여러가지 경우의수를 고려해야할것으로 보인다. 만약 리시버가 느리다면 ? 버퍼 채널이라면 ? 샌더는 자주 락에 걸려 성능저하 가 발생하고 메모리 의 사용 제한이 생긴다면 ? 버퍼 채널인 경우 적절한 사이즈 를 찾는데 있어 고심을 해야한다.

Channels of channels

이러한 채널 또한 타입으로 인정받고 넘길수 있다.

이전 예제로 서버에서 리퀘스트 를 유니크하게 유지하는 방법에 대한 예제를 들었는데

거기서 Request 의 타입에 대한 정의를 내리진 않았다.

type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}
func sum(a []int) (s int) {
    for _, v := range a {
        s += v
    }
    return
}

request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)

클라이언트 는 위와 같은 함수를 제공하고 채널로 데이터를 보내주면 ?

func handle(queue chan *Request) {
    for req := range queue {
        req.resultChan <- req.f(req.args)
    }
}

서버에서는 단순 리퀘스트 안 채널로 받은 함수의 결과값을 받아 넣어주면 된다.

Parallelization

채널 사용의 병렬화 이다. 어떻게 ? 멀티 코어에 각 계산의 조각 을 주고 실행하는것이다. 바로 가보자.

아이템 의 백터를 하는데 있어 비용이 많이들고, 각 값에 대해 모든 아이템은 독립적으로 수행된다고 해보자.

type Vector []float64

// Apply the operation to v[i], v[i+1] ... up to v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1    // signal that this piece is done
}

그래서 우리는 총 실행되는 숫자의 갯수만 신경쓰면된다.

const numCPU = 4 // number of CPU cores

func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // Buffering optional but sensible.
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    // Drain the channel.
    for i := 0; i < numCPU; i++ {
        <-c    // wait for one task to complete
    }
    // All done.
}

여기서 강조하는 문구로는 동시성 과 병렬성을 혼동하지말라고 한다.

고는 동시성 언어이지, 병렬성을 위한 언어가 아니다. 그럼에도 불구하고 종종 구조 적 인 방법으로 병렬성 문제를 쉽게 해결할수 있지만. 고는 동시성 언어 이다. 모든 병렬적 문제를 해결하기 위해 알맞지 않을수도 있다. 라고 한다.

* 아침을 한다고 가정

동시성 => 계란을굽는다(), 식빵을 굽느다(),커피머신을 돌린다()

병렬성 =>  팬(계란을 굽는다(), 베이컨을 굽는다())

로 분리할수 있다.

A leaky buffer

줄줄 새는 버퍼 라는 즉 관리되지 않는 버퍼 라는 타이틀이다.

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
    for {
        var b *Buffer
        // Grab a buffer if available; allocate if not.
        select {
        case b = <-freeList:
            // Got one; nothing more to do.
        default:
            // None free, so allocate a new one.
            b = new(Buffer)
        }
        load(b)              // Read next message from the net.
        serverChan <- b      // Send to server.
    }
}

고루틴 클라이언트 가 데이터 를 소스로 부터 반복적으로 받는다고 가정해보자 . 버퍼 하나를 잡고 리스트 로 부터 데이터를 잡아 넣어주고 준비가 되었다면 ? 서버채널 로 이 버퍼를 날려준다.

func server() {
    for {
        b := <-serverChan    // Wait for work.
        process(b)
        // Reuse buffer if there's room.
        select {
        case freeList <- b:
            // Buffer on free list; nothing more to do.
        default:
            // Free list full, just carry on.
        }
    }
}

서버 는 서버 채널로 부터 데이터를 가져와서 특정 로직을 거치고 그 채널에 메모리가 남아있다면 다시 리스트로 보내는 로직이다.

만약 freeList 가 가득차있다면 ? 현재 가져온 버퍼는 가비지 컬렉터의 수집대상이 된다. 

이 패턴은 오브젝트를 다시 사용하는 단순한 패턴이다. 메모리 할당의 오버헤드 를 줄이기 위해 사용되는 방법중 하나라고 한다.

 

'Go > Go Basic' 카테고리의 다른 글

Ulitmate-Go-01 (string,메모리 패딩)  (3) 2023.08.19
Go Interface, embedded  (0) 2023.02.28
Effective Go 03  (0) 2023.02.10
Effective Go 02  (0) 2023.02.09
Effective Go 01  (1) 2023.02.08

출처 : https://go.dev/doc/effective_go#append

 

Effective Go - The Go Programming Language

Effective Go Introduction Go is a new language. Although it borrows ideas from existing languages, it has unusual properties that make effective Go programs different in character from programs written in its relatives. A straightforward translation of a C

go.dev

Methods

 

Pointers vs. Values

Go 언어를 공부하면서 제일 고민이 많이 되었던 부분이다. 어떤 예제는 메서드로 어떤 예제는 그냥 함수로 설정해 리턴한다.

두 가지 방법을 모두 확인해 보자.

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []byte {
    // Body exactly the same as the Append function defined above.
}

func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // Body as above, without the return.
    *p = slice
}

둘 다 동일한 로직이다. 다만 표현의 차이인데 이에 대해 규칙을 아래와 같이 설명하고 있다.

1. 두  번째 함수는 포인터 그리고 값 모두에서 작동되지만, 값 메서드는 오직 값 타입에서만 함수를 호출할 수 있다 는 차이점이다. ChatGpt 가 준 예시를 보자.

type MyInt int

func (m MyInt) add(x int) MyInt {
    return m + MyInt(x)
}

func (m *MyInt) addPointer(x int) MyInt {
    *m += MyInt(x)
    return *m
}

a := MyInt(5)
b := &a

// Value method can be invoked on a value
fmt.Println(a.add(10)) // 15

// Value method can also be invoked on a pointer
fmt.Println(b.add(10)) // 15

// Pointer method can only be invoked on a pointer
fmt.Println(b.addPointer(10)) // 25

// Pointer method cannot be invoked on a value
// This will result in a compile error: "cannot call pointer method on MyInt value"
// fmt.Println(a.addPointer(10))

여기서 주목할 점은 a , b의 타입이다 a는 값타입 이고, b 는 포인터 타입이다. b 는 값,포인트 메서드 각각을 호출 가능하지만 

a 는 포인터 메서드 호출 시에 에러가 발생한다.

이러한 규칙에 따라 포인터메서드는 호출 값을 수정할 수 있다는 점 과 값메서드는 호출 시 새로운 값을 받는 차이점 이 존재한다.

하나 고에서는 편리한 예외를 적용해 주는데 주소를 지정할 수 있는 변수라면 포인터 타입의 메서드 또한 호출할 수 있다는 점이다.

즉 컴파일러가 이를 도와준다고 한다.

그러면 다시 생각해 보면 위에 예는 틀린 예제이다. chatgpt 한테 다시 물어봤다.

더보기

Okay thanks i see However in view of case on Effective go documentation they said "When the value is addressable, the language takes care of the common case of invoking a pointer method on a value by inserting the address operator automatically. In our example, the variable b is addressable, so we can call its Write method with just b.Write." if this is right your example is wrong, you would be able to call a.addPointer(10) as well.

Would you give me some more details?

Answer is :

Yes, I apologize for that mistake. You're correct, in Go, when the value is addressable (i.e., it has an address), you can call a pointer method on that value and Go will automatically insert the address operator.

In the example from the Effective Go documentation, b is a pointer to an addressable value, so you can call its Write method as b.Write. When you call a pointer method on a value, Go will automatically insert the address operator and convert the value to a pointer.

So, in our example, you could also call a.addPointer(10), and Go would automatically convert the value a to a pointer and call the addPointer method on that pointer. Here's an updated example to show this:

goCopy code
a := MyInt(5) b := &a // Value method can be invoked on a value fmt.Println(a.add(10)) // 15 // Value method can also be invoked on a pointer fmt.Println(b.add(10)) // 15 // Pointer method can be invoked on a pointer fmt.Println(b.addPointer(10)) // 25 // Pointer method can also be invoked on a value fmt.Println(a.addPointer(10)) // 30

In this example, you can see that both b.addPointer(10) and a.addPointer(10) produce the same result.

 

잘못된 예시에 대해 사과를 해주는 게 정말 친절하다 ㅋㅋ
다만 나의 궁금증 은 해결되지 않았다. 어떤 상황에 대해 어떤 메서드를 써야 할지 에 대한 특별한 룰이나 나만의 기준을 정하고 싶어 추가적으로 좀 더 자료를 찾아보았다.

 

대체적으로 값은 변경을 원한다면 포인터 리시버를 그게 아니라면 벨류 리시버 를 사용한다고 한다. 추가적으로 포인터 리시버에 대한 고려사항으로 리시버 자체 의 크기가 너무 크다면? 이건 고려 의 여지없이 포인터 리시버를 사용해야 메모리 이슈가 없을 것이다. 
팀장님 께 여쭤보니 글로벌한 함수에 대해서는 벨류를 보편적으로 사용하는 편이며 일관적으로 사용하는 것을 주로 한다고 하신다.

Interfaces and other types

Interfaces

인터페이스이다 자바에서 도 많이 보던 친숙한 키워드인데 고에서 는 어떻게 정의하는지 확인해 보자. 
객체의 특정 행동 규약을 정의할 때 제공한다고 한다. 

주로 1~2 개의 메서드를 정의하고, io.Writer의 Write를 예시로 들 수 있다.

(포스팅 은 안 했지만 주로 인터페이스 이름 규약으로는 뒤에 er을 붙인다.)

 

자바와 똑같이 하나의 타입은 다양한 인터페이스를 구현할 수 있다. 컬렉션 은 정렬될 수 있는데 sort의 구현체를 구현해야 한다.

Len() Less(i, j int) bool Swap(i, j int) 바로 예시를 들어 정렬을 해보자.

type twitter struct {
	like  int
	title string
}

type twit []twitter

func (t twit) Len() int {
	return len(t)
}
func (t twit) Less(i, j int) bool {
	return t[i].like < t[j].like
}
func (t twit) Swap(i, j int) {
	t[i], t[j] = t[j], t[i]
}

func main() {
	a := twit{}
	a = append(a,
		twitter{249249, "Hi"},
		twitter{123123, "Elon"},
		twitter{1, "Musk"},
		twitter{-1, "Paid for twit"},
	)

	fmt.Println(a)
	sort.Sort(a)
	fmt.Println(a)
}

[{249249 Hi} {123123 Elon} {1 Musk} {-1 Paid for twit}]
[{-1 Paid for twit} {1 Musk} {123123 Elon} {249249 Hi}]

프린트된 값을 보면 정렬된 것이 보이는가? 저기서 레스 함수를 반대로 꺾어준다면 역으로 정렬된다. 

나름 직관적이라고 생각한다. 자바의 Comparable or Compartor 보다 마음에 든다.

Conversions

type Sequence [] int와 변환에 대해 설명하고 있다.

이 부분에서는 자바 인터페이스 Map a = HashMap <> 이런 식의 연관관계 가 생각난다. Map을 구현하고 있는 hashmap에 대해 타입으로 받을 수 있다?

Go에서는 위와 유사하게 형변환이 가능하다.  그래서 예제에서 들어준 sort에 대해 sequence의 len less 기타 등등 구현 없이 바로 가는 거를 보여준다.

type Sequence []int

// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
    s = s.Copy()
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}

 

ㅋㅋㅋㅋ 별다른 구현 없이 이렇게 형변환 해서 들고 갈 수 있다.

저 함수 안으로 들어가면 이미 빌트인으로 다구현되어 있다 , 인트에 대해서

기존 함수 예제에서 String으로 리턴하기 위해 for를 한 번 더 돌리면서 sprint를 해 O(n) 2의 시간복잡도를 가지며 
string을 만들어 갔지만 위와 같이 작성한다면?

o(nlogn)까지 줄어들 수 있다. s의 크기만큼 함수를 돌면서 정렬(고에서는 퀵정렬을 사용) 하기 때문에 o(nlogn) 이 된다.

Interface conversions and type assertions

타입의 변환에 대한 섹션이다. 문서에서 Printf에서 % v를 받을 때 어떻게 핸들링하는지에 대해 설명하고 있다. 

type Stringer interface {
    String() string
}

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

value.(type)을 통해 가능한 타입을 케이스 별로 분기를 나눈다. 이는 다시 말해 혼합된 타입을 사용할 수 있는 걸 의미한다.

이렇게 해서 원하는 타입으로 뽑아낼 수 있다. 예를 들어

str := value.(string)

이렇게 스트링 타입으로 변환 후 변수 설정이 가능하다 이렇게 되면 str의 타입은 string 이 된다.

만약 이런 벨류에서 뽑아낼 타입이 없다면? 컴파일 에러가 아닌 런타임 에러가 발생하게 된다. 이에 따라 go에서는 아래와 같은 방법 이 관용적으로 사용된다고 한다.

func main(){
    if str, ok := value.(string); ok {
        return str
    } else if str, ok := value.(Stringer); ok {
        return str.String()
    }
}

이 섹션에 대해서 잘 이해가 가질 않아서 검색을 해보다 보니 assertion과 conversion에 대한 개념을 먼저 잡아야 했다.

assertion => 벨류 의 인터페이스 안에서 하나의 타입을 끄집어내는 것이 assertion
conversion => 하나의 타입을 다른 타입으로 변환하는 것을 conversion이라고 한다.
아래 예시를 보자.

type MyType interface {
	What() string
}

func PrintHolyMoly(t MyType) {
	fmt.Println("HOlYMOLY", t.What())
}

type A struct {
	a int
}

func (a A) What() string {
	return fmt.Sprintf("This is A %v", a.a)
}

type B struct {
	a int
}

func (b B) What() string {
	return fmt.Sprintf("This is B %v", b.a)
}

func main() {
	var a MyType
	a = A{1} // Type asseriton worked so a is MyType right now

	if b, ok := a.(B); ok { // Type assertion do one more for type conversion to a => b
		a = b
	} else {
		fmt.Printf("Conversion type failed from MyType(A) to B))\n")
	}

	PrintHolyMoly(a)
}

interface MyType을 구현하는 A, B 가 있다. A를 B 타입으로 바꾸는 과정을 작성한 코드이다.

이렇게 타입을 바꾸기 위해서는 우선 타입추론을 통해 인터페이스 타입으로 뽑아 온다음 인터페이스 타입에서 다시 뽑아서 B로 바꾸는 것이 가능한지 확인 후 타입을 변환하는 과정이다.

 

다형성 때문에 내가 많이 헷갈린 거 같은데 다형성은 저렇게 interface 타입으로 생성한 함수(PrintHolyMoly(a))에서

저렇게 사용이 가능한 거지 

A, B 가 동일한 인터페이스를 구현한다고 하더라도 A,B 간의 타입전환은 가능하나 중간 점검이 필요하다.

Interfaces and methods

인터페이스 매머드의 예로 핸들러 인터페이스를 든다 핸들러 인터페이스를 구현하고 있다면 http 요청을 처리할 수 있는 함수가 된다.

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

ResponseWriter는 Wirte 메서드가 있는데 이 메서드 덕분에 io.Wirter 가 들어가는 모든 값에 responseWriter 벨류를 집어넣을 수 있다 왜? 인터페이스 구현을 하고 있으니깐

serveHttp에 관련해 HandlerFunc을 조금 길게 설명하고 있다 그중 아래와 같이 타입을 시그니쳐 함수로 받을 수 있다. 예를 보자.

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers.  If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}

The blank identifier

go에서는 선언한 값에 대해서는 무조건 적인 사용이 필수적이다 그렇지 않다면 컴파일 에러가 발생하는데 이를 해결하기 위해 _선언을 허용한다.

1. 사용되지 않는 임포트 => 이 임포트를 함으로써 init 함수가 실행되어야 프로그램 이 실행될 때

2. for, map을 loop 돌때 => 인덱스 값에 대해 이용하지 않을 때

1번 케이스를 임포트 사이드 이펙트라고 하는데 고의 기본 정책이니 다른 임포트 할 때 도 주의할 필요가 있다.

Interface checks

자바 같은 언어와 달리 고에서는 인터페이스의 함수만 구현하면 그 자체로 인터페이스 타입을 받을 수 있게 된다. 이에 따라 런타임 시점에 인터페이스에 서 타입을 끄집어내는 type assertion 이 가능하다. 예시로 제시하는 marshaler에 대해 알아보자.

우선 Go에서 엔코딩/디코딩, 마샬러 모두 비슷한 역할을 하는데 이에 대해 명확히 인지하고 사용해야 한다.

Encoding/Decoding 은 json의 문자를 스트링 또는 바이너리 데이터로 읽고 쓰기 하는 것을 의미한다. 

Marshaling/UnMarshaling 은 JsonType으로부터 고  의 원시타입으로 맵핑하는 것 을 의미한다.

func main() {
	var data Singer
	var data2 Singer

	jsonData := []byte(`
	{
		"Name" : "NewJeans",
		"Title" : "Attention",
		"DebutDate" : 2022
	}
	`)

	verify := json.Valid(jsonData)
	if verify {
		json.Unmarshal(jsonData, &data)
		fmt.Println(data)
	}

	json.NewDecoder(strings.NewReader(string(jsonData))).Decode(&data2)
	fmt.Println(data2)
}

이에 동일한 결괏값을 받을 수 있다. 내부적으로 까보면 Decode 내에서 Unmarshal을 호출하고 있는 모습을 볼 수 있다. 즉 다시 말해 

디코딩 은 버퍼에 들어있는 값을 들고 와 얼마샬 을 한다고 이해하면 될 거 같다.

349 업 버튼을 받은 답변 중에(https://stackoverflow.com/questions/21197239/decoding-json-using-json-unmarshal-vs-json-newdecoder-decode)

json.Decoder 는 나의 데이터가 io.Reader 에서 부터 온다면 혹은 스트림 데이터로부터 다양한 값을 디코딩해야 한다면 사용하고,

json.Unmarshal 은 이미 json 데이터가 메모리에 존재한다면 사용하라고 한다. 

var _ json.Marshaler = (*RawMessage)(nil)

예시에서 이렇게 빈칸지시자를 활용해서 오로지 타입 체크만 할 때도 사용된다고 한다. 이는 type conversion 이기 때문에 컴파일 시점에 잡을 수 있다.

Embedding

고에서는 서브클래싱 의 타입 핸들? 드리븐? 개념을 제공하지 않는다? 무슨 의미인지 모르겠으나 고에서는 클래스 개념과 상속의 개념이 없어서 위와 같은 말을 한다고 생각된다. 이 문장 이후 제시된 대안으로

구조체 혹은 인터페이스의 임베딩 타입에 의해 구현된 일부분을 가져올 수 있다라고 강조한다. brrow라는 표현을 강조하는데 예시를 확인해 보자.

글을 읽어보면 자바의 class extend 가 생각난다.

subclass에서 superclass를 접근하는 개념과 유사하게 생각하면 될 거 같다 단지 구현의 차이만 있지 의미하는 바가 매우 유사하다.

글에서 제시된 io reader, writer 그리고 이걸 랩핑 하는 readwriter  가 있다면 reader와 writer에 readwriter 인스턴스가 접근할 수 있다.

예시를 보면 보다 이해가 확고해진다.

func main() {
	type animal struct {
		name string
		age  int
	}

	type cat struct {
		animal animal
		breed  string
	}

	c := &cat{
		animal{"somi", 12},
		"Persian",
	}

	fmt.Printf("Breed is %v, name is %v , age is %v", c.breed, c.animal.name, c.animal.age)
    // Breed is Persian, name is somi , age is 12
}

연속된. 을 이용해 랩핑 된 값들을 꺼내서 사용할 수 있다. 자바와 매우 유사한 부분인 것 같다.

이런 임베딩 타입의 문제점으로 제시되는 부분이 깊이에 따른 동일한 이름의 필드가 존재한다면? 

이에 대한 문제는 매우 심플하게 해결된다. 각 깊이별로 이름이 따로 존재하기 때문에 깊이에 맞는. 을 이용해 체이닝 하면 된다.

 

두 번째로  동일한 깊이에 동일한 이름이 존재한다면? 이건 그냥 오류다 애초에 컴파일조차 허용 되지 않는다. 같은 구조체 안에 같은 이름 이라니 끔찍하다.

 

'Go > Go Basic' 카테고리의 다른 글

Ulitmate-Go-01 (string,메모리 패딩)  (3) 2023.08.19
Go Interface, embedded  (0) 2023.02.28
Effective Go 04  (0) 2023.02.12
Effective Go 02  (0) 2023.02.09
Effective Go 01  (1) 2023.02.08

출처 : https://go.dev/doc/effective_go#arrays

Arrays

C와 Go Array의 주요 차이점

1. Array 는 값이다. 하나의 배열을 다른 배열에 할당하면? 전체복사 가 발생된다.

2. 만약 함수변수로 사용한다면 이건 포인터 타입이 아닌, 카피 값이 넘어간다.

3. 배열의 크기는 하나의 타입이다. 

위 3가지를 직접 증명해 보자.

- Array는 값이다. 하나의 배열을 다른 배열에 할당하면? 전체복사 가 발생된다. 

func main() {
	arr := [3]int{1, 2, 3}
    arr2 := [3]int{}
	arr2 = arr
	fmt.Printf("Type is %T, Point is %p, Values %v\n", arr, &arr, arr)
	fmt.Printf("Type is %T, Point is %p, Values %v\n", arr2, &arr2, arr2)
}

결괏값 

Type is [3] int, Point is 0x1400012e018, Values [1 2 3]
Type is [3]int, Point is 0x1400012 e030, Values [1 2 3]

 

다른 배열에 할당하면 이와 같이 전체복사가 발생된다. 메모리 주소의 생성이 보는 바와 같이 4바이트 int 값 3개 총 12바이트 늘어난 e030부터 시작되는 것도 재밌는 포인트인 것 같다.

- 만약 함수변수로 사용한다면 이건 포인터 타입이 아닌, 카피 값이 넘어간다.

 

func main() {
	arr := [3]int{1, 2, 3}
	func(arr [3]int) {
		fmt.Println("In Function")
		fmt.Printf("Type is %T, Point is %p, Values %v\n", arr, &arr, arr)
	}(arr)
	fmt.Printf("Type is %T, Point is %p, Values %v\n", arr, &arr, arr)
}

결괏값

In Function
Type is [3] int, Point is 0x140000ac030, Values [1 2 3]
Type is [3]int, Point is 0x140000 ac018, Values [1 2 3]

 

보는 바와 같이 익명함수는 arr [3] int 를인자로 받는 함수이다. 거기에 기존에 선언한 arr를 넘겨주었지만 보는 바와 같이 다른 주소값을 반환하게 된다. 다시 말해 arr는  포인터 값이 아니기 때문에 레퍼런스 복사가 아닌 값복사가 발생되어 새로운 값을 할당하는 것이다.

- 배열의 크기는 하나의 타입이다. 

배열의 크기 자체가 타입이라는 말은 단순하게 그냥 위에서 선언한 함수에서 인자로 [5] int를 받는다고 하면 바로 컴파일 에러가 발생한다. 

위에서 말한 이유를 생각한다면 저렇게 받아야만 한다 왜? 값 복사가 일어날 때 메모리 낭비, 데이터 유실 이 되지 않으려면 정확하게 계산된 메모리 값 주소를 할당해주어야 하기 때문이다. 

 

저 문서에서 값을 가지는 속성 또한 매우 유용할 수 있으나 비싸다고 한다. 그래서 만약 c와 같은 방식으로 구현하고 싶다면 go에서도 가능하다

func main() {
	arr := [3]int{1, 2, 3}
	func(arr *[3]int) {
		fmt.Println("In Function")
		fmt.Printf("Type is %T, Point is %p, Values %v\n", arr, &arr, arr)
	}(&arr)
	fmt.Printf("Type is %T, Point is %p, Values %v\n", arr, &arr, arr)
}

결괏값

Type is *[3] int, Point is 0x14000120018, Values &[1 2 3]
Type is [3]int, Point is 0x1400012 e018, Values [1 2 3]

주소값을 넘기고 포인터 타입으로 받으면 손쉽게 해결 가능하다. 그러나 이러한 구현 방식은 고 에 어울리지 않는다고 한다. 이에 대한 해결책으로 슬라이스를 제시하는데 바로 가보자.

Slices

슬라이스는 가장 보편적이고 강력하며 편리한 데이터의 연속적인 인터페이스라고 설명한다. 통상 인터페이스{} 이렇게 하면 모든 타입을 소화할 수 있는 마법의 키워드이다. 문서에서도 대부분의 내장라이브러리 의 배열 관련 된 부분은 모두 슬라이스로 처리한다고 한다.

읽다 보면 엄청 강조하는 부분 중 하나가 바로 슬라이스는 레퍼런스를 홀드 한다고 한다. 즉 배열과 달리 레퍼런스를 홀드 하게 되면 함수 인자 혹은 선언 시에 값 복사가 아닌 레퍼런스 참조를 하게 된다는 의미이다. 

그래서 위에 고 에 구현 방식에 어울리지 않는다고 하는데 한번 확인해 보자.

func main() {
	fmt.Printf("Type is %T, Point is %p and arr[0] Point is %p  value are %v \n", arr, &arr, &arr[0], arr)
	fmt.Printf("Type is %T, Point is value for %p and arr[0] Point is %p, value are %v \n", arr2, &arr2, &arr2[0], arr2)
}

Type is []int, Point is 0x1400000c030 and arr[0] Point is 0x1400001c090  value are [1 2 3] 
Type is []int, Point is value for 0x1400000c048 and arr[0] Point is 0x1400001c090, value are [1 2 3]

주의해서 볼 점은 여기서 arr2의 주소값은 물론 arr와 다르다 다만 arr2 [0]의 주소값을  보면 바로 arr [0]의 주소값을 가리킨다 다시 말해 아래 등호가 성립한다.

주소 arr2!= 주소 arr  / 주소 arr [0] == arr2 [0]

이렇게 되면 당연히 복사가 일어나지 않기 때문에 위 배열과 같은 포인터 타입과 주소의 작업을 하지 않아도 된다.

위의 특징 외에도 슬라이스 하면 길이 없는 배열을 가장 먼저 떠올리게 된다. 이걸 가능하게 해주는 append 함수에 대해서  문서에서 준 예시를 보자.

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

기본적인 슬라이스를 선언해서 기존 사이즈 + 현재 들어오는 데이터 *2 만큼의 사이즈를 늘려주는데 자바의 어레이 리스트 내부 로직 중 grow()와 매우 유사하다. copy(목적지, 소스)인 형태이다 카피 구현 설명에 가보면 소스를 목적지로 오버래핑 한다고 되어있다.

추가적으로 인트 리턴 값이 있으나 이는 변경된 요소의 개수이다. 

다시 말해 사이즈를 늘리는 하나의 새로운 슬라이스를 만들고 그에 현재 요소들을 복사해 넣어주고 리턴해주는 방식이다. 그래서 보통 append 빌트인 함수 사용여부를 볼 때 리턴 받은 슬라이스를 대입하곤 한다.

이렇게 되면 당연히 기존 slice의 주소가 바뀌게 된다. 

다시 본다면 기존 포인팅 하던 주소가 바뀌는 걸 의미한다.

func main(){
	arr := make([]int, 3)
	arr[0] = 1
	fmt.Printf("%p %p\n", arr, &arr[0])
	arr = append(arr, 1, 2, 3, 4, 5, 6, 7, 8)
	fmt.Printf("%p %p\n", arr, &arr[0])
}

0x1400012e018 0x1400012e018
0x1400010e060 0x1400010e060

 

결과 값이 보는 바와 같이 주소가 다르다. 

추가적으로 슬라이스는 레퍼런스를 참조한다. 다시 말해 여러 다른 슬라이스에서 접근가능하다는 소리이다. 이는 다시말해 스레드 세이프 하지 않은 문제를 야기한다. 

이 왕같이 포인팅 되게 되면? 모든 곳에서 접근하여 슬라이스 요소를 수정할 수 있다 이는 다시 말해 스레드 세이프 하지 않다는 의미이다. 

아직 문서에서 언급은 없으나 뒷장에서 이에 대해 언급하면 그때 이문제 의 해결법과 트레이드오프에 대해 적어볼까 한다.

자바에서는 이문제를 해결하기 위해 synchronize 키워드와 클래스 하나를 제공해 주는 것으로 기억하고 있는데 어떻게 해결할지 궁금하다.

 

Maps

자바에서 가장 좋아하는 자료구조 중에 하나였다. 고에서도 똑같이 제공해 주며 해쉬맵 형태의 맵을 제공한다.

당연히 키 벨류에는 어느 타입이나 들어갈 수 있으나, 슬라이스는 맵의 키로 지정할 수 없다. 저 문서에는 어렵게 작성해 놨는데 슬라이스는 동등성을 제공하지 않는다.

func main(){
	arr := []int{1,2,3}
    arr2 := []int{1,2,3}
    fmt.Println(arr == arr2) // false
}

벨류를 가져오는 방법으로는 m [KEY]를 하면 VALUE를 리턴하게 된다.

또한 고 의 맵에서 특이한 점으로는 없는 벨류값에 대해 0을 리턴한다. 다시 말해 자바의 contains 같은 함수를 제공하지 않기 때문에 

```java```
class Main(){
	public static void main(){
    	Map<Integer,Integer> map = new HashMap();
        //code .. //
        if(map.conatians("찾는값"){
        	// blarblar
        }
    }
}

```go```

func main(){
	m := make(map[int]int)
    //code .. //
    if v,ok := m["찾는값"]; ok {
    	//blarblar
    }
}

이와 같은 방식으로  자바와 유사하게 로직을 규현 할 수 있다.

추가적으로 delete(map instance, key)를 작성하면 map 안에 있는 요소를 지울 수 있다. 맵 안의 그 벨류가 없더라도 특별한 에러 없이 진행되는데 그냥 사용한다면 버그를 유발할 수 있을 것 같다. 

delete 사용 전에 항상 유무를 체크하고 넘기는 것에 대해 고민해봐야 할 것 같다. 

Append

뭔가 이 문서의 순서가 중구난방인거 같다. 갑자기 print 쪽에 대해 설명하다가 갑자기 append 로 넘어온다.

func append(slice []T, elements ...T) []T

append() 빌트 인 함수 정의 이다. T 는 제네릭 으로 어느 타입이나 받을수 있는것을 의미한다.

실제 함수에서는 T 를 사용할수 없다 이는 호출자에 의해 결정되는 사항이다.

그래서 T 를 컴파일러의 도움을 받아 정의하기에 빌트인 함수인것이다.

 

...T 를 보면 2번쨰 파라미터 로 다량의 데이터를 넘기는것이 가능하다. 다시말해 슬라이스 를 통으로 넘겨도 된다는 의미이다.

타입이 맞으면 x =  append(x,y...) 이런식으로

'Go > Go Basic' 카테고리의 다른 글

Ulitmate-Go-01 (string,메모리 패딩)  (3) 2023.08.19
Go Interface, embedded  (0) 2023.02.28
Effective Go 04  (0) 2023.02.12
Effective Go 03  (0) 2023.02.10
Effective Go 01  (1) 2023.02.08

+ Recent posts