컴퓨터가 느려지지 않게 관리하려고, 쓰던 노트북의 프로세스와 메모리를 처음부터 들여다봤다. 첫 측정에서 바로 이상한 숫자가 나왔다. 메모리를 23.4 GB 쓰고 있었는데, 40분쯤 뒤에는 빈 메모리가 0.2 GB밖에 안 남았다. 그런데도 컴퓨터는 멀쩡했다.

이 글은 그날 한 일을 순서대로 남긴다. 메모리 숫자를 어떻게 읽는지, 무엇으로 재는지, 프로세스 이름 뒤에 실제로 무엇이 있었는지, 그리고 무엇을 끄고 무엇을 남겼는지다.


먼저: 이 글의 값은 Windows 11 노트북 한 대에서 하루 동안 잰 것이다

판단을 좌우한 조건이 셋이다.

  • PC 한 대의 값이다. ThinkPad E14 Gen 6, Ryzen 7 7735HS(CPU가 동시에 돌릴 수 있는 일의 갈래인 논리 코어가 16개), RAM 64 GB다. 이 글에서 「메모리」는 이 RAM을 말한다. Windows는 이 중 62.8 GB를 쓴다. 꽂힌 양과 쓰는 양이 왜 다른지는 RAM 페이지의 6장 몫으로 남겨 두고, 이 글에서는 62.8 GB를 출발점으로만 쓴다
  • Windows 11의 이야기다. 하드웨어 & OS의 메모리 분야에 있는 Study 카드는 Linux 커널 소스를 읽고, 도커 안의 우분투(Linux를 묶어 내놓은 배포판의 하나)에서 잰 값이다. 이름이 비슷한 개념(캐시, 커밋)이 나와도 두 운영체제의 숫자를 섞어 읽으면 안 된다
  • 숫자는 계속 바뀐다. 메모리는 몇 초마다 달라진다. 이 글의 값은 2026-10-19 하루 동안 PowerShell 7로 잰 것이고, 같은 순간에 잰 값끼리만 견준다

메모리 숫자는 「사용 중」 하나가 아니라 네 칸으로 읽는다

처음 잰 값은 이랬다. 전체 62.8 GB 중 사용 23.4 GB(37%), 사용 가능 39.4 GB.

여기서 「사용 가능 39.4 GB」를 「비어 있는 39.4 GB」로 읽으면 틀린다. Windows는 RAM을 네 칸으로 나눠 들고 있다.

graph LR A["사용 중
약 21.6 GB
프로그램이 지금 쓰는 몫"] --- B["변경 대기
1.8 GB
디스크에 아직 안 쓴 몫"] B --- C["대기 (캐시)
15.7 GB
다시 쓸지 몰라 남겨 둔 몫"] C --- D["빈 칸
약 23.8 GB
아무것도 없는 몫"] style A fill:#2d3748,stroke:#f56565,stroke-width:2px,color:#e2e8f0 style B fill:#2d3748,stroke:#ed8936,stroke-width:2px,color:#e2e8f0 style C fill:#2d3748,stroke:#4299e1,stroke-width:2px,color:#e2e8f0 style D fill:#2d3748,stroke:#48bb78,stroke-width:2px,color:#e2e8f0
  • 사용 중은 프로그램들이 지금 쥐고 있는 몫이다
  • 변경 대기(Modified)는 내용이 바뀌었는데 아직 디스크에 안 쓴 몫이다. 프로그램이 파일을 열면 그 내용이 RAM에 올라온다. 그 내용이 바뀌었으면 원래 파일에, 프로그램이 만든 데이터라면 페이지 파일에 먼저 써 둬야 한다. 그래야 RAM에서 지워도 나중에 되살릴 수 있어서, 쓰고 나서야 내줄 수 있다
  • 대기(대기 목록)는 한 번 읽은 파일이나, 끝난 프로그램 또는 더 안 쓴다고 돌려준 페이지(메모리를 4 KB씩 나눈 한 칸)를 지우지 않고 RAM에 남겨 둔 것이다. 다시 쓰면 디스크에 안 가서 빠르고, 누가 메모리를 달라고 하면 그 자리에서 비워 내준다
  • 빈 칸(Free & Zero)은 정말 아무것도 없는 몫이다. Zero는 다른 프로그램의 데이터가 남지 않게 미리 0으로 지워 둔 칸이라는 뜻이다

「사용 가능」은 대기와 빈 칸을 더한 값이다. 그래서 「사용」 23.4 GB는 사용 중과 변경 대기를 더한 값이 된다.

40분쯤 뒤 뒤에서 만든 mod로 다시 쟀을 때 빈 칸이 0.2 GB, 대기가 39 GB였다. 처음엔 메모리가 새는 줄 알았다. 「샌다」는 건 프로그램이 받아 간 메모리를 돌려주지 않아 쓸 수 있는 양이 계속 줄어드는 현상이다. mod 값이 맞는지 보려고 Windows의 성능 카운터로 따로 재 보니 사용 가능은 여전히 39.2 GB였다. Windows가 빈 칸을 파일 캐시로 채워 둔 것이고, 앱이 달라고 하면 내줄 메모리다. 빈 칸이 0인 것은 RAM을 놀리지 않는다는 뜻일 뿐이다. 이걸 모르면 빈 칸 숫자를 보고 멀쩡한 프로그램을 끄게 된다.


「커밋」은 받아 둔 메모리라서 실제 사용량보다 크다

첫 측정에서 커밋은 28.07 GB, 한도는 81.78 GB였다. 사용 23.4 GB보다 커밋이 크다.

커밋은 프로그램들이 「이만큼 쓰겠다」고 받아 둔 양의 합이다. 프로그램이 1 GB를 달라고 하면 Windows는 「1 GB를 쓸 수 있다」고 약속하고 그 1 GB를 커밋에 올린다. 그런데 실제 RAM 페이지는 프로그램이 그 자리를 처음 건드릴 때(페이지 폴트) 붙인다. 약속만 하고 아직 안 건드린 몫이 커밋에는 들어가고 사용 중에는 안 들어간다. 그래서 커밋이 실제 사용량보다 크다.

한도 81.78 GB는 RAM 62.8 GB에 디스크의 페이지 파일(Linux의 스왑과 같은 역할) 약 19 GB를 더한 크기다. Windows는 이 한도 안에서만 약속한다. 그래서 커밋을 보면 두 단계의 위험을 읽을 수 있다.

  • 커밋이 RAM 크기(62.8 GB)를 넘으면 약속한 메모리를 프로그램들이 다 쓰는 순간 RAM에 다 못 들어간다. Windows는 덜 쓰는 페이지를 페이지 파일로 밀어내고, 다시 필요하면 디스크에서 읽어 온다. 이때부터 눈에 띄게 느려진다
  • 커밋이 한도(81.78 GB)에 닿으면 더 약속할 수 없어서 메모리를 달라는 프로그램이 실패한다

이 PC는 커밋 28 GB로 RAM 크기의 절반 아래였다. 느려질 걱정은 없는 상태다.


프로세스 하나의 메모리는 워킹 셋과 Private 두 숫자로 본다

프로세스마다 보는 숫자도 둘이다.

  • 워킹 셋(Working Set)은 지금 실제 RAM 위에 올라와 있는 양이다. 작업 관리자의 「메모리」 칸과 비슷하다
  • Private은 그 프로세스 혼자 쓰려고 커밋한(받아 둔) 양이다. 앞 절의 커밋을 프로세스 하나로 좁힌 값이라고 보면 된다. 아직 안 건드린 몫도, 페이지 파일로 밀려난 몫도 들어간다

둘이 크게 다를 때가 있다. 그날 띄워 둔 Claude Code 창 네 개는 워킹 셋을 합쳐 1.6 GB, Private을 합쳐 2.8 GB였다. 받아 둔 2.8 GB 중 1 GB 넘게가 지금 RAM에 없다는 뜻이다. 아직 안 건드렸거나 페이지 파일로 밀려난 몫인데, 둘이 각각 얼마인지는 재지 않았다.

워킹 셋에는 함정이 하나 있다. DLL 파일은 RAM에 한 벌만 올라가고, 그 DLL을 쓰는 프로세스들이 모두 같은 자리를 가리킨다. 이렇게 여러 프로세스가 같이 쓰는 메모리는 각자의 워킹 셋에 다 들어간다. 그래서 프로세스 167종(이름이 같은 것을 한 종으로 묶었다. 개수로는 408개)의 워킹 셋을 모두 더하니 23.72 GB였고, 같은 순간 실제 사용은 21.82 GB였다. 프로세스별 값은 맞지만, 다 더하면 실제보다 크게 나온다. 「목록을 더해 보니 RAM보다 많이 쓴다」는 계산은 성립하지 않는다.


메모리를 늘 보는 방법은 네 안 중 Claude Code mod로 정했다

정해야 했던 건 이거였다 — 메모리 상태를 어디서, 얼마의 비용으로 늘 볼 것인가.

각 안의 비용은 직접 띄워서 쟀다. 창을 열고 20초 기다린 뒤 다음 20초 동안 쓴 CPU와 메모리다.

안 A — 작업 관리자

Ctrl+Shift+Esc로 바로 열린다. 비용은 CPU 1.85%(16코어 전체 기준), 메모리 198 MB였다. 처음 10초만 쟀을 때는 4.17%였는데, 창이 막 뜨는 비용이 섞였다.

얻는 것은 누구나 아는 화면이라는 점이다. 버린 이유는 대기와 빈 칸을 나눠 보여 주지 않아서다. 앞에서 본 「빈 칸 0.2 GB인데 멀쩡하다」를 이 화면만으로는 판단할 수 없다.

안 B — 리소스 모니터

Win+R로 실행 창을 열고 resmon을 친 뒤 메모리 탭을 누른다. 비용은 CPU 3.15%, 메모리 51 MB였다.

얻는 것은 네 칸이 색깔 막대 하나에 나뉘어 나온다는 점이다. 화면으로 볼 때는 지금도 이걸 추천한다. 버린 이유는 창을 따로 띄워 둬야 한다는 점이다. 나는 처음부터 작업하는 터미널 속 Claude Code 안에 띄워 둘 생각이었고, 다른 창으로 옮겨 가야 보이는 화면은 결국 안 보게 된다.

안 C — 필요할 때 PowerShell 명령을 친다

$os = Get-CimInstance Win32_OperatingSystem
$os.TotalVisibleMemorySize / 1MB   # 전체 (KB 단위라 1MB로 나누면 GB)
$os.FreePhysicalMemory / 1MB       # 사용 가능

첫 줄은 Windows 관리 창구인 WMI에서 운영체제 정보를 꺼내 $os라는 변수에 담는다. PowerShell 변수는 이름 앞에 $를 붙인다. 둘째와 셋째 줄은 그 안의 전체와 사용 가능 값을 GB로 바꾼다. 값이 KB 단위로 오는데, PowerShell에서 1MB는 1,048,576이라는 숫자라서 KB를 그만큼 나누면 GB가 된다.

FreePhysicalMemory는 이름이 Free지만 빈 칸만이 아니라 대기까지 더한 「사용 가능」 값이다. 첫 측정에서 이 값이 39.4 GB였다. 이게 빈 칸만이었다면, 빈 칸 39.4 GB에 대기 15.7 GB와 사용 23.4 GB를 더한 78.5 GB가 RAM에 있어야 한다. 전체 62.8 GB를 넘으니 말이 안 된다.

이 안의 장점은 늘 떠 있는(상주하는) 프로그램이 없어서 명령을 칠 때만 비용이 든다는 점이다. 버린 이유는 실시간이 아니라서다. 느려진 순간에 명령을 쳐야 하고, 그 사이의 변화는 못 본다.

골랐다 — 안 D, Claude Code mod. Claude Code 화면 아래쪽 상태줄에 한 줄을 늘 띄우고, 명령을 치면 Claude Code 화면 안에 따로 칸(패널)을 열어 자세히 보여 준다. 안 A와 B처럼 창을 따로 띄우지 않고, 안 C와 달리 계속 갱신된다. 대신 mod를 내가 만들고 고쳐야 하고, 값을 가져오는 비용도 내가 책임져야 한다. 그 비용을 어떻게 줄였는지가 다음 절이다.


mod는 pwsh 하나를 계속 띄워 두고 가벼운 명령만 돌린다

mod는 TypeScript로 짠다. 그런데 mod 안에서는 Windows 메모리 값을 직접 못 읽어서, 바깥 프로그램을 실행해 결과를 받아야 한다. 여기서는 PowerShell 7의 실행 파일인 pwsh를 쓴다. 프로세스는 다른 프로세스를 띄울 수 있고, 띄운 쪽을 부모, 뜬 쪽을 자식 프로세스라고 한다. mod가 pwsh를 자식으로 띄우면, pwsh가 화면에 찍는 글자(표준 출력)가 부모인 mod 쪽으로 흘러 들어온다. 정해야 했던 건 이거였다 — 5초마다 값을 가져오는 비용을 얼마까지 낮출 수 있는가.

안 A — 5초마다 pwsh를 새로 띄워 명령을 실행한다

가장 단순하다. 그런데 재 보니 1회에 CPU 1,425 ms였다. CPU 시간은 CPU가 이 일에 실제로 매달린 시간이다. 5초(5,000 ms)마다 1,425 ms를 쓰면 코어 하나의 28%다. pwsh라는 프로그램이 뜨는 비용이 측정 비용보다 훨씬 크다. 메모리를 보려고 메모리 감시가 CPU를 잡아먹는 꼴이라 버렸다.

안 B — pwsh를 하나 띄워 두고, 안에서 위의 명령을 반복한다

뜨는 비용은 한 번만 든다. 그래도 1회에 CPU 248 ms였다. 어디가 무거운지 항목별로 쪼개 쟀다.

# 무거운 쪽 (10회 평균)
Get-CimInstance Win32_OperatingSystem                    # 249.7 ms
Get-Process | Group-Object Name | ForEach-Object {
  ($_.Group | Measure-Object WorkingSet64 -Sum).Sum }    # 101.0 ms

# 같은 값을 내는 가벼운 쪽
[W.M]::GlobalMemoryStatusEx([ref]$m)                     # 0.9 ms
$h = @{}
foreach ($p in [Diagnostics.Process]::GetProcesses()) {
  $h[$p.ProcessName] += $p.WorkingSet64 }                # 19.2 ms

윗부분은 안 B가 쓰던 두 명령이다. 한 줄씩 따라가면 이렇다.

  • Get-CimInstance Win32_OperatingSystem은 WMI를 거쳐 메모리 값을 가져온다. WMI가 중간에 관리 서비스를 거쳐서 250 ms가 걸린다
  • Get-Process는 프로세스 목록을 가져온다. |(파이프)는 왼쪽 결과를 오른쪽 명령에 넘긴다
  • Group-Object Name은 이름이 같은 프로세스끼리 묶는다. 묶음마다 새 객체를 만든다
  • ForEach-Object { ... }는 묶음 하나씩 중괄호 안을 실행한다. 자바의 for (var g : groups)와 같고, $_가 지금 도는 묶음 g다
  • Measure-Object WorkingSet64 -Sum은 그 묶음의 워킹 셋을 더한다

묶고, 묶음마다 객체를 만들고, 다시 더하는 과정에서 100 ms가 걸린다.

아랫부분은 같은 값을 다르게 가져온다.

  • GlobalMemoryStatusEx는 Win32 API, 곧 Windows가 프로그램에게 직접 내주는 함수라서 WMI를 안 거친다. [W.M]은 스크립트 맨 앞에서 이 함수를 PowerShell에서 부를 수 있게 C# 코드로 선언해 둔 이름이다. [타입]::메서드()는 자바의 타입.정적메서드() 호출과 같다. $m은 선언 부분에서 미리 만들어 둔, 결과를 받을 구조체(필드만 있는 작은 클래스라고 보면 된다)다. [ref]는 자바에는 없는 문법으로 「함수가 이 변수에 결과를 채워 넣게 넘긴다」는 뜻이다. 선언 부분은 길어서 여기서는 뺐다
  • [Diagnostics.Process]::GetProcesses()도 같은 모양의 정적 메서드 호출이다. 지금 떠 있는 프로세스를 배열로 돌려준다
  • $h = @{}는 빈 해시 테이블을 만든다. 자바의 new HashMap<>()이다
  • foreach는 프로세스를 하나씩 돌며 $h[이름] += 워킹 셋을 한다. 처음 나온 이름은 값이 비어 있는데, PowerShell은 빈 값에 숫자를 더하면 그 숫자가 된다. 자바로 쓰면 if (!h.containsKey(name)) h.put(name, 0L); 다음에 h.put(name, h.get(name) + ws);를 하는 두 줄을 한 줄로 하는 셈이다

둘째 방법의 19 ms는 거의 전부 프로세스 목록을 읽는 시간이다.

골랐다 — 안 C, pwsh 하나를 띄워 두고 가벼운 명령만 돌린다. 안 A의 뜨는 비용과 안 B의 WMI 비용을 둘 다 없앤다. pwsh는 5초마다 결과를 JSON 한 줄로 내보내고, mod는 그 줄을 읽어 화면을 다시 그린다. 처음에는 1회 17 ms로 예상했는데, 실제로 띄워서 60초 재 보니 44 ms였다. 정렬과 JSON 변환을 예상에 넣지 않았기 때문이다. 나중에 CPU · 디스크 · 네트워크까지 넣은 뒤에는 86~105 ms가 됐다. 5초마다면 코어 하나의 2% 안팎, 16코어 전체로는 0.13%다.

대신 pwsh가 늘 떠 있는 비용을 감수한다. 워킹 셋 166~184 MB다. 측정 코드는 몇 KB이고 나머지는 pwsh 자체가 쓴다.

안 D — pwsh 대신 작은 C# 실행 파일로 바꾼다. PowerShell 엔진 전체를 싣지 않고 측정 코드만 돌리니, 메모리가 5~15 MB로 줄 것으로 봤다. 재지는 않았다. 지금 비용이 거슬리지 않아서 아직 하지 않았다.


mod는 상태줄 한 줄과 /mem 패널로 보여 주고 /mem off로 멈춘다

처음 만든 mod는 상태줄의 RAM 22.5/62.8GB 36% 한 줄과, 상위 8개 프로세스를 보여 주는 패널이 전부였다. 써 보니 너무 단순했다. 지금은 이렇게 보인다.

CPU   ████░░░░░░░░░░░░░░░░  25%  16코어
RAM   ████████████░░░░░░░░  38%  24.0/62.8 GB
      사용 중 23.3 · 변경 대기 0.7 · 캐시 39.0 · 빈 칸 0.2 GB
커밋  28.9/81.8 GB · 프로세스 409개 · 켜진 지 45분
C:    ████████░░░░░░░░░░░░  44%  408/930 GB
I/O   디스크 읽기 1.4 쓰기 7.1 MB/s · 네트워크 ↓6.1 ↑0.2 MB/s
전원  배터리 89% 전원 연결

프로세스 167종 · 408개 (메모리 순, 휠이나 화살표로 스크롤)
이름                     개수      메모리     CPU
Orca                       10     2.31 GB    0.5%
...

맨 위 두 줄이 CPU와 RAM 막대다. 막대는 60% 미만이면 초록, 85% 미만이면 노랑, 그 이상이면 빨강이다. 셋째 줄이 앞에서 본 네 칸이다. 사용 중 23.3에 변경 대기 0.7을 더하면 둘째 줄의 24.0 GB가 된다. 처음에는 이 줄을 「사용」이라고 짧게 적었다가, 본문의 「사용」(사용 중 + 변경 대기)과 같은 말이 다른 뜻이 되어 「사용 중」으로 고쳤다. 넷째 줄의 I/O는 입출력(Input/Output), 곧 디스크와 네트워크로 오가는 양이다. 아래 목록은 같은 이름의 프로세스를 묶어 메모리 순으로 전부 보여 준다. 맨 위 Orca는 내가 터미널과 작업 폴더를 여러 개 띄워 쓰는 앱이다. 상위 8개만 보여 주던 표를 목록 하나로 바꿨고, 메모리는 GB로 소수점 둘째 자리까지 보인다.

명령은 셋이다.

  • /mem — 패널을 열고 닫는다. 닫아도 상태줄은 계속 뜬다
  • /mem off — 감시를 멈춘다. 상태줄이 지워지고, pwsh는 5초 안에 꺼진다
  • /mem on — 다시 켠다

/mem off를 친 뒤 mod용 pwsh가 실제로 꺼졌는지 확인했다. 그 뒤 /mem on으로 새로 뜬 것 하나만 남아 있었다. mod는 Claude Code를 켠 그 한 번의 실행(세션) 전용 폴더에 있다. Claude Code는 그 세션에서 「이 폴더의 mod를 불러온다」고 켜 둔 동안만 그 폴더를 읽는다. 그래서 세션을 닫으면 mod도 꺼지고, 새 세션에서는 로드되지 않는다.


이상해 보인 CPU 값은 다른 카운터로 다시 재서 전체 값이 맞는 것까지 확인했다

패널을 처음 띄웠을 때 CPU가 37%였다. 그런데 CPU 상위 프로세스를 다 더해도 8% 남짓이었다. 측정이 틀렸을 수 있어서, 몇 분 뒤 카운터 세 개를 한 번에 같이 읽었다. 37%였던 바로 그 순간의 값은 아니다.

% Processor Utility      24.65
% Processor Time         20.72
% Processor Performance 110.20

세 줄을 하나씩 보면 이렇다.

  • Processor Utility는 작업 관리자가 보여 주는 값이고, mod도 이걸 쓴다
  • Processor Time은 CPU가 쉬지 않은 시간의 비율이다
  • Processor Performance는 지금 클럭이 기본 클럭의 몇 %인가이다. 110%면 터보로 기본보다 빠르게 돌고 있다

Utility는 클럭 비율을 반영해 「기본 클럭 기준으로 얼마나 일했나」를 낸다. 그래서 터보로 돌 때는 Time보다 크게 나온다. 다만 같은 순간에 읽은 값끼리도 Time × 클럭 비율(20.72 × 1.102 = 22.8)이 Utility 24.65와 딱 맞지는 않았다. 정확한 계산식은 확인하지 않았다.

이것으로 확인한 것은 전체 CPU 값 자체는 맞다는 것까지다. 전체 20~25%와 프로세스 합 8%의 차이가 어디서 왔는지는 다 밝히지 못했다. 후보는 셋이다. 첫째, Utility는 클럭 비율 때문에 크게 나온다. 둘째, mod가 그때 상위 8개만 보여 줬다. 셋째, 하드웨어 인터럽트 처리처럼 어느 프로세스의 몫으로도 안 잡히는 CPU 시간이 있다. 셋이 각각 얼마인지는 재지 않았다. 37%가 나온 순간에 무엇이 CPU를 썼는지는 확인하지 못했다. 다시 잴 때 프로세스 카운터에서 Windows Defender(MsMpEng)가 가장 많이 쓰고 있었지만, 그게 37%였던 순간과 같은 원인이라는 근거는 없다.


프로세스는 이름이 아니라 경로 · 부모 · 명령줄로 정체를 가린다

메모리 목록 위쪽을 보다가 이름만으로는 정체를 알 수 없다는 걸 세 번 겪었다.

ChatGPT를 안 쓰는데 ChatGPT가 2.2 GB를 쓰고 있었다. ChatGPT.exe 14개의 실행 경로를 보니 전부 WindowsApps\OpenAI.Codex_26.1013...\app\ChatGPT.exe였다. Codex 데스크톱 앱의 실행 파일 이름이 ChatGPT.exe였다. 진짜 ChatGPT 앱은 그보다 먼저 종료한 「ChatGPT Classic」이었을 가능성이 크다. Codex 앱은 Electron으로 만든 앱이라, 메인 프로세스 하나가 화면마다 그 화면을 그리는 renderer 프로세스를 따로 띄웠다. 큰 renderer 셋만 합쳐도 1.2 GB였다.

이어서 「이 앱이 떠 있는 게 PowerShell에서 언제든 codex 명령(CLI)을 부르기 위해서인가」를 확인했다. 아니었다. PowerShell의 codex는 C:\Users\taegyu\AppData\Local\Programs\OpenAI\Codex\bin\codex.exe에 따로 깔린 실행 파일(319 MB)이다. 앱을 끈 뒤에도 codex --version은 codex-cli 0.162.0으로 바로 응답했다. 앱이 자식으로 띄운 codex.exe 두 개는 경로부터 달랐다. 명령줄, 곧 프로세스를 띄울 때 실행 파일 뒤에 붙인 인자가 app-server와 exec-server였다. 앱 화면 뒤에서 실제 일을 맡는 앱 자신의 백엔드다.

msedgewebview2가 50개였다. Edge를 안 켰는데도 떠 있었다. WebView2는 다른 앱이 화면을 웹 페이지로 그릴 때 빌려 쓰는 Edge 엔진이다. webview2 프로세스마다 부모를 거슬러 올라가니 주인이 나뉘었다. TeamViewer 512 MB, Windows 검색 450 MB, OneDrive 370 MB, M365 Copilot 327 MB, GlobalProtect VPN 310 MB, Teams 230 MB, 카카오톡 97 MB였다. webview2만 골라 끄면 그 앱들의 화면이 깨진다. 줄이려면 주인 앱을 꺼야 한다.

블로그 로컬 서버는 jekyll이 아니라 ruby로 보였다. 이 블로그는 Jekyll로 만든다. 글을 고치면서 내 PC에서 미리 보려고 bundle exec jekyll serve를 치면, 내 PC 안에 블로그를 띄우는 서버가 뜬다(localhost로 접속한다). 명령은 하나를 쳤는데 프로세스는 넷이 생겼다.

graph LR A["cmd
bundle.bat 실행"] --> B["ruby 28 MB
bundle exec"] B --> C["cmd
jekyll.bat 실행"] C --> D["ruby 232 MB
실제 Jekyll 서버
4000 포트"] style A fill:#2d3748,stroke:#718096,stroke-width:2px,color:#e2e8f0 style B fill:#2d3748,stroke:#4299e1,stroke-width:2px,color:#e2e8f0 style C fill:#2d3748,stroke:#718096,stroke-width:2px,color:#e2e8f0 style D fill:#2d3748,stroke:#48bb78,stroke-width:2px,color:#e2e8f0

Jekyll은 Ruby로 만든 프로그램이라 ruby.exe가 Jekyll 코드를 실행한다. 자바 프로그램을 java.exe가 실행하는 것과 같다. 앞의 ruby는 bundle이라는 Ruby 도구로, 라이브러리 버전을 맞춘 뒤 Jekyll을 띄운다. .bat 파일은 Windows에서 cmd가 실행해야 해서 중간에 cmd가 끼었다. 진짜 서버는 4000번 포트를 잡고 있는(브라우저가 4000번으로 들어오면 그 요청을 받는) 마지막 ruby다.

반대로 수상해 보였지만 정상인 것도 있었다. 정식으로 설치한 프로그램은 보통 Program Files 아래에 있고, Temp는 잠깐 쓰고 지우는 파일을 두는 폴더다. 그래서 Temp에서 계속 도는 프로그램은 악성 프로그램을 의심해 볼 만하다. Temp 폴더에서 도는 UnInstDaemon.exe는 Microsoft 코드 서명이 유효했고, Bing Wallpaper에 딸린 프로세스였다.

그래서 프로세스의 정체는 이 순서로 가린다. 실행 경로(어느 앱 폴더에 있나), 부모 프로세스(누가 띄웠나), 명령줄(무슨 역할로 떴나), 서명(누가 만들었나)이다. 이름은 마지막 참고일 뿐이다.


svchost 94개는 Windows 서비스의 그릇이라 끄지 않는다

목록 위쪽에는 늘 svchost가 있다. 94개, 합쳐서 1.82 GB였다.

svchost는 Windows 서비스를 담아 돌리는 그릇 프로세스다. Windows 업데이트, 알림, 클립보드 같은 기능은 대부분 단독 exe가 아니라 DLL로 만들어져 있다. DLL은 혼자 실행될 수 없어서 svchost가 불러와 대신 돌린다. 메모리가 넉넉한 PC에서는 서비스마다 svchost를 따로 띄운다. 서비스 하나가 죽어도 다른 서비스가 같이 죽지 않게 하려는 것이다. 실제로 94개 중 90개가 서비스를 하나씩만 담고 있었다.

가장 큰 svchost는 Windows 업데이트였고 68 MB였다. 그다음이 진단 정책 서비스 53 MB, 원격 분석(Connected User Experiences and Telemetry) 51 MB, 클립보드 기록 46 MB 순이었다. 평균은 20 MB 정도다. 같은 이름이 많아서 목록 위로 올라왔을 뿐, 하나하나는 작다. 어떤 서비스가 든 svchost인지 모르고 끄면 네트워크나 소리가 끊기거나 재부팅된다. 서비스 단위로 끄더라도 하나에 50 MB 안팎이라 얻는 것이 거의 없다. 그래서 손대지 않았다.


지금 끄는 것과 다음 부팅에 안 뜨게 하는 것은 따로 해야 한다

프로세스를 종료하면 메모리는 바로 돌아온다. 그런데 다음에 PC를 켜면 다시 뜬다. 느려지는 원인을 끊는 곳은 시작 앱이다.

시작 앱은 등록하는 자리가 네 곳이었다. Windows 레지스트리의 Run 키, 시작 폴더, 스토어 앱의 시작 작업(스토어에서 받은 앱이 「로그인하면 나를 띄워 달라」고 등록해 두는 자리), 로그인할 때 도는 예약 작업이다. 한 곳만 보면 빠지는 것이 생긴다. Notion과 Slack을 끌 때도 네 곳을 다 뒤져서 Run 키 말고는 없는 것을 확인했다.

여기서 정해야 했던 건 이거였다 — 시작 앱을 어떻게 막을 것인가.

안 A — 등록 항목을 지운다. 목록이 깔끔해진다. 버린 이유는 되돌리기 어렵다는 점이다. 앱마다 등록 경로와 인자(--startup, -bystartup처럼)가 달라서, 다시 켜고 싶을 때 원래 값을 기억해야 한다.

골랐다 — 안 B, 작업 관리자의 「사용 안 함」과 같은 표시를 단다. Run 키와 시작 폴더는 옆의 StartupApproved 키에 「꺼짐」 표시를 쓰고, 스토어 앱은 시작 작업의 상태값을 「사용자가 끔」으로 바꾼다. 등록은 그대로 남아서 작업 관리자에서 다시 켜면 된다. 대신 목록에 꺼진 항목이 계속 남는다.

예외가 두 가지 있었다.

  • ViennaControl은 지우기로 했다. 시작 목록에 있었지만, 가리키는 ViennaServer.exe가 이미 없는 파일이었다. 꺼 둘 대상이 아니라 빈 항목이다
  • TeamViewer는 시작 앱에 없는데도 다시 뜬다. TeamViewer 서비스가 시작 유형 「자동」이라, 서비스가 화면 앱을 띄운다. 시작 앱이 아니라 서비스의 시작 유형을 「수동」으로 바꾸기로 했다

두 가지 모두 관리자 권한이 필요해서 관리자 창에서 실행할 명령으로 넘겼다. 같은 이유로 Samsung Update 예약 작업도 내 권한으로는 「액세스가 거부되었습니다」가 나와서 관리자 창에서 껐다. 끈 뒤에도 상태가 Running으로 나와서 실패한 줄 알았다. 작업 설정의 Enabled는 False로 바뀌어 있었다. 끄기 전에 이미 한 번 실행돼 돌고 있던 프로그램이 끝날 때까지 상태가 Running으로 보인 것이다. 작업을 꺼도 이미 도는 것까지 멈추지는 않는다.


끌 것과 남길 것은 목록을 보고 내가 골랐다

무엇을 안 쓰는지는 Claude가 판단할 수 없다. 그래서 떠 있는 앱과 시작 앱을 전부 뽑아 묶음으로 보여 달라고 했고, 내가 골랐다.

지금 종료한 것은 이렇다.

  • ChatGPT Classic 893 MB
  • Phone Link 460 MB
  • Adobe Express 357 MB
  • Bing Wallpaper 238 MB
  • TeamViewer 119 MB
  • Notion 1.27 GB
  • Slack 0.85 GB
  • Codex 앱 2.38 GB

Notion과 Slack은 「연락 올 일이 없다」는 이유로 껐다.

시작 앱에서 끈 것은 세 묶음이다.

  • 부가 앱: Bing Wallpaper, Adobe Photoshop Express, Riot Client, Microsoft Start 피드, Power Automate, OneNote로 보내기, Copilot, Samsung Update, Lenovo Vantage 툴바
  • Notion · Slack
  • AI 앱과 브라우저 · 폰 연결: ChatGPT, Codex, Gemini, Perplexity, Ollama, Chrome · Edge 미리 띄우기, Figma Agent, Phone Link, CrossDevice. 처음 고를 때는 남겼지만, 글을 쓰며 다시 물었을 때 실제로는 안 쓰는 것들이라 껐다

일부러 남긴 것도 있다. 남긴 이유가 각각 다르다.

  • 보안 프로그램. GlobalProtect(VPN)와 DBSAFER는 회사가 깔았을 가능성이 크다. AhnLab Safe Transaction, MagicLine, SKCert, Veraport는 은행이나 관공서 사이트에서 필요하다
  • java 두 개(1.6 GB). OpenJDK 24로 도는 자바 프로그램이다. 무엇인지는 명령줄의 classpath(자바가 클래스를 찾는 곳 목록)를 봐야 아는데, 내가 출력을 140자에서 잘라 찍어서 그 부분이 안 보였다. 정체를 모르는 것은 끄지 않았다
  • svchost. 앞 절의 이유다

결과적으로 사용 가능 메모리는 정리를 시작할 때 38.74 GB에서 44.69 GB로 약 6 GB 늘었다. 두 값 사이에 한 시간쯤 흘러서, 다른 프로그램이 늘고 준 몫도 섞인 숫자다. 종료할 때마다 바로 앞뒤로 잰 값은 +1.69 GB, +1.50 GB, +1.78 GB였다. 이것도 끈 앱들의 워킹 셋 합보다는 작다. DLL처럼 같이 쓰던 몫은 앱 하나를 꺼도 돌아오지 않기 때문이다.


판단 기준 정리

질문 답 결론
빈 칸이 0에 가깝다 대기(캐시)는 달라면 내준다 「사용 가능」을 본다
정말 모자란가 커밋이 RAM 크기를 넘으면 디스크로 밀려나기 시작한다 커밋을 RAM 크기 · 한도와 견준다
목록 합이 RAM보다 크다 같이 쓰는 메모리가 중복으로 세진다 프로세스별로만 견준다
이 프로세스는 무엇인가 이름은 다를 수 있다 경로 · 부모 · 명령줄 · 서명 순으로 본다
같은 이름이 수십 개다 svchost · webview2는 그릇이다 그릇이 아니라 안에 든 것이나 주인을 본다
꺼도 다시 뜬다 시작 앱이나 서비스가 띄운다 종료와 시작 앱 끄기를 따로 한다
시작 앱을 어떻게 막나 지우면 되돌리기 어렵다 「사용 안 함」 표시를 단다. 빈 항목만 지운다
정체를 모른다 끄면 무엇이 깨질지 모른다 확인하기 전에는 끄지 않는다
늘 보고 싶다 측정 비용이 상주 비용이 된다 항목마다 재서 싼 방법만 쓴다

이 정리를 하기까지

시작은 「지금 무엇이 메모리를 먹고 있나」였다. 전체 값과 상위 프로세스를 뽑았다. 사용 23.4 GB, 프로세스 442개였다.

「사용량은 어떻게 확인하는 건가」를 물었다. 숫자를 내준 명령을 하나씩 풀었다. 이때 대기와 빈 칸을 처음 갈라 봤고, 리소스 모니터가 그 둘을 나눠 보여 준다는 것을 알았다.

「실시간으로 보면 리소스를 많이 먹나」를 직접 쟀다. 작업 관리자는 처음 4.17%가 나왔는데, 창이 막 뜨는 비용이 섞인 값이었다. 기다렸다가 다시 재니 1.85%였다. 첫 숫자를 그대로 믿었다면 작업 관리자가 리소스 모니터보다 무겁다고 잘못 적었을 것이다.

사실 내가 원한 것은 Claude Code mod였다. 그래서 질문이 「창으로 띄우는 GUI 도구의 비용」에서 「명령 한 번의 비용 × 실행 횟수」로 바뀌었다. pwsh를 매번 띄우는 1,425 ms에서 시작해, 띄워 두는 248 ms를 거쳐, 항목별로 쪼개 보니 WMI 조회 하나가 250 ms였다.

만들고 재 보니 예상의 2.5배였다. 17 ms로 잡은 것이 44 ms였다. 정렬과 JSON 변환을 예상에서 빼먹었다. 그래도 16코어 전체의 0.06%라 그대로 두었다.

패널을 여러 수치를 한 화면에 모은 판(대시보드)으로 키우면서 이상한 값 두 개를 만났다. 빈 칸 0.2 GB와 CPU 37%다. 둘 다 다른 카운터로 다시 쟀다. 빈 칸은 정상이었고, CPU는 전체 값이 맞는 것까지만 확인했다. 프로세스 합과의 차이는 다 밝히지 못했다. 빈 칸 0.2 GB 덕에 「빈 칸이 아니라 사용 가능을 본다」는 기준이 생겼다.

webview2와 svchost를 물으면서 「같은 이름이 수십 개」의 정체를 알았다. 둘 다 다른 것을 담는 그릇이었다. 그릇을 끄면 안에 든 것이 깨진다.

정리하자고 했을 때, 무엇을 안 쓰는지는 내가 고르기로 했다. 목록을 묶음으로 받아 골랐다. 보안 프로그램은 처음부터 빼 두었다.

「ChatGPT를 안 쓰는데 왜 가장 많이 먹나」에서 이름을 믿으면 안 된다는 걸 알았다. 실행 경로를 보니 Codex였다. 앱이 PowerShell의 codex 명령을 위해 떠 있는 것도 아니었다. 이 일 뒤로 mod 목록도 GB 단위로 전부 보이게 바꿨다.

마지막으로 끈 것은 시작 앱이었다. 종료만 하면 재부팅 뒤에 다시 뜬다. Notion · Slack, AI 앱, 브라우저 미리 띄우기까지 시작 앱에서 껐다. TeamViewer는 서비스가 띄운다는 것을 알고 서비스 쪽을 손보기로 했다.


아직 안 한 것

  • 재부팅 뒤 확인. 시작 앱을 끈 설정값이 「꺼짐」으로 바뀐 것까지는 다시 읽어 확인했다. 재부팅 뒤 실제로 안 뜨는지는 아직 못 봤다
  • java 두 개의 정체. 전체 명령줄을 뽑아 무엇인지 확인해야 한다
  • mod 비용 줄이기. 정렬과 JSON 변환을 직접 짜면 1회 60 ms 안팎으로 줄 것으로 본다. pwsh를 C# 실행 파일로 바꾸면 상주 메모리가 줄 것으로 본다. 둘 다 재지 않았다
  • 62.8 GB의 이유. 64 GB와의 차이는 RAM 페이지 6장에서 확인한다

정리

  • 메모리는 네 칸으로 읽는다. 사용 중 · 변경 대기 · 대기(캐시) · 빈 칸이다. 대기는 달라면 내주므로 「사용 가능」을 본다. 빈 칸이 0이어도 괜찮다
  • 모자란지는 커밋을 RAM 크기와 한도에 견줘 본다. 커밋은 받아 둔 양이라 실제 사용보다 크다. RAM 크기를 넘으면 느려지기 시작하고, 한도에 닿으면 실패한다
  • 워킹 셋은 다 더하면 실제보다 크다. 같이 쓰는 메모리가 중복으로 세지기 때문이다
  • 프로세스는 경로 · 부모 · 명령줄 · 서명으로 가린다. ChatGPT.exe는 Codex였고, jekyll은 ruby였다
  • svchost와 webview2는 그릇이다. 그릇이 아니라 안에 든 서비스나 주인 앱을 본다
  • 종료와 시작 앱 끄기는 따로 한다. 시작 앱은 지우지 않고 「사용 안 함」 표시를 단다
  • 늘 띄워 두는 감시는 측정 비용이 곧 상주 비용이다. 항목마다 재서 WMI 대신 Win32 API, Group-Object 대신 해시 합산을 골랐다

자신만의 철학을 만들어가는 중입니다.
최상단으로 이동했습니다!
확대 이미지

댓글남기기