[Go] 캔들 차트 적재 서버 후속편: Refactoring (1)

2025. 8. 4. 16:13·Project
If it stinks, change it. 

 

네.. 그래서 합니다..


~블록체인 플랫폼 실시간 ohlcv 차트 개발기 시리즈~

[1] 실시간 ohlcv 차트(캔들 차트) 데이터 적재 서버 개발 in 블록체인 플랫폼 - Part 1. 문제편

[2] 실시간 ohlcv 차트(캔들 차트) 데이터 적재 서버 개발 in 블록체인 플랫폼 - Part 2. 해결편

[3] 블록체인 플랫폼 실시간 ohlcv 차트 적재 서버 후속편: Refactoring (1)

[4] 블록체인 플랫폼 실시간 ohlcv 차트 적재 서버 후속편: Refactoring (2)

[5] 블록체인 플랫폼에서 실시간 ohlcv 차트 적재 서버 후속편: Restructuring


딱 지금 내 심정ㅎ 리팩토링 is my life. 좋은..건가..?

🟧 들어가며

앞선 시리즈에서 소개한 코드를 개선하게 되었다. 이게 회고의 장점 중 하나인 것 같은데 블로그 글을 쓰기 위해 코드를 분석하면서 리팩토링을 해야할 부분을 상당수 발견했다. 원래부터 하고 싶었던 리팩토링과 새롭게 발견한 개선 사항들을 정리해서 각을 잡고 코드를 수정했다.

 

마침 급한 프로젝트가 없는 상황이라서 Claude Code 를 사용해서 후딱은 아니고 좀 시간을 들여 끝냈다..ㅋㅋㅋ

코드를 이상하게 짜길래 투덜거렸더니 클로드 코드한테 갑자기 칭찬받아서 어이없어서 캡쳐함

AI의 도입으로 개발자들의 생산성이 확 늘기 했다. 인정은 하지만 아직 온전히 AI가 짠 코드를 신뢰하기는 한참 멀었다고 생각이든다.

생각보다 멍청하기도 하고, 생각보다 게으르기도 하다. 내 말은 AI가 아이언맨에서 나오는 자비스는 절대절대 아니라는 말이다.

 

만약 복잡하지만 효율적인 방법과 간단하지만 비효율적인 방법이 있다면 AI는(적어도 claude code는) 비효율적이어도 간단한 방법을 선택하는걸 볼 수 있다.(꽤 자주.. 그래서 열불난다.. 싱글톤 하지 말라고 해도 처음엔 듣는척하다가 나중에는 간단하다면서 또 싱글톤 추가함.. ㅂㄷㅂㄷ)

그렇기 때문에 AI가 코드를 짤 때 출력하는 생각들을 유심히 보면서 감독 해주는게 중요하다. 아니면 일을 다 마치고 코드를 검토한 다음 혼내면서 고치던가..ㅋㅋㅋ

기계는 때려야 말을 잘 듣고 LLM은 혼내야 말을 잘듣는다는 속설이 있다. 나는 이 얘기를 무척 좋아한다.

아무튼 우당탕탕 클로드 코드와 함께한 이 리팩토링 과정과 결과를 소개하겠다!

 

🟧 진짜 들어가며

지난 1편과 2편에서 실시간으로 쏟아지는 블록체인 buy/sell 이벤트를 캐치해 OHLCV 캔들 차트를 적재하는 서버를 어떻게 만들었는지(아니면 수정했는지?) 소개했다.

 

Race condition이라는 예측 가능한 적(또 너냐)을 만나 고전했고, 촉박한 시간 속에서 팀원들의 도움 덕에 'Snap shot 아키텍처'를 고안해내어 코드 구조를 싹 엎으므로써 적을 해치웠다.

'그' 플래그. 그치만 해치우긴 했어

급한 불은 껐지만 사실 개발자로서 그렇게 개운하지는 않았던 것 같다. 일단 '동작'은 하지만 코드가... 너무... 구렸기 때문이다..ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

너무 덕지덕지 조건문과 플래그를 사용했다는게 너무 마음에 걸렸고, 공통으로 묶을 수 있는 로직들이 보였으며, 그냥 머릿속으로 '이게 최선입니까? 확실해요?' 가 계속 맴돌았다고나 할까...  근데 그때는 너무 일정이 급해서 동작에 우선순위를 두고 했다..

웃음이 나와? 아니.

내가 가장 걸렸던 문제는 가독성이 너무 떨어진다는 점이었다. 솔직히 말하면 리팩토링을 하면서 코드를 다시 보는데 이해를 하기까지 좀 시간이 걸렸다.  이건 내가 정말 기피하는 코드다. 다른 사람들이 한눈에 이해하기 쉬워야 유지보수가 가능하고, 그래야 이 코드는 더더욱 발전할 수 있다. 우리가 밥먹듯이 사용하는 수많은 오픈 소스들을 생각해보자. 집단 지성은 무조건 필요하고, 다른 개발자들의 지성을 빌리려면 일단 잘 읽혀야 한다!!! 코드가 효율적이어야 그렇게 동작하는 서버, 서비스 등등 모두 문제가 없고 결론적으로 유저가 행복하다. 나는! 이런 코드를! 늘! 추구한다!!! 그러니까 지금은 절대 안돼. 고쳐야해...

 

가독성이 떨어지는 부분들을 단서로 코드를 훑어 나가니까 개선해야할 점이 여러개 발견되었고, 또 회고글을 쓰면서도 자잘하게 더 발견되었다.

그렇게 시작한 리팩토링이다. 그리고 난 리팩토링을 즐겨하는 편이다. (그냥 과거에 내가 짠 코드들은 다 구리더라..ㅠ)

그러니까 내 걱정은 마시라! 아무튼 이번 글에서는 차트 적재 서버의 어떤 점을 어떻게 개선했는지, 내가 어떤 고민을 했고, 어떻게 해결했는지에 대해 풀어보려고 한다.


#1. 차트 캐시 구조 개선 (sync.Map)

🔹 AS-IS: map과 RWMutex의 조합

이전 '[2] 해결편'에서 스냅샷 아키텍처를 구현하면서, 차트 데이터를 메모리에 캐싱하기 위해 map을 사용했다. 수많은 goroutine이 이 map에 동시에 접근하기 때문에 데이터의 정합성을 지키기 위한 sync.RWMutex 락도 추가했다.

// 예시 코드
type ChartManager struct {
    // ...
    ohlcv1MCache   map[string]*Ohlcv
    ohlcv5MCache   map[string]*Ohlcv
    mu           sync.RWMutex
}

func (cm *ChartManager) GetChartFromCache(chartType ChartType) *Ohlcv {
    cm.mu.RLock()
    defer cm.mu.RUnlock()
    // ... cache-lookup logic ...
}

func (cm *ChartManager) SetChartToCache(chartType ChartType) {
    cm.mu.Lock()
    defer cm.mu.Unlock()
    // ... cache-set logic ...
}

이 방식은 Go에서 동시성을 제어하는 뭐랄까 가장 고전적인, 클래식한 방법이다. 하지만 락의 범위를 관리하고, 데드락을 피하기 위해서 코드를 작성하면서 내내 전체 lock 사용처를 신경써야 했다. 코드가 복잡해질수록 lock을 관리하는 비용은 기하급수적으로 늘어날 위험이 있었다.

 

🔹 TO-BE: sync.Map

그래서 '[2] 해결편' 에서 번뜩 sync.Map을 쓰면 되지 않을까? 하는 아이디어가 떠올랐다. sync.Map은 별도의 Mutex lock 없이 안전하게 동시 Read/Write을 수행할수 있도록 설계된 자료구조다.

import "sync"

type ChartManager struct {
    // ...
    ohlcv1MCache   *sync.Map // map[string]*Ohlcv
    ohlcv5MCache   *sync.Map // map[string]*Ohlcv
}

mutex.Lock()과 mutex.Unlock() 호출이 사라지면서 겉보기에는 코드가 한결 간결해졌다. 하지만 모든 것에는 장,단점이 있듯 sync.Map을 도입하면서 한가지 고려해야할 부분이 있었다.

복병: BSON Marshaling

바로 MongoDB에 저장할 때 BSON marshaling이 되지 않는다는 복병이 생겼다.

MongoDB의 Go 드라이버는 BSON 마샬링을 할 때, reflection을 사용해 구조체의 exported된 필드(=대문자로 시작하는 필드)들을 처리한다.

하지만 sync.Map은 내부 구조 상, 데이터를 외부에 노출하는 exported 필드가 존재하지 않았다. 따라서 sync.Map 필드를 가진 구조체를 그대로 DB에 저장하려고 하면 드라이버는 이 필드를 인식하지 못하고 빈 객체 {} 로 마샬링하여 데이터가 유실되는 문제가 발생했다.

 

이 문제를 해결하기 위해, 데이터를 DB에 저장하는 시점에는 sync.Map을 다시 map[string]*Ohlcv 타입으로 변환하는 과정이 필요했다.

// sync.Map을 일반 map으로 변환하는 함수
func (cm *ChartManager) convertCacheToMap(chartType ChartType) map[string]*Ohlcv {
    regularMap := make(map[string]*Ohlcv)
    
    sMap := cm.getChartCache(chartType) // sync.Map을 가져오는 헬퍼 함수
    
    sMap.Range(func(key, value interface{}) bool {
        regularMap[key.(string)] = value.(*Ohlcv)
        return true // to continue iteration
    })
    
    return regularMap
}

// DB 저장 시 변환된 map 사용
func (cm *ChartManager) saveAllChartsToDB() {
    chartsToSave := cm.convertCacheToMap(Chart1M)
    // ... use chartsToSave for MongoDB bulk write operation ...
}

이 코드를 보면 이런 의문이 생길 수 있다.

"어차피 다시 map으로 변환해야 한다면, sync.Map을 쓰는 의미가 있을까?"

나 역시 이 질문에 대한 명쾌한 답을 내리기 위해 이 두가지 타입에 대해서 고민을 했었다. 그럼에도 sync.Map을 쓰기로 결정한 이유는 '접근 빈도'에 대한 분석과 트레이드 오프에 있다.

 

데이터 접근 패턴

현재 고민하고 있는 '차트 데이터'는 어떤 경우에 접근이 이뤄지는지 살펴보자.

  1. 메모리 캐시 접근(Read/Write): 모든 buy, sell 이벤트마다 발생하는데 이는 초당 수십 ~ 수백번이 될 수 있다.
  2. DB 저장(Read): 스냅샷을 생성하는 cron job 주기(1분)와, 서버 재시작시 누락된 데이터를 채우는 로직에서만 발생한다. 메모리 접근에 비해 훨씬 드문 횟수다.

동작 방식

그리고 각각의 동작 방식을 현재 시스템에 맞춰서 살펴보자.

그 전에 알아두어야할 점! sync.Map은 내부적으로 읽기 전용 맵과 쓰기용 맵, 두 개를 가지고 복잡하지만 효율적으로 동작한다. (참고 링크)

동작 방식 쓰기(Write) 읽기(Read)
sync.Map - 전체 맵을 잠그지 않고 atomic operation을 통해 매우 빠르게 값을 교체. 여러 고루틴이 서로 다른 키의 값을 동시에 업데이트하더라도 거의 경합 없이 처리될 수 있음
- 읽기 전용 맵에 없는 새로운 키를 추가할 때는 내부적으로 쓰기용 맵을 사용하며 일부 락이 필요하지만, 전체 맵을 막는 RWMutex 보다 경합 범위가 좁음
- 대부분의 읽기 작업은 락 없이 읽기 전용 맵에서 바로 처리되어 매우 빠름
- 읽기 전용 맵에 데이터가 없으면 쓰기 전용 맵의 뮤텍스를 잠그고 쓰기 전용 맵에서 키를 찾음
map + RWMutex - 한 고루틴이 mu.Lock()을 호출하면, 다른 모든 고루틴(읽기 포함)은 락이 해제될 때까지 대기해야함
- 결국 고루틴은 순서대로 작업을 처리하게 되어 동시성의 이점을 전혀 살리지 못하고 성능이 저하됨
- 수많은 이벤트가 동시에 발생하여 여러 고루틴이 캐시 업데이트를 시도하면, 이 하나의 쓰기 락을 차지하기 위한 lock contention이 발생
- mu.RLock()을 사용하면 여러 고루틴이 동시에 읽을 수 있다.
- 단, 쓰기 락이 걸려있는 동안에는 모든 읽기 작업도 대기해야함

 

장단점

이러한 사용 패턴을 고려했을 때, 각 방식의 장단점은 명확해진다.

방식 sync.Map map + RWMutex
장점 - 원자적 연산으로 안전한 동시성 제공
- 대부분의 락 없이 처리하여 빠른 읽기/쓰기 가능
- 쓰기 락이 없을 때 동시 읽기가 빠름
- DB driver와 호환, DB에 변환 없이 바로 저장 가능
단점 - BSON 마샬링을 위해 map으로 변환하는 오버헤드 발생 - 단일 락으로 인한 데드락 위험, 락 경합으로 인한 성능 저하 가능성

 

결론

분석 결과, sync.Map을 쓰는게 이점이 크다고 판단했다. 이유는 아래와 같다,.

  • 서비스 특성상, concurrent map writes 에러를 원천적으로 방지하고, 여러 고루틴이 안전하게 캐시에 접근하는 것이 훨씬 중요하다.
  • sync.Map.Range()를 통한 변환 과정의 시간 복잡도는 O(n)이지만, DB 저장 빈도가 낮고, 관리하는 토큰의 수가 대부분 수천개 이하이므로 이 오버헤드는 전체 시스템 성능에 미치는 영향이 미미하다.
  • 현재 차트 데이터의 'key-value' 추가나 삭제는 드물지만, 기존 값을 업데이트하는 작업이 빈번하다. 이때, sync.Map은 RWMutex를 사용하는 map보다 성능이 좋다.
  • 복잡한 락 관리를 제거하고 데드락의 위험에서 벗어남으로써 얻는 코드 안정성과 명확성의 이점이 훨씬 크다고 봤다.

결과적으로, mu.Lock()과 mu.Unlock() 호출이 코드에서 사라지면서 코드는 한결 간결해졌고, 동시성 이슈에 대한 잠재적인 위험 요소 하나를 원천적으로 제거할 수 있었다. 또한 이 리팩토링을 진행하며, 여러 곳에 흩어져 있던 차트 캐시 업데이트 로직을 하나의 함수로 통합하고, 관련 함수들의 이름을 명확하게 개선하는 등 코드의 전체적인 품질을 한단계 높일 수 있었다.


#2. 아키텍처 개선 (ChartState)

🔹 AS-IS: ChartManager에 집중된 책임

기존에 ChartManager는 너무 많은 일을 맡고 있었다. 차트 캐시 관리, DB 저장 로직 생성, 스냅샷 cronjob 관리, 심지어 히스토리 데이터 재적재 로직까지 모두 떠안고 있었다. 이는 단일 책임 원칙(Single Responsibility Principle)에 위배되며, 파일을 수백 줄의 코드로 가득 채워 유지보수를 어렵게 만들기도 하는 골칫덩이었다. (이는 이후 소개하는 리팩토링들까지 모두 적용하면 개선되는 사항이다.)

 

특히 가장 문제였던 점은, 1분봉, 5분봉 등 모든 차트 주기에 대한 정보를 개별적인 필드로 가지고 있었다.

// AS-IS: ChartManager with tightly coupled fields
type ChartManager struct {
    // Cache fields for each chart type
    ohlcv1MCache   *sync.Map 
    ohlcv5MCache   *sync.Map 
    // If we add a 10-minute chart, we'd need ohlcv10MCache here
    
    // Cron job fields for each chart type
    cron1M         *cron.Cron
    cron5M         *cron.Cron
    // ... and cron10M here

    // ... other dependencies
}

 

이 구조의 문제는 명확했는데, 새로운 차트 주기를 추가하려면 ChartManager 구조체 자체를 수정해야했다는 것이다.

이는 개방-폐쇄 원칙(Open-Closed Principle)에 위반되며, 코드를 수정하기 어렵고 실수를 유발하기 쉬운 구조였다.

예를 들어, 특정 차트 타입의 캐시를 가져오려면 아래와 같이 switch 문이 반복적으로 사용될 수 밖에 없었다.

// AS-IS: Repetitive switch statement
func (cm *ChartManager) getChartCache(chartType ChartType) *sync.Map {
    switch chartType {
    case Chart1M:
        return cm.ohlcv1MCache
    case Chart5M:
        return cm.ohlcv5MCache
    default:
        // log error
        return nil
    }
}

 

민망하지만, 당시에는 시간이 촉박해서 일단 스파게티 식으로 다 떄려 박았다. 그렇지만 짜면서도 반드시 리팩토링 해주겠다고 다짐한 부분이었다! 이제 시간이 된 것이다!!

믿고 맡겨보시라예,,

🔹 TO-BE: ChartState 구조체 도입

이 코드를 고치기 위해서는 각 차트 주기가 가져야할 공통적인 상태를 ChartState 라는 별도의 구조체로 묶어 분리했다. 이 구조체에는 특정 차트 주기에 대한 모든 정보(캐시, 크론잡, 차트 타입 식별자)를 캡슐화 한다.

// TO-BE: A dedicated struct to encapsulate state
type ChartState struct {
    chartType ChartType // e.g., Chart1M, Chart5M
    cache     atomic.Value // value: *sync.Map, The cache for this specific chart type
    cron      *cron.Cron  // The cron job for this specific chart type
}

이제 ChartManager는 더 이상 개별적인 필드를 갖지 않는다. 대신, ChartType을 키로, *ChartState를 값으로 갖는 map 하나로 모든 차트 주기를 관리할 수 있다. ChartManager의 역할은 이 map을 통해 각 ChartState를 조율하면 되는 것으로 다듬어졌다.

 

atomic.Value를 선택한 이유

여기서 잠깐! 코드를 꼼꼼히 본 당신이라면 의문 하나가 떠올랐을 것이다.

왜 그냥 *sync.Map이 아닌 atomic.Value로 차트 캐시를 관리하고 있는 걸까??

더보기

Q. sync.Map은 그 자체로 동시성 안전성을 보장하는데, 왜 굳이 atomic.Value로 한번 더 감쌌을까?

A. 맵 자체는 안전할지라도, 그 맵을 가리키는 포인터 변수를 읽고 쓰는 것은 안전하지 않기 때문이다.

 

다음 시나리오의 경우를 생각해보자.

  1. Goroutine A (초기화 로직): 서버가 시작되면서 chart cache들을 초기화하는 함수가 실행된다. 이 함수는 DB에서 가장 최신 차트 정보를 읽어와 새로운 *sync.Map을 생성한 후, 이 맵의 주소를 state.cache에 할당하려고 한다.
  2. Goroutine B (실시간 이벤트 핸들러): 바로 그 순간, 새로운 거래 이벤트가 발생하여 UpdateChartModel 과 같은 함수가 동시에 실행된다. 이 함수는 차트 데이터를 업데이트하기 위해 state.cache에 저장된 맵의 주소를 읽어오려고 한다.

Goroutine A가 포인터 주소를 state.cache 변수에 쓰는 도중에, Goroutine B가 그 주소를 읽으려고 시도하면 어떻게 될까?

B는 완전히 새로운 주소도 아니고 그렇다고 이전 주소도 아닌 쓰레기 값을 읽을 수 있다. 이는 panic 으로 이어질 수도 있는 심각한 문제다.

 

atomic.Value는 바로 이 Data Race 문제를 해결하기 위해 존재한다.

atomic.Value는 내부적으로 lock을 사용하여, 값의 저장(Store)과 조회(Load)가 원자적으로 일어나도록 보장한다. 즉, '쓰는 중간에 읽는' 상황을 원천적으로 막아준다. ( = 항상 완전한 포인터 값을 다루도록 보장)


Q. 그러면 그냥 포인터가 아니라 sync.Map을 쓰면 되는거 아닌가?

A. chart cache가 포인터가 아닐 경우, 함수에 인자로 chart cache를 전달할 때 문제가 생긴다.

 

Go의 가장 근본적인 원칙, 'pass-by-value' 때문에 함수 인자를 전달하거나, 변수를 다른 변수에 할당할 때, Go는 항상 값을 복사한다.

해당 서버에는 chart cache를 인자로 받아서 여러 계산들을 하는 함수들이 있는데, 이 함수들이 수정한 값이 그대로 chart cache, 즉 chartState에 반영이 되어야 한다.

그러나 함수 내부에서는 완전히 새로운 sync.Map을 만들어서 처리가 되기 때문에 이런 오작동을 막고, 여러 고루틴이 '단 하나의 동일한 chart cache'를 공유하며 안전하게 데이터를 읽고 쓰기 위해서는 포인터를 사용해야 한다.

결론은 다~ 이유가 있다는 말이다~ 고민을 해서 짠 코드라구!

 

// TO-BE: A much cleaner and more manageable ChartManager
type ChartManager struct {
    states map[ChartType]*ChartState // A single map to hold all chart states
    // ... other dependencies
}

// Initialization logic
func NewChartManager(...) *ChartManager {
    cm := &ChartManager{
        states: make(map[ChartType]*ChartState),
        // ...
    }
    
    // Loop through all defined chart types to initialize states
    for _, chartType := range AllChartTypes {
        state := &ChartState{
            chartType: chartType,
            cache:     &sync.Map{},
        }
        // The cron job is also created and assigned to state.cron here
        cm.addChartSnapshotCronJob(state) 
        cm.states[chartType] = state
    }
    return cm
}

 

이러한 구조 변경으로 switch문으로 가득했던 코드가 사라졌다. 특정 차트의 상태가 필요한 경우, map에서 바로 찾아서 쓰도록 코드가 개선되었다. 즉, 코드가 그 자체로 '확장성'을 갖게 된 것이다. 차트 타입이 추가되어도 코드 변경 없이 기능할 수 있게 되었고, 유연하고 유지보수하기 좋은 구조가 되었다.

// TO-BE: No more switch statement, just a map lookup
func (cm *ChartManager) getChartState(chartType ChartType) *ChartState {
    // Direct lookup provides the entire context for a chart type
    if state, ok := cm.states[chartType]; ok {
        return state
    }
    return nil
}

// Example usage becomes much cleaner
func (cm *ChartManager) someFunction(chartType ChartType) {
    state := cm.getChartState(chartType)
    if state == nil {
        // handle error
        return
    }
    // Now we have everything related to the chart type in one place
    cache := state.cache
    cron := state.cron
    // ... operate on the cache and cron job
}

코드까지 포함되니 글이 너무 길어져서 나눕니다...

다음편에서 계속...

'Project' 카테고리의 다른 글

[Go] 캔들 차트 적재 서버 후속편: Refactoring (2)  (0) 2025.08.06
[Go] encoding/csv 패키지를 이용해 csv 다운로드 API 구현하기  (0) 2025.06.12
[Go] 블록체인 플랫폼에서 실시간 ohlcv 차트(캔들 차트) 설계 방법 (부제: Restructuring)  (0) 2025.06.05
[Go] util 함수 개발기 - klaytn 패키지의 unpack 함수에서 abi 의존성 제거하기  (0) 2024.07.23
'Project' 카테고리의 다른 글
  • [Go] 캔들 차트 적재 서버 후속편: Refactoring (2)
  • [Go] encoding/csv 패키지를 이용해 csv 다운로드 API 구현하기
  • [Go] 블록체인 플랫폼에서 실시간 ohlcv 차트(캔들 차트) 설계 방법 (부제: Restructuring)
  • [Go] util 함수 개발기 - klaytn 패키지의 unpack 함수에서 abi 의존성 제거하기
빵빵0
빵빵0
(아직은) 공부하고 정리하는 블로그입니다.
  • 빵빵0
    Hack Your World
    빵빵0
  • 전체
    오늘
    어제
    • 분류 전체보기 (92)
      • Error Handling (7)
      • Project (5)
        • MEV (2)
      • Architecture (0)
        • API (0)
        • Cache (0)
        • 사소한 고민거리 (0)
      • Computer Science (4)
        • Data Structure (2)
        • Database (1)
        • Cloud (0)
        • OS (0)
        • Infra, Network (1)
        • AI (0)
      • Language (8)
        • Go (8)
        • Rust (0)
        • Python (0)
        • Java (0)
      • Algorithm (40)
        • BaekJoon (18)
        • Programmers (7)
        • LeetCode (6)
        • NeetCode (9)
      • SW Books (9)
        • gRPC Up & Running (1)
        • System Design Interview (2)
        • 스프링 입문을 위한 자바 객체지향의 원리와 이해 (6)
        • 블록체인 해설서 (0)
        • 후니의 쉽게 쓴 CISCO 네트워킹 (0)
      • BlockChain (4)
        • Issues (0)
        • Research (4)
        • Tech (0)
      • Own (8)
        • TIR(Today I Read) (3)
        • Personal (2)
        • Novel (0)
        • Memo (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    EVM
    BFS
    프로그래머스
    Hash Table
    KBW
    DP
    BEAKJOON
    LeetCode
    blockchain
    context
    golang
    블록체인
    BaekJoon
    MEV
    2024
    ethereum
    two pointer
    Python
    NeetCode
    chart
    goroutine
    MongoDB
    Programmers
    큐
    candlechart
    go
    Palindrome
    백준
    Greedy
    스택
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
빵빵0
[Go] 캔들 차트 적재 서버 후속편: Refactoring (1)
상단으로

티스토리툴바