위젯 중복 제거와 프리셋 유연성 개선
목차
운영 중인 시스템에서 "이미 돌아가고 있는 기능"을 건드리는 건 묘하게 긴장된다. 기능 추가는 결과물이 눈에 보이지만, 이런 정리 작업은 잘해야 현상 유지, 잘못하면 사고다. 그래서 이번 작업 전에 영향 범위부터 꼼꼼히 파악했다. 백엔드 로직, JSP 화면, XML 쿼리, 스타일까지 여러 레이어에 걸쳐 있었고, 레이어가 많으면 하나 고쳤다가 다른 데서 뭔가 어긋날 위험도 그만큼 늘어난다.
이번 핵심은 두 가지였다. 여러 위젯에 흩어진 중복 코드를 하나로 모으는 것, 그리고 프리셋 구조를 더 유연하게 만드는 것.
위젯 중복이 생기는 이유, 그리고 공통화의 타이밍
처음 개발할 때 위젯 A와 위젯 B는 요구사항이 달랐다. 자연스럽게 각자 별도 로직으로 구현됐고, 각자의 JSP, 각자의 XML 쿼리, 각자의 스타일이 생겼다. 그런데 운영하다 보면 두 위젯의 공통 동작이 점점 늘어난다. 버그 하나 고치면 두 곳에 패치해야 하고, 기능 하나 추가하면 두 곳에 반영해야 한다. 여기서 미묘한 불일치가 생기기 시작한다. A에서 고친 엣지 케이스를 B에선 빠뜨리거나, A의 에러 메시지 포맷이 B랑 슬쩍 달라지거나. 이번 작업은 그 불일치가 눈에 띄기 시작한 시점에 한 번에 정리한 거다.
중복 코드를 공통화할 때 가장 먼저 하는 건 "진짜 같은가?" 확인이다. 코드가 비슷해 보여도 다른 이유로 존재할 수 있다. 표면만 같고 의도가 다른 걸 억지로 합치면, 나중에 한쪽 요구사항이 바뀔 때 분리 비용이 오히려 더 든다. 이번엔 실제로 같은 역할을 하는 코드가 여러 파일에 흩어져 있었기 때문에 합치는 게 맞았다.
// 정리 전: 위젯마다 거의 같은 프리셋 파싱 로직이 각자 있음
// WidgetA.java
String[] presets = rawPreset.split(",");
for (String p : presets) {
if (p == null || p.trim().isEmpty()) continue;
// 처리...
}
// WidgetB.java
String[] items = rawPreset.split(",");
for (String item : items) {
if (StringUtils.isBlank(item)) continue;
// 거의 같은 처리, 미묘하게 다른 null 처리
}
// 정리 후: 공통 유틸로 추출
public static List<String> parsePreset(String raw) {
if (raw == null || raw.isEmpty()) return Collections.emptyList();
return Arrays.stream(raw.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
단순해 보이지만 효과는 명확하다. null 처리 방식과 공백 처리 방식이 위젯 전체에서 동일해졌다. 전에는 위젯마다 미묘하게 달랐고, 그 차이가 특정 입력에서 다른 결과를 만들어낼 가능성이 있었다.
공통화의 타이밍도 중요하다. 처음 비슷한 코드가 두 번째 등장하는 시점엔 "일단 복붙"하는 게 빠르다. 세 번째 등장하면 그때 공통화를 고민하는 게 현실적이다. 너무 이르게 추상화하면 오버엔지니어링이 되고, 너무 늦으면 이번처럼 흩어진 걸 다시 모아야 하는 비용이 생긴다.
프리셋 유연성을 높인다는 게 구체적으로 무슨 뜻인가
프리셋은 위젯의 초기 상태나 설정을 외부에서 주입할 수 있게 해주는 구조다. 초기 구현이 경직되어 있으면, 운영에서 조금만 다른 케이스가 나와도 예외 처리를 덕지덕지 붙이게 된다. 이번에 개선한 건 프리셋 값의 허용 범위를 더 넓게 처리하도록 한 거다.
기존엔 빈 문자열이나 null이 들어오면 바로 기본값으로 fallback했는데, 실제 케이스를 보니 부분적으로 유효한 값이 섞인 경우가 있었다. 그 케이스를 통째로 "유효하지 않음"으로 버리지 않고, 유효한 부분만 살려서 처리하도록 바꿨다.
엣지 케이스 처리 방식은 이 세 단계로 나눠서 설계하게 됐다:
- 입력이 완전히 유효한 경우 - 정상 처리
- 입력이 부분적으로 유효한 경우 - 유효한 부분만 처리, 무효한 부분은 로그
- 입력이 완전히 유효하지 않은 경우 - 기본값 사용, 명확한 에러 로그
세 번째 케이스까지 명시적으로 설계하면 에러 메시지도 자연스럽게 개선된다. "처리 실패"가 아니라 "어떤 값이 왜 무효였는지"가 로그에 남는다. 새벽에 뭔가 이상하면 로그 보고 바로 원인을 좁힐 수 있는 것과 "처리 실패"라는 메시지만 보고 코드를 뒤지는 것 차이는 크다.
유연성과 복잡도는 트레이드오프가 있다. 프리셋이 허용하는 형식이 늘어날수록 파싱 로직이 복잡해지고, 그 복잡도가 버그를 만들 수도 있다. 이번엔 허용 범위를 늘리되, 처리 경로를 명확하게 분리하는 방향으로 가서 그 균형을 맞추려 했다.
이런 작업이 쌓이면 생기는 것
기능 추가 커밋이 아니라서 PR이 좀 밋밋하다. 변경된 줄 수 대비 눈에 보이는 변화가 없다. 배포해도 사용자 입장에선 아무것도 달라진 게 없다. 티가 안 나는 작업이다.
근데 몇 개월 후 비슷한 영역을 건드릴 때 체감이 다르다. 공통 유틸이 있으면 새 위젯 추가할 때 거기 갖다 쓰면 되고, 프리셋 파싱 버그가 생기면 한 곳만 고치면 된다. "한 곳 바꾸면 여러 곳 수정 필요"한 구조가 있다는 건, 언젠가 어딘가 한 곳을 빠뜨릴 가능성이 있다는 뜻이다. 이번 작업은 그 가능성을 하나 줄인 거다.
| 좋은 코드의 기준 | 나쁜 코드의 신호 |
|---|---|
| 읽으면 의도가 바로 보임 | 주석 없으면 이해 불가 |
| 변경이 한 곳에만 영향 | 한 곳 바꾸면 여러 곳 수정 필요 |
| 테스트 작성이 자연스러움 | 테스트하려면 구조 바꿔야 함 |
운영 중인 서비스를 변경할 때 순서는 항상 같다. 변경 전 현재 동작 파악, 변경 후 동일하게 동작하는지 확인, 배포 후 모니터링. 지루하지만 생략하면 결국 더 많은 시간을 쓰게 된다. 작은 커밋을 유지하는 것도 같은 맥락이다. 변경 단위가 작아야 코드 리뷰가 제대로 되고, 문제가 생겼을 때 어느 커밋이 원인인지 좁히기 쉽다.
코드 작성할 때 스스로에게 묻는 질문은 변하지 않는다. 6개월 후 내가 다시 보면 이해할 수 있는가. 나 말고 다른 사람이 봐도 이해할 수 있는가. 새벽 3시에 장애가 났을 때 이 코드가 원인 파악을 도와주는가. 이번 작업은 그 세 질문에 대한 답을 조금 더 "예스"에 가깝게 만든 거라고 생각한다.
댓글 0
첫 댓글 달아줘.