AWS EC2 Spring Boot 서버가 자꾸 죽는 진짜 이유

큰 꿈을 안고 시작한 사이드 프로젝트, 혹은 이제 막 첫발을 뗀 스타트업의 소중한 서비스를 AWS EC2 프리티어, 특히 t2.micro 인스턴스에 배포하며 뿌듯함을 느끼셨을 겁니다. 월 사용료에 대한 부담 없이 나만의 서버를 가질 수 있다는 것은 정말 매력적인 일이죠. 하지만 그 기쁨도 잠시, 원인을 알 수 없는 서버 다운 현상이 반복되면서 골머리를 앓고 계시진 않나요? SSH 접속은 수시로 끊기고, 애써 만든 웹사이트는 응답이 없으며, 유일한 해결책은 EC2 콘솔에서 인스턴스를 재부팅하는 것뿐인 악순환. 시스템 로그를 아무리 뒤져봐도 '에러'라는 명확한 단서는 보이지 않아 답답함만 커져갑니다.

만약 여러분의 기술 스택이 Spring Boot와 같은 JVM 기반 프레임워크와 MySQL, PostgreSQL 같은 관계형 데이터베이스의 조합이라면, 이 미스터리한 현상의 범인은 거의 명확합니다. 바로 '메모리 부족(Out of Memory)'입니다. 1GB라는 지극히 제한된 물리적 RAM 환경에서 이 둘을 함께 운영하는 것은 마치 비좁은 원룸에서 두 명의 거인이 함께 살려는 것과 같습니다. 이 글에서는 AWS EC2 t2.micro 환경에서 왜 메모리 부족 문제가 필연적으로 발생하는지 그 구조적인 원인을 깊이 파헤치고, 가장 현실적이면서도 강력한 해결책인 '스왑 메모리(Swap Memory)'를 설정하는 모든 과정을 A부터 Z까지, 누구나 따라 할 수 있도록 상세하게 안내합니다. 더 이상 불안정한 서버 때문에 밤잠 설치지 마세요. 이 글을 통해 흔들림 없는 서버 안정성을 확보하고, 오롯이 개발에만 집중할 수 있는 환경을 직접 구축해 보시길 바랍니다.

이 글은 AWS EC2의 t2.micro 또는 t3.micro와 같이 1GB RAM을 가진 저사양 인스턴스를 기준으로 작성되었습니다. 하지만 여기서 다루는 원리와 해결책은 메모리가 제한된 모든 리눅스 서버 환경에 보편적으로 적용될 수 있습니다.

왜 프리티어 서버는 예고 없이 멈추는 걸까?

문제를 해결하기 위한 첫걸음은 원인을 명확하게 이해하는 것입니다. '서버가 죽는다', '서버가 멈춘다'고 표현되는 현상은 다양하지만, aws ec2 프리티어 환경에서는 그 원인이 대부분 메모리 고갈과 직접적으로 연결되어 있습니다. 이 현상을 주도하는 두 명의 주인공, '1GB RAM의 물리적 한계'와 '리눅스 OOM Killer'에 대해 자세히 알아보겠습니다.

프리티어의 명확한 한계: 1GB 메모리의 현실

AWS가 제공하는 프리티어는 클라우드 컴퓨팅을 경험하고 소규모 프로젝트를 운영하기에 더할 나위 없이 좋은 기회입니다. t2.micro 인스턴스는 1vCPU와 1GB RAM이라는 사양으로 12개월간 매월 750시간 무료 사용량을 제공합니다. 하지만 우리가 간과하기 쉬운 것이 바로 이 1GB RAM의 실체입니다.

현대의 웹 애플리케이션 스택은 생각보다 많은 메모리를 요구합니다. 1GB(정확히는 1GiB, 약 1024MB)의 메모리는 다음과 같이 여러 주체에 의해 나뉘어 사용됩니다.

  • 운영체제(OS): Amazon Linux, Ubuntu 등 리눅스 커널 자체와 시스템 운영에 필수적인 각종 데몬(sshd, cron 등)이 부팅과 동시에 수십에서 수백 MB의 메모리를 기본적으로 점유합니다.
  • 애플리케이션 (Spring Boot): 자바 애플리케이션은 JVM(Java Virtual Machine) 위에서 실행됩니다. JVM은 시작 시 운영체제로부터 거대한 메모리 공간(Heap, Metaspace 등)을 할당받습니다. 아무리 간단한 'Hello, World' 애플리케이션이라도 수백 MB의 힙 메모리를 설정하는 것이 일반적입니다.
  • 데이터베이스 (MySQL): 데이터베이스 서버는 빠른 응답 속도를 위해 디스크의 데이터를 메모리에 캐싱하는 '버퍼 풀(Buffer Pool)'과 같은 기법을 적극적으로 사용합니다. 이 역시 수백 MB의 메모리를 차지하는 주요 원인입니다.
  • 기타 유틸리티: 웹 서버(Nginx), 모니터링 에이전트(CloudWatch Agent, Prometheus Exporter) 등 우리가 설치하는 추가적인 소프트웨어도 모두 메모리를 소비합니다.

결국, 사용자의 요청이 하나도 없는 상태에서도 이 모든 구성 요소가 메모리를 나눠 쓰고 나면 실제로 남는 여유 공간(Free Memory)은 얼마 되지 않습니다. 여기에 실제 사용자가 접속하여 요청을 보내고, Spring Boot가 비즈니스 로직을 처리하며, MySQL이 복잡한 쿼리를 실행하기 시작하면 메모리 사용량은 순식간에 임계점에 도달하게 됩니다.

조용한 암살자, 리눅스 OOM Killer의 등장

시스템의 물리적 메모리가 모두 소진되고 더 이상 가용 메모리가 없을 때, 리눅스 커널은 시스템 전체가 멈춰버리는 최악의 상황(Kernel Panic)을 막기 위해 극단적인 조치를 취합니다. 바로 이때 등장하는 것이 OOM(Out of Memory) Killer입니다.

OOM Killer는 시스템의 생존을 위해 어쩔 수 없이 일부 프로세스를 희생시키는 리눅스 커널의 최후의 보루이자 자기방어 메커니즘입니다. 에러 로그를 남기지 않고 프로세스를 강제 종료시키기 때문에, 경험이 적은 개발자에게는 '원인 모를 서버 다운'의 주범으로 오해받기 쉽습니다.

OOM Killer는 아무 프로세스나 무작위로 종료하지 않습니다. 커널은 내부적으로 각 프로세스마다 'oom_score'라는 점수를 매깁니다. 이 점수는 해당 프로세스가 사용하고 있는 메모리의 양에 비례하여 높아집니다. OOM 상황이 발생하면, OOM Killer는 시스템에서 가장 높은 oom_score를 가진 프로세스, 즉 메모리를 가장 많이 사용하고 있는 프로세스를 찾아내어 가차 없이 강제 종료(SIGKILL 신호 전송)시킵니다. 이는 마치 침몰하는 배의 무게를 줄이기 위해 가장 무거운 짐을 먼저 바다에 버리는 것과 같습니다.

안타깝게도, 우리의 Spring Boot 애플리케이션(정확히는 java 프로세스)MySQL 서버(mysqld 프로세스)는 일반적으로 시스템 내에서 가장 많은 메모리를 사용하는 '무거운 짐'에 해당합니다. 따라서 OOM Killer의 첫 번째 희생양이 될 확률이 매우 높습니다. 이것이 바로 우리가 겪는 ec2 서버 다운 현상의 진짜 실체입니다. 애플리케이션이 비정상적으로 종료되었지만, 어디에도 명시적인 에러 로그는 남아있지 않는 이유이기도 합니다.

메모리 포식자: Spring Boot와 MySQL의 공생 관계

왜 하필 이 두 조합이 spring boot 메모리 부족 문제에 특히 취약할까요? 각자의 메모리 사용 특성을 살펴보면 그 이유를 명확히 알 수 있습니다.

컴포넌트 메모리 사용 주체 특징 및 설명 t2.micro 환경에서의 영향
Spring Boot (JVM) 힙 메모리(Heap), Metaspace, 스레드 스택 등 JVM은 시작 시 -Xms(최소), -Xmx(최대) 옵션으로 지정된 크기의 힙 메모리를 OS로부터 미리 할당받습니다. 실제 사용량과 무관하게 큰 공간을 예약하는 방식이며, 애플리케이션의 모든 객체와 데이터는 이 힙 위에서 생성되고 소멸됩니다. 기본 설정으로 실행해도 힙 메모리만 256MB~512MB를 쉽게 차지할 수 있습니다. 이는 전체 RAM의 1/4에서 1/2에 해당하는 엄청난 양입니다.
MySQL (Database) InnoDB 버퍼 풀, 키 버퍼, 쿼리 캐시, 커넥션 스레드 등 성능 향상을 위해 디스크의 데이터와 인덱스를 메모리에 상주시키는 '버퍼 풀'이 메모리 사용의 핵심입니다. 자주 접근하는 데이터를 디스크 I/O 없이 메모리에서 바로 읽어오므로 응답 속도가 비약적으로 향상됩니다. 기본 설정 값도 t2.micro 환경에는 다소 부담스러울 수 있습니다. 128MB 이상의 버퍼 풀이 설정되면 OS 및 애플리케이션과 치열한 메모리 경쟁을 벌이게 됩니다.

결론적으로 1GB RAM이라는 제한된 공간에서, 운영체제가 약 100-200MB, JVM이 약 300-500MB, MySQL이 약 150-250MB를 차지하고 나면 남는 여유 공간은 거의 없습니다. 이 상태에서 순간적인 트래픽 증가, 무거운 배치 작업 실행, 복잡한 SQL 쿼리 처리 등이 발생하면 메모리 사용량이 임계점을 돌파하고, 어김없이 OOM Killer가 출동하여 우리의 소중한 프로세스를 강제 종료시키는 비극적인 시나리오가 반복되는 것입니다.

내 서버의 메모리 상태, 정밀 진단 시작하기

이론적인 배경을 이해했으니, 이제 실제 우리 서버에서 어떤 일이 벌어지고 있는지 직접 증거를 찾아볼 차례입니다. 진단은 크게 AWS 콘솔을 이용하는 방법과 서버에 직접 접속하여 터미널 명령어를 사용하는 방법으로 나뉩니다. 이 두 가지 방법을 통해 문제의 원인이 메모리 부족임을 확신할 수 있습니다.

1단계: AWS 콘솔에서 결정적 증거 확보하기

서버가 다운되어 접속조차 불가능할 때 가장 먼저, 그리고 쉽게 상태를 확인할 수 있는 곳이 바로 AWS Management Console입니다. 다음 단계를 따라 이상 징후를 포착해 보세요.

  1. EC2 대시보드 접속: AWS 콘솔에 로그인한 후, EC2 서비스로 이동하여 문제가 발생한 인스턴스를 목록에서 선택합니다.
  2. [모니터링(Monitoring)] 탭 확인: 인스턴스 상세 정보 하단에 있는 [모니터링] 탭을 클릭합니다. 여기서 CPU 사용률(CPU Utilization) 그래프를 유심히 살펴보세요. 메모리 부족이 심화되면, 시스템이 스와핑(이후 설명)을 시도하거나 프로세스 종료를 위해 자원을 소모하면서 CPU 사용률이 비정상적으로 100% 가까이 치솟는 패턴을 발견할 수 있습니다.
  3. 시스템 로그 확인 (가장 중요): 문제의 핵심 증거는 시스템 로그에 있습니다. 인스턴스를 우클릭하거나 상단의 [작업(Actions)] 메뉴를 열고, [모니터링 및 문제 해결(Monitor and troubleshoot)] → [시스템 로그 가져오기(Get system log)]를 선택합니다.
  4. 'Out of Memory' 검색: 방대한 양의 텍스트 로그가 나타날 것입니다. 여기서 키보드의 찾기 기능(Ctrl+F 또는 Cmd+F)을 사용하여 `Out of Memory` 또는 `oom-killer` 라는 키워드로 검색해 보세요. 만약 아래와 유사한 로그를 발견했다면, 이는 OOM Killer가 작동했다는 명백한 증거이며, 더 이상의 추측은 불필요합니다.
    
    [ 12345.678901] a-java-app invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
    ...
    [ 12345.678901] Out of memory: Kill process 23456 (java) score 890 or sacrifice child
    [ 12345.678901] Killed process 23456 (java) total-vm:2547892kB, anon-rss:987654kB, file-rss:0kB, shmem-rss:0kB
    [ 12345.678901] oom_reaper: reaped process 23456 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
        

    위 로그는 'java'라는 이름의 프로세스(PID 23456)가 높은 oom_score(890점) 때문에 OOM Killer에 의해 강제 종료(Killed)되었음을 명확하게 보여줍니다.

  5. 인스턴스 스크린샷 확인: 드물지만 커널 패닉 등 더 심각한 상태로 시스템이 멈췄을 경우를 대비해 [인스턴스 스크린샷 가져오기(Get instance screenshot)] 기능도 유용합니다. 콘솔 화면의 마지막 상태를 이미지로 보여주어 문제 해결의 단서를 제공할 수 있습니다.

2단계: SSH 터미널에서 실시간 상태 분석하기

서버가 아직 응답하거나 재부팅 직후에 SSH로 접속하여 직접 메모리 상태를 확인하는 것은 매우 중요합니다. 리눅스에서 메모리 상태를 확인하는 가장 기본적인 명령어는 `free`입니다.


# -h 옵션은 human-readable, 즉 사람이 읽기 쉬운 단위(GB, MB)로 결과를 보여줍니다.
$ free -h

스왑 메모리를 설정하기 전의 `t2.micro` 인스턴스에서 이 명령어를 실행하면, 다음과 비슷한 결과를 보게 될 것입니다.


              total        used        free      shared  buff/cache   available
Mem:          981Mi       875Mi        42Mi       0.0Ki        64Mi        25Mi
Swap:           0Bi         0Bi         0Bi

여기서 우리가 주목해야 할 부분은 두 가지입니다.

  • `Swap` 행: `total`, `used`, `free` 값이 모두 `0Bi`로 표시되어 있습니다. 이는 현재 시스템에 비상시 사용할 스왑 메모리가 전혀 설정되어 있지 않다는 것을 의미합니다. 즉, 981MB의 물리 메모리(Mem)가 모두 소진되면 시스템은 더 이상 버틸 여력이 없는 '벼랑 끝 전술' 상태인 것입니다.
  • `Mem` 행의 `available` 열: `available` 메모리는 새로운 애플리케이션이 당장 시작할 때 할당 가능한 예상 메모리 양을 의미합니다. 이 값이 수십 MB 수준으로 매우 낮게 유지되고 있다면, OOM Killer의 등장이 임박했다는 위험 신호입니다.

어떤 프로세스가 메모리를 얼마나 사용하고 있는지 실시간으로 확인하고 싶다면 `top` 명령어를 사용하거나, 더 시각적으로 개선된 `htop`을 사용하는 것이 좋습니다. `htop`은 기본 설치되어 있지 않을 수 있습니다.


# Ubuntu / Debian
sudo apt-get update
sudo apt-get install htop

# Amazon Linux / CentOS / RHEL
sudo yum install htop

`htop`을 실행한 뒤 F6 키를 눌러 정렬 기준을 `PERCENT_MEM`으로 변경하면 메모리 사용량 순으로 프로세스 목록을 볼 수 있습니다. 아마도 `java`와 `mysqld`가 최상단에 위치하고 있을 것입니다.

가장 현실적인 해결책: 스왑 메모리(Swap) 구축하기

진단 결과, 우리 서버가 만성적인 메모리 부족으로 고통받고 있음이 명확해졌습니다. 가장 확실하고 근본적인 해결책은 더 많은 RAM을 가진 인스턴스로 사양을 업그레이드(Scale-up)하는 것입니다. (예: t3.small, 2GB RAM). 하지만 이는 매달 추가적인 비용 발생을 의미하며, 사이드 프로젝트나 초기 스타트업에게는 부담이 될 수 있습니다. 이때 비용 증가 없이 서버 안정성을 획기적으로 개선할 수 있는 가장 경제적이고 효과적인 대안이 바로 스왑 메모리(Swap Space)를 설정하는 것입니다. 이것은 일종의 aws swap 전략입니다.

스왑 메모리란 무엇인가? (가상 메모리의 이해)

스왑 메모리는 물리적인 RAM(Random Access Memory)이 부족할 때, 하드 디스크 드라이브(HDD)나 솔리드 스테이트 드라이브(SSD) 같은 보조 기억 장치의 일부 공간을 마치 RAM처럼 사용하는 가상 메모리 기술입니다. EC2 환경에서는 EBS(Elastic Block Store) 볼륨의 일부가 이 역할을 하게 됩니다.

작동 원리는 다음과 같습니다. 운영체제는 RAM에 있는 데이터 중 당장 활발하게 사용되지 않는 오래된 데이터 조각(메모리 페이지)을 디스크에 마련된 '스왑 공간'으로 임시로 옮겨 놓습니다(이를 'Swap-out'이라 합니다). 이렇게 함으로써 실제 RAM에는 새로운 작업을 위한 여유 공간이 생기게 됩니다. 나중에 디스크로 옮겨졌던 데이터가 다시 필요해지면, 운영체제는 스왑 공간에서 해당 데이터를 다시 RAM으로 불러옵니다(이를 'Swap-in'이라 합니다).

장점:
물리적 RAM 용량을 초과하는 메모리를 사용할 수 있는 유연성을 제공합니다. 갑작스러운 메모리 사용량 급증에도 시스템 전체가 다운되는 것을 막아주는 훌륭한 '안전망' 또는 '보험' 역할을 하여, OOM Killer에 의해 애플리케이션이 강제 종료되는 사태를 예방하고 서버 안정성을 크게 향상시킵니다.

단점:
치명적인 단점은 성능입니다. EBS 볼륨 같은 디스크 I/O 속도는 RAM의 접근 속도에 비해 수백, 수천 배 이상 느립니다. 따라서 스왑 아웃/인이 빈번하게 발생하면 시스템 전반의 응답 속도가 현저히 저하되는 것을 체감할 수 있습니다. 애플리케이션이 '느려지거나' '버벅거리는' 현상이 발생할 수 있습니다.

결론적으로, 스왑은 성능 향상을 위한 도구가 아니라, 시스템 다운을 막기 위한 비상 대책입니다. 평소에는 사용되지 않다가 메모리 압박이 심해지는 위급한 순간에 개입하여 시스템에 '인공호흡'을 해주는 역할을 합니다. t2.micro와 같은 프리티어 인스턴스에서는 이 비상 대책이 선택이 아닌 필수입니다.

실전! 스왑 파일 생성 가이드 (Step-by-Step)

리눅스에서 스왑 공간을 설정하는 방법은 크게 '스왑 파티션' 방식과 '스왑 파일' 방식이 있습니다. 이미 운영 중인 EC2 인스턴스의 디스크 파티션을 변경하는 것은 복잡하고 위험하므로, 파일 시스템 내에 특정 크기의 파일을 만들어 스왑 공간으로 사용하는 '스왑 파일' 방식이 훨씬 간편하고 안전하며 유연합니다.

이제부터 1GB RAM을 가진 인스턴스에, 일반적으로 권장되는 RAM의 2배 크기인 2GB의 스왑 파일을 생성하고 적용하는 전 과정을 단계별로 진행해 보겠습니다. EC2 인스턴스에 SSH로 접속한 후, 아래 명령어들을 순서대로 실행하세요.

1단계: 기존 스왑 공간 확인

작업을 시작하기 전, 현재 시스템에 이미 스왑이 설정되어 있는지 다시 한번 확인합니다. `free -h`로도 확인했지만, `swapon` 명령어를 사용하면 더 명확하게 확인할 수 있습니다. 아래 명령어를 실행했을 때 아무것도 출력되지 않는다면 스왑이 없는 것이 확실합니다.


sudo swapon --show

2단계: 스왑 파일 생성 (디스크 공간 할당)

리눅스의 강력한 디스크 유틸리티인 `dd` 명령어를 사용하여 스왑으로 사용할 파일을 생성합니다. 이 파일은 루트 디렉토리(`/`)에 `swapfile`이라는 이름으로 생성됩니다.


# bs(블록 크기) 1G * count(블록 개수) 2 = 2GB 크기의 /swapfile 생성
sudo dd if=/dev/zero of=/swapfile bs=1G count=2

위 명령어의 각 옵션은 다음과 같은 의미를 가집니다.

  • if=/dev/zero: 입력 파일(input file)로 `/dev/zero`를 사용합니다. 이는 0(null-byte) 값을 끝없이 출력하는 특수한 가상 장치로, 파일을 특정 크기의 0으로 채우는 데 사용됩니다.
  • of=/swapfile: 출력 파일(output file)의 경로와 이름을 `/swapfile`로 지정합니다.
  • bs=1G: 블록 크기(block size)를 1기가바이트로 설정합니다. 한번에 1GB 단위로 데이터를 씁니다.
  • count=2: 지정된 블록 크기(1G)의 블록을 2개(count) 복사합니다. 따라서 최종 파일 크기는 1G * 2 = 2GB가 됩니다.

(참고: 오래된 가이드에서는 `bs=1M count=2048` 처럼 메가바이트 단위를 사용하기도 합니다. 결과는 동일하지만, 블록 크기를 크게 잡는 것이 디스크 I/O 횟수를 줄여주어 일반적으로 더 효율적입니다.)

3단계: 스왑 파일 권한 설정 (⭐ 매우 중요!)

이 단계는 보안상 매우 중요합니다. 스왑 파일에는 메모리에서 내려온 민감한 정보(예: 비밀번호, 개인정보)가 평문으로 저장될 수 있습니다. 따라서 오직 root 사용자만이 이 파일을 읽고 쓸 수 있도록 접근 권한을 제한해야 합니다. 이 단계를 건너뛰면, 시스템이 보안상의 이유로 스왑 파일 활성화를 거부하는 경고 메시지를 출력할 수 있습니다.


sudo chmod 600 /swapfile
  • chmod 600은 파일 소유자(root)에게는 읽기(4)와 쓰기(2) 권한을 부여하고(4+2=6), 그룹 및 다른 모든 사용자에게는 아무 권한도 주지 않는(0)다는 의미입니다.

4단계: 파일을 스왑 영역으로 포맷

지금까지는 단순히 0으로 채워진 일반 파일일 뿐입니다. 이 파일을 리눅스 커널이 스왑 공간으로 인식하고 사용할 수 있도록 특별한 포맷을 적용해야 합니다. `mkswap` 명령어가 바로 이 역할을 수행합니다.


sudo mkswap /swapfile

명령어를 실행하면 "Setting up swapspace version 1, size = 2 GiB..."와 같은 메시지와 함께 스왑 공간의 고유 식별자인 UUID가 출력됩니다. 이제 파일은 스왑 공간으로 변신할 준비를 마쳤습니다.

5단계: 스왑 파일 활성화

모든 준비가 끝났습니다. `swapon` 명령어를 사용하여 시스템이 이 스왑 파일을 즉시 사용하도록 활성화합니다.


sudo swapon /swapfile

이제 모든 설정이 완료되었습니다! 정말로 2GB의 스왑 메모리가 우리 서버에 추가되었는지 확인해 볼 시간입니다.

시스템 재부팅에도 살아남는 스왑 설정 만들기

이전 단계까지 성공적으로 마쳤다면, 여러분의 EC2 인스턴스는 이제 2GB의 든든한 가상 메모리를 갖게 되었습니다. 하지만 중요한 사실이 하나 남았습니다. sudo swapon /swapfile 명령어는 현재 실행 중인 시스템 세션에만 스왑을 활성화합니다. 만약 어떤 이유로든 EC2 인스턴스를 재부팅하게 되면, 이 스왑 설정은 흔적도 없이 사라지고 다시 메모리 부족에 시달리는 상태로 돌아가게 됩니다. 따라서 스왑 설정을 영구적으로 유지하여 재부팅 후에도 자동으로 활성화되도록 만들어야 합니다.

변경 사항 확인

영구 적용에 앞서, 현재 스왑이 잘 적용되었는지 다시 한번 확인해 봅시다. `free -h` 와 `swapon --show` 명령어를 다시 실행해 보세요.


$ free -h
              total        used        free      shared  buff/cache   available
Mem:          981Mi       875Mi        42Mi       0.0Ki        64Mi        25Mi
Swap:         2.0Gi         0Bi       2.0Gi

$ sudo swapon --show
NAME       TYPE SIZE USED PRIO
/swapfile file   2G   0B   -2

`free -h` 결과의 `Swap` 행에 우리가 생성한 2.0GiB가 total로 잡혀있는 것을 볼 수 있습니다. `swapon --show` 결과에서도 `/swapfile`이 파일(file) 타입으로 정상 등록된 것을 확인할 수 있습니다. 이제 여러분의 t2.micro 인스턴스는 1GB 물리 메모리에 2GB 가상 메모리가 더해져, 이론적으로 약 3GB의 메모리 풀을 운용할 수 있게 된 것입니다!

fstab 파일에 등록하여 영구 적용하기

리눅스 시스템은 부팅 과정에서 /etc/fstab (file systems table)이라는 파일을 읽어들여, 어떤 디스크 파티션이나 저장 장치를 어떻게 마운트할지 결정합니다. 우리는 이 파일에 스왑 파일 정보를 한 줄 추가함으로써, 시스템이 부팅될 때마다 자동으로 `/swapfile`을 스왑 공간으로 활성화하도록 만들 수 있습니다.

경고: /etc/fstab 파일은 시스템 부팅에 지대한 영향을 미치는 매우 중요한 설정 파일입니다. 단 하나의 오타나 잘못된 설정으로도 시스템이 부팅되지 않는 '먹통' 상태가 될 수 있습니다. 반드시 아래 절차를 신중하게 따라주시고, 작업 전 파일을 백업하는 습관을 들이는 것이 좋습니다.

1단계: fstab 파일 백업 (안전제일)

만에 하나 발생할 수 있는 실수를 대비해, 현재 정상적으로 작동하는 `fstab` 파일을 다른 이름으로 복사하여 백업해 둡니다.


sudo cp /etc/fstab /etc/fstab.bak

만약 문제가 발생하면, 복구 모드 등으로 부팅하여 이 백업 파일로 원상복구할 수 있습니다.

2단계: fstab 파일에 스왑 설정 추가

텍스트 편집기(vi, nano 등)를 직접 열어 수정하는 것도 좋지만, 간단한 한 줄을 추가하는 경우 실수를 줄이기 위해 `echo`와 `tee` 명령어를 파이프로 연결하여 사용하는 것이 더 안전하고 효율적입니다.


echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

이 명령어는 '/swapfile none swap sw 0 0' 이라는 문자열을 생성하여(echo), 표준 출력을 /etc/fstab 파일의 맨 끝에 추가(tee -a)합니다. `sudo` 권한으로 실행되므로 권한 문제 없이 파일에 쓸 수 있습니다.

추가된 라인의 각 필드는 다음과 같은 의미를 가집니다.

필드 설명
1. Device /swapfile 스왑으로 사용할 장치나 파일의 경로를 지정합니다.
2. Mount Point none 마운트할 디렉토리 위치입니다. 스왑 공간은 특정 디렉토리에 마운트되지 않으므로 none으로 설정합니다.
3. File System Type swap 파일 시스템의 종류를 지정합니다. 스왑 공간이므로 swap으로 명시합니다.
4. Options sw 마운트 옵션을 지정합니다. sw는 이 항목이 스왑 공간임을 나타내는 옵션입니다. defaults를 사용해도 무방합니다.
5. Dump 0 파일 시스템을 덤프(백업)할지 여부를 결정합니다. 0은 덤프하지 않음을 의미합니다.
6. Pass 0 부팅 시 파일 시스템 무결성 검사(fsck) 순서를 지정합니다. 0은 검사하지 않음을 의미하며, 루트 파일 시스템 외에는 보통 0이나 2를 사용합니다.

이제 인스턴스를 재부팅해도 스왑 설정이 사라지지 않고 영구적으로 유지될 것입니다.

전문가처럼 스왑 튜닝하기: Swappiness 최적화

스왑 설정을 완료했지만, 여기서 만족하지 않고 한 걸음 더 나아가 서버의 성능을 최적화할 수 있습니다. 스왑은 안정성을 제공하는 대신 성능 저하라는 비용을 수반합니다. 우리는 리눅스 커널의 파라미터를 조정하여, 이 비용을 최소화하고 스왑이 꼭 필요한 순간에만 작동하도록 만들 수 있습니다. 이 과정의 핵심은 'Swappiness' 값을 조절하는 것입니다.

'Swappiness' 값 조절로 성능 저하 최소화하기

리눅스 커널에는 vm.swappiness 라는 매우 중요한 파라미터가 있습니다. 이 값은 0부터 100 사이의 정수로 설정할 수 있으며, 시스템이 얼마나 '공격적으로' 또는 '소극적으로' 스왑 공간을 사용할지를 결정하는 경향성을 나타냅니다.

  • 값이 높을수록 (예: 100): 커널은 매우 공격적으로 스왑을 사용합니다. 물리 RAM에 충분한 여유 공간이 있음에도 불구하고, 당장 사용되지 않는 비활성(inactive) 메모리 페이지를 선제적으로 스왑 공간으로 옮겨 최대한 많은 RAM 여유 공간을 확보하려고 시도합니다. 이는 상시적으로 많은 애플리케이션을 띄워놓고 사용하는 데스크톱 환경에 더 적합할 수 있습니다.
  • 값이 낮을수록 (예: 0): 커널은 매우 소극적으로 스왑을 사용합니다. 가능한 한 모든 것을 물리 RAM에서 처리하려고 노력하며, 물리 RAM이 거의 완전히 소진되어 OOM Killer가 작동하기 직전의 최후의 순간에만 스왑을 사용합니다.
  • 기본값: 대부분의 리눅스 배포판(Amazon Linux, Ubuntu 등)에서 기본값은 60입니다. 이는 서버 환경에서는 다소 공격적인 값으로, 불필요한 디스크 I/O를 유발하여 성능 저하의 원인이 될 수 있습니다.

우리가 운영하는 애플리케이션 서버는 디스크 I/O로 인한 지연 시간(latency)에 매우 민감합니다. 따라서 물리 RAM을 최대한 활용하고, 스왑은 정말로 필요한 비상시에만 사용하는 것이 성능과 안정성 사이의 최적의 균형을 맞추는 방법입니다. 일반적으로 데이터베이스나 애플리케이션 서버에는 10 정도의 낮은 값이 널리 권장됩니다.

1단계: 현재 Swappiness 값 확인

먼저 현재 시스템에 설정된 swappiness 값을 확인합니다.


cat /proc/sys/vm/swappiness

아마 대부분의 경우 60이라는 결과가 출력될 것입니다.

2단계: Swappiness 값 임시 변경

sysctl 명령어를 사용하여 이 값을 재부팅 없이 즉시 10으로 변경해 보겠습니다.


sudo sysctl vm.swappiness=10

다시 `cat` 명령어로 확인해 보면 값이 10으로 변경된 것을 볼 수 있습니다.

3단계: Swappiness 값 영구 적용

이 설정 또한 재부팅하면 기본값인 60으로 돌아갑니다. 영구적으로 적용하려면 시스템 커널 파라미터 설정 파일인 /etc/sysctl.conf 파일을 수정해야 합니다.


# /etc/sysctl.conf 파일의 맨 끝에 swappiness 설정을 추가합니다.
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf

이제 여러분의 서버는 RAM 사용을 최우선으로 하다가, 정말 위급한 순간에만 스왑 공간에 의지하게 되어 불필요한 성능 저하를 방지하고 안정성은 그대로 유지하는 최적의 상태가 되었습니다.

스왑은 만능이 아니다: 지속적인 모니터링과 다음 단계

스왑 메모리는 저사양 인스턴스의 안정성을 비약적으로 높여주는 구세주 같은 존재이지만, 모든 문제를 해결해 주는 만병통치약은 결코 아닙니다. 만약 여러분의 서버가 간헐적으로 스왑을 사용하는 것이 아니라, 지속적으로 스왑 공간에 데이터를 쓰고 읽는 활동을 반복하고 있다면, 이는 더 근본적인 문제를 알리는 위험 신호입니다.

'스레싱(Thrashing)' 현상을 경계하라

시스템이 처리해야 할 작업에 비해 물리 RAM이 턱없이 부족하여, 디스크의 스왑 공간과 RAM 사이에서 데이터 페이지를 끊임없이 교환하는 현상을 '스레싱(Thrashing)'이라고 합니다. 이 상태에 빠지면 시스템은 실제 유용한 작업을 처리하는 시간보다 데이터를 옮기는(페이징) 데 대부분의 자원을 소모하게 되어, CPU 사용률은 높게 치솟지만 서버의 실제 처리 성능은 극도로 저하됩니다. 사용자는 웹사이트가 '얼어붙은' 듯한 경험을 하게 됩니다.

스레싱이 발생하고 있는지 확인하려면 `vmstat` 명령어가 매우 유용합니다. 이 명령어는 가상 메모리, 프로세스, CPU 활동 등에 대한 통계를 보여줍니다.


# 1초 간격으로 시스템 상태를 계속해서 출력합니다. (Ctrl+C로 종료)
vmstat 1

출력되는 수많은 컬럼 중에서 `swap` 영역의 siso를 주목하세요.

  • `si` (swap-in): 스왑 공간(디스크)에서 메모리(RAM)로 초당 이동한 데이터의 양 (KB 단위)
  • `so` (swap-out): 메모리(RAM)에서 스왑 공간(디스크)으로 초당 이동한 데이터의 양 (KB 단위)

서버가 평온한 상태라면 이 두 값은 거의 항상 `0`을 유지해야 합니다. 하지만 사용자의 요청이 몰리는 특정 시간에 이 값들이 지속적으로 0이 아닌 높은 숫자를 기록한다면, 이는 스레싱이 발생하고 있다는 강력한 증거입니다.

근본적인 해결을 위한 다음 단계

지속적인 스왑 사용이 관찰된다면, 이는 현재 애플리케이션이 요구하는 최소 메모리 양이 인스턴스의 물리적인 한계를 근본적으로 초과했다는 의미입니다. 이때는 다음 두 가지 해결책을 심각하게 고려해야 합니다.

  1. 애플리케이션 메모리 최적화:
    • JVM 힙 사이즈 튜닝: Spring Boot 애플리케이션의 -Xms, -Xmx 옵션을 현재 워크로드에 맞게 정밀하게 조정합니다. 불필요하게 큰 힙은 메모리 낭비의 주범입니다.
    • MySQL 설정 튜닝: innodb_buffer_pool_size와 같은 주요 메모리 파라미터를 인스턴스 사양에 맞게 낮춰 조정합니다.
    • 코드 레벨 최적화: 메모리 누수(Memory Leak)를 유발하는 코드가 없는지 프로파일링 도구를 사용하여 점검하고, 비효율적인 데이터 구조나 알고리즘을 개선합니다.
  2. 인스턴스 사양 업그레이드 (Scale-up):

    최적화를 진행했음에도 메모리 부족이 계속된다면, 더 이상 미루지 말고 EC2 인스턴스 타입을 한 단계 높은 것으로 변경하는 것이 가장 확실하고 빠른 해결책입니다. 예를 들어, t2.micro(1GB RAM)에서 t3.small(2GB RAM)이나 t3.medium(4GB RAM)으로 업그레이드하는 것을 고려해야 합니다. 이는 약간의 비용 상승을 동반하지만, 불안정한 서비스로 인한 기회비용과 스트레스에 비하면 훨씬 합리적인 투자일 수 있습니다.

스왑은 우리에게 시간을 벌어주는 도구이지, 근본적인 문제 해결을 무기한 연기시켜주는 마법이 아님을 반드시 기억해야 합니다.

결론: 안정적인 서버 운영을 위한 첫걸음

지금까지 AWS EC2 프리티어, 특히 t2.micro 인스턴스에서 빈번하게 발생하는 ec2 서버 다운 현상의 진짜 범인이 '메모리 부족'과 그로 인한 OOM Killer의 개입임을 명확히 파악했습니다. 그리고 이에 대한 가장 현실적인 응급 처방이자 효과적인 안전장치인 스왑 메모리를 생성하고, 활성화하며, 재부팅 후에도 유지되도록 영구적으로 적용하는 모든 과정을 단계별로 상세하게 살펴보았습니다. 더 나아가, 'swappiness' 값 조정을 통해 불필요한 성능 저하를 막는 전문가 수준의 튜닝 방법까지 알아보았습니다.

스왑 메모리를 설정하는 것은 단순히 몇 가지 리눅스 명령어를 입력하는 기술적인 행위를 넘어, 내가 운영하는 서버의 물리적 한계를 명확히 인지하고 그에 맞는 대비책을 스스로 마련하는 능동적인 서버 관리의 시작입니다. 이는 안정적인 서비스를 사용자에게 제공하기 위한 가장 기본적이면서도 중요한 첫걸음입니다. 이제 여러분의 소중한 애플리케이션은 예기치 않은 메모리 부족 사태에도 쉽게 쓰러지지 않는 튼튼한 맷집을 가지게 되었습니다. 서버 다운의 공포에서 벗어나, 다시 즐겁고 창의적인 개발의 세계로 돌아갈 시간입니다!

Post a Comment