PC 한 대로 공장 서버 5년 운영 후기

2021년부터 지인이 운영하는 물류 회사의 공장에서 쓰는 WMS(창고 관리 시스템)를 만들고 운영하고 있습니다. 클라우드 없이, 직접 조립한 PC 한 대로 5년째 돌리고 있습니다.

처음에는 그 당시 회사에서 써 볼 수 없던 JPA 같은 기술을 직접 써 보고 싶어서 시작했습니다. 지인이 겪고 있던 불편함도 해결해 주고 싶었고요.

왜 클라우드를 쓰지 않았나

이유는 두 가지였습니다.

첫째는 개인적인 공부였습니다. 서버를 직접 조립하고, 네트워크를 설정하고, 운영까지 해 보면서 배울 수 있는 게 많을 거라고 생각했습니다.

둘째는 비용이었습니다. 아직 쓰일지 안 쓰일지도 모르는 시스템이라, 처음부터 매달 서버비가 나가는 건 지인에게도 부담이었습니다. 초반 규모라면 AWS EC2의 t3.medium(vCPU 2개, 메모리 4GB, 서울 리전 기준 한 달 약 $38) 정도로도 충분했겠지만, 그것도 매달 나가는 돈입니다.

PC는 한 번 사 두면 매달 나가는 돈이 없습니다. 당시 조립하는 데 60만 원 정도가 들었고, 전기 요금은 공장 전기 요금에 묻히는 수준이었습니다. t3.medium으로 따져도 1년 남짓이면 PC 값을 넘는데, 성능은 비교가 안 됐습니다. 5700G는 8코어 16스레드라 스레드 수와 메모리만 맞춰도 EC2로는 c5.4xlarge(vCPU 16개, 메모리 32GB)쯤 되고, 이건 한 달에 약 $560, 70만 원이 넘습니다.

결국 제가 조금 귀찮아지는 대신, 적은 비용으로 좋은 사양의 서버를 쓸 수 있는 선택이었습니다.

어떻게 운영했나

처음에는 이 PC를 음성 공장 사무실 한쪽에 두었습니다. 2025년에 서산 공장 시스템을 만들면서 PC도 서산으로 옮겼고, 지금은 음성 공장에서도 웹으로 접속해서 씁니다.

클라우드 없이 PC 한 대로 서버를 운영하는 구성은 생각보다 단순합니다. 외부에서 들어온 요청은 다음 순서로 서버까지 전달됩니다.

요청 흐름: 인터넷에서 통신사 모뎀, 공유기를 거쳐 내부망의 서버 PC로. 서버 PC에는 내부 고정 IP, 공유기에는 80·443 포트포워딩, 인터넷 쪽에는 도메인 연결

설정은 이 흐름을 거꾸로, 안쪽의 서버부터 바깥쪽의 도메인까지 하나씩 하면 됩니다.

  1. 서버 PC에 고정 IP 설정: 서버 PC의 IP 자동 할당(DHCP)을 끄고, 내부망 IP를 직접 지정합니다. 공유기가 IP를 새로 나눠 주더라도 서버 주소가 바뀌지 않게 하기 위해서입니다.
  2. 공유기에서 포트포워딩: 외부에서 들어온 요청을 서버 PC로 넘겨줍니다. 처음에는 웹용 80·443 포트와 SSH용 22 포트만 열었습니다.
  3. 도메인 연결: 도메인은 Namecheap에서 샀고, 통신사가 할당해 준 공인 IP를 A 레코드로 연결했습니다. HTTPS 인증서는 Let’s Encrypt로 발급받았습니다.

통신사 공인 IP는 고정 IP 상품이 아니었지만, 모뎀이 계속 연결돼 있는 동안에는 거의 달라지지 않았습니다. 그래도 IP가 바뀔 때를 대비해 나중에는 공유기의 DDNS 기능을 켜고, Namecheap 도메인이 그 DDNS 주소를 가리키도록 바꿨습니다. ipTIME이나 ASUS 같은 공유기는 DDNS를 자체적으로 지원해서 설정도 어렵지 않습니다.

처음에는 원격 접속도 외부에서 공장 공인 IP로 SSH에 접속해 아이디와 비밀번호로 로그인하는 게 전부였습니다.

그러다 어느 날 보니 누군가 SSH로 접속을 시도한 흔적이 남아 있었습니다. 22번 포트를 인터넷에 열어 두면 이렇게 누군가 두드려 보는 게 당연한 일이었는데, 그때는 미처 생각하지 못했습니다.

먼저 SSH 로그인에 Google Authenticator를 붙여 비밀번호에 더해 OTP까지 입력해야 들어올 수 있게 했습니다. 이후에는 아예 SSH 포트포워딩을 없애고, 공유기에서 제공하는 OpenVPN으로 내부망에 먼저 접속한 다음 SSH로 들어가도록 바꿨습니다. 이제 서버 PC로 넘겨주는 포트는 웹용 80·443뿐이고, 공유기에는 VPN 포트 하나만 더 열려 있습니다.

가장 무서웠던 날

운영 초기에는 서비스를 띄우는 방법도 단순했습니다. 당시 구성은 Spring Boot 앱과 MariaDB였고, 이 프로세스들을 실행하는 sh 스크립트를 직접 짜서 부팅할 때 자동으로 실행되게 해 두었습니다.

그러다 공장 전원이 나갔습니다. 전기가 다시 들어온 뒤 현장에 서버 전원 버튼을 눌러 달라고 부탁했는데, 부팅하면서 무언가 순서가 꼬였는지 앱이 실행되지 않았습니다.

현장에서는 “시스템이 안 된다”는 연락이 왔습니다. 그날 현장은 하루 동안 엑셀로 기록하며 버텼고, 저는 퇴근하자마자 음성 공장까지 직접 가서 서버를 다시 살려 놓았습니다.

그 뒤로 sh 스크립트를 버리고 docker compose로 옮겼습니다. MariaDB와 Spring Boot 앱을 하나의 설정 파일로 묶고, 각 컨테이너에 restart 정책을 걸어 두었습니다. 그러면 부팅 후 Docker가 올라오면 컨테이너들도 알아서 다시 뜨고, 앱이 DB보다 먼저 떠서 실패하더라도 restart 정책이 다시 띄워 줍니다. 이 구성은 2025년 서산 공장 시스템을 만들면서 k3s로 옮기기 전까지 썼습니다.

운영하면서 챙기게 된 것들

모니터링

모니터링이라고 하면 결국 로그와 리소스, 두 가지였습니다.

로그

로그가 왜 중요한지 가장 크게 느낀 건 PDF 파일 문제 때였습니다.

처음에는 Windows에서 개발하고, 서버는 Linux에서 돌렸습니다. 그런데 운영 서버에서 특정 PDF를 저장하고 다시 읽는 과정에서 에러가 났습니다. 제 PC에서는 아무리 해도 재현되지 않았습니다.

로그를 확인해 보고서야 원인을 찾았습니다. 경로를 "upload\\" + fileName처럼 역슬래시로 이어 붙이고 있었는데, Windows에서는 이게 폴더 구분자지만 Linux에서 \는 그냥 파일 이름에 들어가는 글자입니다. 그래서 Linux에서는 upload 폴더 안이 아니라 upload\a.pdf라는 이름의 파일이 엉뚱한 곳에 생기고 있었습니다.

경로를 문자열로 이어 붙이던 코드를 Paths.get(...).resolve(...)로 바꾸고, 저장 폴더는 설정 파일로 빼서 개발 PC와 서버가 각자 다른 값을 쓰게 했습니다.

@Value("${file.upload-dir}") // 개발 PC: C:/wms/upload, 서버: /data/wms/upload
private String uploadDir;

Path file = Paths.get(uploadDir).resolve(fileName); // 구분자는 Java가 알아서 맞춘다

지금은 Filebeat로 로그를 모아서 보고 있습니다.

리소스

k3s로 옮기고 나서는 하루에 한두 번씩 파드가 죽었습니다.

스크래핑과 자동화 코드는 Playwright를 headless 모드로 띄워서 브라우저를 직접 조작하는 방식으로 짰습니다. 브라우저를 띄우는 건 메모리를 많이 먹지만, RAM이 넉넉하니 괜찮다고 생각했습니다.

이유를 몰라 한참 헤매다가, 나중에 Grafana로 리소스 사용량을 보고서야 알았습니다. 사용량이 많은 시간대에 Playwright가 띄운 브라우저가 작업이 끝난 뒤에도 꺼지지 않고 남아 있었고, 그게 쌓이면서 메모리를 꽉 채웠던 겁니다.

당시에는 launchPersistentContext로 브라우저를 띄웠습니다. 로그인 상태 같은 브라우저 데이터를 폴더에 저장해 두고 이어서 쓸 수 있는 방식인데, 호출할 때마다 Chromium 프로세스가 통째로 하나씩 뜹니다. close()를 부르지 않으면 작업이 끝나도 프로세스가 그대로 남고, 특히 작업 중간에 에러가 나면 닫는 코드까지 가지 못하고 남기 쉽습니다.

작업이 성공하든 실패하든 finally에서 브라우저를 닫도록 고쳤습니다.

const context = await chromium.launchPersistentContext(userDataDir, { headless: true });
try {
  // 로그인, 스크래핑, 납품증 발급 ...
} finally {
  await context.close(); // 에러가 나도 브라우저 프로세스까지 정리
}

이렇게 고치고 나니, 넉넉하게 올려 둔 PC 자원은 평소 20% 정도밖에 쓰지 않습니다. 지금도 Grafana로 CPU와 메모리 사용량을 보고 있습니다.

알림

알림은 디스코드로 받고 있습니다.

특히 필요한 건 스크래핑과 자동화 쪽입니다. 고객사가 사이트를 업데이트하면 화면 구조가 바뀌어 로직이 깨지고, 계정 비밀번호가 바뀌면 로그인부터 실패합니다. 이걸 알림으로 받지 않으면 제가 직접 돌려 보기 전까지는 알 방법이 없습니다. 비밀번호가 틀린 채로 로그인을 계속 시도하다가 계정이 잠길 수도 있습니다.

k3s로 옮긴 뒤로는 파드가 죽는 것 같은 서버 쪽 문제도 디스코드로 알림이 옵니다. 정전 때처럼 현장 연락을 받고서야 아는 일은 이제 없습니다.

데이터 백업

다행히 아직 데이터를 날린 적은 없습니다. 하지만 5년 동안 입출고와 재고 데이터가 계속 쌓였고, 이제 이 데이터가 날아가면 회사 운영 자체에 큰 문제가 생깁니다. DB는 정기적으로 dump해서, 서버에 따로 연결해 둔 NAS용 HDD에 저장하고 있습니다.

외부 서비스 격리

고객사 시스템에서 생산계획과 재고를 가져오는 스크래핑, 고객사 사이트에 출고 내역을 올리고 납품증을 발급하는 자동화는 우리 쪽에서 통제할 수 없는 부분입니다. 고객사가 주소를 바꾸거나, 시스템을 업데이트하거나, 서버에 장애가 나면 그대로 영향을 받습니다.

이 부분을 우리 시스템과 강하게 묶어 두면 큰일 납니다. 고객사 시스템이 멈췄다고 우리 WMS까지 멈추면 안 되니까요. 예를 들어 출고할 때 고객사 사이트에 올리는 작업이 에러가 나도, WMS의 출고 처리는 그대로 완료됩니다. 실패한 건만 고객사 사이트에 수동으로 올리면 됩니다. 고객사 쪽에 문제가 생겨도 현장의 입출고는 멈추지 않습니다.

죽을 수 있다는 것

서버가 죽는다고 하면 보통 서버 PC만 떠올리지만, 실제로 멈출 수 있는 기기는 통신사 모뎀, 공유기, 서버 PC 세 대였습니다. 그리고 이 셋이 모두 멀쩡해도 전기가 나가면 소용이 없습니다. 그래서 하나씩, 멈췄을 때 어떻게 할지 정해 두었습니다.

운영하면서 좋았던 점

처음부터 끝까지 모든 걸 제가 책임져야 한다는 건 힘든 일이었습니다. 개발은 물론이고 서버를 조립하는 것부터 네트워크, 배포, 장애 대응까지 넘길 곳이 없었으니까요. 그래도 그게 진짜 재미있었습니다.

마치며

앞으로도 이 서버에서 이것저것 계속 해 볼 생각입니다. 다만 언젠가는 이 모든 걸 클라우드로 옮길 계획입니다.

마지막으로 하나만 덧붙이자면, 정말 공부가 목적이 아니라면 돈 내고 클라우드 쓰세요. 진짜 귀찮습니다.

책상에 엎드려 머리를 감싸 쥔 모습

5년 운영의 결론