소프트웨어 디자인 패턴에서 싱글턴 패턴
(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다.
주로 공통된 객체를 여러 개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다. - wikipedia

 

싱글턴 패턴의 등장 배경에는 아래와 같은 이유가 있다.

유일한 인스턴스 유지:
일부 시스템에서는 특정 클래스의 인스턴스가 하나만 존재해야 할 때가 있습니다. 예를 들어, 시스템의 설정 정보를 담당하는 클래스, 로그를 관리하는 클래스, DB 연결을 관리하는 클래스 등이 있습니다. 이런 경우, 싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 존재하도록 보장합니다.

전역 접근: 또한 싱글턴 패턴은 이 유일한 인스턴스에 대한 전역적인 접근 방법을 제공합니다. 즉, 어디서든 이 인스턴스를 참조할 수 있게 됩니다. 이는 애플리케이션 전반에서 공유해야 하는 데이터나 리소스를 관리하는 데 유용합니다.

제어된 리소스 공유: 한정된 리소스를 공유해야하는 경우에도 싱글턴 패턴이 유용합니다. 예를 들어, 데이터베이스 연결풀, 파일 시스템, 네트워크 소켓 등의 리소스는 한정되어 있기 때문에 효율적으로 관리하고 사용할 필요가 있습니다.

메모리 및 성능 최적화: 싱글턴 패턴을 사용하면 메모리 사용을 최적화하고 성능을 향상할 수 있습니다. 인스턴스를 한 번만 생성하므로 메모리 낭비를 줄이고, 계속해서 같은 인스턴스를 재사용함으로써 성능을 향상할 수 있습니다.

 

예전 Java 에서 Singleton 패턴에 관련해 정리된 내용이 있어 Go에서는 어떻게 활용하는가? 에 초점을 두어 작성하겠다.

(https://guiwoo.tistory.com/24)

 

전통적인 방식

type db struct {
	/**
	db 에 해당하는 값 이 저장
	*/
}

var obj *db

func GetInstance() *db {
	if obj == nil {
		obj = &db{}
	}
	return obj
}

 

위와 같이 구현을 한다. 다만 이렇게 구현 되었을때 문제점이 발생된다. 바로 멀티스레드 환경인데 

(생성 과정 의 단순함 과 Go Routine의 최적화에 따라 문제점 발생이 너무 희박해서 강제로 상황을 만들었다 "runtime.Gosched()를 활용")

func main() {
	wg := &sync.WaitGroup{}

	for i := 0; i < 30; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			singleton.GetInstance()
		}()
	}
	wg.Wait()
}

DB의 인스턴스를 여러 번 생성하는 것을 확인할 수 있다. 

 

이를 방지하기 위해 Go 에서 제공해 주는 특별한 기능들이 있는데

 

Sync.Once(https://pkg.go.dev/sync)

- 싱크 패키지 에서 제공하는 구조체 중에 하나로 Once는 한 번만 수행되도록 도와주는 구조체이다. 제공되는 메서드 중 하나로 Do 메서드가 있으며 Do에서 실행되는 콜백 function 은 오직 한 번의 실행만을 수행한다.

type db struct {
	/**
	db 에 해당하는 값 이 저장
	*/
}

var obj *db
var o sync.Once

func GetInstance() *db {
	o.Do(func() {
		runtime.Gosched()
		obj = &db{}
		fmt.Println("Crated DB Instance")
	})
	return obj
}

동일한 main func 을 수행한다면?

위와 같은 결과를 얻을 수 있다. 

 

init 함수 활용 하기

- go에서는 패키지 import 간 최초 실행 해주는 함수가 있다. 바로 init() 함수인데 제일 좋아하는 방법이다. 바로 활용해 보자.

type db struct {
	/**
	db 에 해당하는 값 이 저장
	*/
}

var obj *db

func init() {
	obj = &db{}
	fmt.Println("Db created")
}

func GetInstance() *db {
	if obj == nil {
		fmt.Println("Three is no db instance")
	}
	return obj
}

개인적으로는 init 함수를 선호한다. 

 

이러한 싱글턴 디자인 패턴 에는 테스트의 어려움 이 있다. 변수를 아무래도 전역적으로 관리하다 보니 목킹 이 생각보다 어렵다. 

이런 경우 인터페이스를 활용해서 추상화 작업을 통해 작성하는 것도 하나의 방법이 될 수 있다.

 

type MyDB interface {
	DoSomething() // DB 작업을 위한 메서드
}

type db struct {
	// db에 해당하는 값이 저장
}

var obj *db

func init() {
	obj = &db{}
	fmt.Println("DB created")
}

func (d *db) DoSomething() {
	// DB 작업을 수행하는 메서드
}

func GetInstance() MyDB {
	if obj == nil {
		fmt.Println("There is no DB instance")
	}
	return obj
}

 

[프로타입 패턴]

인스턴스는 새 객체를 만들기 위해 자신을 복제(clone)하게 된다.

원칙은 "런타임"에 또 다른 "객체"를 생성한다는 것이다. 다시 말해 이 시점에 가서 클로닝(cloning)을 하는 객체의 "실제 복사본"이 만들어지는 것이다.

 

실행 시간 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. -->

ko.wikipedia.org

 

기존 자바를 사용했던 사람들에게 는 매우 익숙한 패턴처럼 보일 것이다.

모든 자바의 클래스는 Object를 상속하고, 이는 Clone()의 메서드를 제공하게 된다. 즉 객체의 복사를 제공한다.

 

왜 필요할까? 

객체의 정확한 복사본을 만들고자 할 때 필요하다고 정의되어있다. 특히나 Go에서는 slice, pointer에서 예외 상황이 발생한다.

아래의 예제를 보자.

결과 값의 3,4번째 줄을 확인해 보자. 동일한 주소값을 가지고 있다. 분명 새로운 키보드를 만들기 위해서 작성했지만 아래와 같이 동일한 주소값을 가지고 있다.

고에서 슬라이스는 포인터의 래퍼이다. 즉 메모리 주소의 포인터 값을 랩핑 한 배열이다. 당연히 이를 복사하면 우리가 가져오는 값들은 저 포인터 값을 복사한 값들을 들고 오게 된다. 이는 반드시 주의해야 한다.

 

	leopold := &Keyboard{
		Layout: vamilo.Layout,
		Switch: []string{"Gateron Mx Red", "Gateron Mx Blue"},
		KeyCap: vamilo.KeyCap,
	}

위와 같은 새로운 슬라이스를 선언하고 값을 넣어줘야 한다. 

 

아래와 같은 방법을 이용해서 복사하는 방법도 하나의 방법 중 하나이다.

gob를 이용해 스트림화시켜 직렬화 역직렬화시키는 방법을 이용해서 복사를 하는 것도 하나의 방법이 될 수도 있다.

 

Interface의 clone을 선언해서 이용하는 방법을 사용해 보자.

 

이번에는 인터페이스의 클론을 활용해서 인터페이스 자신을 반환하게 설정하였다.
이렇게 설정되면 클라이언트는 복사하고자 하는 대상으로 타입어설션이 가능해지며 유연성 있고, 확장가능한 코드구성이 가능해진다.

func Run() {
	sixty := &Keyboard{"60%", []string{"Cherry MX Blue", "Cherry MX Brown", "Cherry MX Red"}, "DSA"}

	keyboardClone := sixty.Clone().(*Keyboard)

	fmt.Println(keyboardClone)
}

 

[빌더 패턴]

빌더 패턴이란 복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴이다.

생성패턴 의 근간이 되는 기본 구조에 대해서 다시 한번 상기하고 넘어가자.

생성패턴 은 구현하는 구현체가 있고 구현체 의 추상화 클래스가 존재한다.

 

빌더패턴 은 다음 과 같은 문제점을 해결하고자 나왔다.

1. 하나의 객체에서 한 개 이상의 생성자 필드를 받게 된다면 , 생성함에 있어 깔끔하고 유지가능한 코드를 작성하는 것이 생각보다 어려울 수 있다.

2. 객체의 생성이후 필드값의 불변성이 요구될 때, 한 개 이상의 다양한 많은 속성을 불변으로 만드는 것은 생각보다 어려울 수 있다.

3. 객체의 복합 생성 로직은 메인 클래스 의 유지 및 확장성에 어려움을 야기할수 있다. 

 

빌더 패턴 을 만들어 보자.

type House struct {
	windows            int
	door               int
	floor              string
	rooms              int
	bathroom           int
	swimmingPoolSize   int
	swimmingPoolHeight int
	gardenSize         int
	gardenFlower       string
	gardenTree         string
	garageSize         int
	garageDoor         string
}

물론 작은 필드 들이지만 이것보다 많아질 수 있고 다양해질 수 있다고 가정해 보자. House를 생성하고자 한다면?

house := &House{4, 1, "wooden", 2, 1, 0, 0, 0, "", "", 0, ""}

이런 식의 코드가 생기게 되며 읽기 매우 어렵고 난해하다.

우선 구현하고자 하는 기본 골격 구조를 다음과 같이 잡고자 한다.

 

정통적인 빌더 패턴에서는 다음과 같은 구조를 가지게 된다. 

생성패턴의 기본 골격인 인터페이스와 구현체를 빌더패턴에서도 동일하게 가져간다. 

이제는 생각보다 많이 익숙해진 구조이고 , 다만 디렉터라는 구현체가 빌더의 총괄적인 핸들링을 담당하고 있다.

구체적인 클래스 설계도를 확인해 보자.

House 빌더라는 인터페이스가 있으며, 그거를 구현하는 각각의 빌더 구조체들을 구현하고 이 각각 의 구조체는 House라는 거대한 

구조체를 상속받아서 사용할 것이다.

이렇게 생성된 HouseBuilder는 BuildDirector라는 구현체에 의해 핸들링되고 관리될 것이다. 

바로 코드로 가보자.

인터페이스는 위와 같이 설정해 주었으며, 아래 있는 의문에 대해 생각해 보자. 최종 구현체에서 코드를 사용할 때 이유를 확인할 수 있다.

인터페이스의 구현을 위해 위와 같이 작성했으며, 특히나 go에서 ide를 사용하고 있다면, 

var _ HouseBuilder = (*HouseNormalBuilder)(nil)

위 코드를 이용해 자동완성을 애용하자.

핵심이 되는 디렉터 구조체를 확인해 보자. 

디렉터 구조체는 하우스 빌더를 생성간의 주입받게 된다. 이렇게 주입된 하우스 빌더를 통해 원하는 집을 디렉터가 정의한 순서에 맞춰

생산할 수 있다. 마치 프레임워크가 작동하듯이 말이다.

 

중간 함수를 확인하게 되면. 을 이용해 죄다 체이닝 해버렸다. 보다 직관적인 코드의 방식 구현을 위해 인터페이스에서 다음과 같이 정의를 했다. 본인 자신을 반환하게 된다면 위와 같은 체이닝 방식이 가능해진다.

 

그래서 클라이언트는 어떻게 이거를 사용할까?

빌더를 각각 주입해 주면서 우리는 대형 객체를 보다 손쉽게 선언하고 사용할 수 있다. 

SwimmingPool House is {windows:40 door:10 floor:wooden rooms:0 bathroom:1 swimmingPoolSize:200 swimmingPoolHeight:180 gardenSize:0 gardenFlower: gardenTree: garageSize:0 garageDoor:}
Normal House is {windows:20 door:20 floor:wooden rooms:0 bathroom:3 swimmingPoolSize:0 swimmingPoolHeight:0 gardenSize:0 gardenFlower: gardenTree: garageSize:0 garageDoor:}

이렇게 코드가 작성되고 클라이언트가 사용한다고 가정할 때 빌더패턴 이 나오게 된 이유를 모두 충족하는가? 

 

1. 하나의 객체에서 한 개 이상의 생성자 필드를 받게 된다면 , 생성함에 있어 깔끔하고 유지가능한 코드를 작성하는 것이 생각보다 어려울 수 있다.

- 각각의 빌더를 주입받아서 정해진 순서에 따라 하우스를 생산한다. 순서의 관리는 디렉터 가 담당하고, 각각의 구현체끼리는 느슨한 결합 형태를 가지게 된다.

 

2. 객체의 생성 이후 필드값의 불변성이 요구될 때, 한 개 이상의 다양한 많은 속성을 불변으로 만드는 것은 생각보다 어려울 수 있다.

- 하우스 구조체 의 필드값은 소문자로 임포트 되지 않는다. 즉 특정 메서드가 제공되지 않는다면? 불변하는 필드들이다.

 

3. 객체의 복합 생성 로직은 메인 클래스 의 유지 및 확장성에 어려움을 야기할 수 있다. 

- 하우스 새로운 건축물 혹은 수영장 집의 업그레이드 함에 있어 어디를 수정하고 어디를 고쳐야 하는지 명확한가?

 

빌더패턴 의 특징을 다시 한번 알고 넘어가자.

- Builder Interface 가 모든 구현체에서 구현 함수를 정의한다.- Concrete Builder에서 Builder Interface를 구현하고 실제 로직을 캡슐화한다.- Director는 옵션적인 부분이며, Builder Interface의 함수를 호출해 Build의 대상을 제공한다.- Product에서는 Build pattern을 사용해 실질적인 인스턴스를 담당한다.

 

어느 정도 패턴을 공부하고, 자바 사람이라면 이 빌더패턴 은 너무나도 친숙한 패턴이 아닐 수 없다.Lombok 이  이 빌더패턴을 정말 잘 활용해서  객체의 생성을 도와준다. 고 와는 달리 자바에서는 필드의 생성자 값들을 하나씩만 넣으면서 초기화하기 위해서? 그에 해당하는 모든 생성자 클래스를 선언해주어야 한다. 창문 만 있는 생성자 클래스, 창문 욕조 가 있는 생성자 클래스 등등

갑자기 이 이야기를 왜 하냐고 의문이 들 수 있다.

House.builder().window().door().build();

롬복에서 는 builder라는 어노테이션을 제공해 주는데 이런 체이닝 방식을 이용해서 제공한다. 왜 이렇게 유명한 라이브러리(github star 11.9k)에서 우리가 지금 까지 배운 인터페이스, 함수 호출 외순서를 정하는 방식이 아닌 위와 같은 체이닝 방법을 활용해서 제공하고 빌더패턴을 적용했다고 할까? 우리가 배운 생성패턴의 기본 골격 구조는 인터페이스에 의존하는 구현체인데 방식이 많이 다르지 않은가?

 

이런 부분에 있어 어느 정도 유연성을 둔다고 한다. 그래서 글 초창기에 전통적인 빌더패턴이라는 표현을 사용했다.

빌더패턴의 목적  "유연하고, 효과적인 방법으로 객체 생성을 제공해야한다." 이다. 

처음 제공된 코드처럼 마치 프레임워크 같이 작성하는 전통적인 빌더패턴 이 있고, 롬복 과 같이 다양한 라이브러리 에서는 사용자에게 보다 많은 유연성을 주기 위해 최근에는 이런 인터페이스 가  생략된 간단한 빌더패턴을 제공한다고 한다.

아래 코드를 보자.

 

더보기
package simpleBuilder

type Person struct {
	StreetAddress, Postcode, City string

	CompanyName, Position string
	AnnualIncome          int
}

type PersonBuilder struct {
	person *Person
}

type PersonAddressBuilder struct {
	PersonBuilder
}

func NewPersonBuilder() *PersonBuilder {
	return &PersonBuilder{&Person{}}
}

func (b *PersonBuilder) Lives() *PersonAddressBuilder {
	return &PersonAddressBuilder{*b}
}

func (b *PersonBuilder) Works() *PersonJobBuilder {
	return &PersonJobBuilder{*b}
}

func (p *PersonBuilder) Builder() *Person {
	return p.person
}

func (p *PersonAddressBuilder) At(street string) *PersonAddressBuilder {
	p.person.StreetAddress = street
	return p
}
func (p *PersonAddressBuilder) In(city string) *PersonAddressBuilder {
	p.person.City = city
	return p
}
func (p *PersonAddressBuilder) PostCode(code string) *PersonAddressBuilder {
	p.person.Postcode = code
	return p
}

type PersonJobBuilder struct {
	PersonBuilder
}

func (p *PersonJobBuilder) At(comapny string) *PersonJobBuilder {
	p.person.CompanyName = comapny
	return p
}
func (p *PersonJobBuilder) Asa(job string) *PersonJobBuilder {
	p.person.Position = job
	return p
}
func (p *PersonJobBuilder) Earn(income int) *PersonJobBuilder {
	p.person.AnnualIncome = income
	return p
}

 

 

단순하게 큰구조체 안에 빌더2개를 더만들어서 나아가는 방식으로 작성했다.

그러면 다음과 같은 클라이언트 호출을 가져갈수 있다.

 

다시말해 생성패턴 의 기본 골격 구조가 항상 지켜져야 만 패턴이 성립한다는 생각을 이번기회에 버리자. 

이렇게 됨에 우리는 2가지 선택지가 생긴다.

 

1. 각단계의 순서를 강제해 객체 의 생성 로직을 명확히 하는 전통적인 빌더패턴의 방법

2. 사용자 에게 자유로운 방식의 유연성을 주는 방식의 빌더패턴 방법 

 

상황에 맞는 유연한 코드를 작성하자.

 

코드전문

더보기
프로젝트 구조
package houseImpl

type House struct {
	windows            int
	door               int
	floor              string
	rooms              int
	bathroom           int
	swimmingPoolSize   int
	swimmingPoolHeight int
	gardenSize         int
	gardenFlower       string
	gardenTree         string
	garageSize         int
	garageDoor         string
}


package houseImpl

type HouseBuilder interface {
	Window() HouseBuilder
	Door() HouseBuilder
	Floor() HouseBuilder
	Rooms() HouseBuilder
	BathRoom() HouseBuilder
	GetHouse() House
}


package houseImpl

type HouseGarageBuilder struct {
	House
}

func NewHouseGarageBuilder() *HouseGarageBuilder {
	return &HouseGarageBuilder{}
}

func (h *HouseGarageBuilder) GetHouse() House {
	h.setGarage()
	return House{
		windows:    h.windows,
		door:       h.door,
		floor:      h.floor,
		rooms:      h.rooms,
		bathroom:   h.bathroom,
		garageDoor: h.garageDoor,
		garageSize: h.garageSize,
	}
}
func (h *HouseGarageBuilder) setGarage() {
	h.garageSize = 20000
	h.garageDoor = "Marble"
}

func (h *HouseGarageBuilder) Window() HouseBuilder {
	h.windows = 40
	return h
}

func (h *HouseGarageBuilder) Door() HouseBuilder {
	h.door = 10
	return h
}

func (h *HouseGarageBuilder) Floor() HouseBuilder {
	h.floor = "Marble"
	return h
}

func (h *HouseGarageBuilder) Rooms() HouseBuilder {
	h.rooms = 7
	return h
}

func (h *HouseGarageBuilder) BathRoom() HouseBuilder {
	h.bathroom = 5
	return h
}


package houseImpl

type HouseGardenBuilder struct {
	House
}

func NewHouseGardenBuilder() *HouseGardenBuilder {
	return &HouseGardenBuilder{}
}

func (h *HouseGardenBuilder) GetHouse() House {
	h.setGarden()
	return House{
		windows:      h.windows,
		door:         h.door,
		floor:        h.floor,
		rooms:        h.rooms,
		bathroom:     h.bathroom,
		gardenSize:   h.gardenSize,
		gardenFlower: h.gardenFlower,
		gardenTree:   h.gardenTree,
	}
}
func (h *HouseGardenBuilder) setGarden() {
	h.gardenSize = 400
	h.gardenFlower = "Rose and Sunflower"
	h.gardenTree = "White Oak Tree"
}

func (h *HouseGardenBuilder) Window() HouseBuilder {
	h.windows = 40
	return h
}

func (h *HouseGardenBuilder) Door() HouseBuilder {
	h.door = 10
	return h
}

func (h *HouseGardenBuilder) Floor() HouseBuilder {
	h.floor = "Marble"
	return h
}

func (h *HouseGardenBuilder) Rooms() HouseBuilder {
	h.rooms = 7
	return h
}

func (h *HouseGardenBuilder) BathRoom() HouseBuilder {
	h.bathroom = 5
	return h
}

package houseImpl

type HouseNormalBuilder struct {
	House
}

func NewHouseNormalBuilder() *HouseNormalBuilder {
	return &HouseNormalBuilder{}
}

func (h *HouseNormalBuilder) GetHouse() House {
	return House{
		windows:  h.windows,
		door:     h.door,
		floor:    h.floor,
		rooms:    h.rooms,
		bathroom: h.bathroom,
	}
}

func (h *HouseNormalBuilder) Window() HouseBuilder {
	h.windows = 20
	return h
}

func (h *HouseNormalBuilder) Door() HouseBuilder {
	h.door = 20
	return h
}

func (h *HouseNormalBuilder) Floor() HouseBuilder {
	h.floor = "wooden"
	return h
}

func (h *HouseNormalBuilder) Rooms() HouseBuilder {
	h.rooms = 5
	return h
}

func (h *HouseNormalBuilder) BathRoom() HouseBuilder {
	h.bathroom = 3
	return h
}

package houseImpl

type HouseSwimmingPoolBuilder struct {
	House
}

func NewHouseSwimmingPoolBuilder() *HouseSwimmingPoolBuilder {
	return &HouseSwimmingPoolBuilder{}
}

func (h *HouseSwimmingPoolBuilder) GetHouse() House {
	h.setSwimmingPool()
	return House{
		windows:            h.windows,
		door:               h.door,
		floor:              h.floor,
		rooms:              h.rooms,
		bathroom:           h.bathroom,
		swimmingPoolHeight: h.swimmingPoolHeight,
		swimmingPoolSize:   h.swimmingPoolSize,
	}
}
func (h *HouseSwimmingPoolBuilder) setSwimmingPool() {
	h.swimmingPoolSize = 200
	h.swimmingPoolHeight = 180
}

func (h *HouseSwimmingPoolBuilder) Window() HouseBuilder {
	h.windows = 40
	return h
}

func (h *HouseSwimmingPoolBuilder) Door() HouseBuilder {
	h.door = 10
	return h
}

func (h *HouseSwimmingPoolBuilder) Floor() HouseBuilder {
	h.floor = "wooden"
	return h
}

func (h *HouseSwimmingPoolBuilder) Rooms() HouseBuilder {
	h.rooms = 2
	return h
}

func (h *HouseSwimmingPoolBuilder) BathRoom() HouseBuilder {
	h.bathroom = 1
	return h
}

package main

import (
	"builder/houseImpl"
	"builder/simpleBuilder"
	"fmt"
)

type BuildDirector struct {
	builder houseImpl.HouseBuilder
}

func NewBuildDirector(builder houseImpl.HouseBuilder) *BuildDirector {
	return &BuildDirector{builder: builder}
}

func (b *BuildDirector) BuildHouse() houseImpl.House {
	return b.builder.Window().Door().Floor().BathRoom().GetHouse()
}
func (b *BuildDirector) changeBuilder(builder houseImpl.HouseBuilder) {
	b.builder = builder
}

func main() {
	builder := houseImpl.NewHouseSwimmingPoolBuilder()
	director := NewBuildDirector(builder)

	a := director.BuildHouse()
	fmt.Printf("SwimmingPool House is %+v\n", a)
	director.changeBuilder(houseImpl.NewHouseNormalBuilder())
	b := director.BuildHouse()
	fmt.Printf("Normal House is %+v\n", b)

	pb := simpleBuilder.NewPersonBuilder()
	pb.
		Lives().
		At("123 London").
		In("London").
		PostCode("Mortgatan6").
		Works().
		At("Plea").
		Asa("Programmer").
		Earn(12300)

	person := pb.Builder()
	fmt.Println(person)

}

 

 

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

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

 

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

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

 

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

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

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

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

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

 

팀장 님 께서 EC2 서버 하나 구축해 보라고 하셨다. 기존에 있던 테스트 서버를 기준 삼아서 만들던 와중 RedHat으로 되어 있던 기존 꺼에서 aws linux 바꾸기로 해서 다시 만들려고 하던 와중 실수로 기존 테스트 서버를 터미네이트 시켜 버렸다...

 

이렇게 터미네이트 되어버린 인스턴스 는 기존에 가지고 있던 root ebs까지 모두 날려버리기 때문에 기존에 스냅샷 이 없다면.. 복구할 방법이 존재하지 않는다.... ㅠㅠ 

설정이 완벽하게 끝난 서버라면 ebs 를 인스턴스 와 분리하던가 아니면 스냅샷 을 찍어 태그네임 박아서 기록해 두고, AMI까지 생성해서 관리한다면 보다 완벽하다. 꼭꼭 꼭 해두자.

 

어쨌든 구축하면서 했던 삽질 모든 걸 기록해보고자 한다. 

 

EC2의 기본 세팅은 기존 ami를 사용하지 않고 aws에서 제공해 주는 이미지 템플릿을 사용했다. 

 

ssh 키 같은거는 기존 pem 파일을 사용하기로 하였으며 docker를 이용해서 기존 디비들을 구축하려고 했다.

  1. sudo yum update -y
  2. sudo yum install -y docker
  3. sudo service docker start

위 스탭 으로 기존 데이터를 업데이트해주고 도커 설치 후 서비스 시작해 주면 된다.

docker pull [mysql,redis,kafka]

 

docker run --name mysql -e MYSQL_ROOT_PASSWORD=plea2018 -p 3306:3306 -d mysql

docker run -d --name redis -p 6379:6379 redis
docker run --name some-kafka -d -p 9092:9092 confluentinc/cp-kafka:latest

 

하다 보니 거지 같은 sudo 커맨드를 계속 써야 한다. sudo 코드를 없애기 위해 아래 명령어를 실행해 그룹에 추가해 주자.

sudo usermod -aG docker $USER

 

mysql부터 외부에서 접속 테스트를 실행하던 중 자꾸 kafka 도커가 상태가 계속 떨어진다... 로그를 확인해 보자.

KAFKA_ZOOKEEPER_CONNECT is required.
Command [/usr/local/bin/dub ensure KAFKA_ZOOKEEPER_CONNECT] FAILED!

 

위와 같은 에러가 나오고 알아보니 zookeeper의 추가적인 설치와 설정이 필요하다. 왜?

Apache ZooKeeper는 Kafka가 클러스터 환경에서 효율적으로 작동하는 데 필요한 분산 조정 및 관리를 제공하므로 Apache Kafka의 중요한 구성 요소이다. 

그래서 추가적인 zookeper 설치가 필요하다.

그전에 먼저 docker network create kafka-net을 이용해 카프카 용 별도의 네트워크를 설정해 준다.

이렇게 생성된 네트워크를 이용해 주키퍼 컨테이너 와 통신이 가능해진다. 그렇기에 네트워크 를 생성해서 주키퍼와 연결해 준다.

 

docker run -d --name zookeeper --network kafka-net -p 2181:2181 confluentinc/cp-zookeeper

실행해 주키퍼를 실행해 주면 역시나 바로 죽어버린다. 로그를 확인해 보면 
Command [/usr/local/bin/dub ensure-atleast-one ZOOKEEPER_CLIENT_PORT ZOOKEEPER_SECURE_CLIENT_PORT] FAILED!
주키퍼 클라이언트 포트를 설정해 달라고 한다. 그냥 넣어주자. 한 번에 실행이 되는 적이 없다 아주

docker run -d --name zookeeper -p 2181:2181 -e ZOOKEEPER_CLIENT_PORT=2181 zookeeper:latest

주키퍼 실행 이 되었다면 이제 카프카를 켜주자.

 

docker run -d --name kafka --network kafka-net -p 9092:9092 e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 confluentinc/cp-kafka
음 또 죽는다.

Command [/usr/local/bin/dub ensure KAFKA_ADVERTISED_LISTENERS] FAILED!

카프카 리스너 설정이 안 되어 있어 에러가 발생했다.

docker run -d --name kafka --network kafka-net -p 9092:9092 -e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 confluentinc/cp-kafka


이렇게 로컬호스트로 추가 해주었지만 실질적인 연결을 위해서는 hostname or IP address를 저기 로컬호스트에 대신 넣어주어야 하지만 일단 실행이 목표이기 때문에 로컬호스트로 박아주었다.

 

PLAINTEXT://ec2-xx-xx-xxx-xxx.compute-1.amazonaws.com:9092 이런 식의 주소를 써주어야 한다고 하는데 나중에 테스트가 필요한 부분 같다.

 

이렇게 실행이 되는가 싶었는데 또 죽는다. ㅋㅋㅋㅋ

이번에 로그를 까보면 가관이다. 용량이 부족하단다 ㅎㅎ

OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000 c0000000, 1073741824, 0) failed; error='Not enough

 

이걸 해결하기 위해 처음 컨테이너 가 실행될 때 메모리를 제한하는 방법이 있다. "KAFKA_HEAP_OPTS="-Xms100 M"" 100M 만 힙메모리로 지정하겠다는 의미이다. 

docker run -d --name kafka --network kafka-net -p 9092:9092 -e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 -e KAFKA_HEAP_OPTS="-Xms100M" confluentinc/cp-kafka

메모리가 아슬아슬하게 도달하고 

이렇게 실행이 되는 걸 볼 수 있다.

팀장님 이 말하시기를 주키퍼 내장된 컴포즈가 있을 텐데 왜 이렇게 어렵게 하냐라고 하신다.... ㅠㅠ 컴포즈 버전으로 다시 해보자.

저위에 주키퍼 다운로드하는 저 명령어 다필요 없이

docker run -d --name kafka johnnypark/kafka-zookeeper
이거만 실행 하면 그냥 돌아간다 ... ㅠㅠ 

카프카 로그를 보면 이렇게 성공적으로 실행이 된다.

자 이제 팀장님의 추가적인 지시사항으로 이 서버는 오전 09 시에 켜지고 18시에 내려가는 서버이다 즉 이 서비스 들을 데몬에 등록해서 실행시켜주어 한다. 이런 팀장님의 의도를 모르고 aws lambda와 aws event bus , cloud formation이나 알아보고 있으니 그냥 하루를 아무것도 하지 않고 날렸다. 

 

먼저 데몬에 대해 알아보자.

리눅스 시스템이 처음 가동될 때 실행되는 백그라운드 프로세스 일종으로 메모리에 상주하면서 특정 요청이 오면 즉시 대응할 수 있도록 대기 중인 프로세스이다. 바로 가보자.

 

서비스 작성

cd /etc/systemd/system , 으로 이동해서  원하는이름.service 로 작성해준다.
 
touch docker-init.service 
 
vi docker-init.service

이렇게 적어주고 
서비스 등록 을 해주자. 도커도 다운로드한 거기 때문에 이렇게 서비스 등록이 필요하다.
systemctl start docker.service

systemctl start docker-init.service

위와 같이 작성하면 서버를 끄고 킬떄 마다 매번 명령어 실행 필요 없이 백그라운드에서 자동화 실행이 된다.

systemctl status docker
systemctl status docker-init

명령어를 실행해 등록이 되었는지 다시 한번 확인해 준다.


자 이제 시간에 맞춰 켜지고 꺼지는 자동화를 설정해 주자.

먼저 구조 가 어떻게 되는지 알아야 한다. 

aws_event_bridge에서 특정시간에 이벤트를 발행시키는데 그 이벤트의 대상이 aws_lambda 함수가 된다. 그래서 우리는

람다 함수에 원하는 기능을 작성하고

이벤트 브리지 에는 이벤트 발행 규칙을 정해주면 된다.

 

aws_lambda를 생성해서 키고 끄는 것을 설정해 보자.

aws_lambda를 가서 생성한다. 노드 파이썬을 권장한다. 웹에서 수정이 가능하기 때문에 편하다.

이렇게 생성하면 자동으로 역할 이 생기게 되는데 iam 으로 가서 json 으로 역할 수정을 해준다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:Start*",
                "ec2:Stop*"
            ],
            "Resource": "*"
        }
    ]
}

 

태그도 ec2_instance_run_close로 달아주자.

// Ec2 start
import boto3
region = 'ap-northeast-2'
instances = ['당신의 ec2 인스턴스 아이디']
ec2 = boto3.client('ec2', region_name=region)

def lambda_handler(event, context):
    ec2.start_instances(InstanceIds=instances)
    print('started your instances: ' + str(instances))
    
// EC2 stop
import boto3

region = 'ap-northeast-2'
instances = ['당신의 ec2 인스턴스 아이디']
ec2 = boto3.client('ec2', region_name=region)

def lambda_handler(event, context):
    ec2.stop_instances(InstanceIds=instances)
    print('stopped your instances: ' + str(instances))

이렇게 시작과 중지 함수를 각각 생성해서 테스트까지 모두 마무리해야 한다. 실제로 테스트해서 인스턴스 가 켜고 꺼지는지 확인하자.

모두 제대로 동작한다면 이제 aws_eventbridge로 가자

 

규칙생성에 rule type을 일정으로 정하고 크론으로 지정해주자.

instance _close = 0 18 ?  * MON-FRI * 

instance_open = 0 0 ? * MON-FRI *

UTC로 표기되기 때문에 현지 시간에 맞게 잘 조정하자 ㅠㅠ 
이후 대상 선정을 aws_lambda로 선정하고, 검토 및 생성으로 건너뛰어서 생성해 주자.

 

각가의 시작과 종료가 각각 설정되어 있는 모습이다.


슬랙에 알림 메시지  보내주는 것도 설정해 주자. 

슬랙에 가서 incoming webhook을 추가한다. (https://api.slack.com/apps/A04T3RM5WNT/incoming-webhooks?success=1)
커스텀하게 앱 만들어서 추가해도 된다.

import boto3
import json
import urllib.request


region = 'ap-northeast-2'
instances = ['당신의 인스턴스 주소']
ec2 = boto3.client('ec2', region_name=region)

webhook_url = '웹훅 url'

def post_slack(argStr):
    message = argStr
    send_data = {
        "text": message,
    }
    send_text = json.dumps(send_data)
    request = urllib.request.Request(
        webhook_url, 
        data=send_text.encode('utf-8'), 
    )

    with urllib.request.urlopen(request) as response:
        slack_message = response.read()

def lambda_handler(event, context):
    ec2.start_instances(InstanceIds=instances)
    post_slack('started your instances: ' + str(instances))

이렇게 작성하고 배포하고 테스트를 눌러보면?

요렇게 서버 다운 하고 올라올 때 이렇게 메시지 가 전송 되는 모습이다.

 

긴글 읽어주셔서 감사합니다.

최근 디자인 패턴을 공부하면서 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

+ Recent posts