당분간 학부연구생 일 하느냐고 잠시 떠났는데, 다행히 여기로 잘 돌아올 수 있었다. 3~4일 정도지만 일단 긍정적인 변화가 많이 있었기에 서술을 해두려고 한다.
0) Energy관련 수집 시작
Alchemy에서 다행히 무료 API를 제공해서 거기서 Raw block데이터를 가져와서 수집하고 있다. Energy Delegation / UnDelegation은 Native Contract이므로 블록 데이터만 구체적으로 있으면 처리할 수 있다. 이제 필요한 모든 밑재료는 갖춰진 셈이고, 요리만 잘하면 된다.
1) 리팩터링
일단 첫 번째는 리팩터링을 통해서 앱의 구조를 잡아 놓았다는 것이다.
Python의 최상위 asyncio.run을 기준으로 그냥 main을 3개 잡아놓고 대충 function으로 넣고 돌리는 식...이었는데 이때 불편한 점은 크게 2가지, 파생되어 불편한 점도 2가지였다.
1. python의 __init__에서 바로 await를 이용할 수 없으므로, 별도의 init함수를 만들고 직접 호출해야 한다.
1-1. 이것으로 SQL connection, Httpx Client를 한 곳에서 할당하고 제어하기 어려워진다.
2. Control + C로 앱을 멈추고자 할 때, ShutdownFlag객체를 이용해서 앱의 동작 정지를 전파하는데 필요한 동작이 달라 API가 난잡해진다.
2-1. 이것 때문에 동작제어코드를 넣어야 하는데 (while is_running()등...) 인덴테이션이 들어가서 코드를 읽는데 피로감이 증가한다.
이를 개선하기 위해서 크게 패턴을 2개 도입하였다.
1. 제한 Resource의 Singleton화
아까 전에 이야기했듯이 SQL Connection, Httpx Client 등의 Resouce 등은 언제나 생성/소멸시킬 수 있는 자원이 아니라, 앱이 지속적으로 이용해야 하고 관리해야 하는 대상이다. 이 중에서 SQL Connection의 경우를 싱글톤으로 만들었다. 솔직히 값싼 방법이긴 한데, 그럼에도 이렇게 관리를 해야겠다 싶었던 이유는 async with 구문을 이용한 context로 conn을 불러와야 하는 곳이 꽤 많기 때문이다. 내 앱은 DB를 Queue로써 이용하고 있고, 컨트롤러는 물론이고 Transaction 생성을 위해서 뜬금없이 conn이 필요한 곳이 꽤나 많다. 그게 아니더라도 객체 자체에 self.storage에 할당을 해줘야 하는 경우도 많고... 그런데 init을 통해서 그것을 일일이 모든 필요한 객체에 전파하려고 들면 React의 props propagation 같은 골 아픈 구조가 생긴다. 코드도 쓸데없이 늘어나는 경우도 있고.
세줄 요약은 :
1. DB conneciton은 매우 자주 필요하다.
2. 이걸 일일이 argument로 propagation 하는 것도 귀찮거니와,
3. 그렇게 하면 너무 코드가 늘어난다.
1-1. with_conneciton decorator의 도입
그렇다고 컨트롤러(MVC의 C)에서도 맨날 await PostgreSQLStorage.instnace()를 불러다가 async with를 쓰는 건 별로 바람직하지는 못하다는 생각이 들었다. 그래서 with_connection이라는 데코레이터를 통해서 SQL이 실행될 때 언제나 conn이 있음을 보장하였다.
import functools
def with_connection(func):
@functools.wraps(func)
async def wrapper(self, *args, **kwargs):
exisiting_conn = kwargs.get('conn')
if existing_conn:
return await func(self, *args, **kwargs)
else:
async with self.storage.pool.acquire() as conn:
kwargs['conn'] = conn
try:
return await func(*args, **kwargs)
finally: #prevents side-effects
kwargs.pop('conn', None)
return wrapper
대충 이런 식이다. 이걸 이제 컨트롤러의 함수 앞에 붙여두기만 하면 로깅 + connection이 자동적으로 된다. 눈치 빠른 사람들은 conn이 상위에서 주어진다면 여기서 생성을 하지 않는다는 것도 알 수 있을 것이다. 실제로 transaction안에서 굴리거나 계속 커넥션을 1개 이상 소비하는 worker의 경우, 괜히 더 connection을 잡기보다는 상위에서 받은 것을 기반으로 바로 sql을 실행시키는 것이 바람직하다. 결론적으로는 connection이 상위에 없는 경우에도 생성해서 실행을 보장하고, 있는 경우에는 그 connection을 활용해서 자원을 아끼는 것을 자동적으로 할 수 있다.
1-2. init 함수의 일상화, Singleton패턴에 욱여넣기
이건 await을 쓰다 보니 자연적으로 그렇게 하게 된 것에 가깝다. 거의 대부분의 주요한 객체가 await문이 필요한 초기화가 필요한데, 그렇게 하려면 어쩔 수 없이( __init__()에서 async를 못쓰니까),
obj = SomeGreatObject()
await obj.init()
와 같은 방법으로 초기화를 하게 된다. 개 빡치지 않은가? 고작 초기화 주제에 두 줄씩이나 잡아먹고 있다. 물론 init에 자신을 돌려주는 return self와 같이 짜도 좋긴 하다. 하지만 Singleton이면 이걸 더 잘 활용할 수 있다. 어차피 instance() 같은 메서드들은 클래스 메소드 이거니와 async여도 전혀 문제가 없다! 따라서 ABC를 걸어두고 @abstractmethod async init()과 같이 틀을 짜주는 것이다. 그럼 1. 어차피 init코드를 짜게 되는데 이게 구조로 강제되니까 체크가 생겨서 좋고 2. return self를 하지 않아도 문제가 없다.
여기까지는 범용적인 Singleton코드 이야기이다. 하지만 컨트롤러에서는 더욱 날먹 코드를 작성하기 위해서 더 나태해져도 좋다. 예를 들어 원래는 Controller는 schema를 init 하는 SQL이 모여있는 initalize_schema라는 메서드를 작성했다. 이때 다른 타 객체인 PostgreSQLStorage에 실제 db 조작코드가 있으므로(커넥션 제어), 그 객체를 직접 init 해줘야 SQL을 실행할 수 있었다. 이젠 이것을 더 숨길 수 있다. 어차피 init 하는 김에 storage 넣어주고 initalize_schema 해주면 더 좋은 것이다.
1-3. 번외) Loop-Aware Singelton
메인 앱에서는 모든 작업이 최상단의 asyncio.run() 안에서 이루어져서 문제가 없었는데, test의 경우는 그렇지 않으므로 이것을 감안해야 한다. 커넥션은 그 loop안에서만 유효하다.
2. Master -> WorkerFrame 위계 도입, ayncio gathering

ppt로 대충 현재 앱의 구조를 그려보았다.
기본적으로는 밖의 서비스를 지속적으로 호출해서 데이터를 긁어보고, 일단 PostgreSQL에 넣는다.
Refiner는 TimescaleDB에 알맞게 데이터를 추출/가공해서 의미 있는 자료로 만들고
Archiver는 이전의 Raw Data를 파일로 압축해서 저장한다... 와 같은 식이다.
이전에는 Master / Worker와 같은 구분 없이 일단 asyncio gather에만 의존했는데, 이젠 더 체계가 잡혔다고 보면 된다.
앞서 이야기했듯이, 문제는 Worker가 제때 종료되지 않는 것이 컸다. 그래서 WorkerFrame이라는 AbstractClass를 작성하고, init / run / close과 같이 필요한 로직을 짜는 것을 강요했다. 특히 파생클래스에서 run내부의 로직에 ShutdownFlag를 지속적으로 체크하는 핵심 로직만을 코딩한 WorkerFrame을 다시 AbstractClass로써 내보냈는다. 핵심 로직의 동작을 보전하고, 자식 클래스는 구체적인 Controller를 이용한 조작만을 담당하면 되고, 각 Master별로 필요한 로직이 어느 정도 통일 되기 때문에 괜히 머리 아프게 모든 걸 한 번에 처리할 필요가 없어진 것이다.
Master는, Worker를 init 하고 아직 실행되지 않은 asyncio task를 반환해서 최상위 루프에서 gather를 한 번만 실행해서 모든 Worker를 일괄적으로 돌릴 수 있게 만들었다. 앱의 흐름을 init -> get_task -> loop -> interrupt -> close로 정의할 수 있게 된다.
따라서 책임 분배를 이렇게 한 것이다.
Master: 각 분야에 필요한 자원 배분 및 Worker 생성, Task 반환, close 코드 호출
WorkerFrame(Parent): ShutdownFlag를 지속적으로 체크하면서 중단 가능한 핵심 동작 로직을 미리 코딩
Worker(Child): 구체적인 동작을 구현, Controller, Provider를 이용한 통신을 수행하고 상위 Worker로 결과를 반환
Storage관련 코드는 작업 도구로써 쓰기 위해서 Singleton화 하되, 나머지 실제 동작 로직은 Tree형태로 구현한 꼴이 되었다. 뭐 이게 좋은 아키텍처인지는... 흠 일단 잘 동작하니까 패스 아닐까?
2) TroubleShooting
이렇게 바꾸면서 맞닥뜨린 첫 번째 문제는 인덱싱을 너무 과하게 한 나머지 Refiner의 동작이 너무 느려져 버린 것이다.... 제아무리 Partial 인덱싱이라도 여러 개를 거는 것은 무리라고 느꼈고, 지금 당장 필요하지 않은 모든 인덱싱을 제거했다. 그러니까 구체적으로는 12분 걸리던 작업이 약 8분 정도로 줄어들었다.

그것만으로는 그리고 원래는 이것 때문에 구현했던 건 아니지만, transaction 시작 -> temp table을 생성 -> copy protocol로 밀어 넣고 -> 다시 insert 하는 방법도 써보았다. 이렇게 구현하면 map을 써서 insert를 직접 하는 것보다는 더 빠르게 데이터 적재가 가능하다는 점이 좋고, ON CONFILCT핸들링이 쉬워진다... 만. 실제로는 약 8분 -> 6분 정도로 줄어든 걸로 봐서는 원래 무거운 작업이었나 싶다. 그냥 COPY 해서 넣는 것은 겹칠 위험이 있는 경우는 쓸 수 없고, 실제로도 UPSERT작업이 필요한 게 각 address별 stat테이블이라, 어쩔 수 없이 시간이 걸린 듯하다. 뭐 대략 12만 건 UPSERT에 6분이면... 흠... 괜찮은 건가? 그럼 2만 건에 1분? 왜 이렇게 Troughput이 잘 나오진 않는 걸까... 물론 2배 증가긴 한데, 이유는 좀 더 알아보는 게 좋을 것 같다는 생각이 든다.
다행히 이 문제는 크게 걱정하지 않아도 되는데, 당시는 잠시 Refiner가 동작하지 않아 한번에 1000개의 window를 처리하느냐고 그렇게 되었고, 자주 동작하는 경우에는 타임아웃을 걱정할 정도로 오래 transaction을 처리하지는 않는다.
두 번째 문제는 내가 db를 운영하는 ssd가 뜨거워지는데 반해 제대로 쿨링이 안된다는 점이었다. 뭔가 코딩하다가 갑자기 현실로 끌려 나온 기분이긴 한데, 이것도 고려해 줄 수밖에 없다.

80도면 꺼지는 SSD인데 77도는 야랄
m.2 sdd에 케이스 끼운 거라 가지고, 발열 문제가 꽤 심했고, 잘못하면 데이터에 손상이 가해질 수 있었던 차라 오히려 최근 다운타임이 길었던 게 다행으로 느껴질 지경이었다. 다행히도 설루션은 내 손안에 전부 있어서 금방 해결했다. 팬이 없는 외장 케이스인 이렇게 생긴 encolsure를 사용했는데(뒷광고 아니냐고 할까 봐 구체적인 건 언급하지 않으련다...)

이걸 살 때 같이 제공받은 ssd 전용 방열판 때문에 별 문제가 없을 줄 알았는데, 알고 보니 그 방열판 만으로는 발열 해소가 턱없이 부족했다. 문제는 내부의 ssd방열판 -> 외부 알루미늄 케이스로 열전도가 전~혀 안된다.

대충 이런 것이다. 근데 제공해 주는 서멀 패드로는 일단 높이가 부족해서 어떻게 할까 싶었는데, 다행히 부서진 그래픽카드 한 개가 우리 집에 있어서 거기에 있던 패드를 대충 덮어서 발열을 해소했다.

완벽한 건 아니고 여전히 60도가 찍힐 때도 있는데, 애초에 70도 가까이만 안 가면 되므로 초 만만세이다. 지금도 2시간 넘게 돌렸는데 50도 대에서만 머물고 있으므로 문제는 없어 보인다...라고 하기 무섭게 지금 60도대인데 벌 받는 기분이다. 이건 나중에 또 문제가 생기면 그때 살펴보는 게 좋을 것 같다.
3) 성과
그래서 결과적으로는 이만큼 데이터를 모으긴 했다.


구체적으로는 4억 건 가까지 모았으니까 뭐 분석하기에는 남아돌 것으로 보인다. 지금은 라벨 데이터를 어떻게 정제하고 classification label set를 만들어 낼 것인가에 대한 고민을 하고 있다. 결국은 학습을 시킬 타깃 데이터 생성을 해야 하는데, 효율적으로 이를 만들어 내는 방법을 생각해 두었다. 실험결과가 나오는 대로, 또 블로그에 올려두면 좋을 것 같다.
'일지 > Proj4' 카테고리의 다른 글
| [QuakePot] Docker Desktop vs OrbStack(feat. edge-case block) (0) | 2026.01.31 |
|---|---|
| [QuakePot, API Crawling, Infrastructure] 데이터 수집 결과 2 (0) | 2026.01.06 |
| [QuakePot] 데이터 수집 asyncronous 문제 (0) | 2025.12.20 |