쉘 리다이렉션(Redirection) 사용법

쉘 리다이렉션(Redirection) 사용법

쉘 프로그래밍을 처음 하시는 분들이 스크립트를 읽으시다가 2>&1 같은 알 수 없는 문법을 보고 당황하시는 경우가 있습니다. 이것을 보통 리다이렉션(Redirection)이라고 하는데요. 어떤 명령의 입력이나 출력을 다른 곳으로 변경하기 위해서 사용됩니다.

이번 포스팅에서는 쉘 리다이렉션의 기본 사용법을 알아보고 다양한 예제를 통해 어떻게 실제 쉘 프로그래밍에 활용할 수 있는지 배워보겠습니다.

표준 스트림

쉘의 리다이렉션을 이해하려면 우선 macOS와 같은 유닉스 계열 운영체제의 세 가지 표준 스트림(Standard Stream)을 알고 있어야 합니다.

  • 표준 입력(Standard Input, stdin): 키보드로 입력을 받는 스트림입니다.
  • 표준 출력(Standard Output, stdout): 터미널에 정상 출력을 보내는 스트림입니다.
  • 표준 오류(Standard Error, stderr): 터미널에 오류 출력을 보내는 스트림입니다.

이 세 가지 스트림은 모든 프로그램이 시작될 때 운영체제가 자동으로 열어줍니다. 프로그램은 별도 설정 없이도 키보드로 입력을 받고, 정상 결과는 정상 출력으로, 오류 메시지는 오류 출력으로 내보낼 수 있어요.

그리고 리다이렉션 문법을 제대로 이해하기 위해서는 반드시 알고 있어야 할 부분이 하나 더 있는데요. 이 세 스트림에는 운영체제가 매겨놓은 약속된 번호가 붙어 있어서, 이 번호들이 리다이렉션 문법의 핵심 재료가 됩니다.

  • 0: 표준 입력 (stdin)
  • 1: 표준 출력 (stdout)
  • 2: 표준 오류 (stderr)

이 번호를 파일 디스크립터(File Descriptor, 줄여서 fd)라고 부릅니다. 원래 유닉스 계열 운영체제는 프로그램이 입출력에 사용하는 모든 자원에 정수 번호를 매겨서 관리하는데요. 표준 스트림 셋은 어떤 프로그램이 실행되든 항상 0, 1, 2번을 부여받도록 미리 약속되어 있습니다.

뒤에서 보실 >, 2>, 2>&1 같은 리다이렉션 문법에 등장하는 숫자가 모두 이 파일 디스크립터 번호예요. 지금은 “0은 입력, 1은 정상 출력, 2는 오류 출력”이라는 짝만 머릿속에 넣어두시면 충분합니다.

출력 리다이렉션

우리가 쉘에서 어떤 명령어를 실행하거나, 여러 명령어로 이루어진 쉘 스크립트를 실행하면, 각 명령이 정상적으로 처리되었든 오류가 발생하였든 터미널에 결과가 출력이 됩니다.

예를 들어, cat 명령어를 존재하는 파일을 상대로 실행하면 정상적으로 파일 내용이 터미널에 출력이 됩니다.

$ cat .gitignore
.idea
.DS_Store

반대로 존재하지 않는 파일을 상대로 실행하면 오류 메시지가 터미널에 출력이 됩니다.

$ cat gitignore
cat: gitignore: No such file or directory

터미널에서 일회성으로 명령어를 실행할 때는 정상 출력과 오류 출력을 구분 없이 터미널에 출력해도 크게 문제되지 않습니다. 하지만 쉘의 이러한 기본 처리 방식은 스크립트를 실행하거나 오랫동안 떠 있는 프로그램을 실행할 때는 적합하지 않죠. 일반적으로는 정상 출력과 오류 출력을 각각 별도의 파일에 저장해야 로그가 유실될 위험이 적고 디버깅이 용이할 것입니다.

이때 사용하는 것이 바로 > 연산자입니다. >는 명령어의 출력 내용을 터미널에 보내지 않고 다른 곳으로 보낼 수 있도록 해주죠. 가장 흔하게 볼 수 있는 사용 사례로 출력 내용을 로그 파일에 저장하는 것을 들 수 있습니다.

예를 들어, cat .gitignore 명령어의 실행 결과를 test.log 파일로 보내보겠습니다.

$ cat .gitignore > test.log

test.log의 내용을 터미널에 찍어보면 .gitignore 파일과 동일한 내용이 확인될 것입니다.

$ cat test.log
.idea
.DS_Store

> 연산자는 파일이 이미 존재하면 해당 파일을 덮어써 버리기 때문에 주의가 필요합니다.

예를 들어, 제가 이번에는 cat README.md 실행 결과를 test.log 파일로 보내보겠습니다.

$ cat README.md > test.log

test.log 파일에 기존에 있던 .gitignore의 내용이 모두 사라지고, README.md 파일의 내용으로 완전히 대체가 된 것을 볼 수 있습니다.

$ cat test.log
# README
Please read me!

대신에 >> 연산자를 사용하면 기존 내용을 건드리지 않고 명령어 실행 결과를 파일 끝에 추가해줍니다.

$ cat .gitignore >> test.log

test.log 파일의 내용을 다시 확인해보면 README.md 파일의 내용이 .gitignore 파일에 덧붙여진 것을 볼 수 있습니다.

$ cat test.log
# README
Please read me!
.idea
.DS_Store

오류 리다이렉션

이번에는 존재하지 않는 파일을 대상으로 cat 명령어를 실행 후에 출력 결과를 test.log 파일로 보내보겠습니다.

😮 앗! 예상과 달리 오류 메시지가 그대로 터미널에 출력이 되는 것을 볼 수 있습니다.

$ cat gitignore > test.log
cat: gitignore: No such file or directory

test.log 파일은 내용이 비어 있네요.

$ cat test.log

왜 이런 일이 발생하는 걸까요? 🤔

비밀은 바로 맨 처음에 알려드린 표준 스트림을 나타내는 번호에 있습니다. >는 사실 1>의 축약된 형태이기 때문에, 정상 출력만 리다이렉션해줍니다. 그런데 우리는 오류 출력을 리다이렉션 하고 싶으므로 2>를 사용해야 합니다.

$ cat gitignore 2> test.log

이제 원했던 바와 같이 test.log 파일에 오류 내용이 저장되는 것을 볼 수 있습니다.

$ cat test.log
cat: gitignore: No such file or directory

어떤 사유로든 오류 출력을 아예 무시하고 싶다면 /dev/null 파일로 보내면 됩니다. /dev/null 파일은 어떤 출력을 보내도 그냥 무시해버리는 특수한 파일로 생각하시면 이해가 쉬우실 것 같습니다.

$ cat gitignore 2>/dev/null

2를 빼면 정상 출력까지 무시할 수 있겠죠?

$ cat gitignore >/dev/null

참고로 2> 뒤에 파일 경로 대신 다른 스트림을 지정해서 오류 출력을 정상 출력 쪽으로 합쳐 보내는 2>&1 같은 형태도 있는데요. 이 형태는 보통 두 스트림을 한곳에 묶는 용도로 쓰이기 때문에 바로 다음 절에서 따로 다루겠습니다.

통합 리다이렉션

만약에 정상 출력과 오류 출력 모두를 같은 파일로 보내고 싶다면 어떻게 해야 할까요? 가장 흔하게 쓰이는 방법이 바로 도입부에서 잠깐 언급했던 2>&1입니다.

$ 명령어 > any.log 2>&1

처음 보면 외계어 같지만 기호 하나씩 뜯어보면 그렇게 어렵지 않습니다.

  • 2> : 오류 출력(2)을 어딘가로 보낸다.
  • &1 : 그 어딘가가 “정상 출력(1)이 지금 향하고 있는 곳”이다.

여기서 & 기호는 “뒤에 오는 숫자를 파일 이름이 아니라 파일 디스크립터로 봐달라”는 표시입니다. 앞에서 짚었듯이 1은 정상 출력에 할당된 디스크립터 번호죠. 만약에 & 없이 2>1이라고만 쓰면 쉘은 이걸 “오류 출력을 1이라는 이름의 파일로 보내라”로 해석해서 엉뚱한 파일을 만들어 버립니다.

여기서 중요한 점이 하나 있는데요. 쉘은 리다이렉션을 왼쪽에서 오른쪽으로 순서대로 적용합니다.

$ 명령어 > any.log 2>&1

위 명령어는 다음과 같은 두 단계로 처리됩니다.

  1. > any.log : 정상 출력을 any.log 파일로 보낸다.
  2. 2>&1 : 오류 출력을 “지금 정상 출력이 가고 있는 곳”으로 보낸다. 1단계 덕분에 그곳이 any.log다.

그래서 결과적으로 정상 출력과 오류 출력이 모두 같은 파일에 쌓이게 됩니다.

만약에 순서를 뒤집어서 명령어 2>&1 > any.log라고 쓰면 어떻게 될까요? 얼핏 똑같아 보이지만 실제로는 오류 출력이 여전히 터미널에 그대로 찍힙니다. 2>&1을 처리하는 시점에는 정상 출력이 아직 터미널을 향하고 있어서, 오류 출력도 터미널로 복사되어 버리기 때문이죠.

Bash에서는 이 헷갈리는 순서를 신경 쓰지 않아도 되도록 &> 또는 &>>라는 축약형을 제공합니다.

$ 명령어 &> any.log    # 정상 출력 + 오류 출력 모두를 덮어쓰기
$ 명령어 &>> any.log   # 정상 출력 + 오류 출력 모두를 이어쓰기

편하기는 한데 POSIX 표준이 아니라서 /bin/sh 같은 환경에서는 동작하지 않을 수 있습니다. 스크립트 호환성까지 챙기고 싶다면 > any.log 2>&1 쪽이 더 안전합니다.

이제 실제로 동작을 확인해 볼게요. 먼저 존재하지 않는 파일을 상대로 cat 명령어를 실행해서 오류 내용을 test.log 파일에 덧붙여 봅니다. (>> 연산자 사용)

$ cat gitignore &>> test.log
$ cat test.log
cat: gitignore: No such file or directory

이번에는 존재하는 파일을 상대로 cat 명령어를 실행해서 파일 내용을 같은 파일에 덧붙여 보겠습니다.

$ cat .gitignore &>> test.log
$ cat test.log
cat: gitignore: No such file or directory
.idea
.DS_Store

정상 출력이든 오류 출력이든 가리지 않고 test.log 파일에 잘 추가되는 것을 볼 수 있습니다.

반대로 정상 출력은 success.log 파일로, 오류 출력은 error.log 파일로 따로 나눠 보내고 싶다면 두 개의 리다이렉션을 함께 쓰면 됩니다.

$ 명령어 > success.log 2> error.log

마지막으로 파이프와 함께 쓸 때 유용한 축약형 하나만 더 소개할게요. 2>&1을 다음 명령으로 흘려보내는 패턴이 워낙 흔하다 보니, Bash에는 이걸 한 글자로 줄인 |& 연산자가 있습니다.

$ 명령어 2>&1 | tail -10   # 일반형
$ 명령어 |& tail -10        # |&로 줄인 형태

두 명령은 완전히 같은 동작을 합니다. git pushwrangler deploy처럼 출력이 길고 stderr를 통해 진행 상황을 뿜어내는 도구의 마지막 몇 줄만 보고 싶을 때 자주 쓰입니다. 이 역시 &>와 마찬가지로 Bash 4 이상이나 Zsh에서 동작하는 비 POSIX 문법이라서, 휴대성이 필요한 스크립트라면 2>&1 |을 그대로 쓰는 편이 안전합니다.

파이프와 리다이렉션의 차이

지금까지 본문에서 |가 여러 번 등장했는데요. 처음 보면 |>가 비슷해 보여서 헷갈리기 쉽지만, 두 문법은 연결하는 대상이 다릅니다.

리다이렉션(>, <, 2> 등)은 명령어의 입출력을 파일과 연결합니다. 반면에 파이프(|)는 명령어의 출력을 다른 명령어의 표준 입력으로 흘려보내죠.

$ ls > files.txt        # ls의 결과를 files.txt 파일에 저장
$ ls | grep ".md"       # ls의 결과를 grep 명령어로 전달

물론 둘은 함께 쓰일 수도 있습니다. 파이프로 여러 명령어를 거치게 한 뒤 최종 결과만 파일로 저장하는 패턴이 대표적이에요.

$ cat fruits.txt | sort | uniq -c > result.txt

cat부터 uniq -c까지 파이프로 가공한 결과가 result.txt 파일에 저장됩니다. 바로 앞에서 본 2>&1 | tail처럼 리다이렉션이 파이프 안에 끼어들 수도 있고요.

파이프에 대한 자세한 사용법은 쉘 파이프 포스팅에서 별도로 다루니 함께 보시면 두 개념이 깔끔하게 정리됩니다.

입력 리다이렉션

< 연산자는 명령어의 입력 내용을 다른 곳으로부터 가져올 수 있도록 해줍니다.

예를 들어, test.log 파일의 내용을 입력으로 cat 명령어를 실행해보겠습니다.

$ cat < test.log
cat: gitignore: No such file or directory
cat: gitignore: No such file or directory
.idea
.DS_Store

다른 예로, grep 명령어의 입력으로 test.log 파일의 내용을 보내보겠습니다.

$ grep "gitignore" < test.log
cat: gitignore: No such file or directory
cat: gitignore: No such file or directory

좀 응용을 해보자면… grep 명령어의 실행 결과를 터미널에 출력하는 대신에 파일에 저장할 수도 있겠죠? 😉

$ grep "gitignore" < test.log > grep.txt
$ cat grep.txt
cat: gitignore: No such file or directory
cat: gitignore: No such file or directory

히어독으로 여러 줄 입력 넘기기

지금까지는 한 줄짜리 입력만 다뤘는데요. 여러 줄의 내용을 한꺼번에 명령어로 흘려보내고 싶을 때는 히어독(Heredoc)이라는 또 다른 입력 리다이렉션 방식을 사용합니다. << 뒤에 “여기까지가 입력의 끝”이라고 표시할 단어를 적어두고, 그 단어가 다시 나타날 때까지의 본문을 입력으로 전달하는 문법이에요.

$ cat << EOF
첫 번째 줄
두 번째 줄
세 번째 줄
EOF
 번째
 번째
 번째

EOF는 관례적으로 자주 쓰이는 이름일 뿐이고, 본문에 등장하지 않는 단어라면 무엇이든 종료 표시로 쓸 수 있습니다.

이 패턴은 요즘 AI 에이전트가 깃 커밋 메시지를 작성할 때 특히 즐겨 사용합니다. git commit -m은 원래 한 줄짜리 메시지를 받는 옵션이라, 여러 줄짜리 메시지를 따옴표 안에서 줄바꿈으로 직접 표현하면 따옴표 처리가 까다로워지거든요. 이때 히어독과 명령 치환($(...))을 함께 쓰면 다음과 같이 깔끔하게 풀어낼 수 있습니다.

$ git commit -m "$(cat <<'EOF'
리다이렉션 가이드 보완

- 2>&1의 순서 의존성 설명 추가
- 히어독 예제 추가
EOF
)"

cat이 히어독으로 받은 여러 줄 문자열을 표준 출력으로 뱉어내면, $(...)가 그 결과를 통째로 받아 -m의 인자로 전달하는 구조입니다.

여기서 종료 단어를 작은따옴표로 감싼 <<'EOF'가 중요한데요. 따옴표로 감싸지 않으면 본문 안의 $변수나 백틱을 쉘이 그대로 해석해 버립니다. 커밋 메시지에 $1이나 `code` 같은 표현이 섞여 있을 때 의도치 않은 치환을 막아주는 안전장치라고 보시면 됩니다.

마치며

지금까지 쉘의 리다이렉션을 어떻게 사용하는지 다양한 예제를 통해서 살펴보았습니다.

리다이렉션이 얼마나 강력한지 느끼실 수 있는 기회가 되기를 바라며 본 포스팅이 여러분의 쉘 능력을 한 단계 끌어올리는 데 도움이 되었으면 좋겠습니다.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord