홈서버에 n8n 설치: 자동화의 유연함과 Python 전환기

약 15분 읽기

홈서버에 n8n을 올린 이유

홈서버에 n8n을 올린 첫 번째 이유는 자동화의 유연성과 편리함 때문이다. n8n은 다양한 노드를 지원하여 여러 가지 작업을 손쉽게 조합할 수 있다. 예를 들어 웹훅을 통해 외부 이벤트를 받아오거나, 주기적으로 특정 작업을 수행할 수 있는 크론 스케줄 기능이 그 예다. 이러한 기능들은 별도로 코드를 작성하지 않고도 빠르게 설정할 수 있어 큰 매력으로 다가왔다.

또한, n8n의 시각적인 인터페이스는 워크플로우를 구성하는 데 큰 장점을 제공했다. 각 노드의 연결 관계와 데이터 흐름을 한눈에 파악할 수 있었고, 필요에 따라 쉽게 수정할 수 있었다. 이는 특히 복잡한 워크플로우를 다룰 때 유용했다. 데이터의 흐름을 시각적으로 이해하면서 필요한 부분만 수정하거나 추가하는 작업이 직관적이었다.

자동화 작업의 실행 이력을 남길 수 있다는 점도 무시할 수 없는 장점이었다. 실행 로그를 통해 언제 어떤 작업이 수행되었고, 그 결과가 무엇이었는지를 쉽게 확인할 수 있었다. 이는 자동화 작업이 올바르게 작동하는지를 모니터링하고, 문제가 발생했을 때 이를 신속하게 수정하는 데 큰 도움이 됐다.

마지막으로, n8n은 오픈소스 프로젝트로서 커스터마이즈가 용이했다. 필요에 따라 직접 노드를 추가하거나, 기존의 노드를 수정하여 사용할 수 있었다. 이러한 유연성은 다양한 자동화 요구사항을 충족시키기에 충분했고, 초기 설정 단계에서부터 n8n을 선택하게 만든 주된 요인이었다.

SSL 갱신은 아직 n8n으로 돈다

초기에는 다양한 자동화 작업을 위해 n8n을 활용했지만, LLM 관련 작업을 하면서 몇 가지 큰 문제에 부딪혔다. 특히, HTTP Request 노드에서 중첩 JSON을 잘못 처리하거나, chainLlm 노드에서 SystemMessage 설정이 버전마다 달라지는 등의 문제는 큰 골칫거리였다. 이런 문제를 극복하면서도 SSL 인증서 갱신 작업은 여전히 n8n을 통해 처리하고 있다. SSL 인증서는 서버 보안에 필수적인 요소이고, 만료 기간을 놓치면 큰 문제가 될 수 있다. 그래서 안정적으로 갱신될 수 있는 환경을 유지하는 것이 중요했다.

가장 안정적으로 작동하는 부분 중 하나가 Cron 스케줄링 기능이다. SSL 인증서 갱신은 주기적으로 certbot을 실행해 자동으로 갱신을 시도하고, 그 결과를 텔레그램으로 전송하는 방식으로 설계했다. n8n의 Cron 노드는 UI를 통해 손쉽게 설정할 수 있어, SSL 갱신 시점이 다가오면 자동으로 명령을 실행하도록 만들었다. 이러한 구조는 시간을 절약하고, 수작업의 실수를 줄일 수 있어 만족스럽다.

{
  "name": "ssl-renew-check",
  "nodes": [
    {
      "parameters": {
        "rule": { "interval": [{ "field": "cronExpression",
                                 "expression": "0 4 1,15 * *" }] }
      },
      "name": "매월 1일·15일 04시",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [240, 300]
    },
    {
      "parameters": {
        "command": "certbot renew --quiet --deploy-hook 'nginx -s reload'"
      },
      "name": "certbot renew",
      "type": "n8n-nodes-base.executeCommand",
      "position": [460, 300]
    },
    {
      "parameters": {
        "chatId": "={{ $env.TELEGRAM_CHAT_ID }}",
        "text": "=SSL 갱신 결과nexit={{ $json.exitCode }}n{{ $json.stdout }}"
      },
      "name": "결과 알림",
      "type": "n8n-nodes-base.telegram",
      "position": [680, 300]
    }
  ],
  "connections": {
    "매월 1일·15일 04시": { "main": [[{ "node": "certbot renew", "type": "main", "index": 0 }]] },
    "certbot renew":     { "main": [[{ "node": "결과 알림",     "type": "main", "index": 0 }]] }
  }
}

SSL 갱신은 아직도 n8n으로 돈다 (Cron → Execute Command → Telegram)

Execute Command 노드는 서버 내에서 직접 명령어를 실행할 수 있어서 certbot과 같은 CLI 도구와 잘 연동된다. 또한, 텔레그램 알림을 통해 갱신 여부를 즉각적으로 확인할 수 있다는 점도 큰 장점이다. 모든 작업의 결과가 기록으로 남기 때문에 문제 발생 시 원인을 추적하기도 용이하다. 이러한 구성 덕분에, SSL 인증서 갱신 작업만큼은 변함없이 안정적으로 운영되고 있다. 나머지 작업을 Python으로 옮겼지만, 이 부분은 굳이 바꿀 이유가 없었다.

LLM을 붙이면서 막힌 것들

n8n에서 LLM을 붙이려다 보니 몇 가지 문제에 부딪혔다. 처음에는 ChainLlm 노드에 SystemMessage를 전달하는 부분에서 막혔다. 버전마다 설정 방법이 달라지다 보니, 메시지를 messages.messageValues 배열에 넣는 과정이 까다로웠다. 문서도 명확하지 않아서, 처음 시도해본 방식들이 계속 실패로 돌아갔다. 결국은 커뮤니티 포럼과 여러 시도를 통해 해결할 수 있었다.

그러나 문제는 여기서 끝이 아니었다. HTTP Request 노드로 직접 API를 호출할 때 specifyBody를 사용했는데, 이 설정이 중첩된 JSON 구조를 제대로 처리하지 못했다. 키-값 쌍으로 설정하면 데이터가 깨지는 현상이 발생했다. 이러한 문제를 해결하기 위해 JSON으로 변환한 후, jsonBody에 문자열로 넣는 방식을 시도했다. 이 과정은 예상보다 많은 시간을 소모했다.

// X — keypair로 두면 messages 배열이 문자열로 뭉개진다
{
  "parameters": {
    "method": "POST",
    "url": "https://api.openai.com/v1/chat/completions",
    "sendBody": true,
    "specifyBody": "keypair",
    "bodyParameters": {
      "parameters": [
        { "name": "model", "value": "gpt-4o-mini" },
        { "name": "messages", "value": "[{"role":"user"...}]" }
      ]
    }
  }
}

// O — json으로 바꾸고 jsonBody에 통째로 넣어야 한다
{
  "parameters": {
    "method": "POST",
    "url": "https://api.openai.com/v1/chat/completions",
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({n  model: 'gpt-4o-mini',n  response_format: { type: 'json_object' },n  messages: [n    { role: 'system', content: $json.systemPrompt },n    { role: 'user',   content: $json.userPrompt }n  ]n}) }}",
    "options": { "timeout": 180000 }
  }
}

specifyBody를 keypair로 두면 중첩 JSON이 깨진다

API 요청 시, specifyBody를 키-값 쌍으로 둘 경우 중첩 JSON 구조가 손상된다는 점이 발견되었다. 이를 해결하기 위해 JSON 포맷으로 데이터를 변환하고, 그 결과를 문자열로 jsonBody에 할당했다. 이렇게 함으로써 API 호출이 정상적으로 이루어졌고, 문제도 해결되었다. 이상 없이 작동하는 것을 확인하니 한시름 놓을 수 있었다. 이런 시행착오가 있었지만, 결과적으로 안정적인 LLM 통합이 가능해졌다.

워크플로우 20개를 날리고 DB 복구를 시도한 날

n8n을 사용하면서 겪은 가장 큰 문제 중 하나는 컨테이너 재생성 시 볼륨 설정을 잘못 잡았던 것이다. 그 결과, 저장되어 있던 모든 워크플로우가 사라졌다. 당시엔 기본 SQLite 설정을 사용하고 있었는데, 이로 인해 20개의 중요한 워크플로우를 잃게 되었다. 이런 상황에서 복구가 가능한지 알아보기 위해 직접 데이터베이스에 접근해 복구를 시도했다.

workflow_entity 테이블에 INSERT 문을 사용하여 기존 워크플로우 데이터를 다시 넣어보는 방법을 선택했다. 노드 정보는 JSON 형태로 nodes 컬럼에 저장되어 있었으며, connections까지 모두 맞춰야 하기 때문에 꽤 복잡한 작업이었다. 시간과 노력이 많이 들었지만, 그래도 포기하기엔 아까운 워크플로우들이었다.

-- n8n이 SQLite 기본 설정이라 볼륨을 잘못 잡으면 그대로 사라진다.
-- workflow_entity 에 직접 넣어서 복구를 시도했다.
INSERT INTO workflow_entity
  (id, name, active, nodes, connections, settings, "createdAt", "updatedAt")
VALUES
  ('recovered-01', 'blog-publish', false,
   '[{"parameters":{...},"name":"Cron","type":"n8n-nodes-base.cron",
      "position":[240,300],"typeVersion":1}]'::json,
   '{"Cron":{"main":[[{"node":"HTTP Request","type":"main","index":0}]]}}'::json,
   '{"executionOrder":"v1"}'::json, NOW(), NOW());

-- 문제: nodes 의 position, typeVersion, credentials 참조까지 다 맞아야
-- UI에서 열린다. connections 의 노드 이름이 nodes 의 name 과 정확히
-- 같아야 하고 하나만 틀려도 화면이 비어 보인다.
-- 20개를 이렇게 복구하는 건 손으로 다시 만드는 것보다 느렸다.

-- 그 뒤로 postgres 로 옮기고 매일 덤프를 뜬다
-- DB_TYPE=postgresdb / DB_POSTGRESDB_HOST=n8n-postgres

날린 워크플로우를 DB에서 살려보려던 시도

복구 시도는 그리 간단하지 않았다. 데이터베이스에 워크플로우 정보를 직접 넣는 것은 UI에서 열리는 것까지는 성공했지만, 실제로 실행하려면 nodes의 position, typeVersion, credentials 참조 등의 세부 정보까지 정확히 맞아야 했다. 문제는 여기서 끝나지 않았다. connections에서의 노드 이름까지 일일이 맞춰야 실행이 가능했기 때문이다. 1개의 워크플로우를 복구하는 데에만 30분 이상이 걸렸고, 20개 전부를 복구하는 것은 사실상 불가능했다. 결국, 모든 것을 포기하고 손으로 다시 만드는 쪽을 택했다.

postgres로 옮기고 백업을 걸었다

워크플로우를 복구하는 데 드는 시간이 너무 길어서, 결국 데이터베이스를 PostgreSQL로 옮기기로 결심했다. SQLite는 간단한 시스템에는 적합했지만, 대량의 워크플로우를 관리하기에는 한계가 있었다. PostgreSQL로 전환하면서 데이터 관리의 유연성과 안정성을 확보할 수 있었고, 이는 장기적으로 더 나은 선택이었다는 생각이 들었다. 데이터베이스 이전 작업은 생각보다 간단했다. n8n의 설정 파일을 수정해 PostgreSQL 데이터베이스를 사용하도록 하고, 기존의 SQLite 데이터를 덤프해서 PostgreSQL로 가져왔다.

이전이 완료된 후에는 백업 시스템을 설정했다. PostgreSQL의 장점 중 하나는 다양한 백업 옵션을 제공한다는 것이다. 정기적인 백업 스케줄을 작성하고, 만약의 사태에 대비할 수 있도록 했다. 이렇게 대비를 하니, 이전처럼 컨테이너 재생성 시 실수로 데이터를 잃어버리는 상황을 방지할 수 있었다. 더욱이, 이 과정에서 얻은 가장 큰 장점은 데이터의 안정성 뿐만 아니라 관리의 용이성이다. PostgreSQL은 트랜잭션 관리와 데이터 무결성에서 뛰어나, 데이터를 신뢰할 수 있었다.

또한 백업 파일은 클라우드에 자동으로 업로드되도록 설정했다. 이렇게 해서 로컬 데이터 파손 시에도 클라우드에서 복구할 수 있는 이중 안전장치를 마련했다. 이런 준비 덕분에, 앞으로의 운영에서 데이터 손실로 인해 발생할 수 있는 문제를 사전에 예방할 수 있게 되었다. 데이터베이스 이전과 백업 시스템 구축은 일련의 문제를 해결하기 위한 결정적인 조치였다. 이제는 이전에 겪었던 문제를 반복하지 않을 자신이 생겼다. PostgreSQL과 백업 시스템 덕분에, 데이터를 더욱 안전하게 관리할 수 있었다.

무엇을 n8n에 남기고 무엇을 옮겼나

n8n에 남긴 작업들은 주로 복잡하지 않으면서 안정적으로 돌아가는 것들이다. 예를 들어, SSL 인증서 갱신이나 서버 디스크 용량 경고, 백업 성공 및 실패 알림 같은 것들이 있다. 이들은 모두 트리거, 명령 실행, 텔레그램 알림의 구조를 가지며, 설정해 놓으면 별다른 수정 없이 계속 사용할 수 있다. Python으로 전환할 필요가 없는 부분들이다. n8n의 UI와 로그 기능만으로도 충분히 관리가 가능하다.

반면, 블로그 자동발행과 같이 디버깅이 자주 필요한 작업들은 과감히 Python으로 옮겼다. n8n에서의 디버깅은 UI를 통해 클릭해야만 가능하고, 코드의 변화를 추적하기 어려웠기 때문이다. Python에서는 로그를 남기고 버전 관리를 통해 변화 내역을 쉽게 파악할 수 있으며, 테스트 코드 작성이 용이하다. 이런 점이 코드를 수정하거나 문제가 발생했을 때 빠르게 대응할 수 있게 해준다.

LLM과 관련된 작업도 Python으로 옮겼다. n8n에서 SystemMessage 설정이 버전마다 다르게 동작해 혼란을 겪었고, HTTP Request 노드에서 JSON 처리 문제로 반나절을 소비했다. Python에서는 이러한 복잡한 설정을 코드로 명시적으로 작성할 수 있어, 작업의 일관성을 유지할 수 있다. Python은 직관적으로 코드 레벨에서 접근하기에 적합했다.

결국, n8n에서는 손이 많이 안 가는 작업들만 남기고, 디버깅과 유지 보수가 빈번히 필요한 부분들은 Python으로 이동했다. 워크플로우를 보다 안정적으로 관리할 수 있는 방향으로 선택한 결과다. n8n과 Python이라는 두 도구의 장점과 한계를 이해하니, 자연스럽게 어떤 작업을 어디에 두어야 할지 판단이 섰다.

황민

황민 (Hwang Min)

IT·RPA·AI 분야 개발자. 웹앱 개발, UiPath RPA, n8n 자동화 실무 경력 4년. AI·금융·IT 트렌드를 현장 개발자 시각으로 분석합니다.

댓글 남기기

𝕏fin