백엔드 로직 중복 제거와 엣지 케이스 처리로 시스템 안정성 강화
목차
백엔드 파일 6개를 건드렸는데 커밋 로그만 보면 아무것도 안 한 것 같다. JSP 변경이 없고, XML 쿼리 손댄 것도 없고, CSS는 당연히 0. 근데 이게 맞다. 이번 작업의 목적 자체가 사용자한테 보이는 부분이 아니라 내부 로직의 품질이었으니까.
변경 영역을 정리하면 이렇다.
| 레이어 | 파일 수 | 주요 변경 |
|---|---|---|
| 백엔드 로직 | 6개 | 핵심 처리 로직 개선 |
| 화면 (JSP) | 0개 | - |
| 쿼리 (XML) | 0개 | - |
| 스타일 | 0개 | - |
중복 코드 제거, 엣지 케이스 보강, 로그/에러 메시지 개선, 불필요한 코드 정리. 이 네 가지가 전부다.
중복 코드를 왜 그냥 두면 안 되나
레거시 코드베이스에서 중복이 생기는 경로는 거의 정해져 있다. 처음엔 "비슷하지만 다른 케이스"라고 생각해서 복붙으로 시작하고, 나중에 로직이 바뀌면 한쪽만 수정하는 실수가 생긴다. 몇 달 지나면 두 함수가 미묘하게 다른 동작을 하는데 왜 다른지 아무도 모르는 상태가 된다. 이번에도 정확히 그런 케이스를 몇 개 발견했다. 처리 흐름이 같은데 파라미터 조합만 다른 로직이 여러 곳에 흩어져 있었다. 동작은 당장 문제없어 보이지만, 변경 요청이 왔을 때 한 곳만 고치고 나머지를 빠뜨리는 버그의 온상이다.
공통화 방향은 단순하게 잡았다. 핵심 처리를 한 곳으로 뽑고, 분기 조건은 파라미터로 주입하는 구조. 완벽한 추상화보다는 "이 로직이 바뀌면 한 곳만 고치면 된다"는 확신을 만드는 게 목표였다.
// Before: 같은 처리 흐름이 여러 메서드에 흩어진 상태
// 어느 시점부터 조금씩 달라졌는데 이유가 주석에 없음
private void processTypeA(Request req) {
validate(req);
// 핵심 처리 로직
}
private void processTypeB(Request req) {
validate(req);
// 미묘하게 다른 처리 - 왜 다른지 불명
}
// After: 파라미터로 통일, 분기 정책을 명시적으로 분리
private void process(Request req, ProcessType type) {
validate(req);
// 공통 처리
applyTypePolicy(req, type);
}
이렇게 하면 validate 조건이 바뀌어도 한 곳만 손대면 된다. 실수할 여지 자체를 줄이는 것이 핵심이다.
엣지 케이스 - 정상 입력만 생각하면 안 되는 이유
로직 개선보다 시간이 더 걸린 건 사실 엣지 케이스 보강이었다. 정상 입력이 들어올 때야 다 잘 돌아간다. 문제는 null이 올 때, 빈 컬렉션이 올 때, 예상 범위를 벗어난 값이 들어올 때다.
"그런 케이스가 실제로 오겠어?"라는 생각이 가장 위험하다. 오지 않던 케이스가 오는 건 항상 배포 직후이거나 새벽이다.
이번에 보강한 케이스들:
- null 체크 없이 바로 필드에 접근하는 코드 - NPE 잠재 위험
- 컬렉션 사이즈 검증 없이 인덱스를 직접 접근하는 부분
- 외부 입력값 범위 검증이 빠진 처리 구간
- 실패했을 때 그냥 무시하고 넘어가던 빈 catch 블록
마지막이 제일 나쁜 패턴이다. 예외를 삼키고 아무 일 없다는 듯 계속 진행하면, 나중에 전혀 다른 곳에서 이상한 결과로 터진다. 로그조차 안 남기는 빈 catch는 이번에 전부 정리했다. 에러가 났을 때 명확하게 실패하고 명확한 메시지를 남기는 쪽이 훨씬 낫다. 장애 상황에서 원인 파악이 10분이냐 2시간이냐를 가르는 건 대부분 로그 품질이다.
운영 서비스 변경 시 챙겨야 할 것들
운영 중인 서비스에서 리팩터링할 때 제일 무서운 건 "기존 동작이 그대로인가"다. 눈에 보이는 기능은 안 바꿨는데 내부 로직을 건드렸으니까. 이번 작업에서 잡은 원칙 세 가지.
변경 단위를 작게. 파일 6개를 한 커밋에 몰아넣지 않았다. 관련 변경끼리 묶어 커밋을 나눴다. 문제가 생겼을 때 어디서 생겼는지 바로 좁혀지도록.
변경 전 동작을 먼저 파악. 리팩터링 시작 전에 현재 입출력을 손으로 확인해두는 것. 자동화된 테스트가 없는 레거시일수록 이게 더 중요하다. "이게 원래 이랬나?" 하고 헷갈리는 상황을 막는 가장 원초적인 방법이다.
코드가 바뀌면 주석도 같이. 코드는 바뀌었는데 옆에 붙은 주석이 예전 동작을 설명하고 있으면 그게 더 혼란스럽다. 이번에 주석 불일치가 몇 개 있었고, 같이 정리했다.
| 원칙 | 실제로 언제 빛을 발하나 |
|---|---|
| 단일 책임 | 스펙이 바뀌었을 때 수정 범위가 명확해짐 |
| 명시적 코드 | 6개월 후 내가 다시 봤을 때 파악 속도 |
| 실패 우선 처리 | 새벽 장애 대응에서 원인 찾는 시간 |
| 작은 커밋 | 롤백이 필요할 때 되돌릴 지점이 명확함 |
티 안 나는 작업이다. 기능 추가가 아니고, 화면이 바뀐 것도 아니다. 그래서 스프린트 우선순위에서 늘 뒤로 밀리고 기술 부채라는 이름으로 쌓인다. 근데 이게 쌓이면 어떻게 되는지는 경험으로 안다. 새 기능 추가할 때 왜 이렇게 복잡한지 설명하기 어렵고, 버그 하나 고치면 옆에서 다른 게 터지고, 온보딩 비용이 몇 배가 된다.
코드를 쓸 때 계속 머릿속에 두는 기준이 있다. 6개월 후 내가 다시 봤을 때 이해할 수 있는가. 나 말고 다른 사람이 봐도 의도가 읽히는가. 새벽 장애 상황에서 이 코드가 원인 파악을 빠르게 해주는가.
| 좋은 코드의 기준 | 나쁜 코드의 신호 |
|---|---|
| 읽으면 의도가 바로 보임 | 주석 없으면 이해 불가 |
| 변경이 한 곳에만 영향 | 한 곳 바꾸면 여러 곳 수정 필요 |
| 테스트 작성이 자연스러움 | 테스트하려면 구조부터 바꿔야 함 |
| 실패 시 원인이 로그에 남음 | 빈 catch로 예외 삼킴 |
이런 작업들이 쌓이면서 시스템이 조금씩 견고해진다는 걸 느낀다. 당장 배포 노트에 쓸 내용이 없어도 괜찮다. 나중에 디버깅 시간이 줄어드는 걸로 돌아온다.
댓글 0
첫 댓글 달아줘.