문서 읽는 데 72분 · A4

A-4: 파이프·리다이렉션·검색

목차 29
전체 12강 중 4강 · 리눅스
난이도 · 입문

ℹ️서버·배포에서 쓰는 리눅스 셸과 명령어 기본기. 백엔드·인프라로 가기 전에 익혀두면 든든해요.

안녕하세요, 홍순구입니다. 지난 시간엔 권한 10글자를 읽고 chmod로 바꾸며, Permission denied를 무작정 sudo로 때우지 않고 스스로 진단하는 법까지 익혔어요. 이제 파일을 만들고 옮기고 권한까지 다루니, 리눅스 위에서 제법 자유롭게 손을 놀리게 됐습니다.

그런데 ls -l을 칠 때마다 한 가지 욕심이 스멀스멀 올라왔을 거예요. 파일이 몇십 개만 돼도 출력이 화면을 가득 채우고 주르륵 흘러가버리거든요. "이 긴 출력에서 내가 원하는 줄만 쏙 뽑아볼 순 없을까?", "이 결과를 화면 말고 파일로 저장해두면 안 되나?" — 지난 시간 마지막에 제가 "그건 다음 시간에"라고 미뤄뒀던 바로 그 이야기입니다.

오늘은 이걸 현업 상황으로 한번 그려볼게요. 여러분이 운영 서버에 들어갔는데, 로그 파일이 수천 줄이에요. 그중 에러가 난 줄만 찾아야 합니다. 신입 때 많은 분들이 이걸 화면을 위아래로 스크롤하며 눈으로 찾아요. 수천 줄을요. 오늘 배우는 도구 세 가지 — 리다이렉션, 파이프, grep — 이 그 고생을 한 줄 명령으로 끝내줍니다.

이 셋을 관통하는 건 리눅스, 더 거슬러 올라가면 유닉스의 오래된 철학이에요. "작은 도구 하나가 한 가지 일을 아주 잘하고, 그 도구들을 컨베이어 벨트처럼 이어 붙여 큰일을 한다." ls도, grep도, sort도 각자 한 가지만 합니다. 대신 파이프(|)로 이으면 못 할 게 없어져요.

오늘 걸어갈 길을 한눈에 그려둘게요.

텍스트
  [세 갈래 길 이해]  stdin · stdout · stderr  (0 · 1 · 2)
       │
       
  [결과를 파일로]  리다이렉션   >   >>   2>   tee
       │
       
  [도구를 잇기]  파이프  |    ───   [원하는 줄만]  grep + 정규식
       │
       
  [집계·가공]  sort · uniq · wc · cut · tr   ───   로그 에러 Top-N 집계

낯선 기호가 많아 보여도 괜찮아요. 하나씩 "이게 왜 필요한지"부터 풀어가며 천천히 갈 테니 편하게 따라오세요.

💡 오늘 수업의 핵심 — "스트림을 이해하고, 리다이렉션으로 결과를 파일에 보내고, 파이프로 작은 도구를 이어 로그에서 원하는 정보를 뽑는다"

모든 명령에는 입력·결과·에러라는 세 갈래 통로가 있다는 걸 먼저 이해하고, 그 통로를 화면이 아닌 파일로 돌리는 리다이렉션(>)을 배우고, 한 명령의 출력을 다음 명령으로 흘려보내는 파이프(|)로 작은 도구들을 잇고, grep과 정규식으로 수천 줄에서 원하는 줄만 뽑아 sort·uniq로 집계하는 것 — 여기까지가 오늘의 목표입니다.

🎯 학습 목표

  • 모든 명령이 가진 표준 스트림 3종(stdin·stdout·stderr, 파일 디스크립터 0·1·2)을 이해하고, 리다이렉션(>·>>·2>·&>·tee)으로 정상 결과와 에러를 원하는 곳으로 보낸다
  • 파이프(|)로 작은 명령들을 이어 "한 명령의 출력을 다음 명령의 입력으로" 흘려보내는 유닉스식 사고를 익힌다
  • grep과 정규식 기초로 수천 줄에서 원하는 줄만 뽑아내고, sort·uniq·wc·cut·tr로 정렬·집계·가공해 로그에서 에러를 뽑아 집계한다

Step 1: "화면 뒤의 세 갈래 길 — 표준 입력·출력·에러"

리다이렉션이니 파이프니 하는 걸 배우기 전에, 딱 하나만 먼저 머릿속에 깔아둘게요. 이게 오늘 수업 전체의 토대라서, 여기만 확실히 잡으면 나머지는 응용일 뿐이에요.

질문 하나 드릴게요. ls를 치면 파일 목록이 화면에 나오죠. 그런데 없는 파일을 열려고 cat 없는파일을 치면 빨간 에러 메시지가 나옵니다. 둘 다 화면에 글자가 나오는데 — 이 둘은 같은 통로로 나오는 걸까요, 다른 통로로 나오는 걸까요?

정답은 다른 통로입니다. 리눅스에서는 모든 명령이 세 개의 정해진 통로를 가지고 태어나요. 이걸 표준 스트림(standard stream)이라고 부릅니다. 스트림은 "흐름"이라는 뜻인데, 글자가 흘러 들어오고 흘러 나가는 물길이라고 생각하면 돼요.

텍스트
                       ┌─────────┐
   (0) stdin  ────────│   CMD   │──────── (1) stdout
                       │         │──────── (2) stderr
                       └─────────┘

   0  stdin  (standard input)   = 명령에 들어가는 입력    (평소엔 키보드)
   1  stdout (standard output)  = 명령이 내놓는 정상 결과  (평소엔 화면)
   2  stderr (standard error)   = 명령이 내놓는 에러 메시지 (평소엔 화면)

세 통로에는 각각 번호가 붙어 있어요. 입력은 0번, 정상 출력은 1번, 에러는 2번. 이 번호를 파일 디스크립터(file descriptor)라고 부르는데, 지금은 그냥 "통로마다 붙은 번호표"라고만 알아두면 충분합니다. 이 번호가 이따 리다이렉션에서 2> 같은 표기로 등장하거든요.

가장 헷갈리는 게 stdout(1)과 stderr(2)가 따로 있다는 점이에요. 평소엔 둘 다 화면으로 나오니까 구분이 안 됩니다. 그런데 리눅스는 일부러 둘을 갈라놨어요. "정상 결과"와 "뭔가 잘못됐다는 에러"는 성격이 완전히 다르니까요. 이걸 나눠두면 나중에 "정상 결과만 파일로 저장하고, 에러는 화면에 따로 보고 싶다" 같은 게 가능해집니다.

말로만 하면 와닿지 않으니 직접 봅시다. 먼저 오늘 실습할 연습 공간을 하나 만들게요.

Bash
mkdir -p ~/practice/logs
cd ~/practice/logs

이제 정상 결과(stdout)부터 봅니다. 빈 폴더라 목록은 비어 있겠지만, 명령 자체는 정상으로 끝나요.

Bash
ls

아무것도 안 나오고 다음 줄로 넘어가죠. 이번엔 일부러 없는 파일을 건드려 에러(stderr)를 만들어볼게요.

Bash
cat 없는파일.txt
텍스트
cat: 없는파일.txt: No such file or directory

"그런 파일이나 디렉토리가 없다"는 에러가 화면에 떴어요. 여기까지는 둘 다 그냥 화면에 나오니 차이를 못 느낍니다. 그럼 정상 결과와 에러를 한 번에 섞어볼게요. 존재하는 /etc와 존재하지 않는 폴더를 같이 넘깁니다.

Bash
ls /etc 없는폴더
텍스트
ls: cannot access '없는폴더': No such file or directory
/etc:
hostname  hosts  passwd  ...

화면만 보면 에러 줄과 정상 목록이 그냥 뒤섞여 있죠. 하지만 사실 이 둘은 다른 통로(2번과 1번)로 나온 것입니다. 지금은 눈에 안 보이지만, 다음 Step에서 이 둘을 서로 다른 파일로 보내면 "아, 정말 통로가 달랐구나"가 눈으로 증명될 거예요.

💡 한 줄 정리

모든 명령은 세 통로를 가진다 — 입력 stdin(0), 정상 결과 stdout(1), 에러 stderr(2). 평소엔 입력은 키보드, 출력과 에러는 둘 다 화면에 연결돼 있어 구분이 안 되지만, 정상 결과와 에러는 사실 서로 다른 통로로 흐른다.

🙋 학생 질문 — "그냥 화면에 다 나오면 되지, 출력이랑 에러를 굳이 왜 나눠놨나요?"

아주 좋은 질문이에요. 평소 화면만 볼 땐 정말 나눌 이유가 없어 보이죠. 그 효용은 "결과를 화면이 아닌 다른 곳으로 보낼 때" 드러나요.

예를 들어 어떤 명령이 정상 결과 100줄과 에러 3줄을 같이 쏟아낸다고 해봐요. 만약 출력과 에러가 한 통로로 섞여 있으면, 결과를 파일로 저장할 때 에러 3줄도 같이 끼어 들어가 파일이 지저분해집니다. 그런데 통로가 나뉘어 있으면 "정상 결과 100줄은 result.txt에 저장하고, 에러 3줄은 화면에 그대로 띄워 바로 확인" 하는 식으로 깔끔하게 분리할 수 있어요.

현업에서 프로그램을 자동으로 돌릴 때 이게 특히 중요해요. 정상 로그는 로그 파일에 차곡차곡 쌓고, 에러만 따로 모아 알림을 보내거든요. 그 모든 게 "출력과 에러는 애초에 다른 통로"라는 오늘의 토대 위에서 가능해집니다.


Step 2: "결과를 화면 말고 파일로 — 리다이렉션"

자, Step 1에서 "출력과 에러는 다른 통로로 흐른다"를 잡았으니, 이제 그 통로를 화면이 아닌 다른 곳으로 돌리는 법을 배웁니다. 이걸 리다이렉션(redirection), 우리말로 "방향 바꾸기"라고 해요. 통로의 끝을 화면에서 파일로 갈아 끼우는 겁니다.

텍스트
   기본 (리다이렉션 없음):
       ls -l  ──(1 stdout)──  화면

   리다이렉션 ( > ):
       ls -l  ──(1 stdout)──  files.txt     (화면 대신 파일로 보냄)

가장 많이 쓰는 게 > 하나예요. "이 명령의 정상 결과를 화면 말고 이 파일에 넣어라"는 뜻입니다. 아까 길게 흘러가던 ls -l 결과를 파일에 담아볼게요.

Bash
ls -l /etc > files.txt

화면엔 아무것도 안 나오죠? 결과가 전부 files.txt로 흘러 들어갔거든요. 확인해봅시다.

Bash
cat files.txt
텍스트
total 1120
-rw-r--r-- 1 root root  2981 Jan  1 09:00 adduser.conf
drwxr-xr-x 2 root root  4096 Jan  1 09:00 alternatives
...

지난 시간에 배운 ls -l의 그 긴 출력이 고스란히 파일에 저장됐어요. 이제 화면을 스크롤할 필요 없이, 이 파일을 두고두고 들여다보거나 남에게 보낼 수 있습니다.

그런데 >에는 함정이 하나 있어요. 이미 내용이 있는 파일에 >를 쓰면, 기존 내용을 싹 지우고 덮어씁니다. 그래서 기존 내용 뒤에 이어 붙이고 싶을 땐 >를 두 번 겹친 >>를 써요.

Bash
echo "첫 줄" > note.txt
echo "둘째 줄" >> note.txt
echo "셋째 줄" >> note.txt
cat note.txt
텍스트
첫 줄
둘째 줄
셋째 줄

echo는 따옴표 안 글자를 그대로 화면에 내놓는 명령이에요. 첫 줄만 >로 새로 만들고, 나머지는 >>로 이어 붙였더니 세 줄이 차곡차곡 쌓였죠. 만약 둘째·셋째도 >로 했다면 매번 덮어써서 마지막 "셋째 줄" 하나만 남았을 거예요. 로그를 쌓을 땐 거의 항상 >>를 씁니다.

여기까지는 정상 결과(stdout, 1번)를 다뤘어요. 그럼 에러(stderr, 2번)는 어떻게 따로 보낼까요? 통로 번호를 앞에 붙여 2>라고 쓰면 됩니다. 아까 Step 1에서 섞여 나오던 그 결과를, 이번엔 통로별로 다른 파일에 보내 "정말 통로가 달랐다"를 증명해볼게요.

Bash
ls /etc 없는폴더 > out.txt 2> err.txt
Bash
cat out.txt
텍스트
/etc:
hostname  hosts  passwd  ...
Bash
cat err.txt
텍스트
ls: cannot access '없는폴더': No such file or directory

보이시죠? 정상 목록은 out.txt로, 에러 메시지는 err.txt로 깔끔하게 갈라졌어요. Step 1에서 "둘은 다른 통로"라고 했던 게 눈으로 증명된 순간입니다. >는 사실 1>의 줄임말이라, 1번 통로를 다루고 있었던 거예요.

자주 쓰는 표기 몇 개를 정리해둘게요.

표기
> 정상 결과(1)를 파일로, 덮어쓰기
>> 정상 결과(1)를 파일로, 이어 붙이기
2> 에러(2)를 파일로
&> 정상 결과와 에러를 둘 다 한 파일로
2>&1 에러(2)를 정상 결과(1)와 같은 곳으로 합치기
> 파일 2>&1 결과와 에러를 한 파일에 함께 (가장 흔한 조합)

에러를 아예 버리고 싶을 때도 있어요. 화면을 시끄럽게 하는 경고만 잔뜩 나올 때요. 그럴 땐 /dev/null이라는 특별한 곳으로 보냅니다. 여기로 보낸 건 그냥 사라져요 — 리눅스의 "쓰레기통" 같은 곳입니다.

Bash
ls /etc 없는폴더 2> /dev/null

이러면 에러는 /dev/null로 사라지고 정상 목록만 화면에 남아요.

마지막으로 tee를 하나 소개할게요. >는 결과를 파일로 보내버려서 화면엔 안 보이죠. 그런데 "파일에 저장도 하면서 화면으로도 보고 싶다"면? 그게 tee예요. 파이프와 함께 쓰는데(파이프는 다음 Step에서 자세히 봐요), 결과를 두 갈래로 흘려보낸다고 해서 T자 배관을 뜻하는 이름이 붙었어요.

Bash
ls -l /etc | tee files2.txt

이러면 ls -l 결과가 화면에도 나오고 files2.txt에도 저장됩니다. 작업 과정을 눈으로 보면서 기록도 남기고 싶을 때 유용해요.

💡 한 줄 정리

리다이렉션은 통로의 끝을 화면에서 파일로 갈아 끼운다. >는 정상 결과를 파일로 덮어쓰기, >>는 이어 붙이기, 2>는 에러만 따로, &>는 둘 다, 2> /dev/null은 에러를 버리기, tee는 저장하면서 화면에도 보여준다.

🙋 학생 질문 — "> 하나랑 >> 두 개, 헷갈려요. 어느 걸 언제 쓰나요?"

처음엔 다들 헷갈려요. 외우는 대신 "무슨 일이 벌어지는지"로 기억하면 안 헷갈립니다.

> 하나는 새 종이에 쓰기예요. 파일이 이미 있으면 그 안의 내용을 싹 지우고 처음부터 새로 씁니다. 그래서 "이번 결과만 깔끔하게 저장하고 싶다"일 땐 >.

>> 두 개는 공책에 이어 쓰기예요. 기존 내용은 그대로 두고 맨 뒤에 덧붙입니다. 그래서 "기록을 계속 쌓아가고 싶다"일 땐 >>.

실수담을 하나 들려드릴게요. 신입 때 소중한 로그가 담긴 파일에 새 내용을 추가하려다 >> 대신 >를 쳐서, 그동안 쌓인 로그를 통째로 날린 분들이 정말 많아요. 그래서 중요한 파일에 결과를 보낼 땐, 치기 전에 "이건 덮어쓰기인가 이어쓰기인가"를 한 번 더 생각하는 습관을 들이면 좋습니다. 리눅스엔 휴지통이 없어서, 한 번 덮어쓴 내용은 되돌릴 수 없거든요.


Step 3: "작은 도구를 잇는 컨베이어 벨트 — 파이프(|)"

이제 오늘의 진짜 주인공, 파이프(|)입니다. 키보드에서 보통 백슬래시(\) 키와 같은 자리에 있는, 세로 막대 기호예요(Shift와 함께).

리다이렉션이 "명령의 결과를 파일로" 보냈다면, 파이프는 "명령의 결과를 다른 명령으로" 흘려보냅니다. 한 명령의 stdout(정상 결과)을 다음 명령의 stdin(입력)에 곧장 연결하는 거예요.

텍스트
   파이프 없이 (중간 파일을 거쳐야 함):
       ls -l /etc > tmp.txt     (일단 파일로 저장하고)
       less tmp.txt             (그 파일을 다시 연다)

   파이프로 ( | ) — 중간 파일 없이 곧장 흘려보냄:

       ls -l /etc ──(stdout)──┐
                              │   파이프 |
                              
                          (stdin) less

   "한 명령의 결과를 다음 명령의 입력으로" — 컨베이어 벨트처럼

왜 이게 혁명적이냐면, 이게 바로 오프닝에서 말한 유닉스 철학의 핵심이거든요. 리눅스의 작은 명령들은 일부러 한 가지 일만 하도록 만들어졌어요. ls는 목록만 보여주고, less는 긴 글을 한 화면씩 넘겨 보여주고, wc는 개수만 셉니다. 각자는 단순하지만, 파이프로 이으면 조립 블록처럼 무한히 조합할 수 있어요.

직접 해봅시다. 아까 ls -l /etc 결과가 화면을 주르륵 넘어가버렸죠. 이걸 less에 흘려보내면 한 화면씩 천천히 볼 수 있어요.

Bash
ls -l /etc | less

화면이 한 페이지에서 멈추죠. 위아래 화살표나 Space로 넘기고, q를 누르면 빠져나옵니다. ls의 결과가 파일을 거치지 않고 곧장 less로 흘러 들어간 거예요.

이번엔 오늘 실습의 주인공이 될 가짜 로그 파일을 하나 만들게요. 운영 서버의 접속 로그를 흉내 낸 거예요. 정상 접속(INFO)과 경고(WARN), 에러(ERROR)가 섞여 있습니다.

Bash
cat > access.log << 'EOF'
2026-06-16 10:01:22 INFO GET /feed 200 192.168.0.11
2026-06-16 10:01:23 INFO GET /login 200 192.168.0.23
2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:28 WARN GET /config 404 10.0.0.5
2026-06-16 10:01:30 ERROR POST /upload 500 192.168.0.42
2026-06-16 10:01:33 INFO GET /feed 200 192.168.0.11
2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:39 INFO GET /profile 200 192.168.0.23
2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:44 WARN GET /search 404 192.168.0.11
EOF

여기서 잠깐, << 표기가 보이죠? 이건 "EOF라는 글자가 나올 때까지 입력한 여러 줄을, 명령의 stdin으로 한꺼번에 넣어라"는 뜻이에요. Step 1에서 배운 입력 통로(stdin)를 키보드 대신 이 글 덩어리로 채운 거죠. 지금은 "여러 줄을 한 번에 파일로 만드는 방법" 정도로만 알아두면 됩니다.

이제 이 로그가 몇 줄인지 세어볼게요. wc -l은 "들어온 줄 수를 세는" 작은 도구예요. 파이프로 이어봅니다.

Bash
cat access.log | wc -l
텍스트
10

cat이 파일 내용을 stdout으로 내놓고, 그게 파이프를 타고 wc -l의 stdin으로 흘러 들어가 "10줄"이라고 답했어요. 지난 시간에 ls -l 출력이 길어지면 걸러보고 싶다고 했죠? 그 첫걸음이 바로 이겁니다 — 출력을 다음 도구로 흘려보내 가공하기.

파이프는 두 개, 세 개로 계속 이을 수 있어요. 다음 Step부터 grep·sort·uniq 같은 도구들을 이 파이프로 줄줄이 엮어, "로그에서 에러만 뽑아 집계하는" 한 줄 명령을 완성해갈 거예요.

💡 한 줄 정리

파이프(|)는 한 명령의 stdout을 다음 명령의 stdin으로 곧장 흘려보낸다. 리눅스의 작은 도구들은 한 가지 일만 잘하도록 만들어졌고, 파이프로 이들을 컨베이어 벨트처럼 이어 붙이면 복잡한 일을 한 줄로 처리할 수 있다 — 이것이 유닉스 철학의 핵심이다.

🙋 학생 질문 — "파이프(|)랑 리다이렉션(>)이 비슷해 보여요. 뭐가 다른가요?"

정말 자주 나오는 질문이에요. 둘 다 "결과를 다른 데로 보낸다"는 점은 같아서 헷갈릴 만해요. 차이는 보내는 목적지입니다.

>는 결과를 파일로 보내요. 끝에 남는 건 파일이에요. (ls > files.txtfiles.txt가 생김)

|는 결과를 다른 명령으로 보내요. 파일을 거치지 않고, 곧장 다음 명령의 입력이 됩니다. (ls | lessls 결과가 바로 less로 들어감)

그래서 "결과를 저장하고 싶다"면 >, "결과를 또 다른 도구로 가공하고 싶다"면 |예요. 둘을 같이 쓰기도 해요 — ls | sort > sorted.txt처럼, 파이프로 정렬한 다음 그 결과를 파일로 저장하는 식이죠. 가공은 파이프로, 저장은 리다이렉션으로. 이렇게 기억하면 안 헷갈립니다.


Step 4: "수천 줄에서 원하는 줄만 — grep"

드디어 오늘의 간판 도구 grep입니다. 이름이 좀 괴상하죠? "global regular expression print"의 줄임말인데, 뜻은 단순해요 — 파일에서 내가 찾는 글자가 들어간 줄만 뽑아서 보여준다. 오프닝에서 말한 "로그 수천 줄에서 에러 난 줄만 찾기", 바로 이 명령이 해결합니다.

기본 사용법은 grep 찾을글자 파일이름이에요. 아까 만든 access.log에서 에러 줄만 뽑아볼게요.

Bash
grep ERROR access.log
텍스트
2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:30 ERROR POST /upload 500 192.168.0.42
2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5

10줄 중에서 ERROR가 든 4줄만 쏙 뽑혔어요. 화면을 스크롤하며 눈으로 찾을 필요가 없어진 겁니다. 이제 자주 쓰는 옵션 네 개를 붙여볼게요. 하나씩 봅니다.

-n (줄 번호 같이) — 에러가 파일의 몇 번째 줄인지 알려줍니다. 동료에게 "로그 7번째 줄 좀 봐줘"라고 짚어줄 때 유용해요.

Bash
grep -n ERROR access.log
텍스트
3:2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
5:2026-06-16 10:01:30 ERROR POST /upload 500 192.168.0.42
7:2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
9:2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5

-i (대소문자 무시)grep은 기본적으로 대소문자를 구분해요. 우리 로그는 ERROR가 대문자라, 소문자로 찾으면 아무것도 안 나옵니다.

Bash
grep error access.log

아무 결과도 없죠. 그런데 -i를 붙이면 대소문자를 가리지 않고 찾아요.

Bash
grep -i error access.log
텍스트
2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:30 ERROR POST /upload 500 192.168.0.42
2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5

로그마다 Error, ERROR, error 식으로 표기가 제각각인 경우가 많아서, 실무에서 검색할 땐 -i를 습관처럼 붙이는 분이 많아요.

-v (반대로, 빼고) — 찾은 줄을 보여주는 게 아니라, 그 줄만 빼고 나머지를 보여줍니다. "정상(INFO)은 됐고, 문제 있는 것만 보고 싶다" 할 때 정상을 빼버리는 거예요.

Bash
grep -v INFO access.log
텍스트
2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:28 WARN GET /config 404 10.0.0.5
2026-06-16 10:01:30 ERROR POST /upload 500 192.168.0.42
2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:44 WARN GET /search 404 192.168.0.11

INFO 줄 4개를 빼니 ERROR와 WARN, 즉 신경 써야 할 6줄만 남았어요.

-r (폴더 통째로 뒤지기) — 지금까진 파일 하나만 뒤졌는데, 로그가 여러 파일로 쪼개져 있으면? -r을 쓰면 폴더 안의 모든 파일을 재귀적으로 뒤집니다. 어느 파일의 몇 번째 줄인지 앞에 붙여줘요.

Bash
grep -rn ERROR ~/practice/logs
텍스트
/home/alice/practice/logs/access.log:3:2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
/home/alice/practice/logs/access.log:5:2026-06-16 10:01:30 ERROR POST /upload 500 192.168.0.42
/home/alice/practice/logs/access.log:7:2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
/home/alice/practice/logs/access.log:9:2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5

서버에서 "어느 로그 파일에 이 에러가 있는지 모르겠다" 할 때, 로그 폴더 전체를 grep -r로 한 번에 훑는 건 정말 자주 하는 작업이에요.

마지막으로, grep은 파이프와 찰떡이에요. 파일 이름을 안 주면 stdin(앞 명령의 출력)에서 찾거든요. 그래서 에러가 몇 건인지 세려면 Step 3의 wc -l과 이렇게 잇습니다.

Bash
grep ERROR access.log | wc -l
텍스트
4

grep이 에러 4줄을 뽑아 wc -l로 흘려보냈고, wc가 "4줄"이라고 답했어요. 이게 바로 작은 도구를 파이프로 잇는 유닉스식 사고입니다.

💡 한 줄 정리

grep 찾을글자 파일은 그 글자가 든 줄만 뽑는다. -n은 줄 번호, -i는 대소문자 무시, -v는 그 줄만 빼고, -r은 폴더 전체를 뒤진다. 파일 이름 없이 파이프로 받으면 앞 명령의 출력에서 찾으므로 grep은 파이프의 단골 도구다.

🙋 학생 질문 — "grep -v INFO 했더니 WARN, ERROR가 다 나왔어요. 그냥 ERROR만 보려면요?"

좋은 관찰이에요. -v는 "준 글자가 든 줄을 빼는" 거라, INFO만 빼면 INFO 아닌 모든 줄(WARN, ERROR)이 남아요.

ERROR만 깔끔하게 보고 싶으면 -v 없이 그냥 grep ERROR access.log를 쓰면 됩니다. -v는 "이것만 빼고 다"가 목적일 때 써요 — 예를 들어 로그에서 시끄러운 디버그 줄(DEBUG)만 걷어내고 나머지를 다 보고 싶을 때처럼요.

조금 더 나아가면, 파이프로 grep을 두 번 이을 수도 있어요. grep -v INFO access.log | grep -v WARN 하면 INFO도 WARN도 빼서 ERROR만 남아요. "조건을 하나씩 걸러 좁혀간다"는 발상인데, 이렇게 작은 필터를 겹쳐 쓰는 게 파이프의 묘미입니다. 물론 이 경우엔 그냥 grep ERROR가 더 간단하죠.


Step 5: "패턴으로 찾기 — 정규식 기초"

grep ERROR처럼 정확한 글자를 찾는 건 했어요. 그런데 "4로 시작하는 상태 코드(400번대 에러) 전부", "특정 IP로 끝나는 줄"처럼 패턴으로 찾고 싶을 때가 있어요. 이때 쓰는 게 정규식(regular expression), 줄여서 정규표현식이에요.

정규식은 그 자체로 책 한 권이 나올 만큼 깊지만, 오늘은 가장 많이 쓰는 기호 다섯 개만 손에 익히면 충분합니다. 겁먹지 말고 하나씩 봅시다.

기호
^ 줄의 시작 ^ERROR = ERROR로 시작하는 줄
$ 줄의 5$ = 5로 끝나는 줄
. 아무 글자 한 개 1.3 = 1과 3 사이에 아무 글자 하나
* 바로 앞 글자의 0번 이상 반복 GET.*200 = GET 뒤에 뭐가 오든 200
[] 이 중 한 글자 40[34] = 403 또는 404

먼저 ^ (줄 시작)입니다. 우리 로그는 모든 줄이 날짜(2026)로 시작하죠. 그래서 ERROR시작하는 줄을 찾으면 하나도 없어요.

Bash
grep '^ERROR' access.log

아무것도 안 나오죠. ^를 빼면 "ERROR가 줄 어디엔가 있는" 4줄이 다 나오는 것과 대조됩니다. ^는 "맨 앞에 있을 때만"이라는 뜻이에요. 정규식 기호는 셸이 멋대로 해석하지 않도록 작은따옴표로 감싸는 습관을 들이세요.

다음은 $ (줄 끝)예요. 우리 로그 줄은 IP로 끝나니까, "5로 끝나는 줄"을 찾으면 10.0.0.5로 끝나는 줄들이 잡혀요.

Bash
grep '5$' access.log
텍스트
2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:28 WARN GET /config 404 10.0.0.5
2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5

.(아무 글자)와 *(반복)은 짝으로 가장 많이 써요. .*는 "아무 글자가 몇 개든(없어도 됨)"이라는 뜻이라, "A 뒤에 뭐가 오든 결국 B가 나오는" 패턴을 찾을 때 씁니다. 성공한 GET 요청(GET … 200)을 찾아볼게요.

Bash
grep 'GET.*200' access.log
텍스트
2026-06-16 10:01:22 INFO GET /feed 200 192.168.0.11
2026-06-16 10:01:23 INFO GET /login 200 192.168.0.23
2026-06-16 10:01:33 INFO GET /feed 200 192.168.0.11
2026-06-16 10:01:39 INFO GET /profile 200 192.168.0.23

GET200 사이에 경로가 뭐가 오든(/feed/login이든) 다 잡혔죠.

마지막으로 [] (이 중 한 글자)입니다. 40[34]는 "40 다음에 3 또는 4"라는 뜻이라 403과 404를 한 번에 잡아요. 400번대 에러를 모두 찾는 셈이죠.

Bash
grep '40[34]' access.log
텍스트
2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:28 WARN GET /config 404 10.0.0.5
2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:44 WARN GET /search 404 192.168.0.11

여기서 한 가지 짚을 게 있어요. "403 또는 404"를 403|404처럼 세로 막대(|, 또는)로 쓰고 싶을 때가 있는데, 기본 grep은 이 |를 특별 기호로 안 봐요. 이런 확장 기호(|·+·())를 쓰려면 -E(extended) 옵션을 붙입니다.

Bash
grep -E '403|404' access.log

위의 40[34]와 똑같은 5줄이 나와요. 둘 다 같은 결과를 주지만, []는 기본 grep에서도 되고 |-E가 필요하다는 차이를 기억해두세요.

💡 한 줄 정리

정규식은 패턴으로 찾는다. ^는 줄 시작, $는 줄 끝, .은 아무 글자 한 개, *는 앞 글자의 0번 이상 반복(.*는 "뭐가 오든"), [34]는 "3 또는 4". |·+·() 같은 확장 기호를 쓰려면 grep -E를 붙인다.

🙋 학생 질문 — ".이 아무 글자라면, 진짜 마침표(.)를 찾고 싶을 땐 어떡하나요? IP에 점이 있잖아요."

핵심을 짚으셨어요. 정규식에서 .은 "아무 글자 한 개"라서, 192.168이라고 쓰면 "192 다음에 아무 글자, 그다음 168"이라는 뜻이 돼요. 그래서 192X168도 어쩌다 매치될 수 있죠(현실엔 거의 없지만요).

진짜 마침표를 찾고 싶으면 앞에 백슬래시(\)를 붙여 \.이라고 써요. "이건 특별 기호가 아니라 글자 그대로의 점이야"라고 알려주는 거예요. 그래서 192\.168\.0\.5라고 쓰면 정확히 그 IP만 찾습니다.

다만 실무에서는, IP 안의 점은 어차피 그 자리에 점이 있으니 192.168.0.5라고 대충 써도 거의 원하는 결과가 나와요. 그래서 "엄밀하게 한 글자만 매치되게 막아야 할 때만 \.을 쓴다"고 알아두면 충분합니다. 처음부터 모든 기호를 완벽히 외우려 하지 말고, 자주 쓰는 ^ $ . * []부터 손에 붙이세요.


Step 6: "정렬하고 세고 중복 묶기 — sort·uniq·wc"

이제 뽑아낸 줄들을 집계하는 작은 도구 세 개를 배웁니다. sort(정렬), uniq(중복 묶기), wc(개수 세기). 전부 한 가지만 하는 단순한 도구라, 파이프로 이으면 강력해져요.

먼저 wc(word count)는 개수를 셉니다. -l은 줄 수예요. Step 3·4에서 이미 맛봤죠.

Bash
wc -l access.log
텍스트
10 access.log

grep과 이으면 "조건에 맞는 줄이 몇 개인지"를 셀 수 있어요. 에러가 몇 건인지 보죠.

Bash
grep ERROR access.log | wc -l
텍스트
4

다음은 집계의 핵심 짝, sortuniq예요. 그 전에 도구 하나를 더 꺼낼게요 — grep -o입니다. grep-o를 붙이면 매치된 줄 전체가 아니라 매치된 부분만 뽑아줘요. 로그에서 레벨(INFO/WARN/ERROR)만 오려내봅시다.

Bash
grep -oE 'INFO|WARN|ERROR' access.log
텍스트
INFO
INFO
ERROR
WARN
ERROR
INFO
ERROR
INFO
ERROR
WARN

이제 "각 레벨이 몇 번 나왔나"를 세고 싶어요. 중복을 세는 도구가 uniq -c인데, 여기 함정이 있어요. uniq는 바로 붙어 있는(인접한) 중복만 묶습니다. 위 출력은 INFO와 ERROR가 여기저기 흩어져 있죠. 그대로 uniq -c를 쓰면 이렇게 엉망이 됩니다.

Bash
grep -oE 'INFO|WARN|ERROR' access.log | uniq -c
텍스트
      2 INFO
      1 ERROR
      1 WARN
      1 ERROR
      1 INFO
      1 ERROR
      1 INFO
      1 ERROR
      1 WARN

같은 INFO인데도 떨어져 있어서 따로따로 세어졌어요. 그래서 uniq 앞에는 거의 항상 sort를 먼저 둡니다. 정렬로 같은 값끼리 붙여놓은 다음 세는 거예요.

Bash
grep -oE 'INFO|WARN|ERROR' access.log | sort | uniq -c
텍스트
      4 ERROR
      4 INFO
      2 WARN

깔끔하죠. ERROR 4번, INFO 4번, WARN 2번. 이게 "로그 레벨별 집계"입니다. sort | uniq -c는 정말 자주 쓰는 조합이라 손에 붙여두면 좋아요.

마지막으로 sort의 옵션 두 개. 기본 sort는 글자 순(사전 순)으로 정렬해요. 그런데 숫자를 글자로 정렬하면 이상해집니다. 작은 예로 볼게요.

Bash
printf '10\n9\n100\n2\n' | sort
텍스트
10
100
2
9

"10, 100, 2, 9"? 글자로 보면 첫 글자 12·9보다 앞서니 이렇게 돼요. 숫자로 제대로 정렬하려면 -n(numeric)을 붙입니다.

Bash
printf '10\n9\n100\n2\n' | sort -n
텍스트
2
9
10
100

그리고 -r(reverse)을 붙이면 큰 순(내림차순)이 돼요. 그래서 sort -rn은 "숫자 큰 순으로 줄 세우기"인데, 다음 Step에서 "에러를 가장 많이 낸 IP"를 찾을 때 이 조합을 씁니다.

💡 한 줄 정리

wc -l은 줄 수를 세고, grep -o는 매치된 부분만 오려낸다. sort는 정렬, uniq -c는 중복을 세는데 인접한 것만 묶으므로 반드시 sort를 먼저 둔다. sort -n은 숫자 순, -r은 내림차순이라 sort -rn은 "많은 순으로 줄 세우기"다.

🙋 학생 질문 — "uniq -c가 인접한 것만 센다니 좀 불편한데, 왜 알아서 정렬까지 안 해주나요?"

처음 보면 "왜 이렇게 불친절하지?" 싶죠. 그런데 이게 유닉스 철학의 한 단면이에요 — "한 도구는 한 가지만 한다."

uniq의 일은 "인접한 중복을 묶는 것" 하나예요. 정렬은 sort의 일이고요. 둘을 합쳐 "정렬도 하고 중복도 세는" 만능 도구를 만들 수도 있었겠지만, 그러면 "정렬은 하지 말고 중복만 묶고 싶다" 같은 다른 상황에서 못 쓰게 돼요.

실제로 그런 상황이 있어요. 로그에서 "같은 에러가 연속으로 몇 번 터졌나"를 볼 땐, 정렬하면 안 되고 시간 순 그대로 둔 채 인접 중복만 세야 하거든요. 그땐 sort 없이 uniq -c만 씁니다. 작은 도구로 나눠두면 이렇게 상황마다 골라 조합할 수 있어요. 불편해 보이는 그 단순함이, 사실은 유연함의 비결입니다.


Step 7: "필드 뽑고 글자 바꾸기 — cut·tr"

로그 한 줄에는 날짜·시간·레벨·경로·상태·IP가 한 줄에 다 들어 있죠. 여기서 "IP만", "상태 코드만"처럼 특정 칸(필드)만 오려내고 싶을 때 cut을 씁니다.

cut -d' ' -f7은 "공백( )을 구분자로 잘라서, 7번째 칸만"이라는 뜻이에요. -d는 구분자(delimiter), -f는 몇 번째 칸(field)인지. 우리 로그의 IP는 7번째 칸이니까요.

Bash
cut -d' ' -f7 access.log
텍스트
192.168.0.11
192.168.0.23
10.0.0.5
10.0.0.5
192.168.0.42
192.168.0.11
10.0.0.5
192.168.0.23
10.0.0.5
192.168.0.11

IP만 깔끔하게 뽑혔어요. 레벨(3번째 칸)만 뽑으면 Step 6에서 grep -o로 한 것과 같은 결과가 나와요.

Bash
cut -d' ' -f3 access.log
텍스트
INFO
INFO
ERROR
WARN
ERROR
INFO
ERROR
INFO
ERROR
WARN

칸이 아니라 글자 위치로 자를 수도 있어요. -c1-10은 "1번째부터 10번째 글자까지"라, 날짜 부분만 뽑힙니다.

Bash
cut -c1-10 access.log | sort | uniq -c
텍스트
     10 2026-06-16

모든 줄이 같은 날짜라 "2026-06-16이 10번"으로 집계됐죠.

다음은 tr(translate)이에요. 글자를 다른 글자로 바꾸거나 지웁니다. 가장 흔한 건 대소문자 변환이에요.

Bash
echo "hello world" | tr 'a-z' 'A-Z'
텍스트
HELLO WORLD

a-z(소문자 전체)를 A-Z(대문자 전체)로 한 칸씩 짝지어 바꾼 거예요. 특정 글자를 지우려면 -d(delete)를 씁니다.

Bash
echo "010-1234-5678" | tr -d '-'
텍스트
01012345678

전화번호에서 하이픈을 다 지웠죠. 그리고 -s(squeeze)는 연달아 반복되는 글자를 하나로 줄여줘요. 공백이 여러 칸 들어간 지저분한 줄을 정리할 때 써요.

Bash
echo "a      b      c" | tr -s ' '
텍스트
a b c

여기서 cut의 약점을 하나 짚고 갈게요. cut -d' '는 구분자가 정확히 한 칸일 때 깔끔해요. 그런데 로그가 칸을 맞추려고 공백을 여러 개 넣어두면(예: INFO GET처럼), cut이 빈 칸을 별도 필드로 세서 칸 번호가 어긋나버려요. 방금 배운 tr -s ' '로 공백을 한 칸으로 줄인 뒤 cut에 넘기면 어느 정도 해결되지만, 이런 "공백이 들쭉날쭉한 필드"를 알아서 똑똑하게 처리하는 도구가 따로 있어요. 다음 시간에 배울 awk입니다. 오늘은 "cut은 단순하지만 공백에 예민하다"만 기억해두세요.

💡 한 줄 정리

cut -d' ' -f7은 공백으로 잘라 7번째 칸만, -c1-10은 1~10번째 글자만 뽑는다. tr은 글자를 바꾸거나('a-z' 'A-Z') 지운다(-d). -s는 반복 글자를 하나로 줄인다. cut은 구분자가 한 칸일 때 깔끔하고, 공백이 들쭉날쭉하면 어긋난다.

🙋 학생 질문 — "cut이 공백에 약하면, 그냥 항상 awk를 쓰면 되지 왜 cut을 배우나요?"

합리적인 의문이에요. 실제로 awk가 더 강력해서, 복잡한 필드 처리는 awk로 가는 게 맞아요.

그런데 cut은 단순한 만큼 빠르고, 명령이 짧아요. "쉼표로 구분된 CSV에서 3번째 열만", "한 칸으로 깔끔하게 구분된 로그에서 IP만"처럼 구분자가 일정한 경우엔 cut -d',' -f3 한 줄이 awk보다 읽기 쉽고 손도 빨라요. 현업에서도 간단한 추출은 cut, 공백이 지저분하거나 계산이 필요하면 awk로 자연스럽게 나눠 씁니다.

그리고 작은 도구를 여러 개 아는 게 유닉스의 힘이에요. 망치만 있으면 다 못으로 보이지만, 드라이버도 알면 나사는 드라이버로 풀죠. cutawk는 둘 다 도구함에 있어야 상황마다 골라 쓸 수 있어요. 오늘 cut을 손에 익혀두면, 다음 시간 awk를 배울 때 "아, 이래서 더 똑똑한 게 필요했구나"가 확 와닿을 거예요.


Step 8: "다 이어서 — 로그에서 에러 Top-N 집계"

자, 오늘 배운 걸 전부 하나로 잇는 순간입니다. 오프닝에서 그렸던 그림 — "운영 서버 로그 수천 줄에서 에러를 가장 많이 낸 IP를 찾아내기" — 를 한 줄 명령으로 완성해볼게요. 신입이 화면을 스크롤하며 눈으로 찾던 그 일을요.

한 번에 긴 명령을 던지지 않고, 파이프를 한 단계씩 늘려가며 중간 결과를 눈으로 확인해봅시다. 이게 긴 파이프라인을 만드는 가장 안전한 방법이에요.

1단계, 에러 줄만 거릅니다(grep).

Bash
grep ERROR access.log
텍스트
2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:30 ERROR POST /upload 500 192.168.0.42
2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5

2단계, 그 줄에서 IP(7번째 칸)만 오려냅니다(cut).

Bash
grep ERROR access.log | cut -d' ' -f7
텍스트
10.0.0.5
192.168.0.42
10.0.0.5
10.0.0.5

3단계, 같은 IP끼리 모으고(sort) 개수를 셉니다(uniq -c).

Bash
grep ERROR access.log | cut -d' ' -f7 | sort | uniq -c
텍스트
      3 10.0.0.5
      1 192.168.0.42

4단계, 많은 순으로 줄 세웁니다(sort -rn).

Bash
grep ERROR access.log | cut -d' ' -f7 | sort | uniq -c | sort -rn
텍스트
      3 10.0.0.5
      1 192.168.0.42

완성됐어요. 10.0.0.5가 에러 3건으로 1위네요. 아까 본 로그를 떠올리면, 이 IP는 /admin에 계속 403(접근 거부)으로 막히고 있었죠. "허락 안 된 곳을 반복해서 두드리는 수상한 IP" — 차단 후보로 의심해볼 만합니다. 수천 줄을 스크롤하던 일을, 다섯 도구를 파이프로 이어 한 줄로 끝낸 거예요.

이 흐름을 그림으로 정리하면 이렇게 한 갈래로 흐릅니다.

텍스트
   access.log
      │  grep ERROR        (에러 줄만 거르고)
      
   ERROR 줄 4개
      │  cut -d' ' -f7     (IP만 오려내고)
      
   IP 4개
      │  sort              (같은 IP끼리 모으고)
      
   정렬된 IP
      │  uniq -c           (중복을 세고)
      
   "3 10.0.0.5", "1 192.168.0.42"
      │  sort -rn          (많은 순으로 줄 세우기)
      
   Top-N 집계 완성

마지막으로, 진짜 운영 상황이라면 "상위 몇 개만" 보고 싶고, 결과를 파일로도 남기고 싶겠죠. head로 상위 N개만 자르고(head -5는 위 5줄), Step 2의 tee로 화면에 보면서 파일에도 저장합니다.

Bash
grep ERROR access.log | cut -d' ' -f7 | sort | uniq -c | sort -rn | head -5 | tee error_top.txt
텍스트
      3 10.0.0.5
      1 192.168.0.42

화면에 결과가 뜨면서 error_top.txt에도 똑같이 저장됐어요. 이제 이 파일을 팀에 공유하거나 다음 분석에 넘길 수 있습니다. 오늘 배운 표준 스트림·리다이렉션·파이프·grep·정규식·집계 도구가 이 한 줄 안에 전부 녹아 있어요.

💡 한 줄 정리

작은 도구를 파이프로 이으면 "로그에서 에러 IP Top-N 집계" 같은 분석을 한 줄로 한다 — grep(거르기) → cut(오려내기) → sort+uniq -c(세기) → sort -rn(줄 세우기) → head(상위만) → tee(저장). 파이프라인은 한 단계씩 늘려가며 중간 결과를 확인하면 안전하게 만든다.

🙋 학생 질문 — "이 긴 명령을 매번 외워서 쳐야 하나요? 자주 쓰는데 번거로워요."

당연히 매번 외울 필요 없어요. 자주 쓰는 명령을 짧은 이름으로 저장해두는 방법이 있거든요.

가장 간단한 건 별명(alias)이에요. alias toperr='grep ERROR access.log | cut -d" " -f7 | sort | uniq -c | sort -rn'처럼 등록해두면, 다음부터 toperr 한 단어로 그 긴 명령이 실행돼요. 다만 이렇게 등록한 별명은 터미널을 닫으면 사라지니, 계속 쓰려면 설정 파일에 적어둬야 하는데 — 그 이야기는 bash 스크립팅 모듈(A-11)에서 본격적으로 다룹니다.

지금은 "이런 파이프라인을 직접 조립할 수 있다"가 핵심이에요. 한 단계씩 늘려가며 만드는 감각만 손에 익으면, 나중에 그걸 별명이나 스크립트로 묶는 건 금방이에요. 오늘은 조립하는 재미에 집중하세요.


마무리

오늘 정말 많은 도구를 손에 넣었어요. 처음엔 >|^니 기호가 잔뜩 나와서 어지러웠을 텐데, 마지막에 그것들이 한 줄로 줄줄이 이어져 "수상한 IP"를 딱 짚어내는 걸 보니 어떤가요? 이게 바로 리눅스 셸의 진짜 힘이에요. 작은 도구 하나하나는 단순한데, 파이프로 이으면 못 할 게 없어집니다. 핵심을 세 가지로 정리할게요.

오늘 배운 핵심 세 가지

💡 하나 — 모든 명령에는 세 통로가 있다. 입력 stdin(0), 정상 결과 stdout(1), 에러 stderr(2). 리다이렉션으로 이 통로의 끝을 화면에서 파일로 돌린다 — >(덮어쓰기), >>(이어쓰기), 2>(에러만), &>(둘 다), tee(저장하며 화면에도).

💡 — 파이프(|)는 한 명령의 출력을 다음 명령의 입력으로 흘려보낸다. grep으로 원하는 줄만 뽑고, 정규식(^ $ . * [])으로 패턴을 잡는다. "작은 도구가 한 가지를 잘하고, 파이프로 잇는다"는 유닉스 철학이 여기 다 들어 있다.

💡 sort·uniq -c·wc·cut·tr로 정렬·집계·추출·변환을 한다. 이 도구들을 파이프로 줄줄이 이으면 "로그에서 에러 IP Top-N 집계" 같은 분석을 한 줄로 끝낸다.

다음 시간 예고

오늘 우리는 텍스트를 읽고 뽑는 법을 배웠어요. grep으로 찾고, cut으로 오려내고요. 그런데 중간에 cut이 공백 여러 칸에 약하다고 했죠? 그리고 "찾는 건 했는데, 그 글자를 다른 걸로 바꾸려면?", "파일을 직접 열어 고치려면?"이라는 욕심도 생겼을 거예요.

다음 시간(A-5)엔 먼저 "바꾸기"로 넘어갑니다. 글자를 찾아 바꾸는 sed, 공백 들쭉날쭉한 필드도 똑똑하게 처리하고 계산까지 하는 awk — 오늘 cut이 공백에 약했던 그 약점을 awk가 시원하게 풀어줄 거예요. 정규식도 거기서 한 번 더 만나요. 그리고 그다음 시간(A-6)엔 "파일을 직접 열어 고치는" 편집기 차례예요. 현업 신입이 서버에서 한 번씩 갇혀 식은땀 흘리는 그 vim에서 무사히 빠져나오는 생존 가이드까지 챙깁니다. "읽고 뽑기"에서 "바꾸고 편집하기"로, 텍스트를 다루는 손이 한층 자유로워지는 흐름이 될 거예요. 그럼 다음 시간에 만나요.


과제

오늘 과제는 모두 여러분 자신의 리눅스 환경(WSL2·맥 터미널·VM)에서 직접 손으로 쳐보는 실습이에요. 파이프와 리다이렉션은 눈으로 읽기만 해선 손에 안 붙어요. 한 단계 칠 때마다 출력이 어떻게 바뀌는지 직접 확인하며 천천히 따라오세요. 교안에서 만든 ~/practice/logs/access.log를 그대로 쓰면 됩니다.

[기초] 스트림·리다이렉션·파이프 기본기

세 통로와 리다이렉션, 파이프를 직접 손에 익혀보세요.

  • ~/practice/logs로 가서 ls -l /etc > files.txt로 결과를 파일에 저장하고, cat files.txt로 잘 담겼는지 확인한다
  • echo "첫 줄" > memo.txt 뒤에 echo "둘째 줄" >> memo.txt를 하고, >>>를 거꾸로 썼을 때 결과가 어떻게 달라지는지 직접 비교해본다
  • ls /etc 없는폴더 > out.txt 2> err.txt로 정상 결과와 에러를 서로 다른 파일에 나눠 담고, 두 파일을 각각 cat으로 열어 "통로가 정말 달랐다"를 눈으로 확인한다
  • cat files.txt | wc -l로 줄 수를 세고, 같은 결과가 wc -l files.txt로도 나오는지 비교한다

[응용] grep과 정규식으로 로그 캐기

access.log에서 원하는 줄을 패턴으로 뽑아보세요.

  • grep으로 ERROR 줄만 뽑고, -n을 붙여 몇 번째 줄인지 확인한다
  • grep -i가 왜 필요한지 보이려면, 소문자 error로 먼저 검색해 결과가 없는 걸 확인한 뒤 -i를 붙여본다
  • 정규식으로 400번대 상태 코드가 든 줄을 모두 뽑는다 (40[0-9] 또는 -E '403|404' 등 직접 골라서)
  • 10.0.0.5로 끝나는 줄만 $를 써서 뽑고, 그게 몇 줄인지 wc -l로 센다

[심화] 종합 — 수상한 IP를 추적하라 (서버 상황 상상)

현업 상황을 상상해봅시다. "운영 서버 로그를 받았는데, 누군가 계속 에러를 일으키고 있다. 가장 많이 에러를 낸 IP를 찾아 차단 후보로 보고하고 싶다." 교안의 파이프라인을 직접 한 단계씩 조립해 재현해보세요.

  • grepcutsortuniq -csort -rn 순으로 파이프를 한 단계씩 늘려가며, 매 단계 중간 출력이 어떻게 바뀌는지 적어본다
  • 완성된 결과를 tee로 화면에 보면서 error_top.txt에도 저장한다
  • 이번엔 IP 말고 가장 많이 요청된 경로(path, 5번째 칸) Top 3를 같은 방식으로 뽑아본다 (cut -d' ' -f5)
  • 마지막으로, "이 결과만 보고 정말 그 IP가 공격자라고 단정할 수 있을까? 더 확인해야 할 게 있다면 무엇일까"를 두세 문장으로 적어본다

생각해볼 주제

1. 왜 유닉스는 "만능 명령 하나" 대신 "작은 도구 + 파이프"를 택했을까

오늘 우리는 grep·cut·sort·uniq처럼 한 가지만 하는 작은 도구들을 파이프로 이어 큰일을 했습니다. 그런데 누군가는 "옵션 수백 개짜리 만능 로그 분석 명령 하나를 만들면 더 편하지 않나?"라고 생각할 수 있어요. 유닉스는 왜 그 길을 택하지 않고, 굳이 작은 도구를 잇는 방식을 골랐을까요? 작은 도구 조합이 주는 이점(재사용·자유로운 조합·각 도구의 단순함)과, 그 대신 치르는 비용(초보자가 "어떤 도구를 어떻게 이어야 하지" 막막해지는 학습 곡선)을 저울질해보세요. 그리고 여러분이 평소 쓰는 다른 프로그램 중에, 이 "작은 것들의 조합" 철학을 닮은 게 있는지도 떠올려보면 재밌을 거예요.

2. stdout과 stderr를 굳이 나눈 설계가 자동화에서 주는 힘

리눅스는 정상 결과(stdout)와 에러(stderr)를 처음부터 다른 통로로 나눠뒀습니다. 사람이 화면을 직접 볼 땐 둘 다 화면에 나와서 차이를 못 느끼지만, 사람이 잠든 새벽 3시에 자동으로 도는 스크립트를 떠올려보세요. 정상 결과와 에러가 한 통로로 섞여 있다면 어떤 곤란함이 생길까요? 반대로 둘이 나뉘어 있으면 "정상 로그는 파일에 차곡차곡 쌓고, 에러만 골라내 담당자에게 알림을 보내는" 식의 일이 어떻게 쉬워질까요? 통로를 나눈 이 작은 설계 결정이, 사람이 지켜보지 않는 자동화 환경에서 왜 큰 힘이 되는지 자기 말로 정리해보세요.

3. grep으로 로그를 뒤지는 것의 한계는 어디일까

오늘 grep과 파이프로 로그를 분석하는 게 얼마나 강력한지 봤습니다. 그런데 이 방식에도 분명 한계가 있어요. 로그가 한 파일 수천 줄이 아니라, 수십 대의 서버에 흩어진 수억 줄이라면 어떨까요? 매번 서버마다 들어가 grep을 치는 게 현실적일까요? 텍스트 grep이 빛나는 상황과, 그것만으로는 벅차지는 지점을 가늠해보세요. 그리고 그 한계를 넘기 위해 현업에서 "로그를 한곳에 모아 검색·집계하는 시스템"을 따로 두는 이유가 무엇일지 상상해보세요. (이 이야기는 나중에 인프라를 다루는 과목에서 본격적으로 이어집니다. 오늘은 "왜 그다음 단계가 필요해지는가"만 그려보면 충분해요.)

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안입니다. 모든 명령은 여러분 자신의 환경에서 직접 쳐보며 확인하는 게 가장 중요해요. 파일 줄 수나 IP 같은 숫자가 책과 조금 달라도 정상이니, 흐름이 맞는지를 보면 됩니다. 파이프는 특히 한 단계씩 늘려가며 중간 출력이 어떻게 바뀌는지 눈으로 따라가는 리듬이 핵심이에요.


🎯 [과제 1 예시답안] 스트림·리다이렉션·파이프 기본기

채점 포인트

확인 항목 무엇을 보면 되는가
결과를 파일로 >로 결과가 파일에 담기고 화면엔 아무것도 안 나오는 걸 봤는가
> vs >> >는 기존 내용을 덮어쓰고, >>는 이어 붙인다는 차이를 직접 비교했는가
스트림 분리 >2>로 정상 결과와 에러가 서로 다른 파일로 갈리는 걸 확인했는가
파이프 `cat ...

풀이 예시

Bash
$ cd ~/practice/logs

# 결과를 파일로 — 화면엔 아무것도 안 나오고 파일에 담긴다
$ ls -l /etc > files.txt
$ cat files.txt
total 1120
-rw-r--r-- 1 root root 2981 Jan  1 09:00 adduser.conf
drwxr-xr-x 2 root root 4096 Jan  1 09:00 alternatives
...
Bash
# > 와 >> 비교 — 이어쓰기는 차곡차곡 쌓인다
$ echo "첫 줄" > memo.txt
$ echo "둘째 줄" >> memo.txt
$ cat memo.txt
첫 줄
둘째 줄

# 만약 둘째도 > 로 했다면? 첫 줄을 덮어써 "둘째 줄"만 남는다
$ echo "둘째 줄" > memo.txt
$ cat memo.txt
둘째 줄
Bash
# 정상 결과(1)와 에러(2)를 서로 다른 파일로
$ ls /etc 없는폴더 > out.txt 2> err.txt
$ cat out.txt
/etc:
hostname  hosts  passwd  ...
$ cat err.txt
ls: cannot access '없는폴더': No such file or directory
Bash
# 파이프로 줄 세기 — 파일을 인자로 줘도 같은 수가 나온다
$ cat files.txt | wc -l
213
$ wc -l files.txt
213 files.txt

(213/etc 안 파일 수에 따라 환경마다 달라요. 두 명령이 같은 수를 주는 게 핵심이에요. cat | wc -l은 stdin으로 받은 줄을, wc -l files.txt는 파일을 직접 읽어 센 거예요.)

💡 튜터의 한마디> 하나와 >> 두 개의 차이를 몸으로 익혀두세요. 신입 때 로그가 쌓인 파일에 새 줄을 추가하려다 >를 쳐서 그동안의 기록을 통째로 날리는 사고가 정말 흔해요. 리눅스엔 휴지통이 없어 되돌릴 수 없으니, 중요한 파일에 결과를 보낼 땐 "덮어쓰기인가 이어쓰기인가"를 한 번 더 생각하는 습관이 안전망이에요.

🎯 [과제 2 예시답안] grep과 정규식으로 로그 캐기

채점 포인트

확인 항목 무엇을 보면 되는가
ERROR 추출 grep ERROR로 줄을 뽑고 -n으로 줄 번호를 봤는가
-i 의미 소문자 error는 0건, -i를 붙이면 매치되는 걸 비교했는가
정규식 4xx 40[0-9] 또는 `-E '403
$ 끝 매칭 5$로 끝나는 줄을 뽑고 wc -l로 셌는가

풀이 예시

Bash
$ cd ~/practice/logs

# ERROR 줄 + 줄 번호
$ grep -n ERROR access.log
3:2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
5:2026-06-16 10:01:30 ERROR POST /upload 500 192.168.0.42
7:2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
9:2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5
Bash
# 소문자 error 는 대소문자 구분 때문에 0건, -i 를 붙이면 4건
$ grep error access.log
$ grep -i error access.log | wc -l
4
Bash
# 400번대 상태 코드 (403·404)
$ grep -E '403|404' access.log
2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:28 WARN GET /config 404 10.0.0.5
2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:44 WARN GET /search 404 192.168.0.11
$ grep -E '403|404' access.log | wc -l
5
Bash
# 5로 끝나는 줄 (10.0.0.5로 끝나는 줄들 — ERROR뿐 아니라 WARN도 포함)
$ grep '5$' access.log
2026-06-16 10:01:25 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:28 WARN GET /config 404 10.0.0.5
2026-06-16 10:01:36 ERROR GET /admin 403 10.0.0.5
2026-06-16 10:01:41 ERROR GET /admin 403 10.0.0.5
$ grep '5$' access.log | wc -l
4

💡 튜터의 한마디 — 정규식은 처음부터 다 외우려 하지 말고, 오늘 본 다섯 개(^ $ . * [])부터 손에 붙이세요. 그리고 로그를 검색할 땐 표기가 Error·ERROR로 제각각인 경우가 많아 -i를 습관처럼 붙이는 분이 많아요. 정확히 찾으려다 대소문자 하나 때문에 "결과 없음"을 만나는 일이 흔하거든요.

🎯 [과제 3 예시답안] 종합 — 수상한 IP를 추적하라

채점 포인트

확인 항목 무엇을 보면 되는가
단계별 조립 파이프를 한 단계씩 늘리며 중간 출력이 바뀌는 걸 관찰했는가
tee 저장 결과를 화면에 보면서 파일에도 동시에 저장했는가
경로 Top 3 cut -d' ' -f5로 path를 뽑아 같은 방식으로 집계했는가
비판적 사고 "이것만으로 공격자 단정 가능?"에 추가로 확인할 거리를 적었는가

풀이 예시

Bash
$ cd ~/practice/logs

# 한 단계씩 늘려가며 — ① 에러만
$ grep ERROR access.log | cut -d' ' -f7
10.0.0.5
192.168.0.42
10.0.0.5
10.0.0.5

# ② 모아서 세기  ③ 많은 순으로
$ grep ERROR access.log | cut -d' ' -f7 | sort | uniq -c | sort -rn
      3 10.0.0.5
      1 192.168.0.42

# 결과를 화면에 보면서 파일에도 저장
$ grep ERROR access.log | cut -d' ' -f7 | sort | uniq -c | sort -rn | tee error_top.txt
      3 10.0.0.5
      1 192.168.0.42
Bash
# 경로(5번째 칸)별 요청 수 Top 3
$ cut -d' ' -f5 access.log | sort | uniq -c | sort -rn | head -3
      3 /admin
      2 /feed
      1 /upload

/admin이 3건으로 1위, /feed가 2건으로 2위예요. 3위부터는 전부 1건이라 동점이고, 동점인 줄의 순서는 환경에 따라 달라질 수 있으니 위 3위(/upload)는 한 예시로 보면 됩니다.

마지막 질문, "이 결과만으로 10.0.0.5를 공격자로 단정할 수 있을까?"에는 이렇게 적으면 좋아요.

  • 단정하기엔 일러요. 이 IP가 에러 3건을 냈지만, 정상 요청은 얼마나 했는지, 회사 내부망 IP인지, 같은 에러가 짧은 시간에 몰렸는지 등을 더 봐야 해요.
  • 확인할 거리: 그 IP의 전체 요청 수(정상 포함) 대비 에러 비율, 요청이 몰린 시간대, /admin에 반복 접근한 의도(오타인지 스캔인지), 다른 서버 로그와 교차 확인.
  • 즉 이 한 줄 집계는 "어디를 더 봐야 할지 알려주는 출발점"이지, 그 자체가 결론은 아니에요.

💡 튜터의 한마디 — 긴 파이프라인은 절대 한 번에 완성하려 하지 마세요. 한 단계 잇고 출력 확인, 또 한 단계 잇고 확인 — 이렇게 가야 어디서 어긋났는지 바로 보입니다. 그리고 데이터가 가리키는 걸 "단서"로 보되 "결론"으로 단정하지 않는 태도가, 현업에서 신뢰받는 엔지니어의 자세예요.


🤔 [생각해볼 주제 1] 왜 유닉스는 "만능 명령 하나" 대신 "작은 도구 + 파이프"를 택했을까

문제 상황 요약

한 가지만 하는 작은 도구(grep·cut·sort)를 파이프로 잇는 방식과, 옵션 수백 개짜리 만능 분석 명령 하나를 만드는 방식. 유닉스는 왜 전자를 택했고, 그 선택의 이점과 비용은 무엇인가.

튜터의 가이드 및 해설

핵심은 조합의 폭발이에요. 작은 도구 10개가 있으면, 이들을 파이프로 잇는 방법은 거의 무한해집니다. 만든 사람이 상상하지 못한 조합까지 사용자가 그때그때 만들어 쓸 수 있죠. 반면 만능 명령 하나는 "그 명령이 미리 지원하는 기능"안에 갇혀요. 새로운 요구가 생기면 그 명령 자체를 뜯어고쳐야 합니다.

이점을 정리하면 이래요.

  • 재사용: sort는 로그를 정렬하든 이름을 정렬하든 똑같이 쓰여요. 한 번 잘 만든 도구가 모든 맥락에서 재사용됩니다.
  • 단순함과 유지보수: 도구 하나가 한 가지만 하니 코드가 단순하고, 버그도 적고, 고치기도 쉬워요.
  • 자유로운 조합: 작은 블록을 레고처럼 끼워 맞춰 복잡한 일을 표현해요.

물론 공짜는 아니에요. 비용은 학습 곡선입니다. 초보자는 "어떤 도구가 있고, 어떤 순서로 이어야 하지?"부터 막막해요. 만능 명령은 --help 한 번이면 기능 목록이 다 나오지만, 작은 도구 조합은 도구들을 먼저 알고 발상까지 갖춰야 하거든요. 그래서 실무는 양쪽을 섞어 써요 — 대부분은 작은 도구를 잇되, awk처럼 좀 더 강력한 도구도 도구함에 함께 둡니다.

이 철학은 유닉스 밖에서도 보여요. 함수를 잘게 나눠 조합하는 함수형 프로그래밍, 서비스를 작게 쪼개는 마이크로서비스가 같은 결의 발상이에요. "큰 하나"보다 "작은 여럿의 조합"이 변화에 강하다는 오래된 지혜죠.

💡 핵심을 한마디로

"큰 도구 하나는 정해진 그 일에만 강하고, 작은 도구 여럿은 만든 사람도 상상 못 한 조합까지 해냅니다. 유닉스는 미래의 조합 가능성에 걸었고, 그 베팅이 수십 년을 갔어요."

🤔 [생각해볼 주제 2] stdout과 stderr를 굳이 나눈 설계가 자동화에서 주는 힘

문제 상황 요약

리눅스는 정상 결과(stdout)와 에러(stderr)를 다른 통로로 나눠뒀다. 사람이 화면을 직접 볼 땐 둘 다 화면에 나와 차이가 없어 보이는데, 사람이 지켜보지 않는 자동화 환경에서는 이 분리가 왜 큰 힘이 되는가.

튜터의 가이드 및 해설

새벽 3시에 자동으로 도는 백업 스크립트를 떠올려보세요. 아무도 화면을 안 봐요. 만약 정상 결과와 에러가 한 통로로 섞여 있다면, 정상 로그 수천 줄 사이에 에러 한 줄이 묻혀버려요. 다음 날 출근해서 그 한 줄을 찾으려면 또 grep 신세죠.

통로가 나뉘어 있으면 자동화가 깔끔해져요. 이렇게 갈라 쓸 수 있거든요.

Bash
백업스크립트 >> backup.log 2>> error.log

정상 진행 기록(stdout)은 backup.log에 차곡차곡 쌓고, 에러(stderr)는 error.log에만 모아요. 그러면 "error.log에 새 줄이 생기면 담당자에게 알림을 보낸다" 같은 감시를 아주 단순하게 걸 수 있어요. 정상 로그의 소음에 파묻히지 않고, 문제만 콕 집어 사람을 깨우는 거죠.

여기에 종료 코드($?)까지 더하면 더 똑똑해져요. 명령이 성공했는지 실패했는지를 숫자로 알 수 있는데, 이건 마지막 모듈(A-12 스크립팅)에서 다룹니다. "정상은 조용히 쌓고, 에러는 따로 모아 알린다"는 이 패턴이 모든 서버 자동화의 바탕이에요.

결국 stdout/stderr 분리는 "사람이 안 볼 때를 위한 설계"예요. 사람이 보고 있으면 굳이 나눌 필요가 없지만, 기계가 스스로 돌아가는 환경에서는 정상과 비정상을 기계가 구분할 수 있어야 하니까요.

💡 핵심을 한마디로

"정상과 에러를 나눈 건 사람이 지켜볼 때가 아니라, 사람이 지켜보지 않을 때를 위한 설계예요. 정상은 조용히 쌓이게 두고, 에러만 골라 사람을 깨우죠."

🤔 [생각해볼 주제 3] grep으로 로그를 뒤지는 것의 한계는 어디일까

문제 상황 요약

grep과 파이프로 로그를 분석하는 건 강력하다. 하지만 로그가 한 파일 수천 줄이 아니라 수십 대 서버에 흩어진 수억 줄이라면? grep이 빛나는 자리와, 그것만으로는 벅차지는 지점은 어디인가.

튜터의 가이드 및 해설

grep한 대의 서버에서, 한 파일을, 지금 당장 뒤질 때 최강이에요. 도구가 가볍고, 어디에나 깔려 있고, 한 줄이면 끝나죠. 서버에 SSH로 들어가 "방금 무슨 일이 터졌나"를 즉석에서 확인하는 일은 앞으로도 계속 grep으로 할 거예요.

그런데 규모가 커지면 벽에 부딪혀요.

  • 분산: 서버가 50대면, 매번 50대에 일일이 들어가 grep을 칠 순 없어요.
  • 규모: 한 파일이 수억 줄이면 grep 한 번이 한참 걸려요. 자주 하는 검색을 매번 전체 스캔할 순 없죠.
  • 비구조: 텍스트라서 "에러율을 시간대별로 그래프로" 같은 집계는 파이프로 억지로는 되지만 한계가 뚜렷해요.
  • 보존·실시간: 로그가 지워지거나 회전되면 과거를 못 보고, "지금 막 들어오는 에러"를 실시간으로 지켜보기도 어려워요.

그래서 현업은 일정 규모를 넘으면 로그를 한곳에 모으는 시스템을 따로 둬요. 각 서버의 로그를 중앙 저장소로 흘려보내고, 미리 색인(index)을 만들어 빠르게 검색하고, 대시보드로 집계를 그리죠. 로그를 처음부터 구조화된 형식(JSON 같은)으로 남겨 기계가 필드별로 다루기 좋게 만들기도 하고요.

다만 이건 깊이 들어가면 인프라의 영역이에요. 이 과목에서는 "왜 grep만으로는 어느 순간 벅차지는가", "그래서 그다음 단계가 왜 필요해지는가"까지만 그려두면 충분합니다. 그 본격적인 이야기는 나중에 인프라를 다루는 과목이 이어받아요. 중요한 건, 작게 시작할 땐 grep 한 줄이 가장 빠르고 똑똑한 답이라는 거예요. 모든 걸 거창한 시스템으로 시작할 필요는 없어요.

💡 핵심을 한마디로

"grep은 한 대를 지금 당장 뒤지는 데 최고예요. 서버가 여러 대가 되고, 줄이 수억이 되고, 실시간이 필요해지는 순간, 로그를 한곳에 모아 색인하는 시스템이 grep의 바통을 이어받습니다."

전체 목록 리눅스