Contents

role playing go

개요

고 언어를 사용하여 프로젝트를 구성할 때, 제가 주로 작성하는 스타일을 정리하는 글입니다.

대체로 import cycle을 해결하거나 어느정도의 OOP를 구현하는 데에도 용이하다고 생각합니다.

역할

각 패키지는 고유의 역할을 가지게 됩니다. 역할은 2가지 경우로 나뉩니다.

  1. 역할에 대해 단일 구현체만 존재할 경우
  2. 역할에 대해 여러 구현체가 존재할 경우

단일 구현체가 존재할 경우

여러 서비스 간 주고 받는 로그 객체를 만든다고 가정합시다. 저는 log라는 패키지를 만들 것입니다.

package log

type Log struct {
	UnixTime int64  `parquet:"name=unix_time, type=INT64" json:"unix_time"`
	AppID    int32  `parquet:"name=app_id, type=INT32" json:"app_id"`
	Level    int32  `parquet:"name=level, type=INT32" json:"level"`
	Message  string `parquet:"name=message, type=BYTE_ARRAY, convertedtype=UTF8, encoding=PLAIN_DICTIONARY" json:"message"`
}

log라는 패키지 아래에 Log 구조체를 직접 생성하여 log 패키지를 클래스처럼 작성합니다. 여기에 func New() Log로 생성자도 만들어주면 다른 패키지에서 log.New()로 클래스를 쓰듯이 Log 구조체의 인스턴스를 생성할 수 있습니다.

여러 구현체가 존재할 경우

Tester라는 역할이 있다고 가정합시다. 저는 tester라는 패키지를 만들게 될 것입니다.

package tester

type Tester interface {
    Test(...interface{})
}

먼저 추상적인 테스터의 기능을 작성할 인터페이스를 선언합니다. 편의를 위해 매개변수는 빈 인터페이스 가변 인자로 작성합니다. 테스터에는 단순 입력한 걸 기반으로 동작하는 SimpleTester, 자동으로 입력 가능한 랜덤 값을 추출해서 동작하는 FuzzTester가 존재할 수 있습니다.

tester 패키지 하위 디렉토리에 simple_testerfuzz_tester를 작성하고 tester 인터페이스를 구현하여 작성합니다.

package simple_tester

type SimpleTester struct {}

func New() tester.Tester {
    return new(SimpleTester)
}

func (s *SimpleTester) Test(values ...interface{}) {}
package fuzz_tester

type FuzzTester struct {}

func New() tester.Tester {
    return new(FuzzTester)
}

func (f *FuzzTester) Test(values ...interface{}) {}

각각 테스터 역할을 수행할 수 있지만 세부 기능은 다른 형태로 작성되어 동작하게 될 것입니다.

파생과 포함

각 패키지는 고유의 역할을 가지지만 각 역할 간에는 파생 혹은 포함 관계가 성립할 수 있습니다. 그럴 경우 양자 간의 관계일 경우엔 해당 역할의 루트 디렉토리의 하위 디렉토리에 작성합니다.

단일 역할에서 단일 역할이 파생될 경우

제가 작성한 로그스트림(github)의 구조입니다.

.
├── LICENSE
├── README.md
├── consumer
│   ├── consumer.go
│   ├── nats
│   │   └── nats.go
│   └── stdout
│       └── stdout.go
├── go.mod
├── go.sum
├── log
│   ├── logbuffer
│   │   ├── logbuffer.go
│   │   ├── logqueue
│   │   │   ├── pq.go
│   │   │   └── readme.md
│   │   └── logring
│   │       ├── rb.go
│   │       └── readme.md
│   ├── loglevel
│   │   ├── level.go
│   │   └── readme.md
│   ├── readme.md
│   └── struct.go
├── logstream.go
└── trie.go

log 밑의 logbuffer 패키지와 loglevel 패키지의 경우 log의 객체를 직접 활용하고 있으므로 바로 하위에 위치하고 있습니다.

여러 역할에서 파생될 경우 혹은 여러 역할을 포함할 경우

여러 역할에서 파생될 경우에는 최소 공통 최상위 디렉토리에 작성하게 됩니다. virtual-gate 레포(github)가 그런 구조입니다.

.
├── LICENSE
├── README.md
├── balancer
│   ├── balancer.go
│   ├── hashed
│   │   └── hashed.go
│   ├── least
│   │   └── least.go
│   └── round
│       └── round.go
├── breaker
│   ├── breaker.go
│   ├── count_breaker
│   │   └── breaker.go
│   └── simple_breaker
│       └── breaker.go
├── go.mod
├── go.sum
├── limiter
│   ├── bucket
│   │   └── bucket.go
│   ├── limiter.go
│   ├── slide_count
│   │   ├── acc
│   │   │   └── acc.go
│   │   └── slide.go
│   └── slide_log
│       └── log.go
├── lock
│   └── lock.go
└── proxy
    ├── http_proxy
    │   ├── extensions.go
    │   ├── http.go
    │   └── response.go
    ├── proxy.go
    └── tcp_proxy
        └── tcp.go

balancer, limiter, breaker는 모두 독립된 역할을 하지만 proxy는 앞의 3가지 역할을 포함합니다. 그렇기에 proxy도 동일하게 최소 공통 최상위 디렉토리에 위치하게 됩니다.

여러 역할에서 포함하는 역할

net/http 베이스로 작성한 럭스(github)의 디렉토리 구조입니다.

.
├── LICENSE
├── LICENSES.md
├── context
│   ├── context.go
│   ├── get.go
│   ├── header.go
│   ├── reply.go
│   ├── status.go
│   └── websocket.go
├── go.mod
├── go.sum
├── handler
│   └── handler.go
├── logext
│   ├── logext.go
│   └── stdout
│       └── stdout.go
├── lux.go
├── middleware
│   ├── acl.go
│   ├── authorize.go
│   ├── compress.go
│   ├── cors.go
│   └── middleware.go
├── readme.md
├── router
│   ├── router.go
│   └── routergroup.go
├── signal
│   └── signal.go
└── util
    ├── ip.go
    └── mime.go

routermiddleware, context 패키지의 구조는 이 글에서는 안티패턴입니다.

util 패키지는 각 패키지에서 사용할 유용한 기능을 담당하는 역할을 합니다. 다른 역할을 파생시키지는 않지만 포함되어 도와주는 역할을 하게 됩니다. 이 경우에도 공통된 최소 최상위 디렉토리에 위치하게 됩니다.

여러 독립적 기능을 가진 역할들이 파생될 때

기본적으로 이 글에서는 안티패턴입니다. 분리하는 게 맞지만 부득이하게 수정해야할 코드가 많을 경우 이렇게 작성합니다.

위 럭스의 구조에서 context는 2가지의 파생 역할이 포함되어 있습니다.

package context

type LuxContext struct {
	Request     *http.Request
	Response    *Response
	RouteParams httprouter.Params
}

type WSContext struct {
	Conn net.Conn
}

럭스는 두가지 컨텍스트를 가집니다. 하나는 일반적인 HTTP 요청에서 사용하는 컨텍스트이고 다른 하나는 웹소켓 요청에서 사용하는 컨텍스트입니다. 서로 같은 컨텍스트라는 역할을 공유하지만 독립적으로 다른 기능을 제공합니다. 이 경우엔 같은 패키지에 작성하고 파일과 이름을 분리하여 관리합니다.