판다라이브 보안 감사 대응으로 운영 안정성 개선
목차
보안 감사 대응 작업은 보통 두 종류로 나뉜다. 취약점 패치처럼 급하게 틀어막는 것, 그리고 감사 결과를 바탕으로 구조를 정돈하는 것. 이번 판다라이브 작업은 후자였다. 눈에 띄는 버그가 있었던 건 아니고, 감사 과정에서 드러난 입력값 처리와 로직 흐름 일부를 더 견고하게 다듬는 작업이었음.
수정 범위는 내부 클래스 위주였다. Spring MVC + MyBatis 구조에서 내부 클래스는 컨트롤러 안에 중첩되거나, VO·Command 역할로 요청 파라미터를 받는 형태로 많이 쓰인다. 이 영역의 유효성 검사가 군데군데 허술했다. null이 들어오면 그냥 통과하거나, 빈 문자열을 숫자로 변환하다 NPE가 터지는 케이스들. 감사 리포트에서 이런 입력 처리 미흡이 지적됐고, 이번에 한 번 정리하는 방향으로 잡았다.
요청 검증을 어디서 할지
컨트롤러에서 다 막으면 컨트롤러가 두꺼워진다. 서비스 레이어로 내리면 SQL 직전에야 잘못된 값을 발견하게 된다. 어느 쪽도 이상적이진 않아서, 이번엔 컨트롤러 진입 직후 바인딩 단계에서 처리하는 방향으로 정리했다.
기존 코드는 HttpServletRequest를 직접 파싱하는 방식이었다. 파라미터마다 getParameter 호출하고, null 체크는 따로 없거나 간혹 한 줄짜리 조건문으로만 처리. 이걸 Command 객체 바인딩으로 바꾸면 타입 변환 실패도 Spring이 잡아주고, 검증 로직도 한 곳에 모인다.
// 기존 패턴 - HttpServletRequest 직접 파싱, null 체크 없이 서비스 전달
public ModelAndView process(HttpServletRequest request) {
String userId = request.getParameter("userId");
service.doSomething(userId); // NPE 가능
}
// 개선 후 - Command 객체 바인딩, 진입 직후 검증
public ModelAndView process(UserCommand cmd) {
if (cmd.getUserId() == null || cmd.getUserId().isBlank()) {
return errorResponse("userId는 필수입니다.");
}
service.doSomething(cmd.getUserId());
}
내부 클래스를 Command 형태로 정리하는 게 이번 작업의 큰 줄기였음. 클래스 하나 건드리는 거지만, 거기서 파생되는 바인딩 방식, 예외 처리 흐름, SQL 파라미터까지 연결되니 생각보다 범위가 있었다.
MyBatis XML 쪽도 함께 건드렸다. 파라미터가 null일 때 <if test> 분기가 없는 쿼리가 몇 개 있었는데, 조건 없이 WHERE절에 null이 들어가면 의도치 않게 전체 조회로 빠지는 패턴이다. 감사 리포트에도 이 부분이 지적됐고, 해당 쿼리마다 null 체크 분기를 추가했다.
<!-- 개선 전 - null 들어오면 WHERE 조건 없이 전체 조회로 빠짐 -->
<select id="selectUser" parameterType="map" resultType="UserVO">
SELECT * FROM users
WHERE user_id = #{userId}
</select>
<!-- 개선 후 - <where> + <if> 로 null 처리 명시 -->
<select id="selectUser" parameterType="map" resultType="UserVO">
SELECT * FROM users
<where>
<if test="userId != null and userId != ''">
AND user_id = #{userId}
</if>
</where>
</select>
예외 처리와 트랜잭션 정리
트랜잭션 범위도 이번에 손봤다. 기존엔 @Transactional이 빠져 있거나 propagation을 기본값에 맡기던 곳이 몇 군데 있었음. DB를 건드리는 로직이라면 범위를 명확히 하는 게 맞다는 판단. 롤백 조건도 확인했는데, RuntimeException은 기본 롤백이지만 체크 예외는 rollbackFor를 명시해줘야 해서 빠진 곳에 추가했다.
RuntimeException을 그냥 전파할지, 잡아서 응답으로 변환할지는 케이스마다 다르다. 이번엔 아래 기준으로 정리했음.
| 상황 | 처리 방식 |
|---|---|
| 입력값 오류 (클라이언트 실수) | 잡아서 에러 응답으로 변환 (4xx) |
| 시스템 오류 (DB 연결 실패 등) | 전파 후 상위 핸들러에서 처리 (5xx) |
| 비즈니스 룰 위반 | 커스텀 Exception으로 명시적 처리 |
클라이언트 입력 오류가 500으로 떨어지면 운영 로그가 노이즈로 가득 찬다. 400으로 내려주면 실제 시스템 이상과 구분이 되고, 알람도 5xx 기준으로만 걸면 된다. 이 구분 하나가 운영 중 이슈 추적 피로도를 꽤 줄여준다.
로그는 INFO 레벨에 입출력 핵심값만 남겼다. 운영 환경에서 DEBUG는 대부분 꺼져 있으니, 나중에 문제가 생겼을 때 INFO 로그가 유일한 단서가 되는 경우가 많다. 다만 파라미터 전체를 그대로 찍으면 민감 정보가 로그에 남을 수 있어서, 보안 감사 대응이라는 맥락상 그 부분도 챙겼다. 요청 식별자랑 처리 결과 정도만 찍도록 범위를 줬음.
기존 흐름과의 호환성 추적
내부 클래스를 건드리면 그 클래스를 참조하는 다른 곳도 따라가야 한다. IDE에서 참조 추적하고, 수정 범위 바깥에 있는 기존 로직이 영향받지 않는지 확인했다. 바인딩 방식이 바뀌면 JSP에서 EL로 꺼내는 속성명도 맞춰줘야 하는 경우가 있어서, 뷰 레이어도 같이 훑었음.
배포 순서는 로컬 기동 확인 → 개발 DB 반영 → 배포 순으로 처리했다. 이 순서를 지키는 이유가 있는데, 로컬에서 기동 자체가 안 되면 어디서 터지는지 빠르게 볼 수 있고, 개발 DB에서 실제 데이터로 돌려봐야 null 케이스나 엣지 케이스가 드러나는 경우가 있기 때문이다.
작업 규모 자체는 크지 않았다. 내부 클래스 정리, 쿼리 분기 추가, 예외 흐름 정돈 - 커밋 하나로 끝날 수도 있는 변경들. 근데 보안 감사 대응이 가져다주는 효과는 지적 항목 해소에 그치지 않는다. 입력 검증이 촘촘해지고 예외 흐름이 정리되면, 그 후로 같은 영역 작업할 때 코드가 훨씬 읽기 쉬워진다. 이런 작은 정돈들이 쌓여서 전체 유지보수 비용이 줄어드는 방향으로 간다.
댓글 0
첫 댓글 달아줘.