npm error signal SIGSEGV 에러가 발생했다.

찰규

·

2025년 11월 21일 (6일 전)

npm error signal SIGSEGV 에러가 발생했다.
import { hash } from 'bcrypt';

@Transactional()
  async signup(request: UserRequest): Promise<UserResponse> {
    const { token, provider, image, password, email } = request;
    this.LOGGER.log(
      '--------------------유저 생성 서비스 실행--------------------',
    );
    const existingUser: User = await this.userRepository.findByEmail(email);
    this.LOGGER.log(
      `1. 기존 유저 조회: ${existingUser ? '존재함' : '존재하지 않음'}`,
    );
    if (existingUser) {
      throw new ApiException(ErrorCode.ALREADY_EXIST_EMAIL);
    }
    const id = uuid();
    this.LOGGER.log(`2. 새로운 유저 ID 생성`);
    const hashedPassword = provider ? '' : hashSync(password, 10);
    this.LOGGER.log(`3. 비밀번호 해싱 완료`);
    ...
    }

위 코드를 보면 새로운 유저 ID 생성 로그가 발생한 후에 3번 로그인 비밀번호 해싱 완료로그가 발생하지 않았다. 그래서 정말 bcrypt 라이브러리에서 hash를 하는 과정에서 문제였는지 주석처리를 하고 테스트를 해보았다.

// const hashedPassword = provider ? '' : hashSync(password, 10);
const hashedPassword = 'TEST_PASSWORD_HASHED';

이렇게 임의의 문자열을 넣고 회원가입 테스트를 해봤고, db에 정상적으로 잘 입력되었다.

사실 로컬환경에서는 도커를 사용하지 않고, 배포환경에서는 CI/CD 자동화 파이프라인이 구축되어있어서 환경이 많이 다르긴하다. 하지만 비밀번호 해싱해주는 라이브러리가 문제를 일으킬줄은 상상도 못했다.

찾아보니 bcrypt 라이브러리는 C++로 만들어진 C++ 네이티브 모듈이었다.

왜 C++ 만들었을까?

  1. 첫번째 이유는 성능 이다. 비밀번호 암호화는 많은 연산을 필요로 한다. 무차별 대입 공격(Brute-force attack)을 어렵게 만들기 위해 의도적으로 연산량이 많도록 설계된 라이브러리이다. 그래서 C++와 같은 고급언어 중 저수준 언어로 만들어서 성능을 최적화한 것이다.
  2. 두번째 이유는 보안 이다. 핵심 로직을 C++로 구현하면 일부 보안 취약점에 더 강할 수 있다.

물론 bcrypt의 핵심 부분이 C++로 되어 있다고 하여서 무조건 "C++가 JS보다 더 안전하다"는 일반론 적인 이야기는 아니다. 오히려 C++는 메모리에 직접 접근하기 때문에 메모리 버그(예: 버퍼 오버플로우)가 생기기 쉬워서 문제가 생길 수 있다. (설명이 길어질 수 있으므로 나중에 따로 정리하겠다.)

그래서 그게 왜 문제가 되었을까?

문제는 C++로 작성된 부분을 도커 컨테이너에 맞게 컴파일해서 사용한다는 점이다.

  • 로컬환경에서는 이미 C++ 컴파일에 필요한 build-essential같은 도구들이 이미 준비되어 있거나 Node.js가 잘 처리해줘서 npm install할 때 bcrypt의 C++부분이 문제없이 잘 빌드가 되었기 때문에 SIGSEGV에러가 발생하지 않았을 가능성이 있다.
  • 도커 이미지 안에는 개발 또는 빌드에 필요한 모든 도구가 기본적으로 다 제공되진 않는다. build-essential이나 python3같은 C++ 컴파일 관련 도구들이 없으면 npm install할 때 컴파일 도구를 찾지 못해서 에러를 내거나, 빌드에 관련 도구들이 누락이 되버린다. 그래서 나중에 컴파일 도구가 필요한 라이브러리를 사용하게 되면 지금과 같은 상황이 되버린다.

결국 SIGSEGV을 어떻게 해결했나?

이렇게 SIGSEGV가 떴던 원인은 정확히 "bcrypt 라이브러리의 C++ 네이티브 모듈이 도커 컨테이너 환경에서 제대로 빌드되지 않았기 때문" 이었다. 처음에 Dockerfile에 build-essential이랑 python3를 추가해서 컨테이너 안에 C++ 컴파일 도구를 넣어주는 시도도 해봤다. 하지만 이 악독한 SIGSEGV는 결국 나를 떠나지 않았다..

결국, 고민 끝에 내린 특단의 조치는 바로 bcrypt 대신 bcryptjs를 사용하는 것이었다.

bcryptjs는 뭔데?

bcryptjs는 bcrypt와 동일한 암호화 알고리즘을 사용하지만, 순수 JavaScript로만 구현된 라이브러리 다. C++ 네이티브 모듈 부분이 전혀 없기 때문에, 컴파일 문제나 도커 환경에서의 호환성 문제로부터 완전히 자유로울 수 있다는 것이 핵심이다.

bcryptjs가 순수 JS인데 느리지 않을까?

bcrypt는 C++이라 빠르고, bcryptjs는 JS라 느려서 많이 답답할 것이라 생각할 것이다.

하지만 Node.js 환경에서 bcryptjs가 잘 작동하는 데는 중요한 이유가 있다. JavaScript는 기본적으로 싱글 스레드 기반이라 CPU 집약적인 동기 작업은 메인 스레드를 블로킹해서 서버를 먹통으로 만들 수 있다. 하지만 bcryptjs의 hash() 함수는 비동기적으로 동작 하도록 설계되어 있다.

Node.js는 libuv라는 라이브러리를 통해 Worker Pool (스레드 풀) 기능을 제공하는데, bcryptjs의 CPU를 많이 쓰는 해싱 연산은 메인 JavaScript 스레드가 아닌, 이 백그라운드 스레드에서 처리 된다. 즉, 해싱 작업이 진행되는 동안에도 메인 스레드는 다른 요청들을 계속 처리할 수 있다는 것이다.

결국, bcryptjs는 C++ bcrypt보다 단일 해싱 작업 속도는 미세하게 느릴 수 있지만, Node.js의 비동기 처리 덕분에 메인 스레드를 블로킹하지 않아서 전체 서버의 처리량(Throughput)에는 큰 영향을 주지 않는다. 오히려 네이티브 모듈 컴파일 문제 없이 어느 환경에서나 쉽고 안정적으로 배포될 수 있다는 압도적인 장점 이 있는 것이다. 내가 겪었던 SIGSEGV 문제가 바로 bcryptjs가 왜 필요한지를 몸소 보여준 예시라고 할 수 있겠다.

적용 방법

  1. 기존 bcrypt 제거 및 bcryptjs 설치
    npm uninstall bcrypt
    npm install bcryptjs
  1. 코드 수정 (UserService.ts 및 비밀번호 검증 로직) bcrypt를 import 했던 부분을 bcryptjs로 바꾸고, 비동기 hash 함수를 사용하도록 코드를 변경했다.
// UserService.ts
// import { hashSync } from 'bcrypt'; // 기존 import는 제거하거나 주석 처리
import * as bcrypt from 'bcryptjs'; // ⬅️ 이걸로 변경!

async signup(request: UserRequest): Promise<UserResponse> {
  // ...
  const hashedPassword = request.provider
    ? ''
    : await bcrypt.hash(request.password, 10); // ⬅️ await와 함께 비동기 hash 함수 사용
  this.LOGGER.log(`3. 비밀번호 해싱 완료`);
  // ...
  return { /* UserResponse 객체 반환 */ };
}

// 로그인 시 비밀번호 검증 부분도 마찬가지로 bcryptjs의 비동기 compare 함수 사용
// AuthService.ts 예시 (user && (await bcrypt.compare(dto.password, user.password)))
async validateUser(dto: AuthRequest): Promise<UserResponse> {
  const user: User = await this.userService.findByEmail(dto.email);
  if (!user) {
    throw new ApiException(ErrorCode.USER_NOT_FOUND); 
  }
  if (user && (await bcrypt.compare(dto.password, user.password))) { // await와 함께 compare 함수 사용
    const { password, ...rest } = user;
    return rest;
  } else {
    throw new ApiException(ErrorCode.INCORRECT_EMAIL_OR_PASSWORD);
  }
}
  1. Dockerfile 원복 (이제 build-essential 필요 없음) build-essential이나 python3 설치 라인은 bcryptjs에게는 필요 없으니 Dockerfile에서 깨끗하게 지웠다.

  2. 다시 도커 커밋후 배포

결과는? 성공이었다. 드디어 SIGSEGV는 더 이상 뜨지 않았고, 회원가입과 로그인이 정상적으로 작동했다.

moneybuddy-server-1  | info: --------------------이메일 토큰 검증 서비스 종료-------------------- {"context":"AuthService","timestamp":"2025-11-21 23:51:05"}
moneybuddy-server-1  | info: 이메일 토큰 검증 완료 {"context":"AuthController","timestamp":"2025-11-21 23:51:05"}
moneybuddy-server-1  | info: --------------------이메일 토큰 검증 컨트롤러 종료-------------------- {"context":"AuthController","timestamp":"2025-11-21 23:51:05"}
moneybuddy-server-1  | info: {"url":"/api/auth/signup","method":"POST","body":{"name":"박아무개","email":"박아무개@naver.com","token":"865f6508-5bfb-4745-ae97-7dde6513b7fb","password":"12341234"}} {"timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: --------------------회원가입 생성 컨트롤러 실행-------------------- {"context":"AuthController","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 계정 생성 요청 받음 {"context":"AuthController","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: --------------------유저 생성 서비스 실행-------------------- {"context":"UserService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 1. 기존 유저 조회: 존재하지 않음 {"context":"UserService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 2. 새로운 유저 ID 생성 {"context":"UserService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 3. 비밀번호 해싱 완료 {"context":"UserService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 4. 이미지 생성 예정 (일반계정이면 건너뜀 7번으로 넘어감) {"context":"UserService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 7. 유저 엔티티 생성 완료 {"context":"UserService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 8. 유저 저장 완료 {"context":"UserService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 9. 이메일 토큰 삭제  회원가입 이벤트 실행 {"context":"UserService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 10. 이메일 토큰 삭제 완료 {"context":"UserService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: --------------------계좌 생성 서비스 실행-------------------- {"context":"AccountService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 1. 유저 존재 여부 확인 완료 {"context":"AccountService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 2. 계좌 생성 완료 {"context":"AccountService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: --------------------계좌 생성 서비스 종료-------------------- {"context":"AccountService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 11. 회원가입 이벤트 실행 완료 {"context":"UserService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: --------------------유저 생성 서비스 종료-------------------- {"context":"UserService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 계정 생성 완료 {"context":"AuthController","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: --------------------회원가입 생성 컨트롤러 종료-------------------- {"context":"AuthController","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: {"url":"/api/auth/login","method":"POST","body":{"email":"박아무개@naver.com","password":"12341234"}} {"timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: --------------------로그인 컨트롤러 실행-------------------- {"context":"AuthController","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 로그인 요청 받음 {"context":"AuthController","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: --------------------로그인 서비스 실행------------------- {"context":"AuthService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 1. 유저 검증 진행 {"context":"AuthService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 2. 이메일로 유저 조회 {"context":"AuthService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 3. 유저가 존재하는가? 존재함 {"context":"AuthService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 4. 비밀번호 일치함 {"context":"AuthService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 2. 유저 검증 완료 {"context":"AuthService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 3. JWT 토큰 생성 완료 {"context":"AuthService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: --------------------로그인 서비스 종료------------------- {"context":"AuthService","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: 로그인 완료 박아무개님 환영합니다! {"context":"AuthController","timestamp":"2025-11-21 23:51:15"}
moneybuddy-server-1  | info: --------------------로그인 컨트롤러 종료-------------------- {"context":"AuthController","timestamp":"2025-11-21 23:51:15"}

배운 점

이번 삽질 대잔치를 통해 여러 가지를 느꼈다.

  • 네이티브 모듈이 포함된 라이브러리를 사용할 때는 도커 컨테이너의 OS 환경과 빌드 도구 유무를 꼼꼼히 확인해야 한다.
  • SIGSEGV와 같은 메모리 관련 에러는 대부분 저수준(C/C++, 하드웨어) 문제와 연결되어 있음을 인지하고 접근해야 한다. 처음에는 Prisma 문제인 줄 알고 삽질 오지게 했다...
  • 문제가 발생했을 때 로그를 꼼꼼히 작성하고 분석하는 습관이 얼마나 중요한지 다시 한번 실감했다. 3. 비밀번호 해싱 완료 로그가 찍히기 전에 에러가 났다는 걸 아는 것이 문제 해결의 시작점이었다.
  • 순수 JavaScript 대안 라이브러리(예: bcryptjs)의 중요성. 네이티브 모듈의 성능 이점을 잃는 대신, 배포의 복잡성과 에러 가능성을 크게 줄일 수 있다는 트레이드오프를 명확히 이해하게 되었다.

하지만, bcryptjs로 교체한 것은, 임시방편일 뿐.. 기말고사만 끝나면, 다시 한번 bcrypt를 시도해보려고 한다.