이미지 로딩 중...
AI Generated
2025. 11. 18. · 2 Views
명령어 파이프라인과 리다이렉션 완벽 가이드
리눅스 쉘에서 명령어를 연결하고 출력을 제어하는 파이프라인과 리다이렉션의 모든 것을 배워봅니다. 초보자도 쉽게 따라할 수 있는 실전 예제와 함께 프로세스 치환, 에러 처리까지 완벽하게 마스터해보세요.
목차
1. 파이프( ) 기본 개념
시작하며
여러분이 로그 파일에서 특정 에러만 찾아서 개수를 세고 싶을 때, 매번 파일을 열어서 수동으로 세어본 적 있나요? 혹은 명령어 결과를 파일로 저장했다가 다시 읽어서 처리하는 번거로운 과정을 반복하고 계신가요?
이런 방식은 시간도 오래 걸리고, 중간에 임시 파일을 만들어야 해서 디스크 공간도 낭비됩니다. 게다가 실시간으로 처리하기도 어렵죠.
바로 이럴 때 필요한 것이 파이프(|)입니다. 파이프를 사용하면 한 명령어의 출력을 다른 명령어의 입력으로 직접 연결할 수 있어, 마치 물이 파이프를 타고 흐르듯 데이터가 명령어 사이를 흘러갑니다.
개요
간단히 말해서, 파이프(|)는 한 명령어의 결과를 다음 명령어로 바로 전달하는 연결 통로입니다. 예를 들어, 웹 서버 로그에서 404 에러를 찾고, 그 중에서 가장 많이 발생한 URL을 찾고 싶다면 어떻게 할까요?
파이프 없이는 여러 단계를 거쳐야 하지만, 파이프를 사용하면 한 줄로 해결됩니다. 실시간 데이터 처리, 로그 분석, 텍스트 가공 등 거의 모든 상황에서 유용합니다.
기존에는 명령어 결과를 임시 파일에 저장했다가 다시 읽어야 했다면, 이제는 파이프로 메모리상에서 바로 전달할 수 있습니다. 파이프의 핵심 특징은 세 가지입니다.
첫째, 중간 파일이 필요 없습니다. 둘째, 여러 명령어를 무한히 연결할 수 있습니다.
셋째, 실시간으로 처리되어 빠릅니다. 이러한 특징들이 리눅스를 강력한 데이터 처리 도구로 만들어줍니다.
코드 예제
# 로그 파일에서 ERROR 문자열을 찾고 개수를 센다
cat application.log | grep "ERROR" | wc -l
# 프로세스 목록에서 nginx 관련 항목만 필터링
ps aux | grep nginx | grep -v grep
# 파일 목록을 크기순으로 정렬하고 상위 5개만 표시
ls -lh | sort -k5 -h | tail -n 5
# 접속 IP를 추출하고 중복을 제거한 후 정렬
cat access.log | cut -d' ' -f1 | sort | uniq
설명
파이프가 하는 일은 명령어들 사이에 데이터 통로를 만드는 것입니다. 첫 번째 명령어가 무언가를 출력하면, 그 출력이 자동으로 두 번째 명령어의 입력이 됩니다.
첫 번째 예제를 보면, cat application.log가 로그 파일 전체를 출력합니다. 이 출력이 파이프를 통해 grep "ERROR"로 전달되고, grep은 ERROR가 포함된 줄만 필터링합니다.
그 결과가 다시 파이프를 통해 wc -l로 전달되어 최종적으로 줄 개수를 세어줍니다. 만약 파이프가 없었다면 이 과정을 세 번에 나누어 실행하고 중간마다 파일로 저장했어야 할 겁니다.
두 번째 예제에서는 ps aux가 모든 프로세스를 출력하고, 첫 번째 grep이 nginx가 포함된 줄을 찾습니다. 그런데 여기서 문제가 있습니다.
grep 명령어 자체도 출력에 나타나거든요. 그래서 grep -v grep을 한 번 더 사용해서 grep 프로세스 자체는 제외합니다.
이렇게 파이프를 여러 번 사용할 수 있습니다. 세 번째 예제는 파일 크기를 기준으로 정렬하는 실용적인 예시입니다.
ls -lh가 파일 목록을 사람이 읽기 쉬운 형식으로 출력하면, sort -k5 -h가 다섯 번째 컬럼(크기)을 기준으로 정렬하고, tail -n 5가 마지막 5줄(가장 큰 파일 5개)만 보여줍니다. 여러분이 파이프를 사용하면 복잡한 스크립트를 작성하지 않고도 강력한 데이터 처리를 할 수 있습니다.
디스크 I/O를 줄여서 속도가 빠르고, 메모리를 효율적으로 사용하며, 코드가 간결해서 읽기 쉽습니다.
실전 팁
💡 파이프는 무한히 연결할 수 있지만, 너무 길어지면 읽기 어려우니 5개 이내로 유지하는 것이 좋습니다.
💡 grep -v grep 패턴을 자주 사용하는데, 이는 grep 프로세스 자체를 결과에서 제외하는 실전 테크닉입니다.
💡 파이프는 표준 출력(stdout)만 전달합니다. 에러 메시지(stderr)는 전달되지 않으니 주의하세요.
💡 디버깅할 때는 파이프를 하나씩 추가하면서 중간 결과를 확인하는 습관을 들이세요.
💡 cat file | grep pattern 대신 grep pattern file처럼 직접 파일을 읽는 것이 더 효율적입니다.
2. 명령어 체이닝 실전
시작하며
여러분이 서버에서 여러 작업을 순차적으로 실행해야 할 때, 각 명령어가 끝날 때까지 기다렸다가 다음 명령어를 입력하는 번거로움을 겪어본 적 있나요? 특히 긴 작업들을 연속으로 실행해야 할 때, 한 번에 설정해두고 자리를 뜰 수 있다면 얼마나 좋을까요?
더 중요한 것은, 앞 명령어가 실패했을 때 뒤 명령어를 실행하면 안 되는 경우가 많다는 점입니다. 예를 들어, 빌드가 실패했는데 배포 스크립트가 실행되면 큰 문제가 생깁니다.
이럴 때 필요한 것이 명령어 체이닝입니다. &&, ||, ; 같은 연산자로 명령어들을 조건부로 연결하여 자동화된 워크플로우를 만들 수 있습니다.
개요
간단히 말해서, 명령어 체이닝은 여러 명령어를 한 줄에 작성하되, 각 명령어의 성공/실패 여부에 따라 다음 명령어 실행을 제어하는 기법입니다. 쉘에는 세 가지 주요 체이닝 연산자가 있습니다.
&& (AND 연산자)는 앞 명령어가 성공했을 때만 다음을 실행하고, || (OR 연산자)는 앞 명령어가 실패했을 때만 다음을 실행합니다. 그리고 ; (세미콜론)은 성공 여부와 관계없이 무조건 다음을 실행합니다.
CI/CD 파이프라인, 배포 스크립트, 설치 자동화 등에서 필수적입니다. 기존에는 쉘 스크립트 파일을 만들고 if문으로 복잡하게 제어했다면, 이제는 한 줄로 간단하게 조건부 실행을 구현할 수 있습니다.
체이닝의 핵심은 명령어의 종료 상태(exit status)를 활용한다는 것입니다. 리눅스에서 명령어가 성공하면 0을 반환하고, 실패하면 0이 아닌 값을 반환합니다.
이 값을 기반으로 다음 동작을 결정하여 안전하고 효율적인 자동화를 구현할 수 있습니다.
코드 예제
# 디렉토리 생성 후 성공하면 파일 생성 (AND 체이닝)
mkdir project && cd project && touch README.md
# 파일이 없으면 에러 메시지 출력 (OR 체이닝)
test -f config.json || echo "Config file not found!"
# 빌드 성공 시 배포, 실패 시 에러 로그 생성
npm run build && npm run deploy || echo "Build failed" > error.log
# 무조건 순차 실행 (세미콜론)
echo "Starting..."; sleep 2; echo "Done!"
# 복잡한 조합: 백업 후 성공하면 삭제, 실패하면 경고
tar -czf backup.tar.gz data/ && rm -rf data/ || echo "Backup failed, data preserved"
설명
명령어 체이닝이 하는 일은 여러 명령어를 논리적으로 연결하여 자동화된 작업 흐름을 만드는 것입니다. 각 연산자는 앞 명령어의 종료 상태를 확인하고 다음 동작을 결정합니다.
첫 번째 예제에서 mkdir project가 성공하면 (이미 존재하지 않는 디렉토리라면) 다음 명령어 cd project가 실행됩니다. 이것도 성공하면 최종적으로 touch README.md가 실행됩니다.
만약 중간에 하나라도 실패하면 거기서 멈춥니다. 예를 들어 mkdir이 실패하면 cd와 touch는 실행되지 않습니다.
이렇게 하면 "디렉토리 생성 실패했는데 cd를 시도해서 엉뚱한 곳에 파일 생성" 같은 문제를 방지할 수 있습니다. 두 번째 예제는 OR 연산자의 활용입니다.
test -f config.json은 파일이 존재하면 0(성공)을 반환하고, 없으면 1(실패)을 반환합니다. 파일이 있으면 성공이므로 || 뒤는 실행되지 않지만, 파일이 없으면 실패이므로 echo 명령어가 실행되어 경고 메시지를 출력합니다.
이는 기본값 설정이나 에러 처리에 매우 유용합니다. 세 번째 예제는 두 연산자를 조합한 실전 패턴입니다.
먼저 npm run build를 실행합니다. 빌드가 성공하면 && 때문에 npm run deploy가 실행됩니다.
그런데 만약 빌드나 배포 중 하나라도 실패하면, || 연산자가 작동하여 에러 로그가 생성됩니다. 이런 패턴은 CI/CD 파이프라인에서 정말 많이 사용됩니다.
여러분이 명령어 체이닝을 사용하면 별도의 스크립트 파일 없이도 복잡한 작업을 자동화할 수 있습니다. 한 줄로 여러 작업을 안전하게 처리하고, 에러가 발생하면 자동으로 멈추거나 대안 동작을 수행하며, 코드가 간결해서 유지보수가 쉽습니다.
실전 팁
💡 && 체이닝은 "앞이 성공해야 다음 실행"이므로 의존성이 있는 명령어를 연결할 때 사용하세요.
💡 || 연산자는 "기본값 설정" 패턴에 유용합니다: VALUE=${INPUT} || VALUE="default"
💡 ; 연산자는 에러를 무시하므로 주의하세요. 로그 정리 같은 안전한 작업에만 사용하는 것이 좋습니다.
💡 복잡한 체이닝은 괄호로 그룹핑할 수 있습니다: (command1 && command2) || command3
💡 $? 변수로 직전 명령어의 종료 상태를 확인할 수 있습니다: echo $?는 0이면 성공, 그 외는 실패입니다.
3. tee 명령어 활용
시작하며
여러분이 스크립트를 실행하면서 결과를 화면으로도 확인하고 동시에 로그 파일로도 저장하고 싶을 때, 어떻게 하시나요? 파일로만 저장하면 실시간으로 진행 상황을 볼 수 없고, 화면으로만 보면 나중에 기록이 남지 않아 문제를 추적하기 어렵습니다.
특히 긴 시간이 걸리는 배포 작업이나 빌드 프로세스에서 이런 문제는 더욱 심각합니다. 진행 상황을 실시간으로 보면서도 나중에 검토할 수 있도록 로그를 남겨야 하거든요.
바로 이럴 때 필요한 것이 tee 명령어입니다. tee는 T자 파이프처럼 데이터 흐름을 두 갈래로 나누어, 하나는 화면에 표시하고 다른 하나는 파일에 저장합니다.
개요
간단히 말해서, tee는 입력받은 데이터를 화면(표준 출력)과 파일에 동시에 출력하는 명령어입니다. 마치 T자 파이프에서 물이 두 방향으로 흐르듯이 데이터를 복제합니다.
실무에서 정말 자주 사용되는 시나리오가 있습니다. 서버 설치 스크립트를 실행할 때, CI/CD 파이프라인을 디버깅할 때, 데이터베이스 마이그레이션을 수행할 때 등 중요한 작업의 모든 과정을 기록으로 남기면서도 실시간으로 모니터링해야 하는 경우입니다.
기존에는 출력을 파일로 리다이렉션하고 나중에 tail -f로 따로 봐야 했다면, 이제는 tee 하나로 두 가지를 동시에 할 수 있습니다. tee의 핵심 기능은 세 가지입니다.
첫째, 파이프라인 중간에서 데이터를 가로채서 저장할 수 있습니다. 둘째, -a 옵션으로 기존 파일에 추가(append)할 수 있습니다.
셋째, 여러 파일에 동시에 저장할 수도 있습니다. 이런 유연성 덕분에 로깅과 모니터링의 필수 도구입니다.
코드 예제
# 명령어 출력을 화면에 보면서 동시에 파일에 저장
ls -la | tee file_list.txt
# 기존 로그 파일에 추가 (덮어쓰기 않음)
echo "New log entry" | tee -a application.log
# 여러 파일에 동시에 저장
ps aux | tee process1.log process2.log process3.log
# 파이프라인 중간에서 사용하여 중간 결과 저장
cat data.txt | tee original.log | grep "ERROR" | tee errors.log | wc -l
# sudo와 함께 사용하여 권한 문제 해결
echo "new config" | sudo tee /etc/myapp/config.conf
설명
tee가 하는 일은 데이터 스트림을 복제하여 여러 목적지로 보내는 것입니다. 파이프라인의 데이터 흐름을 방해하지 않으면서도 중간 결과를 저장할 수 있습니다.
첫 번째 예제를 보면, ls -la의 출력이 tee로 전달됩니다. tee는 이 데이터를 받아서 두 곳으로 보냅니다.
하나는 file_list.txt 파일이고, 다른 하나는 표준 출력(화면)입니다. 그래서 여러분은 화면에서 파일 목록을 바로 확인할 수 있으면서도, 동시에 file_list.txt에 같은 내용이 저장됩니다.
나중에 이 파일을 다른 스크립트에서 사용하거나 기록으로 보관할 수 있습니다. 두 번째 예제의 -a 옵션은 매우 중요합니다.
기본적으로 tee는 파일을 덮어쓰는데, -a를 사용하면 기존 내용 뒤에 추가합니다. 로그 파일처럼 계속 쌓아가야 하는 경우에 필수적입니다.
echo "New log entry" | tee -a application.log를 실행하면 기존 로그는 유지되고 새 항목만 맨 끝에 추가됩니다. 네 번째 예제는 tee의 진정한 힘을 보여줍니다.
파이프라인 중간 중간에 tee를 배치하여 각 단계의 결과를 저장할 수 있습니다. cat data.txt의 원본 데이터가 original.log에 저장되고, grep으로 필터링된 에러만 errors.log에 저장되며, 최종 개수는 화면에 표시됩니다.
복잡한 데이터 처리 파이프라인을 디버깅할 때 이렇게 중간 결과를 저장해두면 어느 단계에서 문제가 생겼는지 쉽게 추적할 수 있습니다. 다섯 번째 예제는 실전 팁입니다.
sudo echo "text" > /etc/file은 작동하지 않습니다. 왜냐하면 리다이렉션(>)은 sudo 권한이 적용되기 전에 실행되기 때문입니다.
하지만 echo "text" | sudo tee /etc/file은 작동합니다. tee 자체가 sudo 권한으로 실행되어 파일에 쓸 수 있거든요.
여러분이 tee를 사용하면 작업 과정을 놓치지 않고 모두 기록할 수 있습니다. 실시간 모니터링과 로깅을 동시에 하고, 파이프라인 중간 결과를 디버깅용으로 저장하며, sudo 권한이 필요한 파일도 안전하게 작성할 수 있습니다.
실전 팁
💡 tee는 기본적으로 파일을 덮어쓰므로, 로그 파일에는 항상 -a 옵션을 사용하는 습관을 들이세요.
💡 command 2>&1 | tee log.txt로 표준 에러도 함께 tee로 보낼 수 있습니다(이 패턴은 다음 섹션에서 자세히 설명합니다).
💡 tee는 여러 파일에 동시 저장이 가능하므로, 백업용과 분석용을 동시에 만들 수 있습니다.
💡 tee /dev/tty를 사용하면 파이프라인 중간에서 화면으로 출력을 "훔쳐볼" 수 있습니다.
💡 CI/CD 파이프라인에서는 빌드 로그를 tee로 저장하면 실패 시 원인 분석이 훨씬 쉽습니다.
4. 프로세스 치환
시작하며
여러분이 두 파일의 내용을 비교하고 싶은데, 한쪽은 파일이 아니라 명령어의 실행 결과라면 어떻게 하시나요? 예를 들어, 현재 실행 중인 프로세스 목록과 어제 저장해둔 프로세스 목록을 비교하고 싶다면, 먼저 현재 프로세스를 파일로 저장한 다음에 비교해야 할까요?
이런 식으로 임시 파일을 만들고 나중에 삭제하는 과정은 번거롭고, 스크립트가 중간에 실패하면 임시 파일이 남아서 디스크를 낭비하기도 합니다. 바로 이럴 때 필요한 것이 프로세스 치환(Process Substitution)입니다.
<() 문법을 사용하면 명령어의 출력을 마치 파일처럼 사용할 수 있어, 임시 파일 없이 바로 비교나 처리를 할 수 있습니다.
개요
간단히 말해서, 프로세스 치환은 명령어의 출력을 임시 파일 경로처럼 만들어서 다른 명령어에 전달하는 기법입니다. <(command) 형태로 사용하며, 실제로는 /dev/fd/N 같은 특수 파일 디스크립터를 생성합니다.
이 기법은 특히 diff, comm, paste 같이 파일 경로를 인자로 받는 명령어와 함께 사용할 때 진가를 발휘합니다. 두 개의 동적 데이터 소스를 비교하거나, 여러 명령어 출력을 병합하거나, 복잡한 데이터 변환 파이프라인을 구축할 때 임시 파일 없이 처리할 수 있습니다.
기존에는 command1 > temp1.txt, command2 > temp2.txt, diff temp1.txt temp2.txt, rm temp1.txt temp2.txt 처럼 4단계가 필요했다면, 이제는 diff <(command1) <(command2) 한 줄로 끝납니다. 프로세스 치환의 핵심은 명령어 출력을 파일처럼 다루면서도 실제로는 메모리의 파이프를 사용한다는 점입니다.
() 형태도 있어서 출력을 명령어로 보낼 수도 있습니다. 이를 통해 클린하고 효율적인 데이터 처리 파이프라인을 구축할 수 있습니다.
코드 예제
# 두 명령어 출력을 직접 비교 (임시 파일 없이)
diff <(ls /dir1) <(ls /dir2)
# 현재 프로세스와 이전 프로세스 비교
diff <(ps aux | sort) <(cat previous_ps.txt | sort)
# 여러 서버의 로그를 동시에 비교
diff <(ssh server1 'tail -n 100 /var/log/app.log') <(ssh server2 'tail -n 100 /var/log/app.log')
# 출력을 여러 명령어로 분기 (tee처럼)
echo "data" | tee >(grep "error" > errors.txt) >(grep "warn" > warnings.txt)
# 파일과 명령어 출력을 함께 비교
diff myfile.txt <(curl -s https://example.com/remotefile.txt)
설명
프로세스 치환이 하는 일은 명령어를 백그라운드에서 실행하고, 그 출력을 읽을 수 있는 특수한 파일 경로를 만들어 다른 명령어에 전달하는 것입니다. 쉘이 자동으로 파이프를 생성하고 관리합니다.
첫 번째 예제를 보면, diff <(ls /dir1) <(ls /dir2)를 실행할 때 쉘이 두 개의 프로세스를 백그라운드에서 시작합니다. 각각 /dir1과 /dir2의 파일 목록을 출력하고, 이 출력들이 특수 파일 디스크립터(/dev/fd/63, /dev/fd/62 같은)로 연결됩니다.
diff 명령어는 이 특수 파일들을 일반 파일처럼 읽어서 비교합니다. 전체 과정이 메모리에서 일어나므로 디스크 I/O가 없고 빠릅니다.
세 번째 예제는 실전에서 매우 유용한 패턴입니다. 두 서버의 로그를 실시간으로 비교할 수 있습니다.
ssh server1 '...'과 ssh server2 '...'가 각각 실행되어 원격 서버의 로그를 가져오고, 이 두 출력을 diff가 비교합니다. 만약 프로세스 치환 없이 이걸 하려면 두 번 ssh 접속해서 파일로 저장하고, diff로 비교하고, 파일을 삭제하는 복잡한 과정을 거쳐야 합니다.
네 번째 예제는 >() 형태의 프로세스 치환입니다. 이는 출력을 여러 명령어로 보낼 때 사용합니다.
echo "data"의 출력이 tee를 통해 복제되고, 각 복제본이 >(grep "error" > errors.txt)와 >(grep "warn" > warnings.txt) 프로세스로 전달됩니다. 결과적으로 에러는 errors.txt에, 경고는 warnings.txt에 분류되어 저장됩니다.
이는 로그 파일을 실시간으로 분류할 때 매우 유용합니다. 다섯 번째 예제는 로컬 파일과 원격 파일을 비교하는 실용적인 예시입니다.
curl -s로 웹에서 파일을 가져와서 로컬 파일과 바로 비교할 수 있습니다. 원격 파일을 다운로드해서 저장하고 비교하고 삭제하는 과정이 필요 없습니다.
여러분이 프로세스 치환을 사용하면 임시 파일 관리의 번거로움에서 해방됩니다. 코드가 간결해지고, 디스크 I/O가 줄어들어 속도가 빠르며, 임시 파일 정리를 잊어버릴 위험도 없습니다.
특히 복잡한 데이터 비교나 병합 작업에서 매우 강력합니다.
실전 팁
💡 프로세스 치환은 bash와 zsh에서 지원되지만, sh에서는 작동하지 않으니 스크립트 첫 줄에 #!/bin/bash를 명시하세요.
💡 <(command)는 실제로 /dev/fd/N 형태의 경로를 생성하므로, echo <(ls)를 실행하면 /dev/fd/63 같은 경로를 볼 수 있습니다.
💡 프로세스 치환 내부의 명령어는 백그라운드에서 실행되므로, 파이프라인 내부에서 exit을 사용해도 메인 쉘이 종료되지 않습니다.
💡 여러 개의 프로세스 치환을 동시에 사용할 수 있습니다: paste <(cmd1) <(cmd2) <(cmd3)
💡 >(command) 형태는 출력을 명령어로 보낼 때 사용하며, tee와 함께 사용하면 출력을 여러 곳으로 분기할 수 있습니다.
5. 에러 리다이렉션
시작하며
여러분이 스크립트를 실행했는데 에러가 발생했고, 나중에 확인하려고 보니 에러 메시지가 로그 파일에 저장되지 않아서 무슨 문제였는지 알 수 없었던 경험이 있나요? 혹은 정상 출력과 에러 메시지가 뒤섞여서 로그를 분석하기 어려웠던 적은요?
리눅스에서는 정상 출력(stdout)과 에러 출력(stderr)이 별도의 채널로 분리되어 있습니다. 기본 리다이렉션(>)은 stdout만 처리하므로, stderr은 여전히 화면에 표시되고 파일에는 저장되지 않습니다.
바로 이럴 때 필요한 것이 에러 리다이렉션입니다. 2>, 2>&1 같은 문법으로 에러 출력을 제어하여 로그를 완벽하게 관리할 수 있습니다.
개요
간단히 말해서, 리눅스는 세 가지 표준 스트림을 사용합니다. 0번은 표준 입력(stdin), 1번은 표준 출력(stdout), 2번은 표준 에러(stderr)입니다.
에러 리다이렉션은 2번 스트림을 제어하는 기법입니다. 실무에서는 에러 로그를 별도로 관리해야 하는 경우가 많습니다.
배치 작업이 실패했을 때 원인을 파악하거나, 에러만 모아서 모니터링 시스템으로 보내거나, 에러를 완전히 무시해야 할 때(예: cron 작업에서 불필요한 메일 방지) 등입니다. 기존에는 정상 출력과 에러가 섞여서 로그 분석이 어려웠다면, 이제는 2>로 에러만 따로 저장하거나, 2>&1로 둘을 합쳐서 순서대로 기록하거나, 2>/dev/null로 에러를 완전히 숨길 수 있습니다.
에러 리다이렉션의 핵심 패턴은 세 가지입니다. 2> file은 에러만 파일로 보내고, 2>&1은 에러를 stdout과 합치며, &> file 또는 >file 2>&1은 둘 다 같은 파일로 보냅니다.
이를 마스터하면 로그 관리가 훨씬 정교해집니다.
코드 예제
# 에러만 파일로 리다이렉션 (정상 출력은 화면에)
command 2> error.log
# 정상 출력과 에러를 각각 다른 파일로
command > output.log 2> error.log
# 에러를 정상 출력과 합쳐서 하나의 파일로
command > combined.log 2>&1
# 에러를 완전히 무시 (블랙홀로 보냄)
command 2>/dev/null
# 정상 출력과 에러를 모두 한 파일로 (축약형)
command &> all.log
# 파이프에 에러도 함께 전달
command 2>&1 | tee full.log
설명
에러 리다이렉션이 하는 일은 표준 에러 스트림의 목적지를 변경하는 것입니다. 숫자 2는 stderr을 의미하고, &1은 stdout을 가리킵니다.
첫 번째 예제 command 2> error.log를 보면, 명령어의 정상 출력은 그대로 화면에 표시되지만, 에러 메시지는 error.log 파일로 저장됩니다. 이는 에러만 따로 수집하고 싶을 때 유용합니다.
예를 들어 cron 작업에서 정상 동작은 무시하고 에러만 이메일로 받고 싶을 때 사용합니다. 두 번째 예제 command > output.log 2> error.log는 완전한 분리입니다.
정상 출력은 output.log에, 에러는 error.log에 저장됩니다. 큰 배치 작업에서 결과와 에러를 나중에 따로 분석해야 할 때 이 패턴을 사용합니다.
주의할 점은 순서가 중요하다는 것입니다. 반드시 stdout 리다이렉션(>)을 먼저 쓰고 stderr 리다이렉션(2>)을 나중에 써야 합니다.
세 번째 예제 command > combined.log 2>&1은 가장 많이 사용되는 패턴입니다. 이 문법은 두 단계로 동작합니다.
먼저 > combined.log로 stdout을 combined.log로 리다이렉션합니다. 그 다음 2>&1로 stderr(2번)을 stdout(1번)이 가리키는 곳(combined.log)으로 복제합니다.
결과적으로 정상 출력과 에러가 발생한 순서대로 하나의 파일에 기록됩니다. 이는 전체 실행 흐름을 추적할 때 필수적입니다.
네 번째 예제 2>/dev/null은 에러를 완전히 무시합니다. /dev/null은 리눅스의 블랙홀로, 여기로 보낸 데이터는 사라집니다.
find / -name "*.log" 2>/dev/null 같은 명령어에서 권한 에러(Permission denied)를 숨기는 데 자주 사용됩니다. 여섯 번째 예제 command 2>&1 | tee full.log는 파이프와 에러 리다이렉션의 조합입니다.
파이프(|)는 기본적으로 stdout만 전달하므로, 먼저 2>&1로 stderr을 stdout과 합친 후 파이프로 전달해야 tee가 에러도 함께 받을 수 있습니다. 이렇게 하면 정상 출력과 에러를 모두 화면에서 보면서 동시에 파일로 저장할 수 있습니다.
여러분이 에러 리다이렉션을 마스터하면 로그 관리가 프로페셔널해집니다. 에러를 놓치지 않고 기록하고, 정상 동작과 분리해서 분석하며, 불필요한 에러 메시지를 제거하여 깔끔한 출력을 만들 수 있습니다.
실전 팁
💡 2>&1의 순서가 중요합니다. command 2>&1 > file은 작동하지 않습니다. 반드시 command > file 2>&1 순서로 써야 합니다.
💡 /dev/null은 읽기도 가능한데, 읽으면 항상 EOF(파일 끝)를 반환합니다. 빈 입력이 필요할 때 유용합니다.
💡 &> 문법은 bash 4.0 이상에서만 작동하며, >file 2>&1과 완전히 동일합니다.
💡 에러를 표준 출력으로 보내려면 command 2>&1을 사용하세요. 이는 파이프와 함께 사용할 때 필수입니다.
💡 크론 작업에서는 command > /dev/null 2>&1로 모든 출력을 무시하면 이메일이 발송되지 않습니다.
6. 파이프라인 종료 상태
시작하며
여러분이 파이프로 연결된 여러 명령어를 실행했는데, 최종 결과는 성공으로 보이지만 사실 중간에 어떤 명령어가 실패했을 수도 있다는 걸 알고 계셨나요? 예를 들어 cat nonexistent.txt | grep "pattern" | wc -l을 실행하면 0이 출력되는데, 이게 패턴이 없어서인지 아니면 파일이 없어서인지 어떻게 구분하나요?
기본적으로 쉘은 파이프라인의 마지막 명령어의 종료 상태만 반환합니다. 중간 명령어가 실패해도 마지막이 성공하면 전체가 성공한 것처럼 보입니다.
이는 배포 스크립트나 CI/CD 파이프라인에서 심각한 문제를 일으킬 수 있습니다. 바로 이럴 때 필요한 것이 PIPESTATUS와 pipefail 옵션입니다.
파이프라인의 모든 명령어의 종료 상태를 추적하여 숨겨진 실패를 발견할 수 있습니다.
개요
간단히 말해서, 파이프라인의 각 명령어는 독립적인 종료 상태(exit status)를 가지지만, 기본적으로는 마지막 명령어의 상태만 반환됩니다. PIPESTATUS 배열과 set -o pipefail 옵션을 사용하면 모든 단계의 상태를 확인할 수 있습니다.
실무에서 이것이 중요한 이유는 데이터 파이프라인의 무결성 때문입니다. 데이터베이스 백업 파이프라인, 로그 처리 스크립트, 빌드 파이프라인 등에서 중간 단계의 실패를 놓치면 잘못된 데이터나 불완전한 결과를 얻게 됩니다.
기존에는 파이프라인이 성공한 것처럼 보여도 실제로는 일부가 실패했는지 알 수 없었다면, 이제는 각 단계를 정밀하게 모니터링하여 문제를 조기에 발견할 수 있습니다. 핵심 도구는 두 가지입니다.
PIPESTATUS 배열은 파이프라인의 각 명령어 종료 상태를 저장하고, set -o pipefail은 파이프라인 중 하나라도 실패하면 전체를 실패로 처리합니다. 이 둘을 조합하면 견고한 스크립트를 작성할 수 있습니다.
코드 예제
# PIPESTATUS로 각 명령어의 종료 상태 확인
cat file.txt | grep "pattern" | sort | uniq
echo "Exit codes: ${PIPESTATUS[@]}"
# pipefail 옵션으로 중간 실패 감지
set -o pipefail
curl https://api.example.com/data | jq '.results[]' | head -n 10
if [ $? -ne 0 ]; then
echo "Pipeline failed!"
fi
# 각 단계의 성공 여부 개별 확인
database_dump | gzip > backup.gz
if [ ${PIPESTATUS[0]} -ne 0 ]; then
echo "Database dump failed!"
elif [ ${PIPESTATUS[1]} -ne 0 ]; then
echo "Compression failed!"
fi
# 안전한 배포 스크립트 예제
set -euo pipefail # e: 에러 시 종료, u: 미정의 변수 에러, o pipefail: 파이프 실패 감지
npm run build | tee build.log
npm run test | tee test.log
npm run deploy
설명
파이프라인 종료 상태가 중요한 이유는 데이터 처리의 신뢰성 때문입니다. 파이프로 연결된 명령어들은 각각 독립적으로 실행되고, 각자 성공(0) 또는 실패(0이 아닌 값)를 반환합니다.
첫 번째 예제를 보면, 파이프라인 cat file.txt | grep "pattern" | sort | uniq를 실행한 직후 echo "Exit codes: ${PIPESTATUS[@]}"를 실행합니다. PIPESTATUS는 bash의 특수 배열 변수로, 마지막 파이프라인의 각 명령어 종료 상태를 순서대로 저장합니다.
예를 들어 0 1 0 0이 출력된다면, cat은 성공했지만(0) grep이 실패했고(1, 패턴을 못 찾음) sort와 uniq는 성공했다는 의미입니다. 주의할 점은 PIPESTATUS는 다음 명령어 실행 시 덮어써지므로 즉시 확인해야 한다는 것입니다.
두 번째 예제의 set -o pipefail은 파이프라인의 동작 방식을 변경합니다. 이 옵션을 활성화하면 파이프라인 중 하나라도 0이 아닌 값을 반환하면, 전체 파이프라인의 종료 상태가 실패가 됩니다.
기본 동작에서는 curl이 실패해도 head가 성공하면 $?가 0이 되지만, pipefail을 설정하면 curl의 실패가 $?에 반영됩니다. 이는 CI/CD 스크립트에서 필수적입니다.
세 번째 예제는 PIPESTATUS를 활용한 정밀한 에러 처리입니다. database_dump | gzip > backup.gz에서 데이터베이스 덤프와 압축 중 어느 것이 실패했는지 정확히 알 수 있습니다.
${PIPESTATUS[0]}은 첫 번째 명령어(database_dump)의 상태이고, ${PIPESTATUS[1]}은 두 번째 명령어(gzip)의 상태입니다. 이렇게 하면 "백업 압축 파일은 생성되었지만 실제로는 데이터베이스 덤프가 실패해서 빈 파일이다" 같은 위험한 상황을 방지할 수 있습니다.
네 번째 예제는 프로덕션 스크립트의 모범 사례입니다. set -euo pipefail은 세 가지 안전장치를 동시에 활성화합니다.
-e는 명령어가 실패하면 스크립트를 즉시 종료하고, -u는 정의되지 않은 변수를 사용하면 에러를 발생시키며, -o pipefail은 파이프라인의 중간 실패를 감지합니다. 이 세 옵션을 함께 사용하면 스크립트가 훨씬 안전해집니다.
빌드가 실패했는데 배포가 진행되는 재앙을 방지할 수 있습니다. 여러분이 파이프라인 종료 상태를 제대로 다루면 스크립트의 신뢰성이 크게 향상됩니다.
숨겨진 실패를 발견하고, 데이터 파이프라인의 무결성을 보장하며, 프로덕션 환경에서 안전하게 자동화를 운영할 수 있습니다.
실전 팁
💡 모든 프로덕션 스크립트는 set -euo pipefail로 시작하는 것을 강력히 권장합니다.
💡 PIPESTATUS는 다음 명령어 실행 시 사라지므로, 바로 변수에 복사하세요: statuses=(${PIPESTATUS[@]})
💡 -o pipefail은 bash 3.0 이상에서만 작동하므로, 이식성이 중요하면 PIPESTATUS를 수동으로 확인하세요.
💡 $?는 항상 마지막 명령어(또는 pipefail이 활성화되면 첫 번째 실패)의 상태를 가집니다.
💡 디버깅 시에는 set -x를 추가하여 실행되는 모든 명령어를 화면에 출력하면 문제 지점을 찾기 쉽습니다.