마인드셋 - 취준 / 이직의 출발점

오늘은 요즘 내가 듣고 있는 취업 강의 1강을 정리해보려고 한다. 솔직히 코딩 테스트 준비하고 기술 스택 쌓는 것도 중요하지만, 분야를 막론하고, 먼저 취준의 출발점이 가장 중요하다고 생각한다. 첫번째 내용은 MindSet이다. 우리는 보통 Mind + Set의 합성어인 '마음가짐'이 그냥 가볍게 들리지만, 이게 사실 우리가 가진 생각의 모음이자 곧 우리들의 사고방식을 뜻한다. 아침에 일어나서 잠들 때까지, 월요일부터 일요일까지의 모든 루틴이 취업을 바라보는 내 사고방식을 결정한다. 즉, 내가 어떠한 학습을 어떠한 몰입력으로 집중하고, 그 몰입력으로 얼마나 끈기있게 경험을 쌓아가냐에 따라 취업의 결과가 결정된다. 이게 취업뿐만 아니라, 내 인생의 방향과 속도, 그리고 나만의 세계를 만드는 핵심이고 원동력이 된다. 커리어 여정을 시작할 때 꼭 알아야 할 몇가지 아직 1강밖에 안 들었지만, 이건 꼭 기억해야 할 것 같아서 바로 정리해봤다. 1. 나 자신부터 제대로 파악하자. 내가 진짜 잘하는 거, 관심 있는 거: 결국 커리어 여정에서의 주인공은 "나" 자신이다. 그 어떤 타인의 생각, 타인의 삶이 주인공이 될 수 없고, 본인 스스로가 꾸려가는 것이 나의 "커리어"이다. 내가 관심있는 것이 무엇인지, 내가 조금의 시간만 들여도, 남들보다 더 뛰어난 성과를 낸 것이 무엇인지를 파악한다. 내가 못하는 거, 못 견디겠는 거, 관심 없는 거: 내가 잘 못하겠는 것이나, 내가 남들보다 더 노력해도 잘 안되는 것들, 그리고 내가 못 견딜만큼 싫어하는 것은 소거하는게 좋다. 결론: 좋은 선택은, 내가 잘하고 좋아하는 것들을 고려해서 최적의 선택을 내리는 것을 좋은 선택이라고 할 수 있지만, 내가 잘 못하고, 관심없는 것을 소거하는 것도 좋은 선택이 될때도 많다. 2. 우리는 아직 젊다. 커리어는 단기간이 아닌 25~30년의 마라톤과 같은 긴 여정이다. 대학을 졸업하고, 경력이 애매하다거나, 물경력이었거나, 공백기가 길어도, 삽질과 실패의 경험이 많더라도, 충분히 다시 시작할 수있다. 지금까지 실패했던 경험이든, 애매한 경험이든, 다시 돌이켜보면서 내가 무엇을 잘했고, 무엇을 못했는지, 무엇에 관심이 많았고, 무엇이 못견디겠는지를 다시한번 생각해보고 커리어의 시작을 가다듬기에도 이 커리어는 굉장히 긴 시간이다. IT시장은 아직도 갈 길이 먼 시장인 만큼, 무한한 기회가 있다. 3. 고통과 몰입의 시간. 취업준비를 하는 공백기가 가장 고통의 시기이다. 기본적으로 취준생의 우울지수가 가장 높다. 아르바이트를 하지않는 이상, 고정된 수입이 없고, 주변에서의 비교대상이 될 수 있다. 조금 힘들겠지만, "해내고야 만다"는 집념으로, 고시생의 마인드로 단기간에(계절이 바뀌기 전까지) 몰입해서 결과를 낼 수 있도록 해야한다. 4. 우리는 성인이다. 우리는 회사에서 직무수행을 할 수 있어야하고, 또 그것을 증명할 수 있어야한다. 우리는 단순 아르바이트의 목적이 아닌 특정 직무에 대한 전문성을 인정을 받아서, 몇 천만원의 연봉을 받기 위함이다. 즉, 수많은 사람들 중에서 내게 숟가락을 줘야할 이유를 증명해낼 수 있어야한다. 5. 천천히 그리고 꾸준히 호흡. 스트레스와 번아웃 관리가 중요하다. 운동선수들이 중간에 수분보충 많이한다. 당장 갈증을 느껴서가 아니라 예방적 차원에서, 미리미리 수분보충을 많이한다. 신호를 느끼지 않고, "신체적 능력이 잘 발휘되고 있구나"라고 생각하게끔 만든다고 한다. 내가 이미 스트레스를 많이 받고, 번아웃이 오게되면, 다시 원래 상태로 끌어올리는게 정말 힘들다. 그러므로 미리미리 휴식을 갖자. 월요일부터 금요일까지 열심히 해왔다면, 주말에는 게임을 하거나, 여행을 가는 등, 예방적 차원에서 스트레스 관리를 하자. 6. 불합격에 대한 인식 (盡人事待天命) 취업의 합격과 불합격은, 수 많은 이유들과 수 많은 변수들로 결정이 된다. 내가 똑같은 경쟁력을 가졌더라도, 그 당시의 면접관의 성향, 그 당시의 경쟁상황에 따라 달라질 수 있다. 내가 할 수 있는 최선을 다하되, 주변 상황에 대해서 기다릴 수 밖에 없는게 내가 할 수 있는 유일한 태도이다. 불합격이 결정된다고 하더라도 "나 자신"을 탓할 것이 아니라, 이를 받아들이고, 문제점을 정확하게 파악하여, 개선할 수 있도록 노력해야한다. 7. '관심'과 '습관'으로 나를 증명해라. 출처: udemy [실전취업] 구글&아마존 출신이 알려주는 취업/이직 실전강의 커리어는 누적과 궤적이다. 즉, 누적된 결과물이 삶의 궤적이 된다. 하지만 누적된 결과들이 단발성으로 나타나거나, 일회성으로 나타나게 되면, 면접에서는 삶의 궤적이 아닌, 순간의 반짝이는 관심정도로 밖에 여겨지지 않는다. 특정 직무에 관심이 있다는 것을 보여주고자 한다면, 이 누적된 경험들을 많이 쌓아서, 내 이력서와, 자기소개서 그리고 포트폴리오 전반적인 내용에서 이 삶의 궤적이라는 것은 다양한 누적들로 증명이 될 수 있다. 신입의 경우, 전공, 그리고 전공과 관련 프로젝트, 개인 프로젝트, 국비지원 부트캠프에서 참여한 프로젝트들, 자격증들, 다양한 컴퍼런스 참여한 것들, 온라인 세미나 등등 마치면서 모든 첫 커리어 여정은 떨리고, 암흑기를 거칠 수 밖에 없다. 두렵고, "내가 해낼 수 있을까"라는 걱정으로 조마조마할 것이고, 스스로에게 확신이 없을 것이다. 위의 마인드셋을 가다듬고 최선을 다하면 원하는 결과를 얻을 수 있을 것이라고 생각한다.

2025년 11월 27일

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

import { hash } from 'bcrypt'; @Transactional()   async signup(request: UserRequest): Promise {     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++ 만들었을까? 첫번째 이유는 성능 이다. 비밀번호 암호화는 많은 연산을 필요로 한다. 무차별 대입 공격(Brute-force attack)을 어렵게 만들기 위해 의도적으로 연산량이 많도록 설계된 라이브러리이다. 그래서 C++와 같은 고급언어 중 저수준 언어로 만들어서 성능을 최적화한 것이다. 두번째 이유는 보안 이다. 핵심 로직을 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가 왜 필요한지를 몸소 보여준 예시라고 할 수 있겠다. 적용 방법 기존 bcrypt 제거 및 bcryptjs 설치 npm uninstall bcrypt npm install bcryptjs 코드 수정 (UserService.ts 및 비밀번호 검증 로직) bcrypt를 import 했던 부분을 bcryptjs로 바꾸고, 비동기 hash 함수를 사용하도록 코드를 변경했다. // UserService.ts // import { hashSync } from 'bcrypt'; // 기존 import는 제거하거나 주석 처리 import * as bcrypt from 'bcryptjs'; // ⬅️ 이걸로 변경! async signup(request: UserRequest): Promise { // ... 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 { 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); } } Dockerfile 원복 (이제 build-essential 필요 없음) build-essential이나 python3 설치 라인은 bcryptjs에게는 필요 없으니 Dockerfile에서 깨끗하게 지웠다. 다시 도커 커밋후 배포 결과는? 성공이었다. 드디어 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를 시도해보려고 한다.

2025년 11월 21일

배포후 에러없이 강제종료되는 문제

moneybuddy-server-1 | info: Mapped {/api/transaction/expense/create, POST} route {"context":"RouterExplorer","timestamp":"2025-11-19 17:21:44"} moneybuddy-server-1 | info: Mapped {/api/transaction/income/create, POST} route {"context":"RouterExplorer","timestamp":"2025-11-19 17:21:44"} moneybuddy-server-1 | info: Mapped {/api/transaction/monthly, GET} route {"context":"RouterExplorer","timestamp":"2025-11-19 17:21:44"} moneybuddy-server-1 | info: Mapped {/api/transaction/:id, GET} route {"context":"RouterExplorer","timestamp":"2025-11-19 17:21:44"} moneybuddy-server-1 | info: Mapped {/api/transaction/income/update/:id, PUT} route {"context":"RouterExplorer","timestamp":"2025-11-19 17:21:44"} moneybuddy-server-1 | info: Mapped {/api/transaction/expense/update/:id, PUT} route {"context":"RouterExplorer","timestamp":"2025-11-19 17:21:44"} moneybuddy-server-1 | info: Mapped {/api/transaction/delete, DELETE} route {"context":"RouterExplorer","timestamp":"2025-11-19 17:21:44"} moneybuddy-server-1 | info: BudgetController {/api/budget}: {"context":"RoutesResolver","timestamp":"2025-11-19 17:21:44"} moneybuddy-server-1 | info: Mapped {/api/budget/create, POST} route {"context":"RouterExplorer","timestamp":"2025-11-19 17:21:44"} moneybuddy-server-1 | info: Mapped {/api/budget, GET} route {"context":"RouterExplorer","timestamp":"2025-11-19 17:21:44"} moneybuddy-server-1 exited with code 1 상황설명 개발 과정에서 애플리케이션에 명백한 오류가 발생함에도 불구하고 (exited with code 1), 서버 로그에 어떠한 에러 메시지도 출력되지 않는 현상이 관찰되었다. 프론트엔드에서는 500 Internal Server Error를 수신하며 서비스 이상이 감지되나, 백엔드 서버 콘솔이나 로그 파일에는 정상적인 INFO 또는 DEBUG 레벨의 로그만 기록될 뿐, 예외 상황에 대한 어떠한 정보도 확인할 수 없었다. 이로 인해 문제 발생 지점을 특정하기 어렵고, 디버깅에 막대한 시간이 소요되며, 시스템의 안정성을 위협하는 '조용한 실패' 상태에 직면하게 되었다. 근거: Winston 로거가 에러를 삼키는 원인 분석 Winston은 Node.js 환경에서 널리 사용되는 유연한 로깅 라이브러리이나, 특정 설정이나 사용 방식에 따라 에러를 "삼키는" 듯한 동작을 보일 수 있다. 주요 원인은 다음과 같다. exitOnError: false 옵션의 오해: Winston 로거 인스턴스를 생성할 때 exitOnError: false 옵션을 설정할 수 있다. 이 옵션은 Node.js의 uncaughtException 발생 시 애플리케이션 프로세스의 강제 종료를 방지하고 로깅만 수행하도록 하는 기능을 한다. 그러나 이 옵션을 false로 설정했을 때, 만약 Winston 로거 자체의 트랜스포트(콘솔, 파일 등) 구성에 문제가 있거나 로깅 과정에서 또 다른 에러가 발생하여 실제 로깅에 실패한다면, 프로세스는 종료되지 않으면서 에러 정보도 기록되지 않는 상황이 발생할 수 있다. 이는 치명적인 에러의 발생 사실 자체를 인지하지 못하게 만든다. uncaughtException 및 unhandledRejection 핸들러의 부재 또는 잘못된 설정: Node.js 환경에서 비동기 코드(Promise 등)의 에러(unhandledRejection) 및 전역에서 처리되지 않은 동기 예외(uncaughtException)는 애플리케이션의 안정성에 직접적인 영향을 미친다. Winston은 이러한 전역 예외를 처리하기 위한 기능을 제공하나, 로거 설정 시 handleExceptions: true 및 handleRejections: true 옵션을 명시적으로 트랜스포트에 적용하지 않거나, Node.js의 process 객체에 대한 별도의 이벤트 핸들러(예: process.on('uncaughtException'))를 구성하지 않으면, 해당 에러들이 로깅 시스템에 도달하지 못하고 버려질 수 있다. 특히 unhandledRejection의 경우, Node.js v15 이전 버전에서는 기본적으로 프로세스를 종료하지 않았으며, v15 이후에는 uncaughtException과 유사하게 프로세스를 종료시키는 방향으로 변경되었다. 그러나 로거에서 이를 명시적으로 처리하지 않으면 단순히 프로세스만 종료될 뿐 에러 로그를 남기지 못하는 문제가 발생할 수 있다. 로깅 레벨 설정 불일치: 각 트랜스포트(예: Console, File)는 독립적인 로깅 레벨(level)을 가질 수 있다. 만약 특정 트랜스포트의 level이 info로 설정되어 있다면, error나 warn 레벨의 로그는 해당 트랜스포트를 통해 출력되지 않는다. 따라서 error 레벨의 로그가 필요한 경우, 트랜스포트의 level을 error 또는 그보다 낮은 수준(예: debug)으로 설정해야 한다. 해결방법: 안정적인 Winston 로깅 시스템 구축 우리의 Winston 로거가 에러를 다시 제대로 기록하도록 만들기 위해 코드를 어떻게 수정했는지, 이전 코드와 개선된 코드를 비교하며 살펴보겠다. [이전 코드] 내가 처음 사용했던 Winston 로거 설정은 다음과 같았다. import { formatInTimeZone } from "date-fns-tz"; import { WinstonModule, utilities } from "nest-winston"; import * as winston from "winston"; import * as winstonDaily from "winston-daily-rotate-file"; const env = process.env.NODE_ENV; const apeendTimestamp = winston.format((info, opts: { tz: string }) => { if (opts.tz) { info.timestamp = formatInTimeZone( new Date(), opts.tz, "yyyy-MM-dd HH:mm:ss" ); } return info; }); const dailyOptions = { level: "http", datePattern: "YYYY-MM-DD", dirname: __dirname + "/../../../logs", filename: ${process.env.PROJECT_NAME}.log.%DATE%, maxFiles: 30, zippedArchive: true, colorize: false, handleExceptions: true, // 예외 처리는 되어 있었음 json: false, }; export const winstonLogger = WinstonModule.createLogger({ transports: [ new winston.transports.Console({ level: env === "production" ? "http" : "silly", format: env === "production" ? winston.format.simple() : winston.format.combine( winston.format.timestamp(), utilities.format.nestLike(process.env.APP_NAME, { prettyPrint: true, }) ), }), new winstonDaily(dailyOptions), ], format: winston.format.combine( apeendTimestamp({ tz: "Asia/Seoul" }), winston.format.json(), winston.format.printf((info) => { return ${info.timestamp} - ${info.level} [${process.pid}] : ${info.message}; }) ), }); 이전 코드의 문제점: handleRejections 누락: winston.transports.Console과 winstonDaily 트랜스포트 모두 handleRejections: true 옵션이 명시적으로 설정되어 있지 않았다. 이는 비동기 코드에서 발생하는 Promise 거부(Unhandled Rejection) 에러가 로거에 의해 포착되지 않고 조용히 사라질 수 있는 결정적인 원인이었다. winston.format.errors({ stack: true }) 부재: 에러 스택 트레이스를 로그에 포함시키기 위한 winston.format.errors({ stack: true }) 포맷이 winston.format.combine 체인에 없었다. 이로 인해 에러 객체가 제대로 파싱되지 않고 printf 함수로 전달될 때 스택 정보가 유실될 가능성이 높았다. printf 포맷의 한계: printf 함수 내부에서 info.message만 직접 사용하고 있어, winston.format.errors 포맷이 적용되지 않은 상태에서는 에러 객체로부터 스택 트레이스 등 상세 정보를 추출하기 어려웠다. [개선된 코드] 위에서 언급된 문제점들을 해결하여, Winston 로거가 모든 에러를 안정적으로 포착하고 상세한 정보를 함께 기록하도록 코드를 개선하였다. import { formatInTimeZone } from "date-fns-tz"; import { WinstonModule, utilities } from "nest-winston"; import * as winston from "winston"; import * as winstonDaily from "winston-daily-rotate-file"; import * as path from "path"; const env = process.env.NODE_ENV; const apeendTimestamp = winston.format((info, opts: { tz: string }) => { if (opts.tz) { info.timestamp = formatInTimeZone( new Date(), opts.tz, "yyyy-MM-dd HH:mm:ss" ); } return info; }); // 로그 파일 저장 경로 설정 (프로젝트 루트 디렉토리) const logDir = path.join(process.cwd(), "logs"); const dailyOptions = { level: "error", // ⭐ 'http' 대신 'error'로 변경하여 에러만 파일에 기록 (혹은 'info' 등 필요한 레벨로) datePattern: "YYYY-MM-DD", dirname: logDir, filename: ${process.env.PROJECT_NAME}.log.%DATE%, maxFiles: 30, zippedArchive: true, colorize: false, handleExceptions: true, handleRejections: true, // ⭐ 핵심 추가: Unhandled Rejection도 파일에 기록 json: false, // winston.format.json()과 중복될 수 있으므로 주의 (아래 global format 참고) }; export const winstonLogger = WinstonModule.createLogger({ transports: [ new winston.transports.Console({ level: env === "production" ? "info" : "debug", // ⭐ production 레벨을 'info'로 조정 (http는 너무 verbose할 수 있음) format: env === "production" ? winston.format.combine( // ⭐ production도 format combine으로 스택트레이스 포함 apeendTimestamp({ tz: "Asia/Seoul" }), // 타임스탬프는 공통 포맷으로 이동 (혹은 여기에 포함) winston.format.errors({ stack: true }), // ⭐ 핵심 추가: 에러 스택 트레이스 포함 winston.format.simple() // simple() 포맷 뒤에 stack이 붙도록 조정 ) : winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), // ⭐ 핵심 추가: 에러 스택 트레이스 포함 utilities.format.nestLike(process.env.APP_NAME, { prettyPrint: true, }) ), handleExceptions: true, // 콘솔 트랜스포트에서 예외 처리 활성화 handleRejections: true, // ⭐ 핵심 추가: 콘솔 트랜스포트에서 Promise 거부 처리 }), new winstonDaily(dailyOptions), ], // ⭐⭐ Global format 개선: 에러 스택 트레이스를 포함하고 메시지를 더 명확하게 구성 format: winston.format.combine( apeendTimestamp({ tz: "Asia/Seoul" }), winston.format.errors({ stack: true }), // ⭐ 핵심 추가: 에러 스택 트레이스 포함! // winston.format.json(), // 필요에 따라 JSON 포맷 사용 (printf와 중복될 수 있으므로 주의) winston.format.printf((info) => { // ⭐ 에러 스택이 있으면 함께 출력하도록 수정 const message = info.message; const stack = info.stack ? \n${info.stack} : ""; // 스택이 있을 때만 추가 return `${info.timestamp} - ${info.level.toUpperCase()} [${ process.pid }] : ${message}${stack}`; }) ), // ⭐ exitOnError 설정에 대한 주의사항 명시. exitOnError: false, }); 개선된 코드의 주요 변경 사항 및 그 효과: handleRejections: true 추가: Console 트랜스포트와 winstonDaily (dailyOptions) 모두에 handleRejections: true 옵션을 명시적으로 추가하여, 비동기 Promise에서 발생하는 처리되지 않은 거부(Unhandled Rejection) 에러까지 로깅 시스템이 포착하고 기록하도록 변경했다. 이전에는 이 에러들이 로거를 통과하지 못하고 '삼켜질' 수 있었다. winston.format.errors({ stack: true }) 적용: winston.format.combine 체인에 winston.format.errors({ stack: true }) 포맷을 추가했다. 이 포맷은 에러 객체가 로그 레코드에 전달될 때, 에러의 스택 트레이스를 info.stack 속성에 포함시켜 준다. 이로써 printf 함수에서 상세한 스택 트레이스 정보를 쉽게 접근하고 로그에 출력할 수 있게 되었다. Console 트랜스포트의 개발(nestLike) 및 프로덕션(simple) 포맷에도 각각 적용하여, 콘솔 출력에서도 상세한 에러 정보가 나타나도록 했다. printf 포맷 개선: printf 함수 내부에서 info.stack 속성을 확인하여, 스택 트레이스가 존재할 경우 이를 메시지에 추가하도록 수정했다. 이는 에러 발생 시 개발자가 문제의 원인을 파악하는 데 결정적인 단서가 된다. 이전에는 단순히 info.message만 출력하여 스택 트레이스 정보를 놓칠 수 있었다. dailyOptions.level 조정: 파일 로그의 level을 error로 변경하여, 파일에는 치명적인 에러 로그만 집중적으로 기록되도록 조정했다. (필요에 따라 info 등으로 변경 가능) exitOnError 설정에 대한 상세 설명 추가: exitOnError: false 설정이 가지는 양면성에 대해 주석으로 상세 설명을 추가하여, 이 옵션의 사용에 있어 개발자가 충분히 인지하고 신중하게 결정할 것을 강조했다. 결론 로깅 시스템은 애플리케이션의 동작 상태를 파악하고 문제 발생 시 신속하게 원인을 진단하는 개발자의 핵심 도구이다. Winston 로거가 에러를 '삼키는' 현상은 잘못된 설정이나 Node.js의 예외 처리 메커니즘에 대한 오해에서 비롯되는 경우가 많다. 안정적인 로깅 시스템을 구축하기 위해서는 Winston의 각 트랜스포트에서 예외 처리(handleExceptions, handleRejections)를 명시적으로 활성화하고, 전역 예외(uncaughtException, unhandledRejection)를 포착하여 상세한 스택 트레이스까지 기록하는 포맷을 적용해야 한다. 또한, exitOnError 옵션의 의미를 정확히 이해하고 애플리케이션의 안정성 요구 사항에 맞춰 신중하게 설정한다. 로깅 시스템에 대한 면밀한 구성은 '조용한 실패'를 방지하고, 서비스의 신뢰성을 확보하며, 개발 과정의 효율성을 극대화하는 데 필수적인 요소이다. 아래는 숨겨진 에러가 모습을 드러낸 장면이다. moneybuddy-server-1 | info: Mapped {/api/budget, GET} route {"context":"RouterExplorer","timestamp":"2025-11-19 19:52:57"} moneybuddy-server-1 | error: unhandledRejection: Prisma Client could not locate the Query Engine for runtime "linux-musl-openssl-3.0.x". moneybuddy-server-1 | moneybuddy-server-1 | This happened because Prisma Client was generated for "debian-openssl-3.0.x", but the actual deployment required "linux-musl-openssl-3.0.x". moneybuddy-server-1 | Add "linux-musl-openssl-3.0.x" to binaryTargets in the "schema.prisma" file and run prisma generate after saving it: moneybuddy-server-1 | moneybuddy-server-1 | generator client { moneybuddy-server-1 | provider = "prisma-client-js" moneybuddy-server-1 | binaryTargets = ["native", "linux-musl-openssl-3.0.x"] moneybuddy-server-1 | } moneybuddy-server-1 | moneybuddy-server-1 | The following locations have been searched: moneybuddy-server-1 | /app/node_modules/.prisma/client moneybuddy-server-1 | /app/node_modules/@prisma/client moneybuddy-server-1 | /tmp/prisma-engines moneybuddy-server-1 | PrismaClientInitializationError: Prisma Client could not locate the Query Engine for runtime "linux-musl-openssl-3.0.x". moneybuddy-server-1 | moneybuddy-server-1 | This happened because Prisma Client was generated for "debian-openssl-3.0.x", but the actual deployment required "linux-musl-openssl-3.0.x". moneybuddy-server-1 | Add "linux-musl-openssl-3.0.x" to binaryTargets in the "schema.prisma" file and run prisma generate after saving it: moneybuddy-server-1 | moneybuddy-server-1 | generator client { moneybuddy-server-1 | provider = "prisma-client-js" moneybuddy-server-1 | binaryTargets = ["native", "linux-musl-openssl-3.0.x"] moneybuddy-server-1 | } moneybuddy-server-1 | moneybuddy-server-1 | The following locations have been searched: moneybuddy-server-1 | /app/node_modules/.prisma/client moneybuddy-server-1 | /app/node_modules/@prisma/client moneybuddy-server-1 | /tmp/prisma-engines moneybuddy-server-1 | at yl (/app/node_modules/@prisma/client/runtime/library.js:64:805) moneybuddy-server-1 | at async Object.loadLibrary (/app/node_modules/@prisma/client/runtime/library.js:111:10635) moneybuddy-server-1 | at async Gr.loadEngine (/app/node_modules/@prisma/client/runtime/library.js:112:448) moneybuddy-server-1 | at async Gr.instantiateLibrary (/app/node_modules/@prisma/client/runtime/library.js:111:14517) {"date":"Wed Nov 19 2025 10:52:57 GMT+0000 (Coordinated Universal Time)","error":{"clientVersion":"6.6.0","name":"PrismaClientInitializationError"},"os":{"loadavg":[4.97,4.56,4.45],"uptime":2159102.99},"process":{"argv":["/usr/local/bin/node","/app/dist/main"],"cwd":"/app","execPath":"/usr/local/bin/node","gid":0,"memoryUsage":{"arrayBuffers":108285,"external":3400647,"heapTotal":71860224,"heapUsed":41951136,"rss":133869568},"pid":1,"uid":0,"version":"v20.19.5"},"rejection":true,"stack":"PrismaClientInitializationError: Prisma Client could not locate the Query Engine for runtime \"linux-musl-openssl-3.0.x\".\n\nThis happened because Prisma Client was generated for \"debian-openssl-3.0.x\", but the actual deployment required \"linux-musl-openssl-3.0.x\".\nAdd \"linux-musl-openssl-3.0.x\" to binaryTargets in the \"schema.prisma\" file and run prisma generate after saving it:\n\ngenerator client {\n provider = \"prisma-client-js\"\n binaryTargets = [\"native\", \"linux-musl-openssl-3.0.x\"]\n}\n\nThe following locations have been searched:\n /app/node_modules/.prisma/client\n /app/node_modules/@prisma/client\n /tmp/prisma-engines\n at yl (/app/node_modules/@prisma/client/runtime/library.js:64:805)\n at async Object.loadLibrary (/app/node_modules/@prisma/client/runtime/library.js:111:10635)\n at async Gr.loadEngine (/app/node_modules/@prisma/client/runtime/library.js:112:448)\n at async Gr.instantiateLibrary (/app/node_modules/@prisma/client/runtime/library.js:111:14517)","timestamp":"2025-11-19 19:52:57","trace":[{"column":805,"file":"/app/node_modules/@prisma/client/runtime/library.js","function":"yl","line":64,"method":null,"native":false},{"column":10635,"file":"/app/node_modules/@prisma/client/runtime/library.js","function":"async Object.loadLibrary","line":111,"method":"loadLibrary","native":false},{"column":448,"file":"/app/node_modules/@prisma/client/runtime/library.js","function":"async Gr.loadEngine","line":112,"method":"loadEngine","native":false},{"column":14517,"file":"/app/node_modules/@prisma/client/runtime/library.js","function":"async Gr.instantiateLibrary","line":111,"method":"instantiateLibrary","native":false}]} moneybuddy-server-1 | error: unhandledRejection: Prisma Client could not locate the Query Engine for runtime "linux-musl-openssl-3.0.x". moneybuddy-server-1 | moneybuddy-server-1 | This happened because Prisma Client was generated for "debian-openssl-3.0.x", but the actual deployment required "linux-musl-openssl-3.0.x". moneybuddy-server-1 | Add "linux-musl-openssl-3.0.x" to binaryTargets in the "schema.prisma" file and run prisma generate after saving it: moneybuddy-server-1 | moneybuddy-server-1 | generator client { moneybuddy-server-1 | provider = "prisma-client-js" moneybuddy-server-1 | binaryTargets = ["native", "linux-musl-openssl-3.0.x"] moneybuddy-server-1 | } moneybuddy-server-1 | moneybuddy-server-1 | The following locations have been searched: moneybuddy-server-1 | /app/node_modules/.prisma/client moneybuddy-server-1 | /app/node_modules/@prisma/client moneybuddy-server-1 | /tmp/prisma-engines moneybuddy-server-1 | PrismaClientInitializationError: Prisma Client could not locate the Query Engine for runtime "linux-musl-openssl-3.0.x". moneybuddy-server-1 | moneybuddy-server-1 | This happened because Prisma Client was generated for "debian-openssl-3.0.x", but the actual deployment required "linux-musl-openssl-3.0.x". moneybuddy-server-1 | Add "linux-musl-openssl-3.0.x" to binaryTargets in the "schema.prisma" file and run prisma generate after saving it: moneybuddy-server-1 | moneybuddy-server-1 | generator client { moneybuddy-server-1 | provider = "prisma-client-js" moneybuddy-server-1 | binaryTargets = ["native", "linux-musl-openssl-3.0.x"] moneybuddy-server-1 | } moneybuddy-server-1 | moneybuddy-server-1 | The following locations have been searched: moneybuddy-server-1 | /app/node_modules/.prisma/client moneybuddy-server-1 | /app/node_modules/@prisma/client moneybuddy-server-1 | /tmp/prisma-engines moneybuddy-server-1 | at yl (/app/node_modules/@prisma/client/runtime/library.js:64:805) moneybuddy-server-1 | at async Object.loadLibrary (/app/node_modules/@prisma/client/runtime/library.js:111:10635) moneybuddy-server-1 | at async Gr.loadEngine (/app/node_modules/@prisma/client/runtime/library.js:112:448) moneybuddy-server-1 | at async Gr.instantiateLibrary (/app/node_modules/@prisma/client/runtime/library.js:111:14517) moneybuddy-server-1 | at async Gr.start (/app/node_modules/@prisma/client/runtime/library.js:112:2060) moneybuddy-server-1 | at async Proxy.onModuleInit (/app/dist/global/prisma/prisma.service.js:33:9) moneybuddy-server-1 | at async Promise.all (index 0) moneybuddy-server-1 | at async callModuleInitHook (/app/node_modules/@nestjs/core/hooks/on-module-init.hook.js:43:5) moneybuddy-server-1 | at async NestApplication.callInitHook (/app/node_modules/@nestjs/core/nest-application-context.js:234:13) moneybuddy-server-1 | at async NestApplication.init (/app/node_modules/@nestjs/core/nest-application.js:100:9) {"date":"Wed Nov 19 2025 10:52:57 GMT+0000 (Coordinated Universal Time)","error":{"clientVersion":"6.6.0","name":"PrismaClientInitializationError"},"os":{"loadavg":[4.97,4.56,4.45],"uptime":2159103},"process":{"argv":["/usr/local/bin/node","/app/dist/main"],"cwd":"/app","execPath":"/usr/local/bin/node","gid":0,"memoryUsage":{"arrayBuffers":116501,"external":3408903,"heapTotal":71860224,"heapUsed":42067832,"rss":134217728},"pid":1,"uid":0,"version":"v20.19.5"},"rejection":true,"stack":"PrismaClientInitializationError: Prisma Client could not locate the Query Engine for runtime \"linux-musl-openssl-3.0.x\".\n\nThis happened because Prisma Client was generated for \"debian-openssl-3.0.x\", but the actual deployment required \"linux-musl-openssl-3.0.x\".\nAdd \"linux-musl-openssl-3.0.x\" to binaryTargets in the \"schema.prisma\" file and run prisma generate after saving it:\n\ngenerator client {\n provider = \"prisma-client-js\"\n binaryTargets = [\"native\", \"linux-musl-openssl-3.0.x\"]\n}\n\nThe following locations have been searched:\n /app/node_modules/.prisma/client\n /app/node_modules/@prisma/client\n /tmp/prisma-engines\n at yl (/app/node_modules/@prisma/client/runtime/library.js:64:805)\n at async Object.loadLibrary (/app/node_modules/@prisma/client/runtime/library.js:111:10635)\n at async Gr.loadEngine (/app/node_modules/@prisma/client/runtime/library.js:112:448)\n at async Gr.instantiateLibrary (/app/node_modules/@prisma/client/runtime/library.js:111:14517)\n at async Gr.start (/app/node_modules/@prisma/client/runtime/library.js:112:2060)\n at async Proxy.onModuleInit (/app/dist/global/prisma/prisma.service.js:33:9)\n at async Promise.all (index 0)\n at async callModuleInitHook (/app/node_modules/@nestjs/core/hooks/on-module-init.hook.js:43:5)\n at async NestApplication.callInitHook (/app/node_modules/@nestjs/core/nest-application-context.js:234:13)\n at async NestApplication.init (/app/node_modules/@nestjs/core/nest-application.js:100:9)","timestamp":"2025-11-19 19:52:57","trace":[{"column":805,"file":"/app/node_modules/@prisma/client/runtime/library.js","function":"yl","line":64,"method":null,"native":false},{"column":10635,"file":"/app/node_modules/@prisma/client/runtime/library.js","function":"async Object.loadLibrary","line":111,"method":"loadLibrary","native":false},{"column":448,"file":"/app/node_modules/@prisma/client/runtime/library.js","function":"async Gr.loadEngine","line":112,"method":"loadEngine","native":false},{"column":14517,"file":"/app/node_modules/@prisma/client/runtime/library.js","function":"async Gr.instantiateLibrary","line":111,"method":"instantiateLibrary","native":false},{"column":2060,"file":"/app/node_modules/@prisma/client/runtime/library.js","function":"async Gr.start","line":112,"method":"start","native":false},{"column":9,"file":"/app/dist/global/prisma/prisma.service.js","function":"async Proxy.onModuleInit","line":33,"method":"onModuleInit","native":false},{"column":null,"file":null,"function":"async Promise.all","line":null,"method":"all","native":false},{"column":5,"file":"/app/node_modules/@nestjs/core/hooks/on-module-init.hook.js","function":"async callModuleInitHook","line":43,"method":null,"native":false},{"column":13,"file":"/app/node_modules/@nestjs/core/nest-application-context.js","function":"async NestApplication.callInitHook","line":234,"method":"callInitHook","native":false},{"column":9,"file":"/app/node_modules/@nestjs/core/nest-application.js","function":"async NestApplication.init","line":100,"method":"init","native":false}]} moneybuddy-server-1 | [VERY_EARLY_DEBUG] Main TS file loaded and starting bootstrap

2025년 11월 19일

객체지향에 대하여

객체지향 프로그래밍(Object-Oriented Programming, OOP)은 현대 소프트웨어 개발에서 가장 지배적인 패러다임 중 하나입니다. 그러나 종종 '객체(Object)'와 '클래스(Class)'라는 용어 자체에 매몰되어, 그 본질적인 철학과 강력한 메커니즘을 간과하는 경우가 많습니다. 본 글에서는 객체지향의 창시자 중 한 명인 앨런 케이(Alan Kay)의 통찰을 바탕으로, '메시지', '캡슐화', '다형성'이라는 세 가지 핵심 요소를 통해 객체지향의 진정한 의미를 탐구하고자 합니다. 1. 객체란 무엇인가: 속성과 행동의 논리적 단위 객체지향의 첫 단추는 '객체'의 정의에서 시작합니다. 객체는 우리가 다루고자 하는 문제 영역 내에서 고유한 '상태(State; 속성/데이터)'를 가지며, 정의된 '행동(Behavior; 기능/메서드)'을 수행할 수 있는 논리적인 단위를 의미합니다. 상태(Properties): 객체가 보유하고 있는 데이터, 즉 객체의 현재 정보를 나타냅니다. 예를 들어, '자동차' 객체는 '색상', '모델명', '현재 속도'와 같은 속성들을 가집니다. 행동(Actions): 객체가 수행할 수 있는 동작 또는 기능을 의미합니다. '자동차' 객체는 '가속하다', '정지하다', '경적을 울리다' 등의 행동을 통해 자신의 상태를 변경하거나 외부에 영향을 미칠 수 있습니다. 객체는 이처럼 상태와 행동을 포함하는 자율적인 존재이며, 이들을 통합하여 관리하는 것이 객체지향 설계의 기본 전제가 됩니다. 2. 캡슐화와 설계 원칙: '조화로운 묶음'과 '느슨한 결합' 객체의 상태와 행동을 하나의 단위로 묶는 행위는 객체지향의 중요한 원칙 중 하나인 '캡슐화(Encapsulation)' 로 이어집니다. 캡슐화는 객체의 내부 구현을 외부로부터 숨기고, 오직 정의된 인터페이스(메시지)를 통해서만 접근하도록 하여, 정보 은닉(Information Hiding)을 달성하는 메커니즘입니다. 이를 설명하는 좋은 비유는 바로 '햄버거 세트' 입니다. 우리가 햄버거 세트를 주문할 때, 개별 메뉴(햄버거, 감자튀김, 콜라)를 일일이 요청하는 대신 '빅맥 세트'라는 하나의 명칭으로 주문합니다. '빅맥 세트'라는 객체는 여러 구성 요소(속성)와 그들을 함께 제공하는 기능(행동)을 캡슐화하여, 사용자에게 단순하고 효율적인 사용 방식을 제공합니다. 하지만 캡슐화는 단순히 무언가를 묶는 것을 넘어, '조화롭게 묶는 것'이 중요합니다. '햄버거와 된장찌개가 세트로 묶인 경우'를 상상해본다면, 서로 이질적인 요소들의 무분별한 결합은 오히려 비효율과 혼란을 초래할 것입니다. 이것이 바로 객체지향 설계의 핵심 개념인 '응집도(Cohesion)'와 '결합도(Coupling)' 로 설명됩니다. 높은 응집도 (High Cohesion): 객체 내부의 요소들이 하나의 목표를 달성하기 위해 얼마나 긴밀하게 관련되어 있는지를 나타냅니다. 햄버거 세트처럼, 객체의 속성과 행동이 공동의 목적을 지향하며 강하게 묶여 있을 때 응집도가 높다고 합니다. 이는 객체의 단일 책임 원칙(Single Responsibility Principle)과 밀접하게 연결됩니다. 낮은 결합도 (Low Coupling): 객체들이 서로 얼마나 독립적으로 존재하며, 한 객체의 변경이 다른 객체에 미치는 영향이 적은 정도를 나타냅니다. 햄버거 세트의 콜라 종류가 바뀌더라도 다른 구성 요소에 영향을 주지 않는 것처럼, 객체 간의 의존성이 낮을 때 결합도가 낮다고 말합니다. 객체지향 설계의 목표는 항상 '높은 응집도'와 '낮은 결합도' 를 달성하여, 시스템의 유연성, 확장성, 그리고 유지보수성을 극대화하는 것입니다. 3. 객체지향의 세 가지 핵심 기둥: 메시징, 캡슐화, 그리고 다형성 앨런 케이는 "객체지향"이라는 용어를 만든 것을 두고 다음과 같은 아쉬움을 표했습니다. "I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is messaging." ("오래 전 이 주제에 대해 '객체'라는 용어를 만들어낸 것에 대해 미안하게 생각한다. 그 용어가 너무 많은 사람들이 덜 중요한 아이디어에 집중하게 만들었기 때문이다. 진정한 큰 아이디어는 메시징이다.") 이 발언은 객체지향의 본질이 단순히 '객체'나 '클래스'를 만드는 행위를 넘어, 객체들이 서로 소통하는 방식, 즉 '메시징(Messaging)' 에 있음을 강조합니다. 메시징을 중심으로 캡슐화(Encapsulation) 와 다형성(Polymorphism, 동적 바인딩) 이 상호작용한하며 객체지향의 강력함을 이끌어냅니다. 메시징 (Messaging): 객체 간의 유일하고 직접적인 상호작용 수단입니다. 객체는 다른 객체의 내부를 들여다보거나 직접 조작하지 않으며, 오직 메시지를 통해 "요청"을 보내고 "응답"을 받습니다. 이는 마치 서로 격리된 컴퓨터들이 네트워크(LAN선)를 통해 통신하는 것과 유사합니다. "A 객체가 B 객체에게 '이 작업을 수행해 달라'고 메시지를 보내면, B 객체는 자신의 책임 하에 해당 작업을 처리하고 결과를 반환합니다." 이 과정에서 A 객체는 B 객체가 그 작업을 '어떻게' 수행했는지에 대해서는 알 필요도, 관심 가질 필요도 없습니다. 캡슐화 (Encapsulation): 객체의 내부 상태와 행동을 감추어, 외부에서는 정해진 메시지(공개된 메서드)를 통해서만 객체에 접근할 수 있도록 합니다. 캡슐화는 객체의 내부 구현을 보호하고 변경 용이성을 높이며, 객체가 메시징을 통해 상호작용하는 근본적인 이유를 제공합니다. 즉, 객체의 내부가 철저히 감춰져 있기 때문에 외부에서는 '메시지'라는 약속된 통로로만 소통해야만 하는 것입니다. 다형성 (Polymorphism, 동적 바인딩): '다양한 형태(Poly: 다형, morph: 형태)를 가질 수 있음'을 의미하며, 특히 객체지향에서는 "동일한 메시지에 대해 객체의 타입에 따라 서로 다르게 반응하는 능력" 으로 발현됩니다. 예를 들어, print()라는 동일한 메시지를 Dog 객체에게 보내면 "멍멍!" 소리를 내고, Cat 객체에게 보내면 "야옹!" 소리를 내는 것입니다. 이 다형성은 런타임 시점에 실제 호출될 메서드가 결정되는 '동적 바인딩(Dynamic Binding)' 메커니즘을 통해 구현됩니다. 메시지를 보내는 객체는 메시지를 받을 객체의 구체적인 타입에 대해 알 필요 없이, 단순히 '이러한 메시지를 처리할 수 있는 객체'라고만 알고 있으면 됩니다. 이 세 가지 요소(메시징, 캡슐화, 다형성)는 서로 긴밀하게 연결되어 시너지를 창출합니다. 캡슐화는 메시징을 가능하게 하는 토대를 마련하고, 메시징은 다형성을 통해 시스템의 유연성과 확장성을 극대화합니다. 그리고 이 모든 것은 객체 간의 '느슨한 결합' 을 달성하여 시스템의 전반적인 품질을 향상시킵니다. 4. 객체지향의 진정한 본질: 역할과 책임, 그리고 협력 결론적으로, 앨런 케이의 통찰처럼 객체지향의 진정한 본질은 '클래스'나 '객체' 그 자체보다, '메시징'을 통해 이루어지는 객체들 간의 협력 방식에 있습니다. 객체지향 설계는 "각자의 명확한 역할과 책임을 가진 자율적인 객체들이 메시지를 주고받으며 유기적으로 협력하여 복잡한 문제를 해결하는 시스템을 구축하는 사고방식" 입니다. 객체는 자신의 상태를 캡슐화하고, 필요한 기능은 메시지를 통해 다른 객체에게 위임하며, 이 과정에서 다형성을 활용하여 코드의 유연성과 재사용성을 높입니다. 따라서 우리는 더 이상 '어떤 클래스를 만들까?', '어떤 객체를 인스턴스화할까?'에만 매몰되지 않고, '객체들이 어떻게 메시지를 주고받으며 상호작용할까?' 에 집중해야 합니다. 이것이야말로 강력하고 유연하며 유지보수하기 쉬운 소프트웨어를 개발하기 위한 객체지향의 핵심 원칙이자 철학입니다. 마무리 하며 우리가 오늘 나눈 '햄버거 세트'와 '된장찌개 세트'의 비유, 그리고 앨런 케이의 "객체가 아닌 메시지가 본질이다"라는 통찰은 객체지향의 근본적인 의미를 다시 한번 되새기게 합니다. 이제 우리는 더 이상 '어떤 클래스를 만들까?', '어떤 객체를 인스턴스화할까?'라는 표면적인 질문에만 머물러서는 안 될 것입니다. 진정한 객체지향적 사고는 "각각의 객체가 어떤 역할과 책임을 가지며, 어떤 메시지를 통해 서로 유연하고 견고하게 협력할 것인가?" 라는 질문에서 시작됩니다. 이 관점으로 코드를 설계하고 바라본다면, 우리는 단순히 기능을 구현하는 것을 넘어, 변화에 강하고 확장성이 뛰어난 시스템을 구축하는 진정한 개발자로 거듭날 수 있을 것입니다.

2025년 11월 11일