※ 디스커버리 Go 언어 라는 책 내용을 전반적으로 다루지만 책 리뷰보다는 좀 더 간단하게 정리해보겠습니다.
8. 실무 패턴
이번 장에서는 실무에서 맞닥뜨리는 설계 문제들을 어떻게 풀 수 있는지 보도록 하겠습니다. 예를 들면, Go 언어에서는 제네릭 프로그래밍을 지원하지 않는데, 이를 어떻게 흉내낼 수 있는지 아는 것보다 제네릭으로 어떤 문제를 풀고 싶은지가 더 중요합니다. 이렇게 생각하면 굳이 제네릭을 이용하지 않아도 되는 경우가 많습니다.
8.1 오버로딩
오버로딩은 같은 이름의 함수 및 메서드를 여러 개 둘 수 있는 기능인데, Go 언어에서는 지원되지 않습니다. 어떤 경우에 오버로딩이 필요한지 생각해보겠습니다.
- 자료형에 따라 다른 이름 붙이기 : 오버로딩을 반드시 하지 않아도 되는 경우가 많으며, 자료형에 따라 다른 함수의 이름을 붙이자.
- 동일한 자료형의 자료 개수에 따른 오버로딩 : 가변 인자를 활용
- 자료형 스위치 활용하기 : 오버로딩을 반드시 해야 하는 경우엔 인터페이스로 인자를 받고, 메서드 내에서 자료형 스위치로 다른 자료형에 맞춰 다른 코드가 수행되게 한다.
- 다양한 인자 넘기기 : 구조체를 넘김으로써 오버로딩보다 코드가 깔끔해질 수 있다. 아래 예시)
Element getElement(int idx) {
return getElement(idx, DEFAULT);
}
Element getElement(int idx, Language lang) {
return getElement(idx, lang, false);
}
Element getElement(int idx, Language lang, bool excludeEmpty) {
// ...
}
// 위처럼 쓰는 경우엔 아래처럼 구조체를 넘기도록 하는게 더 낫다
type Option struct {
Idx int
Lang Language
ExcludeEmpty bool
}
func GetElement(opt Option) *Element {
// ...
}
- 인터페이스 활용하기 : 아래 예시)
func String(int i) { ... }
func String(double d) { ... }
// 위처럼 작성할 수 없으므로 아래처럼 한다
type Stringer interface {
String() string
}
type Int int
type Double float64
func (i Int) String() string { ... }
func (d Double) String() string { ... }
8.1.1 연산자 오버로딩
Go 는 연산자 오버로딩을 지원하지 않습니다. 연산자 오버로딩은 어떠한 문제를 풀기보다는 편의성을 위한 기능입니다. 인터페이스를 이용해 해결 가능합니다.
8.2 템플릿 및 제네릭 프로그래밍
제네릭은 알고리즘을 표현하면서 자료형을 배제할 수 있는 프로그래밍 패러다임입니다. 정렬 알고리즘을 만든다고 할 때, 정수형 자료나 문자열 자료에 상관없이 동일한 알고리즘이라면 하나의 코드로 작성이 가능합니다. Go 에서는 제네릭을 지원하지 않는데, 어떻게 하면 제네릭을 사용해 풀던 문제들을 해결할 수 있을지 보도록 하겠습니다.
8.2.1 유닛 테스트
JUnit 을 비롯한 xUnit 스타일의 테스트에서는 assertEqual 과 같은 함수를 이용해 두 값이 서로 같은지 비교하는데, 이것을 아래와 같이 대체해 보겠습니다.
// 1. if 를 이용해 직접 비교
if expected != actual {
t.Error("Not equal")
}
// 2. assertEqual 을 직접 작성, 자료형을 한정
func assertEqualString(t *testing.T, expected, actual string) {
if expected != actual {
t.Errorf("%s != %s", expected, actual)
}
}
// 3. reflect.DeepEqual 을 이용해 범용적 assertEqual 을 작성
func assertEqual(t *testing.T, expected, actual interface{}) {
if !reflect.DeepEqual(expected, actual) {
t.Errorf("%v != %v", expected, actual)
}
}
8.2.2 컨테이너 알고리즘
제네릭은 주로 컨테이너에 많이 이용합니다. 컨테이너에 어떤 알고리즘을 적용하고자 할 때, 그 컨테이너가 담고 있는 자료형은 큰 상관이 없이 구현할 수 있게 하기 위해 제네릭을 사용합니다. 이런 경우엔 인터페이스를 활용해 해결 가능합니다.
예를 들어 어떤 자료형도 출력할 수 있는 프린터나 어떤 자료형도 네트워크로 보낼 수 있는 알고리즘은 Stringer 와 같은 인터페이스를 구현하거나 JSONMarshaler 를 이용하면 됩니다.
8.2.3 자료형 메타 데이터
어떤 자료형이 넘어왔는지에 따라 다른 코드가 동작하게 하려면 자료형 스위치를 이용하면 됩니다. 이 자료형에 대한 메타 데이터를 처리하고 싶다면 reflect 패키지를 이용할 수 있습니다. reflect 패키지를 이용하면 동적 자료형 언어에서 할 수 있는 많은 것을 할 수 있습니다. 하지만 reflect 를 사용하면 정적인 자료형 검사를 할 수 없으므로 꼭 필요한 경우에만 이용해야 합니다.
몇 가지 예시가 있지만 특별히 왜 사용해야하는지 모르겠어서 중요하다 생각되면 추후에 따로 정리해보겠습니다.
8.2.4 go generate
C 언어 등에서 제공하는 매크로는 전처리기를 통해 소스코드를 확장하여 컴파일하는데, 비슷한 도구가 go 도구에 있습니다.
go generate 를 이용하면 임의의 명령을 수행하여 프로그램 코드를 생성할 수 있습니다. //go:generate 뒤에 명령을 붙여서 코드에 넣은 뒤 go generate 를 수행하면 됩니다.
enum 값을 const 를 이용해 지정하였는데, 이것을 문자열로 바꾸고 싶으면 반복적인 코드를 작성해야 합니다. C 언어에서는 매크로를 이용하면 쉽게 해결할 수 있는데 Go 에서는 go generate 의 stringer 을 활용하면 됩니다.
> go get golang.org/x/tools/cmd/stringer // cmd 에서 stringer 설치
stringer -type=Pill // Pill 이라는 자료형을 문자열로 변경하는 String() 메서드를 자동으로 생성 명령
//go:generate stringer -type=Pill // 코드내에 이런 형태로 활용, 주석처리되어있지만 go generate 도구가 특별히 처리함
자세한 내용은 여기를 참고하면 좋을 것 같습니다.
8.3 객체지향
Go 는 객체지향을 완전히 지원하지 않습니다. 객체지향을 활용해 풀었던 문제들을 보도록 하겠습니다.
8.3.1 다형성
다형성은 메서드가 호출되었을 때, 어떤 자료형이냐에 따라 다른 구현을 할 수 있게 합니다. 이것은 Go 의 인터페이스로 쉽게 구현이 가능합니다. 도형의 넓이를 구하는 예시를 보도록 하겠습니다.
type Shape interface {
Area() float32
}
type Square struct {
Size float32
}
func (s Square) Area() float32 {
return s.Size * s.Size
}
type Rectangle struct {
Width, Height float32
}
func (r Rectangle) Area() float32 {
return r.Width * r.Height
}
type Triangle struct {
Width, Height float32
}
func (t Triangle) Area() float32 {
return 0.5 * t.Width * t.Height
}
func TotalArea(shapes []Shape) float32 {
var total float32
for _, shape := range shapes {
total += shape.Area()
}
return total
}
func ExampleTotalArea() {
fmt.Println(TotalArea([]Shape{
Square{3},
Rectangle{4, 5},
Triangle{6, 7},
}))
}
8.3.2 인터페이스
자바 등에서 쓰는 인터페이스는 Go 의 인터페이스로 구현이 가능합니다.
8.3.3 상속
객체지향에서 상속은 어떤 클래스의 구현들을 재사용하기 위해 사용됩니다. IsA 관계인 경우와 HasA 관계가 성립합니다.
HasA 관계의 경우 재사용하고자 하는 구현의 자료형 변수를 struct 에 내장하면 됩니다. 이후 이 필드를 사용하면 재사용하는 것입니다.
IsA 관계의 상속에서는 많은 경우에 추상 클래스를 상속합니다. 구현이 없는 경우 Go 언어의 인터페이스를 이용하면 됩니다. 추상 클래스가 아닌 클래스를 상속받는 경우, 인터페이스와 구조체 내장을 동시에 이용하면 됩니다. 상속을 통해 풀려는 문제들을 보도록 하겠습니다.
- 메서드 추가
기존에 있던 코드를 재사용하면서 기능 추가를 하고 싶은 경우에 상속할 수 있습니다. 메서드 추가를 통해 기능을 추가합니다. 아래 예시)
type Rectangle struct {
Width, Height float32
}
func (r Rectangle) Area() float32 {
return r.Width * r.Height
}
// 여기에 메서드를 추가하고 싶은데, 다른 패키지에 있고 직접 수정이 어려운 경우
// 구조체 안에 다른 구조체를 넣어서 메서드 추가 가능
type RectangleCircum struct{ Rectangle }
func (r RectangleCircum) Circum() float32 {
return 2 * (r.Width + r.Height)
}
- 오버라이딩
기존에 있던 구현을 다른 구현으로 대체하고자 하는 경우에도 상속을 쓸 수 있습니다. 이 문제 역시 구조체 내장으로 해결이 가능합니다.
type WrongRectangle struct{ Rectangle }
func (r WrongRectangle) Area() float32 {
return r.Rectangle.Area() * 2
}
메서드 추가와 거의 유사하며, 같은 이름의 메서드를 정의하면 Rectangle 에 있는 메서드는 그대로 사용하면서 Area 메서드만 재정의할 수 있습니다.
- 서브 타입
기존 객체가 쓰이던 곳에 상속받은 객체를 쓰고자 상속하기도 합니다. 인터페이스와 구조체 내장을 모두 사용하면 됩니다.
어떤 자료형이 주어진 인터페이스를 구현하고 있는지를 알아보려면 reflect.Type.Implements 메서드를 이용하면 됩니다. reflect.TypeOf 를 이용해 자료형을 알아낸 다음 Implements 를 호출하면 됩니다.
내장된 구조체가 있는지는 구조체에서 내장된 구조체의 이름으로 필드를 찾은 후 Anonymous 필드를 찾아보면 됩니다.
reflect 로 서브 타입 검사를 할 수 있는 것은 좋습니다만 자주 쓰게 되지는 않을 것입니다.
8.3.4 캡슐화
객체 안에 있는 정보를 바깥에 숨기고자 하는 것이 캡슐화입니다. Go 에서는 상속이 없기 때문에 protected 는 없지만 private, public 을 활용할 수 있습니다. 혹은 내가 만든 다른 패키지에서는 접근이 가능하게 하고 싶은데 남은 접근하지 못하게 하고 싶은 경우, 내부 패키지를 이용하면 됩니다. 패키지 경로에 internal 을 넣으면 internal 이 있는 경로에 있는 패키지를 포함한 범위에서만 참조가 가능합니다.
8.4 디자인 패턴
몇 가지 디자인 패턴들이 풀고자 하는 문제를 Go 에서 풀어보겠습니다.
8.4.1 반복자 패턴
이미 4장, 5장, 7장에서 여러 가지 반복자 패턴을 보았습니다. 해당 포스트를 보면 좋을 것 같아 생략합니다.
8.4.2 추상 팩토리 패턴
추상 팩토리 패턴은 팩토리들을 여럿 묶어놓은 팩토리를 추상화하는 패턴입니다. 인터페이스를 활용하면 쉽게 구현할 수 있습니다.
type Button interface {
Paint()
OnClick()
}
type Label interface {
Paint()
}
// 윈도우용 버튼
type WinButton struct{}
func (WinButton) Paint() { ... }
func (WinButton) OnClick() { ... }
// 윈도우용 라벨
type WinLabel struct{}
func (WinLabel) Paint() { ... }
// 맥용 버튼
type MacButton struct{}
func (MacButton) Paint() { ... }
func (MacButton) OnClick() { ... }
// 맥용 라벨
type MacLabel struct{}
func (MacLabel) Paint() { ... }
// 버튼과 라벨을 생성할 수 있는 UI 팩토리
type UIFactory interface {
CreateButton() Button
CreateLabel() Label
}
// 윈도우용 UI element 생성 가능한 팩토리
type WinFactory struct{}
func (WinFactory) CreateButton() Button {
return WinButton{}
}
func (WinFactory) CreateLabel() Label {
return WinLabel{}
}
// 맥용 UI element 생성 가능한 팩토리
type MacFactory struct{}
func (MacFactory) CreateButton() Button {
return MacButton{}
}
func (MacFactory) CreateLabel() LAbel {
return MacLabel{}
}
// 주어진 os 에 따라 UI 팩토리를 반환하는 함수
func CreateFactory(os string) UIFactory {
if os == "Win" {
return WinFactory{}
} else {
return MacFactory{}
}
}
// 어떤 팩토리를 받았는지 상관없이 동일한 코드로 팩토리에서 인스턴스를 찍어낼 수 있음
func Run(f UIFactory) {
button := f.CreateButton()
button.Paint()
button.OnClick()
label := f.CreateLabel()
label.Paint()
}
8.4.3 비지터 패턴
비지터 패턴 (Visitor Pattern) 은 알고리즘을 객체 구조에서 분리시키기 위한 디자인 패턴입니다. 인터페이스를 이용하여 구현하면 되므로 별로 어려움 없이 구현할 수 있습니다.
8.5 Summary
- 오버로딩을 지원하는 언어에서 오버로딩을 이용하여 풀고자 했던 문제를 Go 언어에서는 인터페이스, 자료형 스위치, 구조체 넘기기, 가변인자 등을 이용해 풀 수 있음
- 컨테이너 알고리즘은 인터페이스를 활용
- reflect 패키지를 이용하면 자료형에 대한 메타데이터를 알 수 있음
- 프로그램 파일을 생성해서 써야 한다면 go generate 이용 가능
- 객체지향의 다형성, 인터페이스, 상속, 캡슐화 등의 주제에 대한 문제도 Go 에서 해결 가능
- 몇 가지 디자인 패턴에 대해서도 Go 의 인터페이스를 통해 해결 가능
출처 : 디스커러비 Go 언어
'Backend > Golang' 카테고리의 다른 글
Go 언어 - [7장 - (2)] (동시성) (0) | 2020.09.12 |
---|---|
Go 언어 - [7장 - (1)] (동시성) (0) | 2020.09.05 |
Go 언어 - [6장] (웹 어플리케이션 작성) (0) | 2020.08.29 |
Go 언어 - [5장] (구조체) (0) | 2020.08.22 |
Go 언어 - [4장] (함수) (0) | 2020.08.16 |