자유게시판
일반

대규모 프로젝트에서 코드를 함부로 바꾸면 안 되는 이유

중년개발자
중년개발자

@loxo

약 22시간 전

3

대규모 코드베이스를 이해한다는 착각, 그리고 LLM이 바꾸는 일하는 방식

대규모 프로젝트 또는 유지 보수 처음 들어가면 이상한 경험을 하게 됩니다.

분명 소스는 열려 있고, 클래스 이름도 읽을 수 있고, 메서드도 따라갈 수 있습니다.
그런데 작은 기능 하나를 고치려고 하면 생각보다 훨씬 많은 질문이 생깁니다.

  • 왜 이 검증은 여기서 하지 않고 세 단계 앞에서 하는가?
  • 동일해 보이는 코드가 왜 모듈마다 조금씩 다른가?
  • 이 조건문은 정말 필요한가?
  • 사용되지 않는 것처럼 보이는 필드는 왜 아무도 지우지 않는가?
  • 더 깔끔한 방법이 있는데 왜 이렇게 복잡하게 구현했는가?

이때 개발자는 흔히 두 가지 착각에 빠집니다.

첫 번째는 **“전체 구조를 충분히 이해한 다음 수정해야 한다”**는 생각이고,
두 번째는 **“기존 코드보다 더 좋은 방식으로 새롭게 구현하면 된다”**는 생각입니다.

Sean Goedecke의 두 글은 이 두 착각을 동시에 깨뜨립니다.


1. 대규모 코드에서 가장 위험한 실수는 나쁜 코드가 아니라 불일치다

첫 번째 글의 가장 중요한 주장은 단순합니다.

대규모 코드베이스에서 가장 치명적인 실수는 기존 코드가 마음에 들지 않는다고 혼자 더 좋은 방식으로 구현하는 것이다.

수백 명이 십여 년 동안 개발한 시스템에서 코드는 단순한 구현물이 아닙니다.
그 안에는 문서에 적히지 않은 수많은 예외와 운영 경험이 들어 있습니다.

예를 들어 새로운 API를 만든다고 생각해보겠습니다.

기존 인증 로직이 복잡해 보입니다.

text
Controller → AuthorizationService → PermissionResolver → LegacyUserPolicy → ProxyUserChecker

새로 들어온 개발자는 이렇게 생각할 수 있습니다.

“그냥 현재 사용자 타입을 확인하고 아니면 403을 반환하면 되는 것 아닌가?”

코드는 훨씬 짧아집니다.

java
if (!currentUser.isAllowedType()) { throw new ForbiddenException(); }

로컬 테스트도 통과합니다.
일반 사용자로 테스트했을 때도 잘 동작합니다.

하지만 운영에는 다음과 같은 사용자가 존재할 수 있습니다.

  • 고객을 대신해 로그인한 상담원
  • 서비스 간 호출에 사용하는 시스템 계정
  • 배치용 가상 사용자
  • 과거 시스템에서 이전된 특수 계정
  • 특정 기업 고객에게만 적용되는 권한 체계
  • 장애 대응을 위해 임시로 허용되는 운영 계정

기존 인증 헬퍼가 복잡했던 것은 개발자가 실력이 부족해서가 아니라, 오랜 운영 과정에서 이런 예외를 흡수했기 때문일 수 있습니다.

Sean은 기존 구현을 지뢰밭을 통과한 안전한 길에 비유합니다.
기존 패턴을 따른다는 것은 모든 지뢰의 위치를 이해한다는 뜻이 아닙니다. 먼저 지나간 사람의 발자국을 따라가는 것입니다.


2. “더 좋은 코드”가 항상 더 좋은 변경은 아니다

작은 프로젝트에서는 보통 다음 기준이 중요합니다.

  • 코드가 간결한가
  • 추상화가 적절한가
  • 테스트하기 쉬운가
  • 최신 기술을 사용하는가
  • 구조가 아름다운가

그러나 대규모 운영 시스템에서는 한 가지 기준이 더 앞에 옵니다.

기존 시스템의 행동과 얼마나 일관적인가?

새로운 구현이 기술적으로 더 아름다워도, 시스템 안에서 혼자 다른 방식으로 작동한다면 장기적으로는 더 큰 비용을 만듭니다.

예를 들어 200개의 API가 모두 공통 인증 헬퍼를 사용한다면, 새로운 사용자 유형을 추가할 때 헬퍼 하나를 수정하면 됩니다.

그런데 20개의 API가 각자 독자적인 인증 로직을 구현했다면 어떻게 될까요?

새로운 정책을 적용할 때마다 20개 구현을 찾아야 합니다.
하나를 빠뜨리면 특정 API에서만 권한 오류가 발생합니다.
그 오류를 수정하면서 또 다른 예외 구현이 추가됩니다.

그렇게 코드베이스는 어느 순간부터 다음과 같은 상태가 됩니다.

text
대부분의 API에서는 동작함 일부 API에서는 동작하지 않음 특정 고객에게만 다르게 동작함 왜 다른지는 아무도 정확히 모름

Sean은 대규모 코드베이스에서 일반적인 개선이 실패하는 이유도 불일치 때문이라고 설명합니다. 가장 어려운 일부 코드가 변경 대상에서 빠지고, 그 결과 일관성이 더 낮아지는 악순환이 만들어집니다.


실제 프로젝트에서 자주 만나는 장면

“이 코드는 왜 이렇게 짰지?”

은행 프로젝트에 새로 투입된 개발자가 이체 처리 코드를 본다고 해보겠습니다.

java
if (request.isSameBank()) { if (account.isLegacyAccount()) { legacyTransferService.transfer(request); } else { transferService.transfer(request); } }

개발자는 생각합니다.

“서비스를 인터페이스로 통합하고 전략 패턴을 쓰면 훨씬 깔끔하겠는데?”

틀린 생각은 아닙니다.

하지만 실제로 확인해야 하는 것은 코드 모양보다 행동입니다.

  • 구계정은 거래 일자가 다르게 계산되는가?
  • 일자 전환 시간에 별도 처리가 있는가?
  • 장애 시 전문 재전송 규칙이 다른가?
  • 회계 원장 반영 시점이 다른가?
  • 거래 취소 방식이 다른가?
  • 감사 로그의 필드 구성이 다른가?
  • 일부 분기에서는 외부 시스템의 응답을 기다리지 않는가?

표면적으로 중복인 코드를 합치는 순간, 서로 달랐던 업무 의미까지 합쳐질 수 있습니다.

대규모 프로젝트에서는 “이 코드는 왜 이렇게 짰지?”보다 먼저 물어야 할 질문이 있습니다.

“이 코드가 이렇게 남아 있어야 했던 운영상의 이유가 무엇이지?”

모든 이상한 코드에 깊은 이유가 있는 것은 아닙니다.
그냥 나쁜 코드일 수도 있습니다.

하지만 대규모 시스템에서는 이유가 없는 것처럼 보이는 코드도, 함부로 이유가 없다고 결론 내리면 안 됩니다.


3. 대규모 코드베이스 전체를 이해하는 것은 불가능하다

두 번째 글은 한 걸음 더 나아갑니다.

대규모 코드베이스에서는 불완전한 이해가 실패 상태가 아니라 정상 상태다.

작은 프로젝트에서는 한 사람이 전체 구조를 머릿속에 넣을 수 있습니다.

text
요청 → Controller → Service → Repository → Database

하지만 대규모 시스템은 다릅니다.

text
사용자 요청 → API Gateway → 인증 → 권한 → 상품 서비스 → 고객 서비스 → 캐시 → 메시지 큐 → 배치 → 외부 기관 → 원장 → 감사 로그 → 정산 → 모니터링

각 단계 안에도 다시 수십 개의 분기와 예외가 존재합니다.

이런 시스템을 완전히 이해하겠다는 목표는 사실상 달성하기 어렵습니다.
심지어 오랫동안 근무한 개발자도 전체 시스템이 아니라 자신이 자주 다루는 영역을 중심으로 부분적인 모델을 가지고 있습니다.

Sean은 충분히 큰 시스템에서는 모든 사람이 어느 정도 잘못된 시스템 이론을 갖고 있다고 설명합니다. 시스템이 너무 크기 때문에 개인이나 한 팀이 전체를 머릿속에 유지할 수 없으며, 실제 역량은 불완전한 이해를 가지고도 안전하게 일하는 방법에 달려 있다는 주장입니다.


실제로는 전체를 이해하지 않고도 개발한다

신규 개발자가 입사 첫날부터 전체 시스템을 이해하는 일은 없습니다.

대개 이런 방식으로 시작합니다.

text
로그인 요청 하나를 따라간다. 로그인 과정에서 호출되는 서비스들을 본다. 권한 확인 구조를 발견한다. 사용자 정보 캐시를 알게 된다. 특정 계정 유형만 예외 처리되는 것을 본다. 감사 로그와 보안 정책까지 이해 범위가 넓어진다.

처음부터 지도를 전부 그리는 것이 아닙니다.

작은 길 하나를 끝까지 걸어본 다음, 그 길에서 옆길을 발견하는 방식입니다.

Sean은 아무도 아는 사람이 남아 있지 않은 시스템도 충분히 되살릴 수 있다고 말합니다. 하나의 흐름을 처음부터 끝까지 이해한 뒤, 거기에서 조금씩 이해 범위를 확장하면 새로운 시스템 이론을 구축할 수 있다는 것입니다.

이 방식은 실제 프로젝트에서도 매우 현실적입니다.

전체 소스를 읽는 것보다 다음 질문을 좁게 따라가는 편이 훨씬 효과적입니다.

text
이 요청은 어디에서 들어오는가? 누가 이 메서드를 호출하는가? 이 값은 어디에서 만들어지는가? 이 분기는 운영에서 실제로 실행되는가? 실패하면 누가 재처리하는가? 결과는 어디에 기록되는가?

4. 대규모 소스 변경은 코딩보다 영향 범위 관리다

작은 프로젝트에서 변경의 중심은 구현입니다.

text
무엇을 만들 것인가? 어떻게 구현할 것인가?

대규모 프로젝트에서 변경의 중심은 영향 범위입니다.

text
누가 이 코드를 사용하고 있는가? 어떤 흐름에 연결되어 있는가? 운영 데이터에서는 어떤 상태가 존재하는가? 실패했을 때 어디까지 전파되는가? 되돌릴 수 있는가? 관찰할 수 있는가?

그래서 대규모 변경은 단순히 소스를 수정하는 일이 아닙니다.

변경 전

  • 기존 유사 구현을 찾는다.
  • 호출자와 피호출자를 확인한다.
  • 운영에서의 실제 호출 빈도를 확인한다.
  • 핵심 경로인지 확인한다.
  • 데이터 상태와 과거 호환성을 조사한다.
  • 관련 팀과 소유자를 찾는다.

변경 중

  • 기존 패턴을 가능한 한 따른다.
  • 변경 범위를 작게 유지한다.
  • 다른 팀 영역의 변경을 먼저 분리한다.
  • 기능 플래그나 병행 실행을 고려한다.
  • 로그와 지표를 먼저 추가한다.

변경 후

  • 일부 사용자나 트래픽부터 적용한다.
  • 오류율과 지연 시간을 본다.
  • 기존 로직과 결과를 비교한다.
  • 롤백 가능성을 유지한다.
  • 사용되지 않는 코드를 충분히 관찰한 뒤 제거한다.

대규모 프로젝트는 가능한 모든 상태를 개발 환경에서 재현하기 어렵습니다. 따라서 핵심 경로를 테스트하고, 방어적으로 코드를 작성하며, 점진적 배포와 모니터링으로 남은 위험을 관리해야 한다는 것이 첫 번째 글의 조언입니다.


5. 큰 변경일수록 한 번에 완성하려 하지 말아야 한다

예를 들어 오래된 결제 모듈을 새로운 방식으로 바꾼다고 해보겠습니다.

처음부터 이렇게 계획하면 위험합니다.

text
기존 결제 모듈 제거 새 결제 모듈 구현 모든 호출부 교체 한 번에 배포

현실적인 접근은 다음과 같습니다.

text
1. 기존 호출 흐름에 관찰 로그 추가 2. 실제 사용 중인 분기와 미사용 분기 구분 3. 새 인터페이스를 기존 코드 앞에 배치 4. 기존 구현을 어댑터로 연결 5. 일부 요청만 새 구현으로 전환 6. 신·구 결과를 비교 7. 트래픽을 점진적으로 확대 8. 기존 호출자를 0으로 만든 뒤 제거

중요한 것은 새 시스템을 만드는 행위가 아닙니다.

기존 시스템에서 새 시스템으로 안전하게 이동하는 과정 자체가 개발의 핵심입니다.

Sean도 성공적인 재작성은 기존 시스템을 통째로 버리는 방식이 아니라, 작은 격리 단위로 나눈 뒤 하나씩 교체하는 방식이라고 설명합니다. 기존 시스템을 안전하게 변경할 수 없다면, 그것을 안전하게 대체하는 것도 불가능하다는 주장입니다.


6. “마이크로서비스로 분리하면 해결된다”는 착각

대규모 모놀리스를 보면 흔히 이런 이야기가 나옵니다.

“이걸 서비스별로 잘게 나누면 구조가 깨끗해질 텐데.”

기술적으로는 가능할 수 있습니다.

하지만 먼저 알아야 할 것이 있습니다.

  • 어디까지가 하나의 트랜잭션인가?
  • 데이터 정합성은 언제 보장되는가?
  • 장애 시 재시도 책임은 누가 가지는가?
  • 서비스 사이의 시간 차이를 허용할 수 있는가?
  • 기존에는 메서드 호출이던 것이 네트워크 호출이 되어도 괜찮은가?
  • 운영자가 한 화면에서 처리하던 업무가 여러 서비스로 나뉘어도 되는가?
  • 과거 데이터는 어느 시스템이 소유하는가?

모놀리스를 분리한다는 것은 코드를 디렉터리별로 옮기는 일이 아닙니다.

암묵적으로 결합되어 있던 책임을 명시적인 계약으로 바꾸는 일입니다.

그래서 큰 시스템을 가장 잘 분리할 수 있는 사람은 대개 모놀리스를 싫어하는 사람이 아니라, 그 모놀리스 안에서 여러 번 기능을 출시해본 사람입니다.

기존 시스템의 이상한 제약과 중요한 예외를 경험하지 못했다면, 분리된 새 시스템에서도 똑같은 문제를 다시 만들 가능성이 높습니다.

첫 번째 글 역시 큰 코드베이스를 분해하려면 먼저 그 안에서 기능을 배포할 수 있을 만큼 익숙해져야 한다고 강조합니다. 실제 수익을 만드는 시스템에는 처음부터 설계해서는 쉽게 발견할 수 없는 세부 조건들이 축적되어 있기 때문입니다.


7. LLM은 대규모 코드베이스 이해에 분명히 도움이 된다

저 역시 이 부분에는 동의합니다.

다만 LLM의 가치는 전체 시스템을 대신 이해해주는 것이 아니라,
개발자가 필요한 부분적인 이해를 더 빠르게 만드는 데 있습니다.

대규모 소스에서 개발자가 가장 많은 시간을 쓰는 일은 단순히 코드를 읽는 것이 아닙니다.

text
어디부터 읽어야 하는지 찾기 관련 파일들을 모으기 호출 관계를 따라가기 이름이 다른 유사 기능을 찾기 설정과 환경별 분기를 비교하기 코드에 흩어진 규칙을 하나의 설명으로 만들기

LLM은 이 탐색 과정의 비용을 크게 줄일 수 있습니다.

Sean도 LLM이 상세한 시스템 이론을 만드는 과정을 방해할 수 있지만, 반대로 부분적인 이해를 빠르게 만들고 그 불완전한 이해를 더 효과적으로 활용하게 해준다고 평가합니다. 즉, 장점과 손실이 동시에 있는 도구라는 시각입니다.


사례 1. 호출 흐름을 처음 파악할 때

신규 개발자가 다음 업무를 받았다고 해보겠습니다.

“해외송금 신청 시 특정 국가에 추가 검증을 넣어주세요.”

검색 결과 관련 파일이 70개 나옵니다.

  • TransferController
  • OverseasTransferService
  • RemittanceValidationService
  • CountryPolicyService
  • ComplianceService
  • AMLCheckService
  • TransferBatchProcessor
  • LegacyTransferAdapter

기존 방식이라면 여러 파일을 열고 메서드를 직접 따라가야 합니다.

LLM에 관련 소스를 제공하고 다음처럼 요청할 수 있습니다.

text
해외송금 신청 요청이 들어온 뒤 최종 저장될 때까지의 호출 흐름을 정리해줘. 각 단계마다 다음을 표시해줘. 1. 클래스와 메서드 2. 주요 입력과 출력 3. 검증이 실행되는 위치 4. 외부 시스템 호출 5. 트랜잭션 경계 6. 예외가 변환되는 위치 7. 국가 코드가 변경되거나 정규화되는 위치

LLM은 흩어진 코드를 읽고 첫 번째 탐색 지도를 만들어줄 수 있습니다.

text
TransferController → OverseasTransferService.apply() → CountryCodeNormalizer.normalize() → RemittanceValidationService.validate() → AMLCheckService.check() → TransferRepository.save() → TransferEventPublisher.publish()

이 지도는 완벽하지 않을 수 있습니다.
하지만 개발자는 이제 70개 파일을 무작위로 읽지 않고, 중요한 7개 파일부터 확인할 수 있습니다.

이것이 LLM의 첫 번째 가치입니다.

정답을 주는 것이 아니라, 읽어야 할 순서를 만들어준다.


사례 2. 비슷한 기존 구현을 찾을 때

대규모 코드에서 새 기능을 만들기 전에 가장 먼저 해야 할 일은 기존 사례를 찾는 것입니다.

그러나 실제 코드에서는 이름이 다를 수 있습니다.

예를 들어 “거래 한도 검증”이 다음처럼 흩어져 있을 수 있습니다.

text
validateTransferLimit() checkDailyThreshold() verifyMaximumAmount() applyCustomerGradePolicy() ensureAvailableQuota()

단순 문자열 검색으로는 모두 찾기 어렵습니다.

LLM 기반 코드 검색은 이름이 아니라 의미를 기준으로 질문할 수 있습니다.

text
이 코드베이스에서 사용자의 거래 금액을 제한하는 모든 로직을 찾아줘. 메서드 이름에 limit가 없어도 포함하고, 일일 한도, 건별 한도, 고객 등급별 한도, 국가별 한도, 상품별 한도를 구분해줘.

이런 검색은 대규모 코드베이스에서 매우 강력합니다.

왜냐하면 중요한 규칙은 항상 한 파일에 모여 있지 않기 때문입니다.


사례 3. 오래된 코드의 존재 이유를 추론할 때

다음과 같은 코드를 발견했다고 해보겠습니다.

java
if (transferDate.equals(LocalDate.of(2021, 12, 31))) { request.setLegacySequence(request.getSequence() + 1); }

누구라도 지우고 싶어집니다.

LLM에 해당 코드만 보여주면 아마 이렇게 말할 수 있습니다.

특정 날짜에만 적용되는 임시 대응 코드로 보이며 제거를 고려할 수 있습니다.

이 답변은 위험합니다.

대신 관련 정보를 함께 탐색해야 합니다.

text
이 조건문과 관련된 다음 항목을 조사해줘. - 해당 필드의 모든 참조 - 2021년 12월 31일과 관련된 주석 및 커밋 - legacySequence가 외부 전문에 포함되는지 - DB 저장 여부 - 배치 재처리에서 사용되는지 - 테스트에 해당 날짜가 존재하는지

LLM이 관련 코드를 묶어보면 이런 맥락이 드러날 수 있습니다.

text
2021년 말 외부기관의 일련번호 중복 장애로 인해 추가됨 해당 날짜의 거래를 재처리할 때 여전히 사용됨 일반 거래에서는 실행되지 않지만 과거 거래 복원에 필요함

LLM은 코드의 존재 이유를 확정할 수는 없지만,
관련 단서를 사람이 훨씬 빨리 모으도록 도울 수 있습니다.


사례 4. 변경 영향 범위를 조사할 때

공통 DTO의 필드 이름을 변경한다고 해보겠습니다.

java
private String customerType;

이를 다음처럼 바꾸려 합니다.

java
private CustomerType customerType;

겉으로는 간단한 타입 개선입니다.

하지만 실제 영향은 넓을 수 있습니다.

  • JSON 직렬화 형식
  • DB 컬럼 매핑
  • 메시지 큐 이벤트
  • 외부 API 전문
  • 캐시 키
  • 배치 파일
  • 프론트엔드 타입
  • 테스트 픽스처
  • 로그 분석 쿼리
  • 과거 이벤트 재처리

LLM에 다음과 같이 요구할 수 있습니다.

text
customerType 필드를 enum으로 변경할 때 영향을 받을 가능성이 있는 지점을 분류해줘. 직접 참조뿐 아니라 다음도 조사해줘. - JSON 필드명 - SQL 및 MyBatis 매핑 - Kafka 메시지 - Redis 저장값 - 문자열 비교 - 테스트 데이터 - MapStruct 매핑 - 파일 출력

LLM은 단순한 Find References보다 넓은 관점에서 영향 범위를 정리해줄 수 있습니다.


8. 하지만 LLM의 설명을 시스템의 진실로 받아들이면 안 된다

LLM은 코드를 읽고 그럴듯한 이야기를 만드는 데 매우 능숙합니다.

문제는 대규모 코드베이스 자체도 이미 불완전하다는 것입니다.

  • 사용되지 않는 코드가 남아 있다.
  • 코드와 운영 동작이 다르다.
  • 설정에 따라 다른 구현이 실행된다.
  • 리플렉션이나 동적 로딩이 사용된다.
  • SQL이나 배치 파일에 업무 규칙이 숨어 있다.
  • 외부 시스템이 실제 동작을 결정한다.
  • 특정 고객 데이터에서만 분기가 발생한다.

따라서 LLM이 코드를 완벽히 읽더라도 실제 시스템을 완전히 이해할 수는 없습니다.

예를 들어 LLM이 이렇게 설명할 수 있습니다.

text
이 메서드는 현재 호출되는 곳이 없으므로 제거할 수 있습니다.

그러나 실제로는 다음 방식으로 호출될 수 있습니다.

text
스프링 빈 이름 기반 호출 리플렉션 배치 작업 XML 외부 스크립트 DB에 저장된 클래스명 플러그인 로딩 운영자 수동 실행

그래서 LLM의 결론은 다음처럼 취급하는 편이 안전합니다.

text
사실: 코드에서 직접 확인됨 추론: 코드 구조상 가능성이 높음 미확인: 운영 로그나 외부 설정 확인 필요

LLM에게도 이 구분을 명시적으로 요구해야 합니다.

text
각 결론을 다음 세 단계로 나눠줘. - CONFIRMED: 제공된 코드에서 직접 확인됨 - INFERRED: 구조를 바탕으로 추론함 - UNKNOWN: 코드만으로 확인할 수 없음

9. LLM은 개발자의 이해를 대체하는 것이 아니라 이해의 범위를 조절한다

LLM이 없던 시절에는 대규모 시스템에서 하나의 기능을 이해하기 위해 많은 파일을 직접 읽어야 했습니다.

그 과정에서 우연히 주변 구조도 함께 배우게 됩니다.

반면 LLM을 사용하면 필요한 답에 빠르게 도달할 수 있습니다.

이것은 장점이지만 동시에 위험이기도 합니다.

text
LLM 없이 작업 → 느리지만 주변 맥락까지 자연스럽게 학습 LLM으로 작업 → 빠르지만 질문한 범위 밖의 맥락을 놓칠 수 있음

예를 들어 LLM에게 특정 오류를 수정해달라고 하면 정확한 코드 패치를 만들 수 있습니다.

그러나 개발자는 다음 사실을 모를 수 있습니다.

  • 이 로직이 매초 수천 번 실행되는 핵심 경로라는 것
  • 동일한 규칙이 배치에도 있다는 것
  • 장애 시 별도 보상 트랜잭션이 있다는 것
  • 다른 팀이 이 이벤트를 소비한다는 것
  • 이 변경이 월말 정산에 영향을 준다는 것

따라서 LLM을 사용할 때 질문은 단순히 이렇게 끝나면 안 됩니다.

text
이 버그를 수정해줘.

다음 질문까지 포함해야 합니다.

text
이 버그를 수정해줘. 그리고 수정 전에 다음을 분석해줘. 1. 이 메서드의 모든 진입 경로 2. 동일한 규칙을 구현한 다른 코드 3. 트랜잭션과 재시도 영향 4. 외부 시스템과 메시지 영향 5. 성능상 핵심 경로 여부 6. 호환성 위험 7. 추가해야 할 로그와 지표 8. 롤백 방법

LLM을 잘 활용한다는 것은 더 많은 코드를 생성하게 하는 것이 아닙니다.

놓칠 수 있는 질문을 대신 제기하도록 만드는 것입니다.


10. 대규모 코드 변경에서 제가 추가로 중요하다고 보는 원칙

변경 전에 먼저 관찰 가능성을 만든다

무엇을 바꿀지 결정하기 전에 현재 시스템이 실제로 어떻게 동작하는지 측정해야 합니다.

text
이 분기는 얼마나 호출되는가? 어떤 고객이 사용하는가? 실패율은 얼마인가? 평균 처리 시간은 얼마인가? 어떤 상태값이 실제로 존재하는가?

코드만 보고 판단하지 말고 로그, 메트릭, 트레이싱, DB 데이터를 함께 봐야 합니다.

특히 코드 제거는 검색 결과가 0이라는 이유로 진행하면 안 됩니다.

text
참조 검색 0건 ≠ 운영 사용 0건

먼저 계측하고, 일정 기간 실제 호출이 0인지 확인해야 합니다.


구조 변경과 행동 변경을 분리한다

대규모 PR에서 가장 위험한 형태는 다음이 섞여 있는 경우입니다.

text
파일 이동 클래스 이름 변경 공통화 비즈니스 규칙 변경 성능 개선 라이브러리 교체

문제가 발생하면 어느 변경이 원인인지 찾기 어렵습니다.

더 안전한 방식은 다음과 같습니다.

text
PR 1: 행동을 바꾸지 않고 구조만 정리 PR 2: 관찰 로그와 테스트 추가 PR 3: 새로운 로직을 비활성 상태로 추가 PR 4: 일부 트래픽에 적용 PR 5: 기존 로직 제거

변경량보다 의미 변화량을 작게 유지한다

코드가 10줄 바뀌었다고 작은 변경은 아닙니다.

java
if (status != COMPLETE)

java
if (status == PENDING)

으로 바꾸는 것은 한 줄 변경이지만 의미는 완전히 다를 수 있습니다.

반대로 클래스 20개의 패키지를 이동하는 것은 변경 파일은 많아도 동작 의미는 변하지 않을 수 있습니다.

따라서 PR 크기를 볼 때는 단순한 라인 수보다 다음을 봐야 합니다.

text
업무 규칙이 몇 개 바뀌는가? 데이터 의미가 바뀌는가? 외부 계약이 바뀌는가? 실패 방식이 바뀌는가?

코드 소유자보다 업무 소유자를 찾아야 한다

Git 기록에서 가장 많이 수정한 개발자를 찾는 것도 중요합니다.

하지만 더 중요한 사람은 다음일 수 있습니다.

  • 해당 업무를 운영하는 담당자
  • 장애를 처리해본 개발자
  • 정산 결과를 확인하는 담당자
  • 고객 문의를 받는 상담 조직
  • 외부기관 연계를 담당하는 사람

대규모 시스템의 진실은 코드 저장소에만 존재하지 않습니다.

일부는 운영 매뉴얼에 있고, 일부는 담당자의 경험에 있으며, 일부는 장애 보고서에 있습니다.

LLM이 코드 전체를 읽어도 이 조직적 지식까지 자동으로 알 수는 없습니다.


롤백은 배포 이전 상태로 돌아가는 것만을 의미하지 않는다

DB 스키마나 메시지 형식을 변경하면 애플리케이션만 롤백해서는 복구되지 않을 수 있습니다.

예를 들어 새 버전이 enum 값을 추가한 뒤 데이터를 저장했다면, 구버전은 그 값을 읽지 못할 수 있습니다.

따라서 롤백 가능성을 판단할 때는 다음을 함께 봐야 합니다.

text
코드 롤백 데이터 롤백 스키마 호환성 메시지 호환성 캐시 호환성 외부 시스템 처리 결과

가장 안전한 변경은 일정 기간 구버전과 신버전이 같은 데이터와 메시지를 모두 이해할 수 있도록 만드는 것입니다.


11. 결국 대규모 프로젝트에서 필요한 능력

대규모 코드베이스에서 뛰어난 개발자는 모든 코드를 아는 사람이 아닙니다.

오히려 다음을 잘하는 사람입니다.

text
모르는 부분을 빠르게 발견한다. 현재 알고 있는 범위를 명확히 말한다. 기존 구현에서 안전한 길을 찾는다. 작은 변경으로 가설을 검증한다. 운영에서 실제 행동을 관찰한다. 문제가 생기면 빠르게 되돌린다. 도메인 전문가가 검토하기 쉽게 만든다.

전체를 이해하려는 태도는 훌륭합니다.

하지만 전체를 이해하기 전에는 아무것도 바꿀 수 없다고 생각하면 대규모 시스템에서는 영원히 시작할 수 없습니다.

반대로 일부만 이해하고 자신 있게 큰 변경을 하면 사고가 납니다.

그래서 현실적인 개발 방식은 그 중간입니다.

부분적으로 이해하되, 자신의 이해가 부분적이라는 사실을 알고 작업하는 것.


마무리

대규모 코드베이스는 잘 설계된 하나의 작품이라기보다 오래된 도시와 비슷합니다.

도시에는 계획적으로 만든 도로도 있고, 과거의 흔적을 따라 이상하게 굽은 골목도 있습니다.
사용되지 않는 것처럼 보이지만 재난 상황에서 필요한 통로도 있고, 누구도 정확한 이유를 모르는 규칙도 있습니다.

새로 온 사람이 지도를 보고 이렇게 말할 수 있습니다.

“이 골목은 비효율적이니 직선 도로로 바꾸면 되겠네요.”

하지만 그 골목 아래에 수도관이 있을 수도 있고,
옆 건물의 비상 통로일 수도 있으며,
시장 상인들의 물류 동선일 수도 있습니다.

LLM은 이 도시를 탐색하는 데 매우 좋은 안내자입니다.

관련된 길을 찾아주고, 주변 건물을 설명하며, 오래된 표지판의 의미도 정리해줍니다.
그러나 LLM도 그 도시에서 십 년간 살아본 주민은 아닙니다.

따라서 가장 좋은 조합은 다음과 같습니다.

text
LLM으로 빠르게 지도를 만든다. 기존 코드로 실제 길을 확인한다. 테스트로 통행 가능성을 검증한다. 운영 데이터로 사람들이 실제로 다니는지 확인한다. 작은 구간부터 공사한다. 문제가 생기면 즉시 되돌린다.

LLM이 대규모 코드베이스를 완전히 이해하게 해주는 것은 아닙니다.

하지만 우리가 전체를 이해해야만 움직일 수 있다는 부담에서 벗어나,
필요한 부분을 더 빠르게 이해하고, 더 좋은 질문을 던지고, 더 안전하게 변경하도록 도와주는 것은 분명합니다.

참고 자료

#Software Engineering#Legacy Code#Maintenance#Refactoring#LLM

댓글 0

Ctrl + Enter를 눌러 등록할 수 있습니다
※ AI 다듬기는 내용을 정제하는 보조 기능이며, 최종 내용은 사용자가 확인해야 합니다.