1P by GN⁺ | ★ favorite | 댓글 1개
  • 크고 복잡한 레거시 C++ 코드베이스를 맡았다면 목표는 “깨끗한 코드”가 아니라 보안, 개발자 경험, 정확성, 성능을 수용 가능한 상태로 끌어올리는 것임
  • 출발점은 조직의 동의와 시간 제한을 확보하고, 지원 플랫폼을 README에 적은 뒤, 로컬 빌드와 테스트가 안정적으로 통과하게 만드는 일임
  • 빌드·테스트 속도 개선은 의존성 테스트 제거, 불필요한 타깃 정리, mold 같은 빠른 링커 실험처럼 낮은 난이도 개선부터 측정하며 진행해야 함
  • 미사용 코드와 미지원 플랫폼 코드를 줄인 뒤 clang-tidy, cppcheck, clang-format, -fsanitize=address,undefined, CI로 자동 검증 루프를 만들어야 함
  • 리라이트나 최신 C++ 표준 도입은 목적이 아니라 수단이며, 메모리 안전 언어 재작성도 강한 이유가 있을 때만 검토할 만함

처음에는 코드보다 사람과 절차를 정리함

  • 레거시 C++ 코드베이스 개선은 퇴근 후 혼자 밀어붙이는 작업이나 장기적인 “죽음의 행군”이 아니라, 지속 가능한 소프트웨어 엔지니어링 작업이어야 함
  • 상사, 동료, 비기술 구성원에게 문제, 해결책, 시간 제한을 쉬운 말로 설명해야 함
    • 새 입사자가 로컬 빌드와 첫 기여까지 3주가 걸렸다면, 이를 몇 분으로 줄이는 목표를 제안할 수 있음
    • 간단한 퍼징 설정이 몇 초 만에 앱을 253번 크래시시킨다면, 운영 환경에서의 위험을 설명할 근거가 됨
    • 8년 동안 지원되지 않은 FreeBSD 9 빌드 서버 하나에 배포가 의존한다면, 서버 장애 시 배포가 막히는 상태임
    • 업계 표준 린터가 운영 버그의 원인이 된 정의되지 않은 동작을 즉시 잡아낸다면, 변경 때마다 실행할 이유가 충분함
    • 수동으로 복사해 수정한 암호화 라이브러리 때문에 취약점 영향을 알 수 없다면, 의존성 정리와 자동 알림이 필요함
  • 피해야 할 접근도 명확함
    • 테스트가 없는데 최신 C++ 표준으로 2주 동안 전면 업그레이드함
    • 몇 달 동안 별도 브랜치에서 대규모 변경을 만들고 언젠가 병합될 것이라 기대함
    • 몇 주면 끝난다는 가정으로 처음부터 전체 재작성을 시작함
    • 무엇을 언제까지 할지 모르는 상태로 “코드베이스 개선”을 시작함

안전한 개선 방식

  • 모든 변경은 작고 점진적이어야 하며, 변경 전후 앱이 동작하고 테스트와 린터가 통과해야 함
  • 긴급 버그 수정은 기존처럼 가능해야 하며, 개선 작업 때문에 막히면 안 됨
  • 각 변경은 측정 가능한 개선이어야 하고, 비전문가에게도 설명하거나 시연할 수 있어야 함
  • 우선순위나 예산 문제로 전체 작업이 중단돼도 시작 전보다 측정 가능한 순이익이 남아야 함

지원 플랫폼을 README에 명시함

  • README에는 공식 지원하는 <architecture>-<operating-system> 쌍을 적어야 함
    • 예: x86_64-linux, aarch64-darwin
  • 이 목록은 모든 지원 플랫폼에서 빌드되는지 확인하는 기준이며, 나중에 지원하지 않는 플랫폼용 코드를 제거하는 근거가 됨
  • 필요하다면 ARMv6, ARMv7 같은 아키텍처 버전도 명시할 수 있음
  • 지원 플랫폼 목록은 다음 질문에 답하게 해줌
    • 부동소수점, SIMD, SHA256 하드웨어 지원에 의존할 수 있는지
    • 32비트 지원이 필요한지
    • 빅엔디언 플랫폼에서 실행되는지
    • char가 7비트일 가능성을 고려해야 하는지
  • 개발자 워크스테이션도 이 목록에 반드시 포함해야 함

로컬 빌드와 테스트부터 안정화함

  • 성공적인 제품의 핵심 C++ 코드베이스도 안정적으로 컴파일되지 않는 경우가 많으며, 목표는 “가끔 되는 빌드”가 아니라 모든 지원 플랫폼에서 일관되게 되는 빌드
  • 가장 좋은 상태는 개발자 머신에서 직접 빌드하고 실행할 수 있는 것임
    • 프로젝트가 너무 커서 RAM이 부족하면 큰 서버를 빌드용으로 빌릴 수 있음
    • io_uring 같은 플랫폼 전용 API가 필요하면 심(shim)을 구현하거나 워크스테이션의 가상 머신 안에서 빌드할 수 있음
    • 그래도 직접 로컬 빌드가 가장 좋은 선택임
  • 테스트가 없다면 코드 변경 전에 테스트를 먼저 작성해야 함
    • 현실에서 실행되는 프로그램의 입력과 출력을 캡처해 엔드투엔드 테스트를 만드는 방식이 가장 쉬운 출발점임
    • 이런 테스트는 기존 동작이 올바른지 보장하지는 않지만, 변경 시 회귀를 줄여줌
  • 테스트 스위트가 있는데 일부가 실패하면 일단 비활성화하고, 전체 테스트가 몇 시간이 걸려도 통과하는 상태를 먼저 만듦

빌드와 테스트 방법을 문서화함

  • README에는 앱을 빌드하고 테스트하는 방법을 적어야 함
  • 이상적인 형태는 빌드 명령 하나와 테스트 명령 하나임
  • 처음에 절차가 복잡하다면 build.sh, test.sh에 명령을 감싸 복잡성을 숨길 수 있음
  • 목표는 C++ 전문가가 아닌 사람도 질문 없이 빌드와 테스트를 실행할 수 있는 상태임
  • 프로젝트 구조나 아키텍처 문서화는 나중에 불필요한 코드를 제거한 뒤 하는 편이 나음

낮은 난이도로 빌드와 테스트 속도를 줄임

  • 빌드 시스템을 갈아엎거나 영웅적인 최적화를 하지 않고, 낮은 난이도 개선부터 측정해야 함
  • 먼저 확인할 항목들
    • 의존성의 테스트를 매번 빌드하고 실행하는지 확인함
      • unittest++를 CMake 서브프로젝트로 썼을 때 테스트 프레임워크의 테스트까지 매번 빌드·실행된 사례가 있음
    • 의존성의 예제 프로그램을 빌드하고 실행하는지 확인함
      • mbedtls도 CMake 변수로 예제 빌드를 끌 수 있었음
    • 자신의 프로젝트가 다른 프로젝트의 서브프로젝트로 포함될 때 테스트와 예제를 기본 빌드하는지 확인함
      • MYPROJECT_TEST 같은 빌드 변수를 기본 비활성화하고, 직접 개발할 때만 켜는 방식을 권장함
    • 서드파티 의존성 전체를 빌드하지만 실제로는 작은 일부만 쓰는지 확인함
      • mbedtls는 필요 없는 부분을 끄는 컴파일 타임 플래그를 많이 제공함
    • 타깃 의존성이 잘못돼 작은 변경에도 전체가 다시 빌드되는지 확인함
      • 많은 빌드 시스템은 의존성 그래프를 출력할 수 있음
    • mold 같은 빠른 링커를 실험함
    • 가능하다면 컴파일러도 비교함
      • 어떤 프로젝트는 clang이 gcc보다 2배 빠른 사례가 있었고, 어떤 프로젝트는 차이가 없었음
  • 추가로 실험할 수 있지만 이득이 작거나 음수일 수 있는 항목
    • LTO 끄기/켜기/thin
    • 디버그 정보 분리
    • Make와 Ninja 비교
    • 파일 시스템 종류와 설정
  • 빌드가 너무 오래 걸리면 코드 수정 자체가 현실적이지 않음

불필요한 코드를 제거함

  • 코드베이스의 30% 이상이 완전히 죽은 코드였던 사례가 있으며, 이런 코드는 컴파일과 리팩터링 비용을 계속 늘림
  • 제거 방법
    • 컴파일러의 -Wunused-xxx 경고를 사용함
      • 예: -Wunused-function
      • 대부분은 삭제 후 재빌드와 테스트로 끝나지만, 드물게 잘못된 함수가 호출된 버그의 증상일 수 있어 완전 자동화는 조심해야 함
    • cppcheck 같은 린터로 미사용 함수나 클래스 필드를 찾음
      • 상속과 가상 함수에서는 거짓 양성이 많을 수 있지만, 컴파일러가 못 찾은 미사용 요소를 찾을 수 있음
    • 링커가 각 함수를 별도 섹션에 넣고 미사용 섹션 제거를 출력하게 하는 방식도 있지만, 표준 라이브러리 함수 노이즈가 많아 실용적이지 않을 수 있음
    • 생성된 어셈블리와 소스 코드를 비교하는 방식은 가상 함수에는 잘 맞지 않음
  • 지원 플랫폼 목록을 활용해 미지원 플랫폼 코드를 제거함
    • 실제로 FreeBSD에서만 돌던 프로젝트의 오래된 Solaris 지원 코드
    • 실제 플랫폼에는 항상 난수 생성기가 있는데 자체 난수 생성기를 둔 코드
    • 현대 Linux와 macOS에서만 실행되는데 POSIX 2001 미지원 대비 코드
    • 빅엔디언 CPU 여부를 검사해 바이트 스왑하는 코드
    • 수년 전 들어갔지만 실제 기능으로 이어지지 않은 코드
  • 대량의 코드 삭제 PR은 빌드 시간 감소와 유지보수 비용 감소를 동시에 보여줄 수 있음

린터, 포맷터, 새니타이저를 넣음

  • 린터는 규칙을 과도하게 켜지 말고 기본적인 몇 가지부터 개발 생명주기에 넣어야 함
    • clang-tidy, cppcheck는 유용할 수 있지만 느리고 노이즈가 많을 수 있음
    • 린터가 전혀 없는 상태는 선택지가 아니며, 처음 실행하면 컴파일러 경고만으로 잡히지 않던 실제 문제를 많이 찾을 수 있음
  • 코드 포맷팅은 적절한 시점에 한 번에 적용해야 함
    • 활성 브랜치가 없는 때를 기다려야 끔찍한 병합 충돌을 피할 수 있음
    • 스타일 논쟁에 시간을 쓰지 말고 clang-format 같은 도구로 전체 코드베이스를 예외 없이 포맷팅함
    • 설정도 함께 커밋함
  • 새니타이저는 실제 운영에 영향을 줄 수 있는 찾기 어려운 버그를 잡는 데 필요함
    • 기본값으로 -fsanitize=address,undefined를 권장함
    • 대체로 거짓 양성이 없으므로 감지되면 수정해야 함
    • 테스트도 새니타이저를 켠 상태로 실행해 문제를 찾음
    • 성능 예산이 허용된다면 일부 새니타이저를 켠 운영 실행도 고려할 수 있음
  • 배포용 컴파일러가 새니타이저를 지원하지 않더라도 개발과 테스트에서는 clang 같은 컴파일러를 사용할 수 있음
  • 새니타이저를 켜면 오랫동안 숨어 있던 버그와 메모리 누수가 드러날 수 있으며, 수정에 많은 작업과 리팩터링이 필요할 수 있음
  • 가능하면 테스트 시 서드파티 의존성도 새니타이저를 켜고 컴파일해 의존성 내부 문제도 찾는 편이 좋음

CI로 자동화함

  • CI는 지금까지 설정한 린터, 포맷팅, 테스트 등을 깨끗한 환경에서 자동화함
  • 매 변경마다 운영 바이너리를 생성할 수 있어야 함
  • 대부분의 CI 시스템은 여러 플랫폼 매트릭스를 지원하므로, README의 지원 플랫폼 목록이 실제로 빌드되는지 확인할 수 있음
  • 일반적인 파이프라인은 make all test lint fmt처럼 단순할 수 있음
  • 린터와 새니타이저가 보고한 문제는 파이프라인 실패로 이어져야 하며, 그렇지 않으면 아무도 고치지 않음

코드는 점진적으로 단순화함

  • 안정적인 빌드·테스트·검증 루프가 생긴 뒤에는 코드를 조금씩 단순화할 수 있음
  • 복잡한 클래스가 실제로는 포인터를 할당하고 null 여부만 확인하는 역할이었다면, 사실상 boolean으로 대체 가능한 사례가 있음
  • 이 단계는 단순화를 할수록 더 단순화할 지점이 열리기 때문에 시간 제한을 정하기 어렵고, 보수적으로 판단해야 함
  • 목표는 보안, 정확성, 성능 같은 구체적 가치여야 하며, “클린 코드” 같은 주관적 기준을 피해야 함
  • C++ 표준 업그레이드는 목적이 아니라 수단임
    • 예를 들어 수동 이터레이터 증가 코드를 for (auto x : items) 루프로 바꾸는 데 도움이 될 수 있음
    • 필요한 것이 std::clamp 하나뿐이면 직접 작성하는 편이 나을 수 있음

메모리 안전 언어 재작성은 별도 판단이 필요함

  • 일부를 메모리 안전 언어로 다시 쓰는 선택지는 가능하지만 주의할 점이 많음
  • 강력한 이유가 있을 때만 진행해야 함

의존성 관리는 소스 빌드 중심을 선호함

  • C++에는 일관된 의존성 관리가 없으며, 많은 프로젝트가 시스템 패키지 관리자를 사용함
  • 시스템 패키지 의존 방식의 문제
    • 설치 지침이 OS, 배포판, 배포판 버전에 의존함
    • Ubuntu 20.04에서 22.04로 옮길 때 패키지 버전이 달라져 100개 의존성을 동시에 업그레이드해야 하는 상황이 생길 수 있음
    • 패키지로 없는 서드파티 의존성은 결국 소스에서 빌드해야 함
    • 패키지가 원하는 플래그로 빌드돼 있지 않음
      • 새니타이저, LTO, -march, 디버그 정보, 프레임 포인터, C++ ABI 차이 등이 문제가 될 수 있음
    • 현재 쓰는 정확한 버전의 소스를 감사·개발·디버깅 때 보기 어려움
    • 의존성을 패치하고 다시 빌드하기 어려움
    • macOS, Ubuntu, FreeBSD 사이에서 정확히 같은 패키지 버전을 쓰기 어려움
    • 자동화된 BOM 생성이 어려워짐
    • 정적/동적 라이브러리 형태 등 필요한 패키지 변형이 없을 수 있음
  • Conan, vcpkg 같은 C++ 패키지 관리자도 개선일 수 있지만 제약이 있음
    • 외부 의존성 때문에 CI가 복잡하고 느려질 수 있음
    • 모든 패키지 버전이 없을 수 있음
    • 필요한 OS나 아키텍처를 지원하지 않을 수 있음
  • 권장 방식은 git submodule로 의존성을 가져와 소스에서 컴파일하는 것임
    • 단순함
    • 수동 벤더링보다 git 이력과 diff를 활용할 수 있음
    • 의존성 버전을 커밋 단위로 정확히 알 수 있음
    • 단일 의존성 업그레이드는 git checkout으로 가능함
    • 모든 플랫폼에서 동작함
    • 의존성마다 컴파일 플래그와 컴파일러를 선택할 수 있음
    • C++ 경험이 없어도 개발자들이 git을 알고 있음
    • 재귀적으로 동작함
  • CMake의 add_subdirectorygit submodule foreach make로 각 서브모듈을 빌드할 수 있음
  • 서브모듈이 어렵다면 Neovim처럼 스크립트 하나가 의존성을 가져오고 빌드하는 방식도 가능함
  • 의존성 그래프가 매우 크다면 Buck2 같은 하이브리드 로컬·원격 빌드 시스템과 빌드 산출물 재사용이 필요할 수 있음
  • Go, Rust 같은 컴파일 언어의 패키지 관리자는 소스에서 컴파일하는 접근을 사용함

독자 제안에서 보강된 점

  • 테스트는 더 강조할 필요가 있으며, C++ 테스트 스위트는 새니타이저 아래에서 실행해야 거짓 안전감에 빠지지 않음
  • vcpkg는 요구사항과 크로스 컴파일을 만족한다면 git submodule보다 나은 선택일 수 있음
  • Nix는 C++ 의존성 관리자 역할을 할 수 있지만 복잡성과 느림이 문제임
  • 1년에 버그 하나만 고친다면 대규모 리팩터링 투자는 판단 문제지만, 죽은 코드 제거와 새니타이저는 드문 수정에도 가치가 있음
  • 코드 제거는 정적 분석으로 호출되지 않음을 확인할 수 있는 경우에 집중해야 하며, 의심되면 제거하지 않는 편이 안전함
    • 가상 메서드는 런타임에 호출 대상이 결정되므로 정적 분석으로 제거하기 어려움
    • 영업, 제품 관리자, 사용자에게 기능·플랫폼 사용 여부를 물어보는 15분 대화가 기술 작업을 크게 줄일 수 있음
  • LLM에 코드를 넣고 질문하는 방식은 법적으로 안전하고 가능하면 로컬에서 실행하며, 결과를 신중히 받아들여야 함
  • 코드 분석 도구로 다이어그램과 클래스 관계를 생성해 전체 구조를 파악하는 아이디어도 있음
  • 소스 제어 시스템이 없다면 0단계는 코드를 VCS에 넣는 것임
  • CI를 1단계로 봐야 한다는 의견도 있으며, 로컬이 더 빠르더라도 타당한 관점임
  • 거의 쓰이지 않는 플랫폼을 제거하면 조합 복잡도가 줄고 큰 단순화가 가능할 수 있음
  • 완전한 재현 가능한 빌드는 일반적인 C++ 코드베이스에서 현실적이지 않을 수 있지만, 신뢰할 수 있는 빌드는 현실적임
  • 전체 코드 포맷팅 커밋은 git이 특정 커밋을 blame에서 무시하도록 설정해 이력 추적 부담을 줄일 수 있음
  • VCS 이력 통계로 변경이 잦은 부분과 함께 바뀌는 파일을 찾는 아이디어도 있음
  • 이 접근은 C++뿐 아니라 다른 레거시 코드베이스에도 적용할 수 있으며, 새니타이저 같은 C++ 특화 부분은 제외하면 됨
  • Working effectively with Legacy Code는 관련 조언을 담은 책으로 추천됨
  • 실제 가치에 집중해야 하지만, 큰 C++ 코드베이스를 보안 관점에서 보면 많은 취약점이 발견될 수 있고 이는 재무 이익이 아니라 위험 감소로서 가치가 있음

댓글과 토론

Hacker News 의견들
  • 좋은 조언도 있고, 다소 논쟁적인 조언도 있음. 여러 거대한 C++ 프로젝트를 물려받아 보니 초기에 확실히 이득이 큰 작업은 몇 가지로 보임
    재현 가능한 빌드부터 만들고, Docker나 선호하는 패키징 도구로 빌드 환경을 감싸 도구와 의존성을 명시적이고 재현 가능하게 해야 함
    -Wall로 경고 없이 빌드되게 만들면 나쁜 코드, 정의되지 않은 동작, 버그가 드러나고, 이후 내가 수상한 코드를 넣었을 때 즉시 경고를 볼 수 있음
    초기에 valgrind 같은 도구로 읽기/쓰기 오류를 확인하는 것도 안정성 측면에서 쉬운 이득이고, 전체 구조를 이해하기 전에는 리팩터링을 국소적으로 유지하는 편이 안전함

    • 명시성과 재현성을 원한다면 Docker부터 잡는 건 피하는 게 좋음. 신경을 많이 쓰지 않으면 Docker는 가장 희석된 형태의 재현성만 주고, 오히려 안전하다는 착각을 줄 수 있음
      무결성 해시 없이 변하는 이미지 태그를 가리키거나 apt-get을 호출하는 Dockerfile은 흔하고, 둘 다 미묘하게 다른 상태로 끝날 여지를 크게 남김
      배우기는 더 어렵지만 Bazel이나 Nix 같은 해법이 훨씬 더 나은 기반을 줌
    • 0단계는 재현 가능한 빌드이고, 1단계는 모든 테스트를 돌려 불안정한 테스트를 표시하는 것임
      2단계는 모든 테스트를 새니타이저 아래에서 돌려 실패하는 항목을 표시하고, 3단계는 그 새니타이저 실패를 모두 고치는 것, 4단계가 나머지 작업에 해당함
    • 마이크로서비스에는 Docker가 괜찮지만, 항상 모든 것을 Docker로 배포할 수는 없음. 특히 앱을 Docker 안에서 쓰려는 사람에게는 더 그렇고, Docker-in-Docker는 없어야 할 상황임
      컨테이너는 좋지만 문제가 없는 척하기 위한 방식으로는 끔찍함
      의존성을 모두 묶고, /usr/lib에 수십억 개가 올바른 버전으로 깔려 있어야만 동작하는 상태를 피해야 함
    • -Wall로 깨끗하게 빌드하는 건 괜찮지만, 운영 빌드에 -Wall -Werror를 넣는 건 강하게 반대함
      컴파일러 경고 중 일부는 의견에 가깝고, 새 컴파일러 버전이 새 경고를 추가하면 예전에는 깨끗하던 코드가 갑자기 거부될 수 있음
      -Werror가 필요하다면 디버그 빌드에서만 쓰는 편이 낫다
  • 2번과 3번의 순서를 바꾸겠음. CI, 린팅, 자동 포매팅 등을 먼저 갖추는 게 뭔가를 뜯어내는 것보다 우선임
    아직 무엇을 제거해야 할지, 제거했을 때 어떤 결과가 생길지 모르기 때문임
    린터와 정적 분석 도구는 프로그램의 어느 부분에 작업이 필요한지 많은 통찰을 줌
    오늘 정적 분석 도구가 표시하는 부분은 나중에 STL 개념을 재구현한 함수, 클래스, 파일 전체를 제거할 수 있는 곳인 경우가 많음
    직접 만든 반복자 라이브러리, 직접 만든 스마트 포인터, C 문자열 함수 사용 등을 STL 알고리즘, 실제 스마트 포인터, C++ 문자열 클래스로 바꿀 수 있음
    하지만 코드를 스캔하기 전에는 잘 보이지 않고, 빠른 테스트 빌드나 배포 쪽으로 밀어붙이기 전에는 결과도 평가하기 어려움

    • 에이전시 일을 하며 코드베이스를 구하러 들어간 경험이 꽤 있는데, 가장 먼저 단위 테스트를 돌리고 커버리지를 확인함
      빠진 곳에는 기본 스모크 테스트를 추가함
      이 작업은 느리게 만드는 게 아니라 오히려 빠르게 해줌. 적당한 커버리지를 확보하면 리팩터링할 때 훨씬 더 빠르게 움직일 수 있어서 작은 투자 대비 보상이 큼
    • 반대로 자동 포매팅은 버전 이력을 망가뜨리고, 이 줄이 언제 왜 추가됐는지 분석하기 어렵게 만듦
    • 레거시 코드베이스에 자동 포매팅을 적용하는 건 절대 권하지 않음
      큰 C++ 프로젝트에는 코드 생성 스크립트뿐 아니라 보통 코드 생성용 데이터를 모으려고 코드를 파싱하는 스크립트도 있음
      자동 포매팅이 이를 깨뜨릴 수 있고, 심지어 사용자가 공개 헤더 파일을 취약한 스크립트로 파싱하는 저주받은 프로젝트도 본 적 있음
    • 여기서 CI는 다른 항목과 다름. 최소한 재현 가능한 결과로 실행되고 모든 커밋마다 도는 정상 경로 테스트 하네스를 만드는 것이 첫 단계이며, 코드베이스를 이해하는 데도 도움이 됨
      여기저기 건드리게 될 가능성이 높고, 로컬에 변경이 많이 쌓이다 보면 관심사가 섞인 커밋을 실수로 만들 수 있음
      그때 CI는 구세주가 됨
    • 세부적으로는 원문에서 뜯어내라고 한 범위가 컴파일러 경고와 지원하지 않는 아키텍처로 가이드되는 죽은 코드에 한정되어 있음
      그 길로 간다면 최소한 브랜치를 squash 후 병합할 준비가 될 때까지는 바로 삭제하지 말고 주석 처리해서 diff를 단순하게 유지하는 걸 권함
  • 코드 이해 도구와 기법이 빠져 있음. 예전에 Source Navigator라는 Tcl/Tk로 작성된 도구를 썼는데, 코드베이스 색인에 아주 좋았음
    현재 메서드의 호출 계층을 확인하고, 이를 이용해 UML 시퀀스 다이어그램을 만들 수 있었음
    비슷한 도구로 아래의 Source Insight도 있음
    그리고 노트가 중요함. 누군가를 가르치듯이 쓰는 게 핵심임
    수년 동안 코드 이해를 꽤 잘하게 됐고, 한때는 하루 약 2억 달러 규모를 4~5개 거래소에서 거래하는 Java 알고리즘 트레이딩 코드베이스를 혼자 적극적으로 지원하고 개발했음
    그 코드에 대한 문서가 35MB나 있었고, 핵심 인력 리스크는 제쳐두면 책임감이 좋았음
    솔직히 대부분의 대형 코드베이스에는 과설계와 중복이 많음
    [1] References in "Source Insight" https://d4.alternativeto.net/6S4rr6_0rutCUWnpHNhVq7HMs8GTBs6...

    • Source Navigator를 실제로 써본 사람을 야생에서 만나다니 믿기지 않음
      대학 컴퓨터 구조 수업에서 이 유물을 강제로 쓰게 했는데, 다른 곳에는 없는 괴상한 기능 세트와 ARM 에뮬레이터 지원이 있었기 때문임
      우리는 베어메탈 ARM 어셈블리 프로그래밍에 썼음
  • std::cmake를 표준 라이브러리에 넣으면 게임 체인저라는 대목에서 속이 뒤집히는 느낌이 들었음
    회사나 오픈소스 프로젝트가 광고하고 판매하는 기능을 제공하는 데 절대적으로 필요하지 않은 것을 전기톱으로 다 뜯어내라는 조언은 이해하지만, 엄청나게 위험함
    본질적으로 체스터턴의 울타리 문제라서, 어떤 것이 왜 소프트웨어에 들어갔는지와 현재 소프트웨어가 어떻게 쓰이는지 완전히 이해하지 못하면 제거할 수 없음
    최악의 경우 한 달쯤 뒤 릴리스했을 때 사용자가 중요한 기능이 미묘하게 깨졌다는 걸 발견하고, 정확히 어떻게 깨졌는지 추적하느라 며칠을 쓰게 됨
    CI, 린터, 퍼징, 자동 포매팅 등을 추가하는 건 좋은 생각이지만 어렵기도 함
    한 명은 VIM, 다른 한 명은 emacs, 또 다른 사람은 QTCreator, 또 다른 사람은 VSCode를 주로 쓰는 식이면 모두를 같은 페이지에 올리기 매우 힘듦
    새 도구 설치가 필요한 선택 단계라면 실제로는 일어나지 않고, 린터도 프로젝트를 열자마자 경고 2000개 이상이 뜨면 도움이 안 됨

    • 저자가 나열한 것들은 보통 IDE 통합이 아님. C++ 개발 환경에서 cpplintclang-tidy, 퍼저가 IDE에 통합된 걸 본 적이 없고, 키 입력마다 자동 실행하기엔 너무 느림
      자동 포매팅만 가끔 통합됨
      이런 것들은 각자의 개발 환경과 무관하게 명령줄에서 할 수 있으니, 텍스트 편집기가 두 개 쓰인다는 이유로 시작 전부터 포기하지 말고 적어도 시도해야 함
      C++에서 팀이 아무 도구도 설치하지 않겠다면 고생길이 열림. 도구를 컨테이너화해서 쉽게 만드는 방안도 고려할 만함
    • 한 명은 VIM, 다른 사람은 emacs, QTCreator, VSCode를 쓴다는 이유가 더는 받아들일 만한 답이 아니라고 봄
      우리는 프로답게 일해야 하고, 일이 IDE부터 도구 체인을 갖춰야 한다면 배워서 써야 함
      내 컴퓨터, 내 IDE, 내가 좋아하는 방식에서 빌드되고 동작한다는 건 소프트웨어가 아니라 공예품에 가까움
    • 편집기 밖에서도 자동 포매팅을 할 수 있음. pre-commit을 설정하고 CI에서 돌리면 되며, 사소한 일임
      로컬 훅 설치가 선택이라 해도 PR이 CI에서 실패하게 만들면 결국 하게 됨
      이런 건 정말 기본적인데, CI와 인프라를 제대로 다루는 지식 격차가 꽤 커 보임
    • 상사가 요구하는 기본 사항을 그냥 거부해도 된다는 메모를 놓친 모양임. 월급을 주는 그 사람 말임
    • 로컬의 선택 단계인 pre-commit 훅은 CI의 필수 단계로 뒷받침해야 함
      테스트, 린트, 퍼징, 포매팅, YAML 형식 검증, EOF 줄바꿈 누락 확인 등을 로컬에서 실행할 수 있게 해 개발자가 push 전에 CI 실패를 막도록 도와야 함
      린터가 프로젝트를 열 때 수천 개 경고를 낸다면, 린터를 추가하는 개발자가 그 변경을 병합하기 전에 경고가 0개가 되도록 해야 함
      특정 경고나 파일을 비활성화하거나, 일부를 수정하거나, 이를 조합해서 달성할 수 있음
  • 첫 단계는 이전 유지보수자에게 연락해서 찾아가 차나 맥주를 사주고, 결국 코드베이스에 대해 이야기하는 것이라고 봄
    옛 유지보수자라는 마법사들이 많은 걸 알려줄 수 있음
    나머지 제안들, 예를 들어 여러 플랫폼에서 실행되게 하거나 테스트를 통과시키는 일은 견고함과 이해로 이어지는 좋은 스트레스 테스트라고 생각함
    그래도 먼저 지나간 사람들과 대화하는 낮게 달린 열매를 꼭 따러 갈 것 같음

    • 그걸 첫 단계로 삼지는 않겠음. 그러면 그들의 시간을 낭비할 가능성이 큼
      먼저 조금 직접 작업해 보고 여러 곳에서 막힌 뒤에 이전 유지보수자와 이야기하면 훨씬 생산적임
      그들도 그 노력을 고맙게 여길 것임
    • 먼저 코드베이스를 빠르게 훑어서 가장 큰 WTF를 찾아낸 뒤 질문하는 게 좋을 수 있음
      테스트가 없고, 빌드가 두 번에 한 번 실패하고, 의존성 정보가 불명확하며, 심하게 오래된 OS가 깔린 단일 서버에서만 빌드되는 코드베이스를 물려받았다면 이전 유지보수자가 진짜 마법사인지 확신할 수 없음
      모든 문제가 시간이 부족해서 생긴 것인지, 아니면 직업 안정성을 위해 망가진 채 유지했거나 새것을 배우기 싫어한 “마법사”인지도 따져봐야 함
    • 레거시 코드에서 이전 유지보수자에게 물어본 적이 있다면 알겠지만, 인수인계가 어제였더라도 6개월보다 오래전에 한 유용한 일은 거의 말해주지 못함
      그게 최선의 경우이고, 흔한 경우는 모든 질문에 “나도 넘겨받을 때 이미 그랬다”는 답만 돌아오는 것임
    • 군사용 폐쇄망에서 오래된 맞춤형 OS 위에 소프트웨어를 배포하는 일을 맡은 적이 있음. 큰 프로그램은 아니고 약 5만 줄이었음
      작업 중 여러 버그와 문제를 만나서, 고용주를 위해 맞춤 제작한 그 소프트웨어를 작성한 개발자들에게 연락하려 했음
      알고 보니 한 명의 계약자가 작성했고, 그는 몇 년 전에 사망했음
      방위 산업에서는 이런 일이 흔함. 특정 시스템을 위해 만든 맞춤형 일회성 물건이 많고, 특히 하드웨어 쪽에서는 장비를 만든 엔지니어가 오래전에 떠났거나 은퇴한 경우가 드물지 않음
    • 말처럼 쉽지 않음. 내 0단계는 UML 도구에 넣어 클래스 다이어그램과 다른 다이어그램을 뽑는 것임
      이게 큰 도움이 됨
      C++ 코드베이스에 테스트가 있다니, 낙관적이라고 봄
  • 왜 리팩터링이나 개선에 이렇게 초점이 많은지 잘 모르겠음. 기능을 코드에 덧붙여 추가할 수 있다면 다른 건 건드리지 말고 그렇게 하면 됨
    충분히 큰 변경이라면 레거시 코드에서 필요한 것을 외부 함수 호출, 네트워크 계층 도입, 동일 코드를 라이브러리로 분리하는 식으로 빼내고, 나머지는 새 환경에서 하면 됨
    앞으로 여러 사람이 그 코드를 작업해야 하고 그룹이 쉽게 협업할 수 있도록 특정 가정과 표준이 필요하지 않은 한, 큰 리팩터링은 시도하지 않겠음

    • 원문은 큰 리팩터링에 반대하고 있음. 제안하는 점진적 작업들은 코드를 점점 다루기 쉽게 만드는 쪽임
      덧붙이는 방식은 어느 순간까지는 통하지만, 변경했을 때 갑자기 무언가 깨지고 너무 어질러져 있어서 원인을 필요 이상으로 오래 못 찾는 상황이 옴
      원글의 요지는 큰 재작성은 피하되 응집도를 유지하면서 변경을 쉽게 하기 위해 정리 정돈하자는 것에 가까움
    • 정답은 미래에 달려 있음. 대체품이 이미 시장에 나와 있지만 구버전 코드를 몇 번 더 릴리스해야 하는 C++ 코드를 작업한 적이 있음
      때로는 같은 기능을 양쪽 버전에 모두 추가해야 함
      마지막 릴리스가 곧 올 걸 아는 코드와 앞으로 수십 년 더 유지보수하고 기능을 추가할 코드에는 대하는 방식이 크게 다름
    • 운 좋은 몇몇 경우에는 기능을 그냥 덧붙일 수 있음
      현실에서는 새 기능이 이미 존재하는 동작의 변경인 경우가 많고, 갑자기 여러 곳에서 무거운 리팩터링을 해야 하게 됨
  • 좋은 조언이 많은 스레드임. C/C++에 한정하지 않고 덧붙이자면, 버전 관리 시스템을 쓸 수 있는 여유가 있다면 그 가치를 충분히 활용해야 함
    많은 팀이 단순 협업 도구로만 쓰지만, 그 이상을 할 수 있음
    이력을 가져와 간단한 데이터베이스를 만들면 됨. RDB일 필요는 없고 JSON 파일이나 스프레드시트로도 시작할 수 있음
    데이터 기반 접근만으로도 거의 즉시 유용한 정보를 많이 뽑을 수 있음
    자주 변경된 파일과 함수는 앞으로 작업의 핫스팟일 가능성이 높으니, 단위 테스트를 도입하거나 병합 충돌을 줄이려면 거기에 집중하면 됨
    서로 멀어 보이는 파일들이 자주 함께 바뀐다면 코드만 봐서는 드러나지 않는 암묵적 구조를 암시할 수 있음
    각 모듈의 실제 소유권 모델도 이력에서 추론할 수 있고, 불명확한 소유권은 리팩터링 필요성을 알리는 신호가 될 수 있음
    C/C++에서는 빌드 시간 개선도 데이터 기반으로 중요한 모듈에 집중해야 함. 무작정 임의 파일 의존성을 제거하기보다 자주 바뀌는 모듈을 쪼개고, 헤더 의존성과 결합해 실제 빌드 시간 영향도를 점수화할 수도 있음
    다른 개발 도구와 VCS를 통합하면 더 많은 걸 할 수 있고, LLM 시대에는 프로젝트 이력과 메타데이터를 모델에 넣어 흥미로운 통찰을 물어볼 수도 있을 것 같음
    거대한 컨텍스트 창 없이 하려면 전용 모델 엔지니어링이 필요할 수 있지만, 시도해볼 가치가 있다는 직감이 듦

    • 이런 분석을 자동화하는 데 도움이 되는 소프트웨어 팁이 있는지 궁금함
  • CI, 린터, 퍼징, 자동 포매팅 등을 추가하라는 조언은 더 쪼개서 봐야 함
    CI는 내 컴퓨터뿐 아니라 다른 곳에서도 빌드됨을 보장해 컴파일 기반 회귀를 막아야 함
    컴파일러 경고와 정적 분석기는 대체로 나보다 똑똑하므로, 포인터로 이상한 일을 해서 무섭다는 경고가 나오면 확인할 강한 신호임
    단위 테스트는 중요한 코드가 낮은 수준까지 정확히 기대한 일을 하는지 검증해야 하고, 실제로 그렇지 않을 가능성이 높으므로 이유를 이해해야 함
    어떤 걸 고치면 다른 것이 터질 수 있는데, 기존 코드가 버그 있는 동작을 전제로 작성됐을 수 있기 때문임
    자동 포매팅은 우선순위가 아니고, 기존 유지보수자의 스타일을 따르는 편이 낫다고 봄
    물려받은 C++ 코드베이스의 마지막 단계가 메모리 안전 언어로 재작성이라는 것도 잘 안 맞을 가능성이 큼
    고장 나지 않은 것에 추가 작업 리소스를 받기 어렵고, C++뿐 아니라 추가 언어 지식도 필요해지며, 테스트가 더 복잡해질 수 있음
    메모리나 성능 제약 때문에 여러 언어로 쓰기 적합하지 않을 가능성도 크고, 애초에 레거시 코드베이스를 물려받았다는 건 시간, 돈, 지식 같은 자원이 부족해 새로 쓰지 못했다는 인정에 가깝음

    • 실제로 메모리 안전하지 않은 C++ 를 제거하고 지침을 강제하는 것만으로도 C++ 코드베이스 안에서 이 목적을 달성할 수 있음
      “X로 재작성”은 유행이라는 이유로 복잡도만 더함
      이미 C++로 코드베이스의 큰 부분을 다시 쓰고 있다면, 더 제한적인 C++ 부분집합을 따르는 편이 낫고 High Integrity C++가 괜찮다고 봄
      최신 MISRA 표준을 구할 수 있다면 그것도 좋을 가능성이 큼
      새 언어의 날카로운 모서리를 팀 전체가 다시 배우게 하기보다, 이미 아는 언어를 쓰되 알려진 함정을 피하는 지침을 강제하는 편이 낫다
  • 저자가 BOM 자동화, 패키지 버전 관리, 의존성 출처 등을 많이 비판한 뒤 git submodules를 패키지 관리자보다 낫다고 제안하는 건 이상함
    이런 비판을 하기 전에 vcpkg를 써봐야 함
    약간의 날카로운 부분은 있지만, 거의 모두 vcpkg로 직관적으로 충족됨
    의존성 업데이트는 git submodules보다 조금 어렵지만, 이는 버그가 아니라 기능에 가깝다고 봄. 의존성은 개별 샌드박스에서 빌드된 뒤 지정 디렉터리에 설치됨
    vcpkg는 공식 저장소 대신 내부 저장소를 레지스트리로 설정해 vendored-in 성격을 유지할 수 있고, 도구 체인을 체인로드해 고정 플래그 세트로 모두 컴파일하게 하며, 포트별 사용자 지정도 허용함
    이런 추상화가 유용하기 때문에 패키지 관리자가 인기 있는 것이고, 모두가 컴파일 플래그, 매크로, 경고 등을 담은 끝없는 문자열을 직접 다루지 않아도 됨

  • 글은 재미있었고 배운 것도 있음. 다만 사람들이 “메모리 안전 언어로 재작성”이라고 할 때 어떤 언어를 말하는지 궁금했음
    Go, Java, C#으로 일부를 다시 쓰자는 건지, 아니면 부정 가능성을 남긴 채 Rust로 다시 쓰자는 식의 비꼼인지 궁금함

    • 저자로서 말하자면, 이 주제는 두 번째 글에서 다룰 예정임
      결론은 팀과 제약에 전적으로 달려 있음. 예를 들어 가비지 컬렉션이 가능한지, 그렇다면 Go가 좋은 선택인지, 보안이 최우선인지 등이 중요함
      대부분의 C++ 개발자는 대체로 Rust를 쉽게 사용할 수 있고 동등한 성능을 얻을 수 있다고 봄
      하지만 애초에 프로젝트가 C++이어야 할 좋은 이유가 없던 경우도 있고, Java로 성공적으로 재작성한 사례도 봤음
      Apple은 일부 C++ 코드를 Swift로 다시 쓰고 있음
      팀이나 회사가 편하게 다룰 수 있는 언어가 좋은 경험칙임
    • C++에 대한 글이었고 “Rust”라는 단어는 한 번도 나오지 않았으며, 수십 가지가 있는 “memory safe” 언어만 언급됐는데도 Rust 밈을 끼워 넣어 깎아내리는 방식이 흥미로움
      이제 작성자가 사악한 암호화된 Rust 프로그래머인지 의심하는 단계까지 온 셈임. 애초에 Rust가 나오지 않았으니 직접 불평할 수 없었기 때문이겠지만 말임