문제해결, 디버깅, 코드를 짜는 법

26년 초 라이브 환경에서, 이슈 대응에 있어 날카로운 직감이나 지나고보면, 당시가 스스로 최고점이라고 생각했었다.

시간이 지나 그때의 경험이나, 관점이나 이런걸 토대로 회사를 옮기고, 현재 라이브 조직에 있지않다보니, 당시의 감이 조금씩 떨어지는걸 느끼기에 더 늦기전에, 예전부터 생각하고 있었던 부분들은 좀 주절주절 써보고자 한다.

windows os기반의 C++ 서버를 전제로 합니다.

이슈, 장애

이슈에 대해서 정의를 하고 현상을 파악해야한다. 가장 기본적인 분리는 서버가 죽었는가 살았는가? 이다.

가장 처음 프로세스가 왜 죽는가?에 답을 할 수 있어야한다. 죽는 케이스는 어떤것들이 있는가? OS의 기본적인 작동 방식에 대한 질문이다.

서버가 죽었는가? 정상종료인가? 정상 종료라면 관련 로그가 남았는가, 비정상 종료인가? 그렇다면 덤프가 남았고, 덤프를 열어서 분석하면 된다. 덤프가 남지않았으면, 덤프 확보를 우선시한다. (왜 남지 않았을까로 부터 원인 추적 또한 같이 진행한다.)

보통 분석 방법은 크게 아래와 같다.

  1. dump 분석을한다.
  2. 재현 조건을 추적한다.
  3. 라이브에서 발생하는 이슈는 직전 반영 코드에서 발생하는 경우가 대다수이기에, 해당 코드를 조사한다.

Dump

덤프의 풀 명칭은 메모리 덤프이다.

프로세스가 점유하고 있는 램과 레지스터를 당시 상황 그대로 스냅샷을 떠 파일로 만든 것이다.

프로세스 메모리 구조는, 직장인이라면 아마 대다수 알 것이다. heap, 스택, 코드, data 영역

이중에서 가장 작은 구조인 미니덤프는 stack 영역만 파일로 만든것이다. 따라서 stack 영역의 메모리 영역을 다 뒤져볼 수 있다.

WinDbg, Visual Studio 모두 이 파일을 분석하여 우리 눈에 해석하기 편하게 보여줄 뿐 본질을 그냥 메모리 덩어리다.

메모리 덩어리 라는건 결국에 주소 하나만 정확하게 아는게 있다면, 모든것을 역으로 뽑아 낼 수 있다는 것이다.

예를들어 어떤 주솟값 구조체 A의 주소를 안다고 치자. 메모리를 좀 들여다 봐야하지만 a,b,c 의 값을 모두 알 수 있다.

1
2
3
4
5
6
struct a
{
    int a;
    int b;
    int c;
}

주소 0xbb

  • int a의 주솟값 0xbb
  • int b의 주솟값 0xbb+4
  • int c의 주솟값 0xbb+8

물론 패딩, 구조체 정렬을 모두 고려해야하지만 어쨋든 구할 수 있다. 여러 자료구조가 엮여있다면 이 또한 모두 파악 할 수 있다.

스택 영역에서는 콜스택이 제대로 나오지 않지만, 메모리를 직접 뜯어내면 콜스택을 모두 알 수도 있다. 특정 어느 메모리가 손상되었다면, 해당 주소 위치로부터 어느 변수가 손상시켰는지도 뽑을 수도 있다.

덤프에는 정말 정보가 많이 있기에, 파면 팔수록 많은 정보를 뽑아올 수 있다.

어떤 변수인지 파악이 되지않더라도 구조체 패딩값으로 부터 특정 변수를 알아낼 수도 있다.

어셈도 어느정도 알아야하지만, 덤프 분석은 OS 내부가 어떻게 동작하는지, 프로세스는 어떻게 이루어지는지 변수, 코드는 어떻게 작동하는지에 대한 모든 이해와 응용을 요구한다.

또한 modern을 많이 사용할 수록 cpp STL의 내부 구조또한 깊은 이해를 요구한다. 몰라도 디버깅하면 어차피 알게된다.


서버가 죽지 않았다면?

요청이 몰려서 서버가 느린가? -> 최적화 할 부분을 찾는다. 평소 구축한 주요 지표(없다면 구축을 고려한다.)와 프로파일링을 시도한다. 최적화에 대한부분은 여기에 자료구조의 최적화 주요 포인트

현상과, 이번에 반영된 작업으로부터 연관성을 찾고 원인이 될 부분을 짚는다. 단서가 전무하다면 굉장히 난처한 상황이 발생한다.

구축한 지표에 문제가 있지않는한 지표값을 정확하게 해석한다. 지표가 이상해 -> 왜 이런 결과가 나왔는가, 어떻게하면 이런 결과가 나오는가로 추적을 해야한다. 급할수록 꼼꼼히 정확하게 파악해야한다. 잘못 해석하면 거기서 시간이 몇시간씩 날아가는거다.

이 상황에서 우리가 참고할 수 있는 단서는 대개 서버가 남긴 데이터뿐이다.

덤프가 있든 없든 데이터가 있든 없든 분석 원칙은 간단하다

가정을 세우고, 차근차근 체크한다. 발견된 부분에 대해서, 소거법으로간다. 여기에 정말 맞는지? 내가 제대로 파악한게 맞는지? 이 가정이 맞는지? 추가적인 아이디어가 있다면 이게 앞의 가정과 어긋나진않는지? 무한한 순서도를 돌면서 검토를 해야한다.

그럼 이제 시스템 구축방안에 대해서 한번 고민해보자

위험 알림 방향

시간

  1. 발생 즉시
  2. 주기마다

방식

  1. 의도적 크래시
  2. 로그 / 로그만 남기거나, 알림 구축까지 같이하거나
  3. DB / 로그만 남기거나, 알림 구축까지 같이하거나

방지

위와 같이 밤새 불려다니거나, 발을 동동굴리며 수명이 깎이듯 사는 끔찍한 일이 일어나지 않으려면 어떻게 해야할까?

이슈가 이미 발생한 후에 후처리

  1. 개발중에 ASSERT로 로컬에서 알 수 있는가?
  2. 유닛테스트로 알 수 있는가?
    1. 효율적인 테스트 코드 작성법
  3. 로그를 남기는가?

근본적인 구조 문제로 인하더라도 라이브로 가기전에 알아차릴 방법이 있거나 개발 단계에서 쉽게 발견할 수 있는 부분이면 크게 문제 되지는 않는다. 가능한 개발 단계에서 놓치지않고, 쉽게 발견 가능하도록 한다.

작업을 할때는

실수가 발생하고, 의도치않은 버그의 발생은 아래 순서에 따라 적절하게 처리한다. 빠를 수록 좋다.

  1. 컴파일 시점에 잡는다 : static_assert, 유닛테스트
  2. 런타임 시점에 체크되도록한다.
    1. ASSERT를 가능한 넣는다.
    2. 당연하지만 ASSERT가 릴리즈 빌드 어떻게 처리할지도 고민해야한다.
  3. 발생시 크래시를 발생시킨다.
  4. 로그를 잘 쓴다.
  5. 롤백 방안을 고려한다.
    1. 빌드 시점에서 분리하거나 (매크로 이용)
    2. 특정 input 값으로 on/off 가능하도록 하거나
    3. 동적으로 켰다 껏다 할 수 있는 기능이 있다면 사용

코드 작업

돌고돌아 버그를 줄이려면 코드베이스가 깔끔해야한다. 최적화와, 디버깅하는 시간을 줄이는것도 깔끔한 코드베이스가 전제된다. 이는 가독성과 직결된다.

그러기 위해서는 잔정리를 자주 자주할 수 있는 환경이어야한다. 코드를 반영하는데 허들이 낮아야한다.

모든 commit 반영을 리뷰를 봐야만 하는 구조라면 한번 동작하는 코드가 들어가버리면 그 이후로는 코드 변경을 안하려고할 것이다. 따라서 큰 시스템 기반이 되는 코드가 들어갈때, 구조에 대한 리뷰를 보고, 이 시스템이 적절한지 논의 (사실 코드리뷰보다는 구조에 대한 리뷰에 가깝다, 팀 전체가 리뷰보는게 좋다고 생각) , 이후에는 post commit review 방식이 좋다고 본다. 코드 리뷰는 위치상 상급자가 보는게 적절하다.

동급의 동료는 효과가 떨어진다.

코드가 쉽게 들어가려면, 결과적으로 코드 빌드가 빨라야하고, 테스트가 쉬운환경 이어야한다.

하단부 디버깅을 많이 한 사람일 수록 코드와 cpu, os레벨의 도는 방식이 직관적이길 선호하게 되는데 그렇다보니 자연스레 스타일은 C언어 방식을 좀 선호하게 된다.

코드가 잘게 흩어져있고, 추상화 정도가 높은게, 좋은 코드라는 생각은 별로 안한다.

정말 도메인에서 높은 지식과 경험이 많은 개발자가 초기부터 구축해놓은것이 아닌이상, 대부분의 미래를 대비한 추상화는 의미가 없을뿐더러 악영향일 가능성이 높다.

반복성이 보일때, 묶거나 추상화를 고려하면된다.

년차 적은 개발자가 할 수 있는 유일한것이라고 생각한다.

원하는건 함수 하나가 깔끔한것, 코드가 각자의 위치에 잘 모여있으면 된다. 좋은 코드는 읽으면 작업자의 의도가 읽어지고, 읽다가 머리에 물음표가 뜨는게(코드 흐름에서 크게 벗어나는 로직, 이게 왜 여기에…?) 보통 코드가 이상한 케이스라고 생각한다.

Posted 2026-08-15