Joyuno's Dev Blog

2026-04-07 (화) 개발일지

dev-blog

오늘의 TIL: 소명 처리 시스템 확장과 디스코드 봇 충돌 해결

오늘은 세 가지 프로젝트를 왔다갔다 하면서 여러 문제를 해결한 하루였습니다. 가장 많은 시간을 쓴 건 소명 처리 시스템의 테넌트 계층 구조 구현이었고, 가장 짜증났던 건 디스코드 봇이 멋대로 중복 실행되는 문제였네요.

테넌트 트리 API의 라우트 충돌

문제: SUPER_ADMIN으로 로그인했는데 테넌트 트리 전체를 볼 수 없었습니다. 프론트에서 GET /tenants/tree를 호출하면 계속 404 에러가 나왔어요.

시도: 백엔드 코드를 확인해보니 /{tenant_id}/tree 엔드포인트만 있고 전체 트리를 위한 엔드포인트가 없었습니다. 그래서 GET /tenants/tree를 추가했는데, 이게 /{tenant_id} 라우트에 걸려버리는 문제가 발생했습니다. FastAPI가 tree를 UUID로 파싱하려고 시도하다가 실패하면서 404를 뱉는 거였죠.

해결: 라우트 순서가 문제였습니다. FastAPI는 위에서부터 매칭하기 때문에 /{tenant_id}보다 위에 GET /tenants/tree를 등록해야 했습니다. 간단한 순서 변경이지만 30분은 헤맸네요.

# 잘못된 순서
@app.get("/tenants/{tenant_id}")
@app.get("/tenants/tree")

# 올바른 순서  
@app.get("/tenants/tree")
@app.get("/tenants/{tenant_id}")

배운 점: FastAPI 라우트 순서가 중요하다는 걸 다시금 깨달았습니다. 동적 경로(/{id})보다 정적 경로(/tree)를 먼저 정의해야 한다는 기본 원칙을 잊고 있었어요.

디스코드 MCP 서버 중복 실행 문제

문제: 자동매매 프로젝트의 디스코드 봇이 메시지를 받아도 Claude가 자동으로 응답하지 않았습니다. 가끔 되다가 안 되고, 로그를 보면 메시지가 중복 처리되는 것 같았어요.

시도: 처음에는 채널 권한 문제인 줄 알았는데, access.json 설정은 정상이었습니다. notification은 정상 전송되는데 Claude가 “깨어나지” 않는 현상이라서 이상했죠.

해결: ps aux로 프로세스를 확인해보니 같은 디스코드 MCP 서버가 2개 실행 중이었습니다! PID 19501과 PID 27056로 두 개의 터미널에서 각각 실행되고 있었어요. 같은 봇 토큰으로 두 서버가 Discord 게이트웨이에 동시에 연결되어 충돌을 일으키고 있었습니다.

근본 원인: Claude 데스크톱 앱을 열어보니 10개 이상의 세션이 동시에 실행 중이었고, Discord 플러그인이 전역 설정으로 활성화되어 있어서 새 세션마다 MCP 서버를 띄우려고 했던 거였습니다.

# 모든 디스코드 MCP 서버 죽이기
kill 19501 27056

# 플러그인 전역 비활성화
~/.claude/settings.json에서 discord 플러그인 비활성화

배운 점: MCP 서버는 프로세스 기반이라 중복 실행을 방지하는 메커니즘이 없습니다. 특히 전역 플러그인을 사용할 때는 사용하지 않는 Claude 세션을 정리하는 습관이 필요하겠네요.

테넌트 계층 구조와 권한 시스템 확장

문제: 기존 시스템이 단일 테넌트만 지원해서 SUPER_ADMIN이 모든 데이터를, TENANT_ADMIN이 자기 테넌트만 볼 수 있었습니다. 하지만 고객사 요구로 ROOT → PARENT → BRANCH의 3계층 구조가 필요해졌어요.

시도:

  1. tenant.py 모델에 parent_id(자기참조 FK), tenant_type, site_mapping 컬럼 추가
  2. UserRole에 TENANT_ADMIN, BRANCH_ADMIN 추가
  3. 트리 조회를 위한 tenant_tree.py 서비스 생성
  4. 프론트에 TenantTreeView.tsx, TenantSwitcher.tsx 컴포넌트 추가

해결: 가장 까다로웠던 건 권한 체계였습니다. TENANT_ADMIN은 자기 테넌트 + 모든 하위 테넌트를 관리할 수 있어야 했죠. get_tenant_scope() 함수로 BFS 방식으로 모든 하위 테넌트 ID를 수집하고, 미들웨어에서 db.info["tenant_scope"]에 설정하는 방식으로 해결했습니다.

배운 점: 계층형 권한 시스템을 설계할 때는 “자신 + 모든 하위” 접근 패턴이 자주 필요합니다. DB에서 재귀 쿼리보다 메모리에서 BFS로 처리하는 게 성능상 더 나을 수 있다는 걸 배웠어요.

로그프레소 리포트 API 연동

문제: 로그프레소에는 리포트 상세 내용(항목 1,2,3과 쿼리문)이 있는데 우리 시스템에서는 기본 정보만 가져왔습니다.

시도: 로그프레소 API 문서를 다시 확인하고, LogpressoConnector 클래스에 리포트 관련 메서드 5개를 추가했습니다. get_reports(), get_report_details(), get_report_queries() 등이 필요했죠.

해결: 리포트 타입에 LOGPRESSO_REPORT = "LOGPRESSO_REPORT"를 추가하고, 프론트에 LogpressoReportTab.tsx 탭을 생성해서 내부 리포트와 로그프레소 리포트를 분리해서 표시했습니다.

배운 점: 외부 시스템 연동시 그들이 제공하는 모든 데이터 필드를 확인해야 합니다. 우리가 필요한 데이터가 API에 있는지 없는지 먼저 확인하는 절차를 체크리스트로 만들어야겠어요.

UI 디자인 시스템 개선

문제: 고객사에서 “UI가 너무 허접하다”는 피드백을 받았습니다. 특히 테넌트 트리 시각화가 기본적인 노드와 선으로만 되어 있어서 전문성 있어 보이지 않았죠.

시도:

  1. global.css에서 sky 색상 팔레트를 네이비/블루 기업 SaaS 스타일로 교체
  2. Pretendard Variable 폰트 CDN 추가
  3. Shadcn/ui 라이브러리 패턴 적용
  4. 트리 노드에 타입별 배지(ROOT/PARENT/BRANCH)와 상태 점 추가

해결: 디자인 시스템을 일관되게 적용하는 게 핵심이었습니다. Layout/index.tsx에서 모든 sky-* 클래스를 blue-*, gray-* 계열로 일괄 교체하고, 컴포넌트별로 일관된 스타일 가이드를 적용했습니다.

배운 점: 디자인 시스템은 점진적 개선보다는 일괄 전환이 더 효과적일 수 있습니다. 사용자에게 “변했다”는 인상을 주려면 부분적 개선보다는 통일된 변화가 필요하더라구요.

오늘의 한줄

“라우트 순서 하나가 30분을 잡아먹고, 디스코드 봇 두 마리가 서로 싸우는 걸 보니 소프트웨어 개발은 때로 예상치 못한 곳에서 문제가 터지는 법이더라.”