/img/avatar.jpg

고루틴과 채널을 활용한 유사 액터 모델

액터 모델?

액터 모델은 동시성 문제를 해결하기 위해 등장한 개념입니다. 각 액터는 독립된 스레드에서 실행되며, 메시지를 통해 상호작용합니다. 액터 모델은 다음과 같은 특징을 가집니다.

  1. 액터는 메모리를 공유하지 않습니다.
  2. 액터는 고유의 상태를 가질 수 있습니다.
  3. 액터 간의 통신은 메시지를 통해 이루어집니다.
  4. 액터 간의 통신은 비동기적입니다.
  5. 액터는 해당 메시지에 대응하는 동작을 수행합니다.

CSP와 너무 잘 맞지 않아?

고 언어는 기반 패러다임에 CSP가 있는 만큼, 고루틴과 채널이라는 강력한 도구가 있습니다. 이를 이용해 타 언어에 비해 손 쉽게 액터 모델을 구현할 수 있습니다. 액터는 고루틴으로 띄우고, 메시지는 채널로 주고 받습니다. 그러면 자연스럽게 액터 간의 통신은 비동기적으로 동작합니다. 해당 메시지에 대해 라우팅이 필요한 경우에도, 여러 채널을 이용하여 select 문법을 통해 처리할 수 있습니다.

고랭과 아레나

Arena?

고랭은 가비지 컬렉터를 쓰는 언어이고, 덕분에 사용자는 메모리를 관리하는 데에 크게 신경을 쓸 필요가 없습니다. 하지만 프로젝트 크기가 커지고, 사용해야할 힙 메모리가 커질수록 더욱 빈번하게, 그리고 한번에 많은 양의 메모리를 수집하여 처리하게 됩니다. 하나의 기능을 수행하고 난 후에는 물론이고, 수행하는 도중에도 GC가 동작하여 응답이 늦어지는 상황이 생겨날 확률이 늘어납니다.

그런 상황에서 유용하게 쓸 수 있는 아레나(arena)라는 개념이 등장했습니다. 아레나는 미리 사용할 힙 메모리를 할당 받아서 사용합니다. 이 때 할당받는 큰 힙 메모리 덩어리 하나를 페이지라고 합니다. 일반적인 경우는 IO 버퍼 사이즈로, 4096(4K) 혹은 8192(8K), 16384(16K) byte 중 하나입니다. 이 페이지들이 이중 연결리스트 형태로 이어지고, 필요한 메모리를 기존 페이지에서 충당할 수 없을 때마다 새로운 페이지를 추가합니다. 그리고 아레나를 활용한 모든 작업이 끝나면 마지막에 최종적으로 아레나 전체를 반환합니다.

러스트로 알고리즘 테스트를 풀어봅시다

왜 러스트?

개인적으로 러스트로 알고리즘 문제를 푸는 건 좋은 선택이라 생각합니다.

  1. 아래의 다양한 자료구조를 빌트인으로 제공합니다.
    1. Vector
    2. VecDeque
    3. LinkedList
    4. HashMap
    5. HashSet
    6. BtreeMap
    7. BtreeSet
    8. BinaryHeap
  2. 유용한 이터러블과 빌트인 고차함수들이 있습니다.
    1. 일단 이터러블로 변환하면 뭐든지 가능하다는 장점이 있습니다.
    2. 이 과정은 당연히 정적 타입으로 이루어지기에 실수가 줄어듭니다.
  3. 튜플과 패턴 매칭 문법이 훌륭하고 표현식입니다.
    1. ()로 만드는 튜플은 어떤 타입, 어떤 길이로도 가능하여 유용합니다.
    2. match 문법은 튜플조차 패턴 매칭을 해내기에 어떤 조건도 직관적으로 표현할 수 있습니다.
    3. if, match, 심지어 loop가 값을 반환할 수 있어 유연하게 작성할 수 있습니다.
  4. 메모리를 효율적으로 재사용하고 반납합니다.
    1. 메모리를 어느정도 막 써도 용서받을 수 있습니다.
    2. 알고리즘 테스트 과정에서 메모리로 인한 실패 가능성이 줄어듭니다.
  5. 소유권에 익숙하면 좋지만 그렇지 않아도 좋습니다.
    1. C++의 &(레퍼런스) 정도로 알고 있어도 무리가 없습니다.
    2. 너무 큰 객체가 아니라면 clone() 메서드로 복사값을 넘겨줘도 됩니다.
    3. 솔직히 어지간하면 소유권으로 에러가 나는 케이스가 적습니다.

변수 할당

불변(immutable)

let a = 123;

러스트는 기본적으로 할당되는 변수는 불변입니다. 해당 변수, a는 값을 변경할 수 없습니다. a = a + 3a = 100같은 재할당을 할 수 없습니다. 이 불변성은 변수에 할당된 값에도 동일하게 적용됩니다. 예를 들어 VectorHashMap을 할당하게 되면 값을 추가하거나 삭제할 수 없게 됩니다.

enum in go

이 페이지의 고 코드는 제네릭이 포함되어 있습니다.

enum?

고에서 열거형은 아래의 단순한 형태, 혹은 조금의 변형으로밖에 작성되지 않습니다.

package week

const (
    Monday = iota
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
    Sunday
)

단순하게 week 패키지 내에서 상수를 선언하여 가져다 쓰는 정도입니다. 하지만 그건 어디까지나 열거형을 위한 패키지를 분리하지 않았기에 제한되는 방식이라 생각합니다.

package as class

단일 역할에 대해 단일 구현체만 패키지에 작성할 경우 패키지를 클래스처럼 이용할 수 있습니다.

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 구조체의 인스턴스를 생성할 수 있습니다.

간단한 로드밸런서와 HTTP 프록시 서버 구현

레이트 리미터

Limiter interface

package limiter

type Limiter interface {
	TryTake([]byte) (bool, int)
}

레이트 리미터 객체가 구현해야할 인터페이스로 Limiter 인터페이스가 있습니다. 바이트 슬라이스를 받아서 해당 슬라이스를 기반으로 레이트 리밋을 계산하여 이번 요청이 사용 가능하면 true와 적절한 status code를 반환합니다.

slide count struct

type SlideCount struct {
	lock       *lock.Lock
	unit       int64
	maxConnPer float64
	prevTime   int64
	prevCount  int64
	curCount   int64
	nextTime   int64
}

func New(maxConnPer float64, unit time.Duration) limiter.Limiter {
	now := int64(time.Now().UnixNano())
	return &SlideCount{
		lock:       new(lock.Lock),
		unit:       int64(unit),
		maxConnPer: maxConnPer,
		prevTime:   now - int64(unit),
		prevCount:  0,
		curCount:   0,
		nextTime:   now + int64(unit),
	}
}

슬라이드 카운트 구조체는 sliding window count 방식의 레이트 리밋을 구현한 것입니다. 일반적인 케이스와 달리 제 주관적인 해석이 들어가 있으므로 코드가 좀 다릅니다.

전략 패턴에 대해서

행위 패턴의 하나로, 어떤 문제를 해결함에 어떤 방법을 사용하는 지 적절히 선택, 혹은 작성할 수 있게 해주는 패턴


코틀린으로 평균을 구하는 계산기 만들기

이 계산기의 평균을 구하는 방법은 총 3가지가 있습니다.

  1. 산술 평균 : (a + b) / 2

  2. 기하 평균 : root2(a * b)

  3. 조화 평균 : 2ab / (a + b)

이 3가지를 각각 따로 입력 받아 실행하는 평균만 구하는 계산기의 코틀린 코드입니다.

import kotlin.math.sqrt

fun main(args: Array<String>) {
    val (a, b) = Pair(30, 80)
    val arithmeticCalculator = Calculator(ArithmeticMean())
    println(arithmeticCalculator.average.calculate(a, b))

    val geometricCalculator = Calculator(GeometricMean())
    println(geometricCalculator.average.calculate(a, b))

    val harmonicCalculator = Calculator(HarmonicMean())
    println(harmonicCalculator.average.calculate(a, b))
}

class Calculator(val average: Average)

interface Average {
    fun calculate(a: Int, b: Int): Int
}

class ArithmeticMean: Average {
    override fun calculate(a: Int, b: Int): Int {
        return (a + b) / 2
    }
}

class GeometricMean: Average {
    override fun calculate(a: Int, b: Int): Int {
        return sqrt(a.toDouble() * b.toDouble()).toInt()
    }
}

class HarmonicMean: Average {
    override fun calculate(a: Int, b: Int): Int {
        return (2*a*b) / (a + b)
    }
}

ArithmeticMean, GeometricMean, HarmonicMean 클래스는 Average 인터페이스를 상속 받아 평균을 구하는 메서드를 오버라이드하고 있습니다.

고루틴 풀링

이 글은 제 레포를 기반으로 작성되었습니다.

왜?

오픈톡방에서 고루틴에 대한 이야기가 나왔었습니다. 고루틴을 안전하게 관리하기 위한 보일러플레이트에 대한 것과 join과 반환값의 처리에 대한 것이었습니다.

그래서 한번 해당 건에 대해 나름의 해답을 라이브러리로 만들어봤습니다.

gopool

GoPool

type GoPool struct {
	pool    sync.Pool
	max     int64
	count   int64
	running int64
    sync.Mutex
}

고풀 구조체는 고루틴을 풀링할 sync.Pool, 그리고 int64 타입의 max, count, running을 가집니다.
max는 최대 고루틴 수, count는 현재 생성된 고루틴 수, running은 현재 실행되고 있는 고루틴 수를 의미합니다.