
데몬(Daemon) 프로세스: 보이지 않는 일꾼
악마(Demon)가 아닙니다. 그리스 신화의 '수호신'입니다. 백그라운드에서 묵묵히 일하는 서버의 영웅들.

악마(Demon)가 아닙니다. 그리스 신화의 '수호신'입니다. 백그라운드에서 묵묵히 일하는 서버의 영웅들.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

나는 비전공 창업자로 첫 프로젝트를 만들 때, AWS EC2에 챗봇 하나를 배포하려고 했다. Python으로 짠 디스코드 봇이었는데, SSH로 접속해서 python bot.py를 실행하면 잘 돌아갔다. 성공적이었다. 만족스러웠다.
그런데 문제는 내가 SSH 연결을 끊는 순간 봇이 죽어버린다는 거였다. 터미널을 닫으면 프로세스도 같이 사라졌다. 처음엔 코드 문제인 줄 알았다. 로그를 찾아봤고, 에러가 없었다. 그냥 "조용히 종료"됐다. 아무 이유 없이.
나는 당시 "서버는 24시간 돌아야 한다"는 건 알았지만, "왜 내가 SSH를 끊으면 프로그램이 죽는지"는 몰랐다. 그게 운영체제의 프로세스 관리 메커니즘 때문이라는 것, 터미널과 프로세스의 생명 주기가 연결되어 있다는 것을 그때 처음 알았다.
해결책을 찾다가 마주친 단어가 Daemon(데몬)이었다. 그 순간, 나는 "아, 내가 지금까지 포그라운드 프로세스를 백그라운드에서 돌리려고 했던 거구나"라는 걸 깨달았다. 데몬은 그냥 백그라운드 프로그램이 아니었다. 터미널에서 독립된, 영구적으로 살아있는 프로세스였다.
처음 데몬을 접했을 때, 몇 가지가 너무 헷갈렸다.
첫째, 이름이 무서웠다. systemd, sshd, httpd, mysqld. 전부 끝에 d가 붙어있었고, 나는 이게 "Demon"인 줄 알았다. 악성 프로그램인가 싶었다. 나중에 알고 보니 그리스 신화의 Daemon(다이몬)이었다. 악마가 아니라 수호신, 사람을 몰래 돕는 보이지 않는 존재였다. MIT 개발자들이 붙인 이름이라고 한다. 이 비유가 너무 적절하게 느껴졌다.
둘째, 백그라운드 프로세스와 데몬의 차이. python bot.py &로 백그라운드에서 실행시키면 되는 거 아닌가? 싶었다. 실제로 시도해봤다. 역시 SSH를 끊으면 죽었다. 왜냐면 &는 단지 "터미널에서 입출력을 받지 않는 프로세스"로 만들 뿐, 터미널 세션에는 여전히 종속되어 있기 때문이었다. 터미널이 닫히면 SIGHUP 신호가 날아오고, 프로세스는 그대로 종료된다.
셋째, nohup과 screen의 존재. nohup python bot.py &를 쓰면 SSH를 끊어도 살아있었다. 신기했다. screen이나 tmux 같은 도구도 있었다. 이건 "가짜 터미널 세션"을 만들어서, 실제 SSH가 끊겨도 프로세스가 계속 돌게 해주는 거였다. 그럼 이게 데몬인가? 아니었다. 이건 임시방편이었다. 진짜 데몬은 시스템 레벨에서 관리되는, 서비스로 등록된 프로세스였다.
나는 이 모든 개념이 뒤섞여서 며칠 동안 혼란스러웠다. 결국 내가 받아들인 핵심은 이거였다: 데몬은 부모도 터미널도 없는, 시스템이 직접 관리하는 불멸의 프로세스다.
데몬을 이해하려면 먼저 "프로세스는 어떻게 죽는가"를 알아야 한다.
일반적으로 우리가 터미널에서 실행하는 프로그램은 세션(Session)에 속한다. 세션은 여러 프로세스 그룹을 묶는 단위인데, 터미널이 닫히면 커널이 그 세션에 속한 모든 프로세스에게 SIGHUP(Hangup) 신호를 보낸다. "터미널이 끊어졌으니 너도 끝내라"는 의미다.
데몬은 이 세션에서 완전히 벗어나야 한다. 그래야 터미널이 닫혀도 죽지 않는다. 이 과정을 "터미널로부터의 detach(분리)"라고 부른다.
내가 처음 이해했을 때 든 비유는 이거였다. 부모가 집을 나가면서 아이에게 "엄마 따라 나가"라고 하는 게 일반 프로세스다. 데몬은 "너는 이제 독립이야, 혼자 살아"라고 법적으로 성인 선언을 받은 프로세스다. 부모(터미널)가 사라져도 나(데몬)는 내 인생을 산다.
이 셋을 명확히 구분해보면:
&): 터미널 입출력을 안 받지만, 여전히 세션에 속함. 터미널이 닫히면 죽음.나는 이 차이를 명확히 받아들인 뒤에야, 왜 nohup이나 screen이 "완전한 해결책"이 아닌지 이해했다. 이것들은 데몬처럼 보이게 만드는 꼼수일 뿐, 진짜 데몬은 시스템에 등록되고, 부팅 시 자동으로 시작되며, systemd나 init이 관리하는 프로세스다.
데몬을 직접 만드는 법을 찾다가, "Double Fork Technique"라는 걸 봤다. 처음엔 뭔 소린가 싶었다. 왜 Fork(프로세스 복제)를 두 번이나 해야 하는가?
그런데 이 로직을 하나씩 뜯어보니, 너무 영리했다. 이건 운영체제의 프로세스 관리 규칙을 정확히 이용한 "합법적 탈출"이었다.
// 1단계: 첫 번째 Fork
pid_t pid = fork();
if (pid > 0) {
exit(0); // 부모 프로세스는 즉시 종료
}
// 이제 자식 프로세스만 남음
// 2단계: 새로운 세션 생성
setsid(); // 세션 리더가 되고, 터미널에서 완전히 분리
// 3단계: 두 번째 Fork
pid = fork();
if (pid > 0) {
exit(0); // 세션 리더도 종료
}
// 이제 "세션 리더가 아닌" 프로세스만 남음
// 4단계: 작업 디렉토리 변경
chdir("/");
// 5단계: 파일 디스크립터 닫기
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 이제 진짜 데몬이 됨
while (1) {
// 무한 루프로 서비스 제공
}
왜 첫 번째 Fork를 하는가? 부모 프로세스를 죽여서, 자식 프로세스를 orphan(고아)으로 만든다. 고아 프로세스는 자동으로 init 프로세스(PID 1)에게 입양된다. 이제 터미널이 아니라 init이 부모다. 터미널이 닫혀도 영향을 안 받는다.
왜 setsid()를 호출하는가?
setsid()는 새로운 세션(session)을 만들고, 호출한 프로세스를 세션 리더(session leader)로 만든다. 동시에 터미널과의 연결(controlling terminal)을 끊는다. 이제 프로세스는 터미널이 없다.
왜 두 번째 Fork를 하는가? 이게 제일 신기했다. 첫 번째 Fork만으로도 터미널에서 분리됐는데, 왜 또 Fork를 하는가? 이유는 "혹시라도 나중에 터미널을 다시 열 수 없게 하려고"였다. Unix 시스템에서는 세션 리더만이 터미널을 열 수 있다. 두 번째 Fork를 하면, 세션 리더(첫 번째 자식)는 죽고, 그 자식(두 번째 자식)만 남는데, 이 프로세스는 세션 리더가 아니므로 영원히 터미널을 열 수 없다. 완벽한 보험이다.
이 순간, 나는 "아, 이게 진짜 엔지니어링이구나"라고 느꼈다. 한 번의 fork로는 부족하다는 걸 알고, 두 번 fork해서 모든 가능성을 차단한 거였다.
이 개념들도 같이 정리해본다.
wait()를 안 불러서 시체(exit status)만 남은 프로세스. 자원은 안 먹지만 프로세스 테이블을 차지함.데몬을 만들 때 첫 번째 fork에서 부모를 죽이는 건, 일부러 자식을 orphan으로 만들기 위함이다. Zombie는 버그고, Orphan은 의도된 설계다.
Double Fork를 직접 코딩하는 건 2000년대 초반까지의 이야기다. 지금은 systemd라는 시스템 관리자가 모든 걸 대신 처리해준다.
내 Python 봇을 데몬으로 만들고 싶다면, 코드를 하나도 안 바꿔도 된다. 그냥 systemd unit file 하나만 작성하면 끝이다.
# /etc/systemd/system/discord-bot.service
[Unit]
Description=My Discord Bot Service
After=network.target
# network.target = 네트워크가 준비된 후 시작
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/bot
ExecStart=/usr/bin/python3 /home/ubuntu/bot/main.py
Restart=always
RestartSec=10
# 죽으면 10초 후 자동 재시작
# 환경 변수 설정
Environment="DISCORD_TOKEN=your_token_here"
# 로그 관리
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
# 부팅 시 자동 시작
이 파일을 /etc/systemd/system/에 넣고, 다음 명령어만 실행하면 된다:
sudo systemctl daemon-reload # systemd에게 새 파일 읽으라고 알림
sudo systemctl enable discord-bot # 부팅 시 자동 시작 설정
sudo systemctl start discord-bot # 지금 당장 시작
sudo systemctl status discord-bot # 상태 확인
이제 SSH를 끊어도, 서버를 재부팅해도, 봇은 자동으로 살아난다. Systemd가 알아서 데몬으로 만들어주고, 관리해주고, 죽으면 부활시켜준다.
Systemd는 내부적으로 Double Fork 같은 걸 자동으로 처리해준다. 개발자는 그냥 "어떤 프로그램을 실행할지"만 알려주면 된다.
특히 좋았던 건:
Restart=always는 진짜 생명 보험이다.journalctl -u discord-bot -f로 실시간 로그를 볼 수 있다. Syslog로 자동 전달된다.After=network.target으로 "네트워크 준비된 후 실행"을 보장한다.나는 이 방식을 알고 나서, 모든 서버 프로그램을 systemd로 관리하기 시작했다. Cron으로 돌리던 것도, nohup으로 돌리던 것도 전부 systemd unit file로 바꿨다.
리눅스 시스템에서 `ps aux | grep 'd를 실행하면 수십 개의 데몬이 보인다. 각자 고유한 임무가 있다.
모든 프로세스의 조상. 커널이 부팅할 때 제일 먼저 실행하는 프로세스다. 예전엔 init이 이 역할을 했지만, 요즘은 systemd가 대세다. 모든 orphan 프로세스의 양부모 역할을 한다.
SSH 접속을 기다리는 데몬. 22번 포트를 감시하고 있다가, 누군가 SSH 클라이언트로 접속하면 자식 프로세스를 fork해서 인증을 처리한다. 이 데몬이 없으면 원격 접속 자체가 불가능하다.
웹 서버 데몬. 80번(HTTP) 또는 443번(HTTPS) 포트를 듣고 있다가 HTTP 요청이 오면 응답한다. httpd는 Apache, nginx는 Nginx의 마스터 프로세스다.
데이터베이스 서버 데몬. SQL 쿼리를 받아서 처리한다. 이 친구들도 평생 돌면서 클라이언트 연결을 기다린다.
예약 작업(스케줄링) 데몬. 매분마다 깨어나서 "/etc/crontab"과 사용자 crontab을 확인하고, 실행할 게 있으면 실행한다. 백업 스크립트 같은 걸 자동화할 때 쓴다.
Systemd의 로그 수집 데몬. 모든 시스템 로그를 한곳에 모아서 저장한다. journalctl 명령어로 조회할 수 있다.
이 친구들은 전부 무한 루프로 대기 중이다. 기본 구조는 이렇다:
# 간단한 데몬 패턴 (의사 코드)
while True:
request = wait_for_request() # Blocking I/O
if request:
pid = fork()
if pid == 0:
handle_request(request)
exit(0)
else:
# 부모는 계속 대기
wait_for_child(pid) # 좀비 방지
데몬은 터미널 입출력이 없으니, 사용자가 직접 제어할 방법이 필요하다. 그게 바로 Signal(신호)이다.
kill -HUP <PID>systemctl stop 명령어가 이걸 보낸다.kill -9 <PID>데몬을 만들 때는 이런 신호를 처리하는 핸들러를 작성해야 한다.
import signal
import sys
def handle_sighup(signum, frame):
print("설정 파일 다시 읽기...")
reload_config()
def handle_sigterm(signum, frame):
print("정상 종료 시작...")
cleanup()
sys.exit(0)
signal.signal(signal.SIGHUP, handle_sighup)
signal.signal(signal.SIGTERM, handle_sigterm)
while True:
# 메인 루프
do_work()
Systemd는 서비스를 중단할 때 기본적으로 SIGTERM을 보내고, 일정 시간(기본 90초) 후에도 안 죽으면 SIGKILL을 보낸다.
데몬은 터미널이 없으니 print() 같은 걸 써도 화면에 안 나온다. 그럼 디버깅은 어떻게 하나? 로그 파일이다.
예전엔 /var/log/ 디렉토리에 각 서비스별로 로그 파일을 만들었다. /var/log/apache2/error.log, /var/log/mysql/error.log 이런 식으로.
Python에서 syslog를 쓰려면:
import syslog
syslog.syslog(syslog.LOG_INFO, "데몬 시작됨")
syslog.syslog(syslog.LOG_ERR, "에러 발생!")
Systemd 시대에는 journald가 모든 로그를 중앙 관리한다. Systemd unit file에 StandardOutput=journal을 설정하면, 프로그램의 stdout/stderr이 자동으로 journal로 간다.
로그 보는 법:
journalctl -u discord-bot # 특정 서비스 로그
journalctl -u discord-bot -f # 실시간 tail
journalctl -u discord-bot --since today # 오늘 로그만
journalctl -p err # 에러 레벨 이상만
나는 journald가 정말 편하다고 느꼈다. 로그 파일 경로를 기억할 필요도 없고, 시간대별 필터링도 쉽고, 심지어 자동으로 로그 로테이션도 해준다.
정식 데몬을 만들 필요 없이, "SSH 끊어도 살아있는 프로세스"만 필요하다면 더 간단한 방법들이 있다.
"No Hang Up"의 약자. SIGHUP 신호를 무시하게 만든다.
nohup python bot.py &
# 출력은 nohup.out 파일에 저장됨
간단하지만 한계가 있다:
가상 터미널을 만들어서, SSH가 끊겨도 세션이 유지된다.
screen -S bot # bot이라는 이름의 screen 세션 생성
python bot.py # 여기서 실행
# Ctrl+A, D로 detach
screen -r bot # 나중에 다시 접속
이건 임시 작업이나 개발 중일 때 유용하다. 하지만 프로덕션에서 쓰기엔 부족하다. 서버 재부팅하면 screen 세션도 날아간다.
나는 개발할 때는 tmux를 쓰고, 프로덕션 서비스는 무조건 systemd로 등록한다.
처음 디스코드 봇을 배포할 때, 나는 이런 시행착오를 겪었다:
python bot.py - SSH 끊으면 죽음python bot.py & - 역시 SSH 끊으면 죽음nohup python bot.py & - 성공! 근데 서버 재부팅하면 수동으로 다시 실행해야 함@reboot에 등록 - 작동은 하는데 로그 확인이 불편함Systemd로 바꾸고 나니, 봇이 크래시해도 자동 재시작되고, 서버 재부팅해도 알아서 살아나고, 로그도 journalctl로 쉽게 볼 수 있었다. 진작에 이렇게 할 걸 싶었다.
데몬 프로세스를 이해하고 나니, "서버가 24시간 돌아간다"는 게 얼마나 정교한 설계의 결과인지 알게 됐다. Nginx, MySQL, SSH 서버가 언제나 응답하는 건, 이 데몬들이 무한 루프를 돌면서 요청을 기다리고 있기 때문이다.
데몬은 "불멸의 프로세스"가 아니라, "시스템이 책임지고 관리하는 프로세스"다. Systemd가 감시하고, 죽으면 살려내고, 로그를 기록한다. 개발자는 그냥 "무슨 일을 할지"만 코드로 작성하면 된다.
나는 비전공자로서 이 개념을 처음 마주쳤을 때 어려웠지만, 지금은 이렇게 정리한다: 데몬은 서버의 심장박동이다. 멈추지 않고, 보이지 않지만, 모든 서비스의 기반이다. 그리고 그 심장박동을 만드는 건, Double Fork라는 영리한 트릭이든, Systemd라는 현대적 관리자든, 결국 "독립적으로 살아갈 수 있는 프로세스"를 만드는 기술이다.
이제 나는 서버에 새로운 서비스를 배포할 때마다, 가장 먼저 systemd unit file을 작성한다. 그게 데몬을 대하는, 그리고 서버를 대하는 올바른 자세라고 받아들였다.`를 실행하면 수십 개의 데몬이 보인다. 각자 고유한 임무가 있다.
모든 프로세스의 조상. 커널이 부팅할 때 제일 먼저 실행하는 프로세스다. 예전엔 init이 이 역할을 했지만, 요즘은 systemd가 대세다. 모든 orphan 프로세스의 양부모 역할을 한다.
SSH 접속을 기다리는 데몬. 22번 포트를 감시하고 있다가, 누군가 SSH 클라이언트로 접속하면 자식 프로세스를 fork해서 인증을 처리한다. 이 데몬이 없으면 원격 접속 자체가 불가능하다.
웹 서버 데몬. 80번(HTTP) 또는 443번(HTTPS) 포트를 듣고 있다가 HTTP 요청이 오면 응답한다. httpd는 Apache, nginx는 Nginx의 마스터 프로세스다.
데이터베이스 서버 데몬. SQL 쿼리를 받아서 처리한다. 이 친구들도 평생 돌면서 클라이언트 연결을 기다린다.
예약 작업(스케줄링) 데몬. 매분마다 깨어나서 "/etc/crontab"과 사용자 crontab을 확인하고, 실행할 게 있으면 실행한다. 백업 스크립트 같은 걸 자동화할 때 쓴다.
Systemd의 로그 수집 데몬. 모든 시스템 로그를 한곳에 모아서 저장한다. journalctl 명령어로 조회할 수 있다.
이 친구들은 전부 무한 루프로 대기 중이다. 기본 구조는 이렇다:
# 간단한 데몬 패턴 (의사 코드)
while True:
request = wait_for_request() # Blocking I/O
if request:
pid = fork()
if pid == 0:
handle_request(request)
exit(0)
else:
# 부모는 계속 대기
wait_for_child(pid) # 좀비 방지
데몬은 터미널 입출력이 없으니, 사용자가 직접 제어할 방법이 필요하다. 그게 바로 Signal(신호)이다.
kill -HUP <PID>systemctl stop 명령어가 이걸 보낸다.kill -9 <PID>데몬을 만들 때는 이런 신호를 처리하는 핸들러를 작성해야 한다.
import signal
import sys
def handle_sighup(signum, frame):
print("설정 파일 다시 읽기...")
reload_config()
def handle_sigterm(signum, frame):
print("정상 종료 시작...")
cleanup()
sys.exit(0)
signal.signal(signal.SIGHUP, handle_sighup)
signal.signal(signal.SIGTERM, handle_sigterm)
while True:
# 메인 루프
do_work()
Systemd는 서비스를 중단할 때 기본적으로 SIGTERM을 보내고, 일정 시간(기본 90초) 후에도 안 죽으면 SIGKILL을 보낸다.
데몬은 터미널이 없으니 print() 같은 걸 써도 화면에 안 나온다. 그럼 디버깅은 어떻게 하나? 로그 파일이다.
예전엔 /var/log/ 디렉토리에 각 서비스별로 로그 파일을 만들었다. /var/log/apache2/error.log, /var/log/mysql/error.log 이런 식으로.
Python에서 syslog를 쓰려면:
import syslog
syslog.syslog(syslog.LOG_INFO, "데몬 시작됨")
syslog.syslog(syslog.LOG_ERR, "에러 발생!")
Systemd 시대에는 journald가 모든 로그를 중앙 관리한다. Systemd unit file에 StandardOutput=journal을 설정하면, 프로그램의 stdout/stderr이 자동으로 journal로 간다.
로그 보는 법:
journalctl -u discord-bot # 특정 서비스 로그
journalctl -u discord-bot -f # 실시간 tail
journalctl -u discord-bot --since today # 오늘 로그만
journalctl -p err # 에러 레벨 이상만
나는 journald가 정말 편하다고 느꼈다. 로그 파일 경로를 기억할 필요도 없고, 시간대별 필터링도 쉽고, 심지어 자동으로 로그 로테이션도 해준다.
정식 데몬을 만들 필요 없이, "SSH 끊어도 살아있는 프로세스"만 필요하다면 더 간단한 방법들이 있다.
"No Hang Up"의 약자. SIGHUP 신호를 무시하게 만든다.
nohup python bot.py &
# 출력은 nohup.out 파일에 저장됨
간단하지만 한계가 있다:
가상 터미널을 만들어서, SSH가 끊겨도 세션이 유지된다.
screen -S bot # bot이라는 이름의 screen 세션 생성
python bot.py # 여기서 실행
# Ctrl+A, D로 detach
screen -r bot # 나중에 다시 접속
이건 임시 작업이나 개발 중일 때 유용하다. 하지만 프로덕션에서 쓰기엔 부족하다. 서버 재부팅하면 screen 세션도 날아간다.
나는 개발할 때는 tmux를 쓰고, 프로덕션 서비스는 무조건 systemd로 등록한다.
처음 디스코드 봇을 배포할 때, 나는 이런 시행착오를 겪었다:
python bot.py - SSH 끊으면 죽음python bot.py & - 역시 SSH 끊으면 죽음nohup python bot.py & - 성공! 근데 서버 재부팅하면 수동으로 다시 실행해야 함@reboot에 등록 - 작동은 하는데 로그 확인이 불편함Systemd로 바꾸고 나니, 봇이 크래시해도 자동 재시작되고, 서버 재부팅해도 알아서 살아나고, 로그도 journalctl로 쉽게 볼 수 있었다. 진작에 이렇게 할 걸 싶었다.
데몬 프로세스를 이해하고 나니, "서버가 24시간 돌아간다"는 게 얼마나 정교한 설계의 결과인지 알게 됐다. Nginx, MySQL, SSH 서버가 언제나 응답하는 건, 이 데몬들이 무한 루프를 돌면서 요청을 기다리고 있기 때문이다.
데몬은 "불멸의 프로세스"가 아니라, "시스템이 책임지고 관리하는 프로세스"다. Systemd가 감시하고, 죽으면 살려내고, 로그를 기록한다. 개발자는 그냥 "무슨 일을 할지"만 코드로 작성하면 된다.
나는 비전공자로서 이 개념을 처음 마주쳤을 때 어려웠지만, 지금은 이렇게 정리한다: 데몬은 서버의 심장박동이다. 멈추지 않고, 보이지 않지만, 모든 서비스의 기반이다. 그리고 그 심장박동을 만드는 건, Double Fork라는 영리한 트릭이든, Systemd라는 현대적 관리자든, 결국 "독립적으로 살아갈 수 있는 프로세스"를 만드는 기술이다.
이제 나는 서버에 새로운 서비스를 배포할 때마다, 가장 먼저 systemd unit file을 작성한다. 그게 데몬을 대하는, 그리고 서버를 대하는 올바른 자세라고 받아들였다.