안녕하세요, 올리브영에서 백오피스 시스템 개발을 담당하고 있는 찬입니다.
형상관리 시스템에 커밋하고 배포까지 마쳤는데, 운영 서버의 동작이 로컬 환경과 다르게 나타난 적이 있으신가요? Single Source of Truth(단일 진실 공급원)여야 할 형상관리 시스템과 운영 환경의 실행 코드가 어긋나는 순간, 개발자는 커밋 이력으로 설명되지 않는 기술 부채와 마주하게 됩니다.
제가 실제로 마주한 더 절망적인 경우는 운영 서버에는 빌드된 .class 파일만 있고, 그에 대응하는 최신 소스 코드는 유실된 상태였습니다.
이런 상태는 과거 누군가 긴급 수정을 위해 CI/CD 파이프라인 밖에서 수동으로 파일을 주입(Hot-fix injection)했거나, 형상관리 자체가 누락되었을 때 나타나는 전형적인 '형상-운영 불일치'입니다.
이 글에서는 10년 동안 운영해 온 올리브영 백오피스 시스템에서 이 불일치를 디컴파일러와 AI로 해소한 과정을 공유합니다. 디컴파일 소스로 전부 덮어쓰려던 1차 작업과 디컴파일러마다 결과가 달라 멈춘 2차 작업을 거쳐, AI로 소스 차이를 분류하고 재컴파일한 클래스를 운영 클래스와 다시 비교해 검증하는 4단계 절차를 정리했습니다.
운영 서버를 직접 고친 뒤 형상관리로 되돌리는 절차의 부재
문제가 된 레거시 시스템은 2016년 9월 '차세대' ERP로 운영을 시작한 올리브영의 백오피스 시스템입니다. 영업, 상품, SCM, 재무 등 본사 업무 전반을 처리하는 시스템으로, 도입 당시의 모습은 전자신문 기사 「올리브영, '올리브원' ERP 도입...운영 효율화 박차」(2016년 9월 26일)에서 확인할 수 있습니다.
이후 10년 동안 배포 방식이 크게 바뀌지 않은 채 운영되면서, 운영 서버와 형상관리 소스가 어긋나는 문제가 누적되었습니다. 어긋나는 경로는 두 가지였습니다. 하나는 Java 빌드 도구인 ANT(Another Neat Tool)로 바뀐 파일만 골라 배포하는 증분 배포에서 파일이 빠지는 경우였고, 다른 하나는 파일 전송 프로토콜인 SFTP(SSH File Transfer Protocol)로 운영 서버에 파일을 직접 올려 긴급 수정하는 경우였습니다. 경로는 달랐지만 근본 원인은 하나였습니다. 운영 서버를 형상과 다르게 만드는 것은 언제든 가능했는데, 그렇게 벌어진 격차를 형상관리 서버로 되돌리는 절차가 조직에 없었습니다. 그 결과 개발 → QA → 운영 배포로 이어지는 흐름 곳곳에서 불일치가 생겼고, 다음과 같은 장애가 수차례 발생했습니다.
- 형상관리 서버의 파일이 최신인 줄 알고 수정해 배포했다가, 기존에 동작하던 기능이 동작하지 않는 장애
- 설정 파일을 바꿔 개발 환경에서 테스트를 마치고 배포했으나, ANT 배포 대상에서 설정 파일이 빠져 있어 발생한 장애
- 라이브러리는 배포 시 별도의 수동 반영 절차가 필요하다는 것을 몰랐던 개발자가 형상관리 서버에만 커밋한 채 배포를 진행해, 해당 라이브러리가 운영 서버에 반영되지 않은 장애
- 급한 건을 SFTP로 운영 서버에 직접 반영하고 형상관리 서버에는 반영하지 않아, 이후 정기 배포에서 해당 수정 사항이 통째로 덮어써져 발생한 장애
직접 수정 후 형상관리 반영을 확인하는 장치의 부재
급한 건이 생기면 개발자나 서버 담당자가 SFTP로 로컬 파일을 운영 서버에 직접 올리거나, 아예 운영 서버에 접속해 파일을 직접 수정하는 경우가 있었습니다. 문제는 SFTP 접근 자체가 아니라, 이렇게 수정한 내용을 형상관리 서버에 반영하도록 강제하는 절차나 도구가 없었다는 점입니다. '수정 후 반드시 형상관리 서버에 커밋하고 정식 배포를 다시 태운다'는 원칙은 있었지만 이를 확인하는 장치가 없어, 실제로는 담당자의 기억에만 의존했습니다. 운영 서버에만 있는 변경은 그렇게 누적되다가 다음 정기 배포 시점에 형상-운영 불일치로 드러났습니다. 당시 백오피스 시스템은 외주 관리 인력이 운영했고, 배포 담당자와 인프라 조직, IT 조직이 역할을 나눠 맡고 있었습니다. 급한 요청에는 운영 서버에서 바로 대응할 수 있는 구조였지만, 그렇게 수정한 내용을 형상관리 서버까지 되돌리는 일은 어느 한 조직의 책임으로 정해져 있지 않았습니다.
ANT 증분 배포의 구조적 한계
당시 배포는 ANT로 배포 목록을 만들고, 목록에 있는 파일만 증분 배포하는 방식이었습니다. 이 방식에는 형상과 운영을 어긋나게 만드는 한계가 있었습니다.
- 형상관리에서 삭제한 파일이 운영 서버에서는 삭제되지 않음
- 형상관리 서버에 파일을 올렸어도 배포 목록에서 빠지면 운영 서버에 반영되지 않음
- 라이브러리를 폴더 단위로 관리해 배포 시 수동 작업이 필요했고, 같은 파일을 다른 버전으로 올리면 버전이 뒤섞이는 경우가 있음
- 10년 동안 ANT 스크립트 XML에 task가 계속 추가·수정되면서, 실제로 쓰이지 않는 task와 XML 파일이 다수 남아 운영에 혼란을 줌
앞서 본 SFTP 직접 수정과 ANT 증분 배포 누락을 함께 놓고 보면, 필요한 작업은 이미 어긋난 소스를 맞추는 데서 끝나지 않았습니다. 소스를 바로잡는 것, ANT를 Gradle 등으로 전환하고 SFTP 접근을 차단해 불일치가 새로 생기는 길을 막는 것, 그리고 불가피하게 직접 수정이 발생했을 때 형상관리 서버로 되돌리는 절차를 의무화하는 것까지 세 가지 과제가 함께 필요했습니다.
운영 서버와 형상관리 서버의 소스를 맞춘 세 번의 작업
작업을 시작하기 전에 규모부터 가늠해 보니, 10년째 수백 명의 담당자와 수천 명의 현업 사용자가 쓰던 업무 시스템이라 생각보다 컸습니다. 업무 화면 약 1,300개, 웹 서버 파일 약 5,500개, 상용 UI 프레임워크로 만든 프론트엔드(FE) 화면 파일 약 4,800개, WAS 파일 약 1,000개였고, 운영 서버끼리도 파일 개수와 내용이 서로 맞지 않는 상태였습니다. 소스를 맞추는 작업은 세 차례에 걸쳐 진행했습니다.
| 작업 | 방식 | 결과 |
|---|---|---|
| 1차 | 디컴파일 소스로 형상관리 서버의 소스를 전부 덮어쓰기 | 중단. 주석이 사라지고 루프와 변수명이 바뀌어 유지보수가 어렵다고 판단 |
| 2차 | 차이 나는 부분만 반영, 디컴파일러를 JAD에서 CFR로 교체 | 전환. CFR의 추론 기능 때문에 기존과 다른 부분이 대거 나타나 디컴파일러 비교 검토로 |
| 3차 | AI로 소스 차이를 분류·검증, CFR을 기본으로 JAD·Fernflower와 교차 검증 | 완료. 99% 일치, 나머지는 로직과 바이트코드가 동일한 것을 확인 |
디컴파일 소스로 전부 덮어쓰려던 1차 작업
기본 정책
- 운영 서버와 형상관리 서버의 소스가 다르면, 운영 서버 파일을 디컴파일한 소스를 형상관리 서버에 반영
- 운영 서버에만 있는 파일도 디컴파일해서 형상관리 서버에 반영
진행 절차
- 운영 서버의 클래스를 모두 내려받고, 형상관리 서버의 Java 소스를 모두 내려받아 빌드해 클래스를 생성
- 두 클래스를 디컴파일러로 각각 디컴파일한 뒤 소스 비교 도구로 비교
- 차이가 있는 파일과 운영 서버에만 있는 파일의 목록을 만들어 반영
진행 결과
절차 3단계에서 작업을 중단했습니다. 디컴파일된 파일로 그대로 덮어쓰면 기존 주석이 사라지고 루프와 변수명이 바뀌어, 이후 정상적으로 유지보수하기 어렵다고 판단했기 때문입니다.
차이 나는 부분만 반영하려던 2차 작업
기본 정책
- 실제 코드를 비교해서 다른 부분만 운영 서버의 디컴파일 소스로 반영
진행 절차
1차와 같은 방식으로 디컴파일을 진행했으나, 사용하던 JAD가 최신 Java 문법을 지원하지 않아 AI 도구가 추천한 CFR로 디컴파일러를 교체했습니다.
진행 결과
디컴파일 소스를 비교하자 기존과 다른 부분이 대거 나타났고, 확인해 보니 CFR이 가독성 높은 코드를 만들려고 추론(Inference) 기능을 적용하고 있었습니다. 사실 처음에는 어떤 디컴파일러를 쓰든 같은 결과가 나온다고 착각했습니다. 디컴파일러마다 같은 바이트코드에서 서로 다른 소스를 만든다는 것을 확인하고, 디컴파일러 간 비교 검토에 들어갔습니다.
AI로 소스 차이를 분류하고 검증한 3차 작업
기본 정책
- 상황 분석과 검증에 AI를 도입
- CFR을 기본 디컴파일러로 하되, JAD와 Fernflower도 함께 사용해 소스를 교차 검증
진행 절차
- 운영 서버의 클래스를 내려받아 디컴파일하고, 형상관리 서버의 소스는 내려받아 컴파일한 뒤 다시 디컴파일
- 디컴파일된 두 서버의 파일을 AI로 포맷팅(pretty-print)
- AI로 diff를 수행하고, 차이 수준을 상·중·하로 구분한 현황 분석 보고서 생성
- diff 도구와 IntelliJ를 사용해 메서드 단위로 일치 작업 수행
- 운영 클래스와 작업한 소스를 비교하는 AI 스킬을 작성해 작업할 때마다 검증
- 재컴파일로 생성된 클래스의 MD5/SHA-256 해시값과 바이트코드를 AI로 비교
- 검증 스킬은 운영 서버와 형상관리 서버에 반영된 소스를 모두 내려받아 다시 컴파일한 뒤, 같은 디컴파일러로 디컴파일해 비교
진행 결과
99% 동일하게 맞췄습니다. 일부 파일은 라인이 달라지거나 변수명이 여전히 일치하지 않았는데, 디컴파일러가 코드를 재구성하면서 생길 수 있는 차이로 보고 로직과 바이트코드가 동일한 것을 확인해 검증 완료로 처리했습니다.
AI로 소스 차이를 상·중·하로 분류한 현황 분석
복구 작업의 첫 단계는 전체 위험도와 작업 규모를 산정하는 것이었습니다. 모든 파일을 사람이 전수 조사하기에는 리소스가 부족해, 디컴파일 → 포맷팅 → 비교 → AI 분석으로 이어지는 파이프라인을 만들어 현황을 빠르게 파악했습니다. 이 파이프라인에서 AI는 두 소스의 로직을 비교해 아래 세 가지 위험 등급으로 분류했고, 저는 이 분류를 반영 순서와 검증 범위를 정하는 핵심 지표로 활용했습니다.
- 상(High): 비즈니스 핵심 로직(계산, 검증 등)에서 유의미한 차이가 다수 발견된 파일로, 최우선 반영 및 전수 검증 대상입니다.
- 중(Medium): 주요 로직은 유사하나, 일부 조건문이나 디컴파일러 아티팩트로 인한 미세한 코드 불일치가 섞인 파일입니다.
- 하(Low): 비즈니스 로직은 동일하고, 디컴파일 과정에서 생긴 라인 변경, 변수명 차이, 무의미한 구문 차이만 있는 파일입니다.
이 파이프라인으로 실제로 분류해 보니, 앞서 말한 WAS 파일 약 1,000개 중 운영 서버와 형상관리 서버의 내용이 다른 파일은 10% 정도였습니다. 더 어려웠던 쪽은 FE 화면 파일(약 4,800개)로, 20% 정도가 불일치했습니다. 상용 UI 프레임워크 개발사에서도 디컴파일은 지원하지 않는다고 했고, 운영 서버에만 있는 파일까지 있어 어떻게 해소할지가 가장 큰 고민이었습니다.
디컴파일러 비교와 디컴파일 소스의 위험 영역
JAD·CFR·Fernflower의 특징과 선택 기준
운영 서버의 바이트코드로 소스를 복구하려면 디컴파일러를 잘 골라야 합니다. 아래 표는 형상 소스와 나란히 놓고 diff를 볼 때 노이즈가 적은가를 기준으로 각 도구를 정리한 것입니다. CFR의 과도한 추론 문제는 제가 2차 작업에서 직접 확인했고, JAD와 Fernflower의 특징은 잘 알려진 도구 스펙을 참고했습니다.
| 디컴파일러 | 특징 및 장점 | 기술적 한계 및 주의점 |
|---|---|---|
| JAD | 빠른 속도의 고전 도구 | Java 5 이상의 구문(Enum, Generic, Lambda 등)을 지원하지 않아 최신 환경에서 디컴파일 오류나 해석 실패가 발생합니다. |
| CFR | 최신 Java 스펙(Lambda, Switch expression) 지원 우수 | 코드 추론 기능이 가독성을 높이는 대신 원본과 다른 형태로 코드를 재구성해, 형상 소스와 비교하면 실제 로직과 무관한 차이가 대거 나타납니다. |
| Fernflower | IntelliJ IDEA 내장 엔진. 가독성과 안정성의 균형이 좋음 | 복잡한 익명 클래스나 중첩된 람다 구조에서 디컴파일 결과가 불분명할 수 있습니다. |
💡 참고: 재컴파일과 동작 동등성을 기준으로 한 공개 실측에서는 CFR의 재컴파일 성공률이 79%로 Fernflower(68%)보다 높게 나와, 평가 기준에 따라 순위가 달라집니다(Harrand 외, SCAM 2019).
디컴파일 소스를 그대로 반영할 때 깨지는 세 영역
디컴파일러는 바이트코드를 재구성하는 과정에서 원본 주석을 유실하고, 지역 변수명을 var1, param1 등으로 대체하며, 컴파일러 최적화에 따라 루프 구조(for/while)를 바꿉니다. 제가 직접 확인한 바로는 일반적인 비즈니스 로직이나 단순 클래스는 디컴파일해도 가독성이 크게 떨어지지 않았지만, 람다, 제네릭, 프레임워크 의존 코드는 눈에 띄게 읽기 어려워졌습니다. 그래도 전체 로직을 이해하는 데는 무리가 없어 참고용으로는 충분히 쓸 만했습니다. 다만 디컴파일 소스는 참고용이지, 그대로 형상에 반영할 대상이 아닙니다. 이 문제로 실제 장애를 겪지는 않았지만, 특정 코드는 디컴파일 결과가 원본과 다르게 나온다는 것을 알고 있었기 때문에 반영 전에 아래 세 영역을 특히 주의해서 살펴봤습니다.
1. 직렬화 불일치(serialVersionUID)
- 원인: 원본 클래스에
serialVersionUID가 없거나 디컴파일하면서 사라져, 재컴파일 시 자동 계산값이 달라짐 - 증상: 과거에 저장된 바이트스트림을 읽을 때
java.io.InvalidClassException: local class incompatible: stream classdesc serialVersionUID = …발생 - 예: 캐시·세션(웹), 메시지 큐, 파일 저장 데이터의 역직렬화 실패
2. 리플렉션·프레임워크 메타데이터 깨짐
- 원인: 디컴파일로 파라미터 이름, 애노테이션, 제네릭 시그니처가 바뀌거나 빠짐
- 증상:
- Spring/Jackson:
@JsonProperty나 파라미터 이름이 빠져 생성자 인자 바인딩 실패 - Swagger/Bean Validation:
@NotNull,@Size등이 빠져 검증·문서화 오작동 - JPA: 기본 생성자의 접근제어자나 애노테이션이 바뀌어
InstantiationException, 매핑 실패
- Spring/Jackson:
3. 람다·익명 클래스의 직렬화·호출 차이
- 원인: 람다는 invokedynamic과 SerializedLambda 메타데이터에 의존하는데, 재컴파일 시 캡처 순서, 합성명(synthetic), 브리지 메서드가 달라짐
- 증상: 람다 직렬화·역직렬화 실패, 메서드 참조 시그니처 불일치로 런타임
NoSuchMethodError
이 세 가지 외에 아래 환경에서도 비슷한 문제가 보고되지만, 저희 스택과는 거리가 있어 이번에는 별도로 점검하지 않았습니다. 비슷한 구조의 시스템을 다루신다면 함께 점검해 보시길 권합니다.
- Lombok/AutoValue/Record 등 코드 생성기 의존 클래스
- Kotlin/Scala 등 JVM 기반의 다른 언어 → Java 디컴파일
- Android(DEX) 바이트코드 디컴파일
- Spark/Flink 등 분산 처리 프레임워크
안전한 형상 동기화를 위한 4단계 표준 절차
디컴파일된 소스로 형상을 덮어쓰면 주석과 변수명이 사라져 유지보수성이 무너집니다. 세 차례 작업을 거쳐 정리한 동기화 절차는 다음 4단계입니다. 상황에 따라 조정할 수 있지만, 이 순서를 따를 때 가장 안전했습니다.
| 단계 | 할 일 |
|---|---|
| 1. 수집과 빌드 | 운영 서버의 대상 .class를 내려받습니다. 동시에 현재 형상관리 시스템의 소스를 같은 빌드 환경에서 컴파일해, 비교 기준이 될 .class를 만듭니다. |
| 2. 같은 조건의 디컴파일과 diff | 양쪽 .class는 반드시 같은 디컴파일러로 변환합니다. 디컴파일러 고유의 서식이나 명명 규칙 같은 디컴파일러 아티팩트가 양쪽에 똑같이 생겨, 비교 도구의 시각적 노이즈가 사라지고 비즈니스 로직의 차이만 남습니다. 디컴파일러마다 옵션과 추론 기능이 다르므로 옵션은 일관되게 쓰고, 가급적 추론 기능을 끈 옵션을 고릅니다. |
| 3. 핀포인트 수동 반영 | AI 분석 결과를 참고하되, 실제 반영은 선택적 패치(Selective Patch) 방식으로 합니다. diff 결과에서 로직이 다른 부분만 형상 소스(.java)에 직접 고쳐 넣어 기존 주석과 변수명을 보존합니다. 해당 부분만 반영하기 어려우면 최소한 메서드 단위로 수정합니다. 운영 서버에만 있는 파일은 어쩔 수 없이 디컴파일 소스를 그대로 형상관리 서버에 반영합니다. |
| 4. 재컴파일 비교 검증 | 수정한 형상 소스를 다시 빌드해 새 .class를 만들고, 운영 .class와 함께 디컴파일해 비교합니다. 차이가 0개인 동일(Identical) 상태를 확인하면 완료입니다. 이 반복 작업은 AI 스킬로 만들어 최대한 자동화하면 동기화율을 높일 수 있습니다. |
AI와 엔지니어의 역할 분담
동기화 과정에서 AI는 사전 분석에는 탁월했지만, 실제 코드 반영과 복잡한 환경의 검증에서는 한계가 뚜렷했습니다. 특히 상용 UI 프레임워크 기반 코드나 의존성이 복잡한 로직에서는 AI의 비교 신뢰성이 급격히 떨어졌습니다.
그래서 AI 분석 결과에만 의존하지 않고, 역할과 책임을 나누고 추가 검증 단계를 함께 마련했습니다. AI에게는 diff 생성과 위험도 분류(상·중·하)까지를 맡기고, 반영 여부와 최종 검증은 반드시 엔지니어가 판단했습니다. FE 화면 파일처럼 AI 신뢰도가 떨어지는 영역은 담당 엔지니어가 전수 검증하도록 책임을 명확히 했습니다.
이 글에서 다루지는 않았지만, 동기화 과정 전체를 AI로 자동화하려는 시도도 함께 진행했습니다. 당시 AI 성능으로는 동기화를 안정적으로 처리하기 어려워 사람이 개입해 핀포인트로 반영하는 방식으로 마무리했습니다. 올해 초 다른 개선 과제에서는 AI 스킬을 정교하게 설계하고 반복 검증해, 사람의 개입을 최소화한 자동화를 구현하기도 했습니다. 그래도 현재 시점에서 가장 안정적으로 동기화하려면 앞서 설명한 4단계 표준 절차의 핀포인트 수동 반영을 추천합니다.
지금의 배포 구조와 다음 목표
이 작업은 10년 동안 운영하며 쌓인 문제를 개선 과제로 삼아 진행한 것이었고, 형상 불일치를 해소한 뒤 구조 개선까지 이어갔습니다. 서로 연관성 없는 프로젝트들이 멀티모듈로 묶여 있던 구조는 MSA 전환을 위해 분리했고, CI/CD 파이프라인도 함께 개선해 적용했습니다.
앞서 짚은 세 가지 과제 중 ANT는 Gradle로 모두 전환했습니다. 운영 서버의 직접 수정은 없애고 모든 수정이 정식 배포로만 반영되도록 강제했으며, SFTP는 정보보안 정책에 따라 권한자만 접근할 수 있게 바꿨습니다.
이번 작업으로 사람이 수동으로 통제하는 방식에는 한계가 있다는 것을 다시 한번 확인했습니다. 불필요한 서버 접근은 통제하고, 자동화된 프로세스 안에서 검증과 안전한 개발·빌드·배포가 가능한 환경을 만드는 것이 맞다고 봅니다. 앞으로는 AI로 형상과 운영의 동기화 상태를 검증하고, 어긋나는 즉시 알려 주는 조기 알림 체계를 만들어 작은 실수도 미리 막으려 합니다.
형상-운영 불일치 복구에서 얻은 교훈
- 도구보다 절차의 문제: ANT의 배포 누락도, SFTP를 통한 직접 수정도 벌어진 격차를 형상관리로 되돌리는 절차가 없어서 쌓였습니다. 원칙은 있었지만 이를 확인하는 장치가 없어 담당자의 기억에만 의존했습니다.
- 같은 조건에서의 비교와 완료 기준: 양쪽을 같은 디컴파일러로 변환하고, 재컴파일한 클래스가 운영 클래스와 바이트코드 수준에서 동일한지 확인하는 것은 선택이 아닌 필수입니다.
- 목적이 정하는 도구의 기준: CFR의 추론 기능은 가독성을 높이지만, 형상 소스와 비교할 때는 로직과 무관한 차이를 만들었습니다. 공개 실측과 순위가 다르게 나온 것도 평가 기준이 달랐기 때문입니다.
- AI는 분류, 판단은 사람: AI에게는 차이를 상·중·하로 분류하는 데까지 맡기고, 최종 반영과 정합성 검증의 책임은 엔지니어가 졌습니다. 프레임워크에 특화된 검증 환경도 함께 있어야 합니다.
- 복구는 최후의 수단: 이번 작업은 어디까지나 '최후의 수단(Last Resort)'입니다. 근본적인 해결책은 배포 파이프라인 밖의 수동 수정을 기술적으로 막고, 불가피하게 직접 수정이 발생하면 형상관리 서버로 되돌리는 절차를 강제하는 것입니다.
가장 좋은 상태는 이런 복구 가이드가 필요 없는 환경입니다. 형상관리 시스템이 단일 진실 공급원 역할을 하고, 운영 서버에는 CI/CD 파이프라인을 거친 결과물만 반영된다면 수동 .class 주입은 일어날 수 없습니다.
비슷한 상황을 마주하고 계신 분들께 이 글이 도움이 되었으면 좋겠습니다. 읽어 주셔서 감사합니다. 🙇♂️

