대시보드 편집기 운영 안정성 개선
목차
대시보드 편집기 쪽 내부 클래스를 손봤다. 작업 규모는 크지 않았는데, 운영에서 묵혀뒀던 개선 포인트를 정리하는 성격의 커밋이었음. 기능을 화려하게 추가한 건 아니고, 실제 사용자 흐름에서 빠져 있던 처리나 불안정하게 돌아가던 부분을 정돈한 작업이다.
Spring MVC + MyBatis 구조에서 대시보드 편집기 같은 기능은 보통 하나의 컨트롤러 내부에 여러 내부 클래스(Inner Class)를 두고, 요청 파라미터 바인딩부터 서비스 호출, 뷰 반환까지 일련의 흐름을 모아 관리한다. 코드베이스가 그렇게 쌓여있으면 새 기능을 붙이기가 수월한 반면, 예외 케이스가 늘어날수록 내부 클래스 하나가 조용히 터지면 원인 추적이 번거로워진다. 이번 작업이 그 흔한 케이스였다.
구현할 때 실제로 고민한 것들
요청 파라미터 검증은 생각보다 신경 써야 할 지점이 많다. null 체크로 끝나면 편하지만, 빈 문자열("")이 들어오는 경우나 공백만 있는 경우(" ")를 따로 처리 안 하면 SQL 조건에서 의도하지 않은 전체 조회가 나가거나, 반대로 조건 누락으로 잘못된 데이터가 응답에 실린다. MyBatis XML에서는 <if test="param != null and param != ''"> 패턴을 쓰는데, 이걸 서비스 레이어까지 믿고 올리면 안 되고 컨트롤러 바인딩 시점에서 먼저 걸러두는 게 낫다.
트랜잭션 범위는 이번에도 한 번 정리했다. DB를 건드리는 로직이 내부 클래스 여러 곳에 분산되어 있으면 @Transactional 범위 설정이 애매해지는 상황이 생긴다. 보통 서비스 메서드 단위로 경계를 잡는데, 내부 클래스에서 직접 DAO를 호출하는 패턴이 남아 있으면 트랜잭션이 기대한 대로 묶이지 않는다. 이번엔 해당 흐름을 서비스 레이어로 올려서 정리했다.
예외 처리 결정도 매번 선택이다. RuntimeException을 그대로 올려서 글로벌 핸들러가 잡게 할지, 잡아서 응답 객체로 변환해 내려줄지. 편집기처럼 사용자 인터랙션이 많은 기능은 후자가 맞다고 판단했다. 저장 실패를 500으로 터뜨려버리면 프론트가 따로 처리할 방법이 없으니까.
// 예외를 잡아서 응답 변환하는 패턴 (실제 코드 아님, 패턴 참고용)
try {
service.saveDashboard(param);
return ResponseEntity.ok(ResultDto.success());
} catch (IllegalArgumentException e) {
log.warn("[dashboard-editor] 잘못된 파라미터: {}", e.getMessage());
return ResponseEntity.badRequest().body(ResultDto.fail(e.getMessage()));
} catch (Exception e) {
log.error("[dashboard-editor] 저장 실패", e);
return ResponseEntity.internalServerError().body(ResultDto.fail("저장 중 오류가 발생했습니다."));
}
로그도 챙겼다. 운영 환경에서 재현이 안 되는 버그는 대부분 로그가 없어서 못 잡는다. 중요한 입출력 값은 INFO로 남겨둬야 나중에 배포 상태 로그에서도 따라갈 수 있다. DEBUG로만 찍으면 운영에선 레벨 조정 없이는 보이지 않으니까.
CI/CD 파이프라인에서 자주 걸리는 부분
이번 배포 과정에서도 파이프라인 관련 잡일이 있어서 겸사겸사 정리해둔다. 반복해서 겪는 문제들인데 팀원들이랑 공유용으로도 남겨두면 좋을 것 같아서.
| 증상 | 원인 | 대응 |
|---|---|---|
| SSH 연결 끊김 | 빌드 시간이 길어지면 타임아웃 | ServerAliveInterval, command_timeout 명시 |
| 키 인증 오류 | PEM 파일에 개행 문자 포함 여부 | base64 인코딩 후 secrets 등록, 디코딩 시 확인 |
| Gradle 빌드 실패 | CI 캐시 꼬임 | --no-daemon 플래그 또는 캐시 디렉토리 초기화 |
| 앱이 안 뜸 | Secrets 누락 | 배포 성공 로그만 보지 말고 앱 기동 헬스체크 필수 |
SSH 타임아웃은 처음엔 파이프라인 문제인 줄 모르고 코드 탓으로 돌리기 쉽다. 빌드 로그가 특정 단계에서 멈추면서 연결이 조용히 끊기면, 타임아웃 설정을 먼저 의심하는 게 맞다. Gradle 캐시 문제도 로컬에선 잘 되는데 CI에서만 실패하는 대표적인 패턴이다. 처음 보면 당황스럽지만 캐시 클리어 한 번으로 해결되는 경우가 대부분이라 체크리스트 앞쪽에 넣어두면 시간 아낀다.
배포 순서는 늘 같다. 로컬 기동 확인 → 개발 DB 반영 → 스테이징 확인 → 운영 배포. 순서를 건너뛰고 싶을 때가 있지만 개발 DB 검증 없이 운영으로 바로 올리면 예상 못한 데이터 형태로 쿼리가 터지는 경우가 생긴다. 특히 MyBatis에서 결과 매핑 시 컬럼명이 DB 스키마랑 살짝 다를 때 오류가 없이 null로 내려오는 경우가 있어서, 화면 렌더링 결과까지 눈으로 봐야 안심이 된다.
작은 수정이 쌓이는 방식
이번 커밋 자체가 특별히 크거나 극적인 변화는 아니다. 내부 클래스 정리, 파라미터 검증 강화, 예외 흐름 정돈 정도. 그런데 이런 작업을 미루면 미룰수록 나중에 기능 추가할 때 더 많이 걸린다. 손댈 로직이 불안정하면 새 코드를 그 위에 올리기가 찜찜하고, 결국 수정 범위가 커지거나 아예 건드리기를 꺼리게 된다.
운영 안정성 개선 작업의 체감 가치가 낮아 보이는 이유가 거기 있다. 장애가 안 났으니까 성과가 안 보이는 거다. 하지만 장애가 안 났다는 게 곧 그 작업의 결과이기도 하다. 팀장 포지션에서 이런 커밋을 꾸준히 챙기는 게 중요한 이유가 그거다. 팀원들이 기능 개발에 집중할 수 있도록, 기반이 되는 흐름들을 조용히 다져두는 작업.
기존 로직과의 호환성 검토도 빠뜨리면 안 된다. 수정한 내부 클래스가 다른 흐름에서도 참조되는지, SQL 변경이 다른 화면에 영향을 주는지. 코드 리뷰에서 이 부분을 명시적으로 추적하는 습관을 들이면 롤백 상황을 많이 줄일 수 있다.
댓글 0
첫 댓글 달아줘.