코드는 혼자 실행되지 않는다
개발자가 작성한 코드는 결국 컴퓨터의 자원 위에서 실행된다. 여기서 자원이란 CPU, 메모리, 디스크, 네트워크 같은 하드웨어와, 그것을 관리하는 운영체제 기능을 말한다.
예를 들어 API 서버 코드 한 줄이 실행된다고 해도 실제로는 여러 일이 함께 일어난다. CPU는 명령을 계산하고, 메모리는 실행 중인 데이터를 보관한다. 데이터베이스에 요청을 보내면 네트워크를 사용하고, 로그를 남기면 디스크에 기록한다. 운영체제는 이 모든 요청을 조정하며 어떤 프로그램을 먼저 실행할지, 메모리를 얼마나 줄지, 파일 접근을 허용할지 결정한다.
그래서 컴퓨터 구조와 운영체제를 이해하면 오류를 더 정확히 설명할 수 있다. "서버가 느리다"라는 말은 너무 넓다. CPU가 부족한지, 메모리가 부족한지, 디스크 쓰기가 밀리는지, 외부 API 응답이 늦는지에 따라 원인과 해결책이 완전히 달라진다. 코드를 보는 눈에 자원을 보는 눈이 더해져야 문제를 더 좁혀서 볼 수 있다.
AI 생성 이미지
CPU, 메모리, 디스크, 네트워크는 서로 다른 병목을 만든다
CPU는 계산을 담당한다. JSON을 파싱하고, 암호화를 수행하고, 이미지를 변환하고, 정렬 알고리즘을 실행하는 일은 CPU 사용량을 높인다. CPU가 병목이면 보통 프로세스가 계속 바쁘게 실행된다. 이때는 알고리즘 개선, 불필요한 반복 제거, 작업 분산, 캐싱 같은 접근이 효과적이다.
메모리는 실행 중인 데이터가 머무는 공간이다. 웹 서버가 요청 정보를 저장하고, 애플리케이션이 객체를 만들고, 데이터베이스가 자주 쓰는 데이터를 캐시에 올릴 때 메모리를 사용한다. 메모리가 부족하면 운영체제는 디스크를 임시 메모리처럼 쓰는 스와핑을 할 수 있다. 하지만 디스크는 메모리보다 훨씬 느리기 때문에, 스와핑이 발생하면 서버가 갑자기 멈춘 것처럼 느려질 수 있다.
디스크 I/O는 파일을 읽고 쓰는 작업이다. 로그를 과도하게 남기거나, 큰 파일을 자주 읽거나, 데이터베이스가 디스크에 많은 데이터를 기록하면 병목이 된다. CPU 사용률은 낮은데 응답이 느리다면 디스크나 네트워크처럼 기다리는 시간이 긴 작업을 의심해야 한다.
네트워크 I/O는 다른 컴퓨터와 데이터를 주고받는 작업이다. API 서버가 데이터베이스, 캐시 서버, 결제 서버, 외부 인증 서버와 통신할 때 네트워크가 사용된다. 코드 자체는 빠르게 작성되어 있어도 외부 서비스 응답이 느리면 전체 요청은 느려진다. 실무에서 "느린 API"의 원인은 애플리케이션 내부 계산보다 데이터베이스 쿼리, 외부 API, 네트워크 지연인 경우가 많다.
중요한 점은 병목을 추측으로 단정하지 않는 것이다. CPU 사용률, 메모리 사용량, 디스크 I/O, 네트워크 지연, 로그를 함께 보며 어디에서 시간이 쓰이는지 확인해야 한다. 사용률이 높다는 사실만으로 원인이 확정되는 것은 아니다. 어느 자원에서 대기가 쌓이는지, 요청이 어느 구간에서 오래 머무는지 함께 봐야 한다.
프로세스와 스레드는 실행 단위다
프로세스는 실행 중인 프로그램의 단위다. 운영체제는 기본적으로 각 프로세스에 독립된 메모리 공간을 준다. 한 프로세스가 다른 프로세스의 메모리를 함부로 읽거나 쓰지 못하게 막는 것은 안정성과 보안을 위해 중요하다.
스레드는 프로세스 안에서 실행되는 더 작은 작업 단위다. 같은 프로세스 안의 스레드들은 메모리를 공유한다. 이 덕분에 데이터를 주고받기 쉽지만, 동시에 같은 데이터를 수정하면 문제가 생길 수 있다. 예를 들어 여러 스레드가 같은 주문 수량을 동시에 변경하면 값이 꼬일 수 있다. 이런 문제를 막기 위해 잠금, 큐, 트랜잭션 같은 동기화 장치가 필요하다.
웹 서버를 예로 들어 보자. 사용자가 동시에 여러 요청을 보내면 서버는 요청을 하나씩만 처리하지 않는다. 여러 프로세스나 스레드, 또는 비동기 이벤트 루프를 사용해 많은 요청을 동시에 다룬다. 이미지 업로드 요청은 파일 저장을 기다리고, 주문 조회 요청은 데이터베이스 응답을 기다린다. 서버는 기다리는 동안 다른 요청을 처리해 자원을 더 효율적으로 사용한다.
컨텍스트 스위칭, 동시성, 병렬성
CPU 코어 하나는 한순간에 제한된 실행 흐름만 처리할 수 있다. 그런데 우리는 여러 프로그램이 동시에 실행되는 것처럼 느낀다. 운영체제가 아주 짧은 시간 단위로 실행 대상을 바꾸기 때문이다. 이 전환을 컨텍스트 스위칭이라고 한다.
컨텍스트 스위칭이 일어나면 운영체제는 현재 실행 중인 작업의 상태를 저장하고, 다음 작업의 상태를 불러온다. 이 과정에도 비용이 든다. 그래서 스레드를 무조건 많이 만들면 성능이 좋아지는 것이 아니다. 작업 수가 너무 많으면 실제 계산보다 전환 비용과 대기 비용이 커질 수 있다.
동시성은 여러 일을 번갈아 처리하며 동시에 진행되는 것처럼 다루는 능력이다. 병렬성은 실제로 여러 CPU 코어에서 여러 일을 같은 순간에 처리하는 것이다.
예를 들어 코어가 하나인 서버에서도 동시성은 가능하다. 네트워크 응답을 기다리는 동안 다른 요청을 처리하면 된다. 반면 대량 이미지 변환처럼 CPU 계산이 많은 작업은 여러 코어에서 나누어 실행할 때 병렬성의 효과를 얻는다.
이 차이를 알아야 서버 구조를 판단할 수 있다. I/O 대기가 많은 API 서버는 비동기 처리나 적절한 스레드 풀이 도움이 된다. CPU 계산이 많은 작업은 워커 프로세스 분리, 작업 큐, 병렬 처리 전략이 더 중요하다.
스택과 힙: 메모리가 쓰이는 방식
프로그램이 사용하는 메모리는 단순히 하나의 큰 공간이 아니다. 대표적으로 스택과 힙을 구분해 이해하면 좋다.
스택은 함수 호출과 지역 변수를 관리하는 공간이다. 함수가 호출되면 필요한 정보가 스택에 쌓이고, 함수가 끝나면 제거된다. 그래서 스택은 빠르고 관리가 단순하다. 하지만 너무 깊은 재귀 호출이 계속되면 스택 공간이 부족해져 스택 오버플로가 발생할 수 있다.
힙은 실행 중 동적으로 만들어지는 객체가 저장되는 공간이다. 웹 요청을 처리하면서 만든 사용자 객체, 배열, 문자열, 응답 데이터 등이 힙에 놓일 수 있다. 힙은 스택보다 관리가 복잡하다. 객체가 언제 더 이상 필요 없어지는지 판단해야 하기 때문이다.
C나 C++처럼 개발자가 직접 메모리를 해제해야 하는 언어도 있고, Java, JavaScript, Python처럼 가비지 컬렉션이 사용하지 않는 객체를 정리해 주는 언어도 있다. 하지만 가비지 컬렉션이 있다고 해서 메모리 문제가 사라지는 것은 아니다.
메모리 누수는 더 이상 필요 없는 데이터가 계속 참조되어 해제되지 않는 상태를 말한다. 예를 들어 요청마다 데이터를 전역 배열에 계속 추가하고 지우지 않으면, 가비지 컬렉션은 그 데이터를 "아직 사용 중"이라고 판단한다. 시간이 지날수록 메모리 사용량은 증가하고, 결국 서버가 느려지거나 종료될 수 있다.
실무에서는 메모리 사용량이 요청 수와 함께 계속 증가하는지, 특정 작업 후 줄어들지 않는지 관찰해야 한다. 단순히 "가비지 컬렉션 언어니까 괜찮다"고 생각하면 안 된다.
파일 시스템, 표준 입출력, 환경 변수, 권한
운영체제는 프로그램에 여러 기본 기능을 제공한다. 개발자가 매일 접하는 기능도 많다.
파일 시스템은 파일과 디렉터리를 관리하는 방식이다. 애플리케이션 설정 파일, 업로드 파일, 로그 파일, 빌드 결과물은 모두 파일 시스템 위에 있다. 경로가 잘못되었거나, 실행 위치가 달라졌거나, 파일 권한이 없으면 로컬에서는 되던 코드가 서버에서 실패할 수 있다.
표준 입출력은 프로그램이 입력을 받고 결과를 내보내는 기본 통로다. 표준 입력은 데이터를 받아들이는 통로이고, 표준 출력은 정상 메시지를 내보내는 통로이며, 표준 에러는 오류 메시지를 내보내는 통로다. 서버 프로그램의 로그 수집 도구는 보통 이 출력을 읽어 중앙 로그 시스템으로 보낸다.
환경 변수는 실행 환경에 따라 달라지는 값을 주입하는 방법이다. 데이터베이스 주소, API 키, 실행 모드, 포트 번호 같은 값은 코드에 직접 넣기보다 환경 변수로 관리하는 경우가 많다. 같은 코드라도 개발 환경, 테스트 환경, 운영 환경에서 다른 설정으로 실행할 수 있기 때문이다.
권한 모델도 중요하다. 운영체제는 어떤 사용자가 어떤 파일을 읽고, 쓰고, 실행할 수 있는지 관리한다. 배포 후 애플리케이션이 로그 파일을 만들지 못하거나, 업로드 디렉터리에 파일을 저장하지 못하거나, 실행 스크립트가 실행되지 않는 문제는 권한 설정에서 자주 발생한다.
Linux 셸은 운영 문제를 보는 도구다
많은 서버는 Linux 환경에서 운영된다. 그래서 셸 명령어는 단순한 편의 기능이 아니라 문제 해결 도구다.
로그를 확인할 때는 tail, less, grep 같은 명령어를 자주 쓴다. 예를 들어 최근 로그를 보려면 tail -f app.log를 사용하고, 특정 오류만 찾으려면 grep "ERROR" app.log처럼 검색한다.
프로세스를 조사할 때는 ps, top, htop, kill 같은 도구를 사용한다. 어떤 프로세스가 CPU를 많이 쓰는지, 메모리를 얼마나 사용하는지, 아직 살아 있는지 확인할 수 있다. 포트 사용 여부를 볼 때는 ss나 lsof가 도움이 된다. 서버가 "이미 사용 중인 포트" 때문에 실행되지 않을 때 원인을 찾을 수 있다.
디스크 문제를 볼 때는 df와 du가 유용하다. df는 파일 시스템의 남은 공간을 보여 주고, du는 특정 디렉터리가 얼마나 큰지 확인하게 해 준다. 로그 파일이 계속 쌓여 디스크가 꽉 차면 애플리케이션이 정상적으로 동작하지 못할 수 있다.
권한과 실행 문제를 볼 때는 ls -l, chmod, chown을 사용한다. 배포 스크립트가 실행되지 않는다면 실행 권한이 없는지 확인해야 한다. 파일 소유자가 애플리케이션 실행 사용자와 맞지 않아 쓰기 실패가 발생할 수도 있다.
이 명령어들을 모두 외울 필요는 없다. 중요한 것은 문제를 볼 관점을 갖는 것이다. 로그를 본다. 프로세스를 본다. 포트를 본다. 디스크를 본다. 권한을 본다. 이 순서만 익혀도 배포와 운영 문제를 훨씬 차분하게 추적할 수 있다.
운영체제 지식은 원인 설명 능력이다
컴퓨터 구조와 운영체제는 시험용 지식처럼 보일 수 있다. 하지만 실무에서는 매우 직접적으로 쓰인다.
API 응답이 느릴 때 CPU 계산이 많은지, 데이터베이스나 외부 API를 기다리는지 구분해야 한다. 서버가 멈춘 것처럼 보일 때 메모리가 부족한지, 디스크가 가득 찼는지, 프로세스가 죽었는지 확인해야 한다. 배포가 실패했을 때 환경 변수가 빠졌는지, 권한이 잘못되었는지, 포트가 충돌했는지 살펴야 한다.
좋은 개발자는 오류 메시지만 보는 데서 멈추지 않는다. 코드가 운영체제를 통해 어떤 자원을 사용하고 있는지 함께 본다. 이 관점을 갖추면 문제를 더 작게 나눌 수 있고, 추측보다 근거로 원인을 찾을 수 있다.
이 장에서 기억할 핵심은 하나다. 프로그램은 CPU, 메모리, 디스크, 네트워크, 운영체제 기능 위에서 실행된다. 따라서 성능과 오류를 이해하려면 코드뿐 아니라 코드가 사용하는 자원을 함께 보아야 한다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.