npm error signal SIGSEGV 에러가 발생했다.
위 코드를 보면 로그가 발생한 후에 3번 로그인 로그가 발생하지 않았다. 그래서 정말 bcrypt 라이브러리에서 hash를 하는 과정에서 문제였는지 주석처리를 하고 테스트를 해보았다. 이렇게 임의의 문자열을 넣고 회원가입 테스트를 해봤고, db에 정상적으로 잘 입력되었다. 사실 로컬환경에서는 도커를 사용하지 않고, 배포환경에서는 CI/CD 자동화 파이프라인이 구축되어있어서 환경이 많이 다르긴하다. 하지만 비밀번호 해싱해주는 라이브러리가 문제를 일으킬줄은 상상도 못했다. 찾아보니 bcrypt 라이브러리는 C++로 만들어진 C++ 네이티브 모듈이었다. 1. 첫번째 이유는 성능 이다. 비밀번호 암호화는 많은 연산을 필요로 한다. 무차별 대입 공격(Brute-force attack)을 어렵게 만들기 위해 의도적으로 연산량이 많도록 설계된 라이브러리이다. 그래서 C++와 같은 고급언어 중 저수준 언어로 만들어서 성능을 최적화한 것이다. 2. 두번째 이유는 보안 이다. 핵심 로직을 C++로 구현하면 일부 보안 취약점에 더 강할 수 있다. 물론 bcrypt의 핵심 부분이 C++로 되어 있다고 하여서 무조건 "C++가 JS보다 더 안전하다"는 일반론 적인 이야기는 아니다. 오히려 C++는 메모리에 직접 접근하기 때문에 메모리 버그(예: 버퍼 오버플로우)가 생기기 쉬워서 문제가 생길 수 있다. (설명이 길어질 수 있으므로 나중에 따로 정리하겠다.) 문제는 C++로 작성된 부분을 도커 컨테이너에 맞게 해서 사용한다는 점이다. - 로컬환경에서는 이미 C++ 컴파일에 필요한 같은 도구들이 이미 준비되어 있거나 Node.js가 잘 처리해줘서 할 때 bcrypt의 C++부분이 문제없이 잘 빌드가 되었기 때문에 SIGSEGV에러가 발생하지 않았을 가능성이 있다. - 도커 이미지 안에는 개발 또는 빌드에 필요한 모든 도구가 기본적으로 다 제공되진 않는다. 이나 같은 C++ 컴파일 관련 도구들이 없으면 할 때 컴파일 도구를 찾지 못해서 에러를 내거나, 빌드에 관련 도구들이 누락이 되버린다. 그래서 나중에 컴파일 도구가 필요한 라이브러리를 사용하게 되면 지금과 같은 상황이 되버린다. --- 이렇게 가 떴던 원인은 정확히 "bcrypt 라이브러리의 C++ 네이티브 모듈이 도커 컨테이너 환경에서 제대로 빌드되지 않았기 때문" 이었다. 처음에 에 이랑 를 추가해서 컨테이너 안에 C++ 컴파일 도구를 넣어주는 시도도 해봤다. 하지만 이 악독한 는 결국 나를 떠나지 않았다.. 결국, 고민 끝에 내린 특단의 조치는 바로 * 대신 를 사용하는 것이었다. 는 와 동일한 암호화 알고리즘을 사용하지만, *순수 JavaScript로만 구현된 라이브러리* 다. C++ 네이티브 모듈 부분이 전혀 없기 때문에, 컴파일 문제나 도커 환경에서의 호환성 문제로부터 완전히 자유로울 수 있다는 것이 핵심이다. 는 C++이라 빠르고, 는 JS라 느려서 많이 답답할 것이라 생각할 것이다. 하지만 Node.js 환경에서 가 잘 작동하는 데는 중요한 이유가 있다. JavaScript는 기본적으로 싱글 스레드 기반이라 CPU 집약적인 동기 작업은 메인 스레드를 블로킹해서 서버를 먹통으로 만들 수 있다. 하지만 의 함수는 *비동기적으로 동작* 하도록 설계되어 있다. Node.js는 라는 라이브러리를 통해 *Worker Pool (스레드 풀)* 기능을 제공하는데, 의 CPU를 많이 쓰는 해싱 연산은 메인 JavaScript 스레드가 아닌, 이 *백그라운드 스레드에서 처리* 된다. 즉, 해싱 작업이 진행되는 동안에도 메인 스레드는 다른 요청들을 계속 처리할 수 있다는 것이다. 결국, 는 C++ 보다 단일 해싱 작업 속도는 미세하게 느릴 수 있지만, Node.js의 *비동기 처리 덕분에 메인 스레드를 블로킹하지 않아서 전체 서버의 처리량(Throughput)에는 큰 영향을 주지 않는다.* 오히려 네이티브 모듈 컴파일 문제 없이 *어느 환경에서나 쉽고 안정적으로 배포될 수 있다는 압도적인 장점* 이 있는 것이다. 내가 겪었던 문제가 바로 가 왜 필요한지를 몸소 보여준 예시라고 할 수 있겠다. 1. *기존 제거 및 설치* 1. *코드 수정 ( 및 비밀번호 검증 로직)* 를 import 했던 부분을 로 바꾸고, 비동기 함수를 사용하도록 코드를 변경했다. 2. * 원복 (이제 필요 없음)* 이나 설치 라인은 에게는 필요 없으니 에서 깨끗하게 지웠다. 3. *다시 도커 커밋후 배포* 결과는? *성공이었다. 드디어 는 더 이상 뜨지 않았고, 회원가입과 로그인이 정상적으로 작동했다.* --- 이번 삽질 대잔치를 통해 여러 가지를 느꼈다. - *네이티브 모듈이 포함된 라이브러리를 사용할 때는 도커 컨테이너의 OS 환경과 빌드 도구 유무를 꼼꼼히 확인해야 한다. - 와 같은 메모리 관련 에러는 대부분 저수준(C/C++, 하드웨어) 문제와 연결되어 있음을 인지하고 접근해야 한다. 처음에는 Prisma 문제인 줄 알고 삽질 오지게 했다... - 문제가 발생했을 때 **로그를 꼼꼼히 작성하고 분석하는 습관이 얼마나 중요한지 다시 한번 실감했다. 로그가 찍히기 전에 에러가 났다는 걸 아는 것이 문제 해결의 시작점이었다. - 순수 JavaScript 대안 라이브러리(예: )의 중요성. 네이티브 모듈의 성능 이점을 잃는 대신, 배포의 복잡성과 에러 가능성을 크게 줄일 수 있다는 트레이드오프를 명확히 이해하게 되었다. 하지만, bcryptjs로 교체한 것은, 임시방편일 뿐.. 기말고사만 끝나면, 다시 한번 bcrypt를 시도해보려고 한다.
2025년 11월 21일
배포후 에러없이 강제종료되는 문제
개발 과정에서 애플리케이션에 명백한 오류가 발생함에도 불구하고 (), 서버 로그에 어떠한 에러 메시지도 출력되지 않는 현상이 관찰되었다. 프론트엔드에서는 500 Internal Server Error를 수신하며 서비스 이상이 감지되나, 백엔드 서버 콘솔이나 로그 파일에는 정상적인 INFO 또는 DEBUG 레벨의 로그만 기록될 뿐, 예외 상황에 대한 어떠한 정보도 확인할 수 없었다. 이로 인해 문제 발생 지점을 특정하기 어렵고, 디버깅에 막대한 시간이 소요되며, 시스템의 안정성을 위협하는 '조용한 실패' 상태에 직면하게 되었다. 은 Node.js 환경에서 널리 사용되는 유연한 로깅 라이브러리이나, 특정 설정이나 사용 방식에 따라 에러를 "삼키는" 듯한 동작을 보일 수 있다. 주요 원인은 다음과 같다. 1. * 옵션의 오해:* - 로거 인스턴스를 생성할 때 옵션을 설정할 수 있다. 이 옵션은 Node.js의 발생 시 애플리케이션 프로세스의 강제 종료를 방지하고 로깅만 수행하도록 하는 기능을 한다. - 그러나 이 옵션을 로 설정했을 때, 만약 로거 자체의 트랜스포트(콘솔, 파일 등) 구성에 문제가 있거나 로깅 과정에서 또 다른 에러가 발생하여 실제 로깅에 실패한다면, 프로세스는 종료되지 않으면서 에러 정보도 기록되지 않는 상황이 발생할 수 있다. 이는 치명적인 에러의 발생 사실 자체를 인지하지 못하게 만든다. 2. * 및 핸들러의 부재 또는 잘못된 설정:* - Node.js 환경에서 비동기 코드( 등)의 에러() 및 전역에서 처리되지 않은 동기 예외()는 애플리케이션의 안정성에 직접적인 영향을 미친다. - 은 이러한 전역 예외를 처리하기 위한 기능을 제공하나, 로거 설정 시 및 옵션을 명시적으로 트랜스포트에 적용하지 않거나, Node.js의 객체에 대한 별도의 이벤트 핸들러(예: )를 구성하지 않으면, 해당 에러들이 로깅 시스템에 도달하지 못하고 버려질 수 있다. - 특히 의 경우, Node.js v15 이전 버전에서는 기본적으로 프로세스를 종료하지 않았으며, v15 이후에는 과 유사하게 프로세스를 종료시키는 방향으로 변경되었다. 그러나 로거에서 이를 명시적으로 처리하지 않으면 단순히 프로세스만 종료될 뿐 에러 로그를 남기지 못하는 문제가 발생할 수 있다. 3. 로깅 레벨 설정 불일치: - 각 트랜스포트(예: , )는 독립적인 로깅 레벨()을 가질 수 있다. 만약 특정 트랜스포트의 이 로 설정되어 있다면, 나 레벨의 로그는 해당 트랜스포트를 통해 출력되지 않는다. 따라서 레벨의 로그가 필요한 경우, 트랜스포트의 을 또는 그보다 낮은 수준(예: )으로 설정해야 한다. 우리의 로거가 에러를 다시 제대로 기록하도록 만들기 위해 코드를 어떻게 수정했는지, 이전 코드와 개선된 코드를 비교하며 살펴보겠다. --- [이전 코드] 내가 처음 사용했던 로거 설정은 다음과 같았다. 이전 코드의 문제점: 1. * 누락:* 과 트랜스포트 모두 옵션이 명시적으로 설정되어 있지 않았다. 이는 비동기 코드에서 발생하는 거부(Unhandled Rejection) 에러가 로거에 의해 포착되지 않고 조용히 사라질 수 있는 결정적인 원인이었다. 2. * 부재:* 에러 스택 트레이스를 로그에 포함시키기 위한 포맷이 체인에 없었다. 이로 인해 에러 객체가 제대로 파싱되지 않고 함수로 전달될 때 스택 정보가 유실될 가능성이 높았다. 3. * 포맷의 한계:* 함수 내부에서 만 직접 사용하고 있어, 포맷이 적용되지 않은 상태에서는 에러 객체로부터 스택 트레이스 등 상세 정보를 추출하기 어려웠다. --- [개선된 코드] 위에서 언급된 문제점들을 해결하여, 로거가 모든 에러를 안정적으로 포착하고 상세한 정보를 함께 기록하도록 코드를 개선하였다. 개선된 코드의 주요 변경 사항 및 그 효과: 1. * 추가:* - 트랜스포트와 (dailyOptions) 모두에 옵션을 명시적으로 추가하여, 비동기 에서 발생하는 처리되지 않은 거부(Unhandled Rejection) 에러까지 로깅 시스템이 포착하고 기록하도록 변경했다. 이전에는 이 에러들이 로거를 통과하지 못하고 '삼켜질' 수 있었다. 2. * 적용:* - 체인에 포맷을 추가했다. 이 포맷은 에러 객체가 로그 레코드에 전달될 때, 에러의 스택 트레이스를 속성에 포함시켜 준다. 이로써 함수에서 상세한 스택 트레이스 정보를 쉽게 접근하고 로그에 출력할 수 있게 되었다. - 트랜스포트의 개발() 및 프로덕션() 포맷에도 각각 적용하여, 콘솔 출력에서도 상세한 에러 정보가 나타나도록 했다. 3. * 포맷 개선:* - 함수 내부에서 속성을 확인하여, 스택 트레이스가 존재할 경우 이를 메시지에 추가하도록 수정했다. 이는 에러 발생 시 개발자가 문제의 원인을 파악하는 데 결정적인 단서가 된다. 이전에는 단순히 만 출력하여 스택 트레이스 정보를 놓칠 수 있었다. 4. * 조정:* - 파일 로그의 을 로 변경하여, 파일에는 치명적인 에러 로그만 집중적으로 기록되도록 조정했다. (필요에 따라 등으로 변경 가능) 5. * 설정에 대한 상세 설명 추가:* - 설정이 가지는 양면성에 대해 주석으로 상세 설명을 추가하여, 이 옵션의 사용에 있어 개발자가 충분히 인지하고 신중하게 결정할 것을 강조했다. 로깅 시스템은 애플리케이션의 동작 상태를 파악하고 문제 발생 시 신속하게 원인을 진단하는 개발자의 핵심 도구이다. 로거가 에러를 '삼키는' 현상은 잘못된 설정이나 의 예외 처리 메커니즘에 대한 오해에서 비롯되는 경우가 많다. 안정적인 로깅 시스템을 구축하기 위해서는 의 각 트랜스포트에서 예외 처리(, )를 명시적으로 활성화하고, 전역 예외(uncaughtException, unhandledRejection)를 포착하여 상세한 스택 트레이스까지 기록하는 포맷을 적용해야 한다. 또한, 옵션의 의미를 정확히 이해하고 애플리케이션의 안정성 요구 사항에 맞춰 신중하게 설정한다. 로깅 시스템에 대한 면밀한 구성은 '조용한 실패'를 방지하고, 서비스의 신뢰성을 확보하며, 개발 과정의 효율성을 극대화하는 데 필수적인 요소이다. 아래는 숨겨진 에러가 모습을 드러낸 장면이다.
2025년 11월 19일