개발 slecs

관리자용 소명자료 PDF 생성

목차

소명자료 요청이 들어올 때마다 담당자가 DB에 직접 붙어 쿼리를 날리고, 결과를 엑셀로 내보내고, 화면 캡처해서 워드 편집하고, PDF 변환해서 전달하는 식이었다. 단순 반복이지만 한 건에 꽤 시간이 걸렸고, 실수가 끼어들 여지도 컸다. 기간 설정 하나 잘못 잡거나 필터 조건 하나 빠뜨리면 숫자가 틀리게 나오는데, 그게 법적·감사 목적으로 나가는 문서라면 나중에 굉장히 번거로워진다. 이번 작업은 그 흐름 전체를 관리자 화면 안에서 완결시키는 거였다.

구성은 세 덩어리다.

구성 요소 역할
관리자 화면 대상자 선택, 기간 설정, PDF 다운로드
PDF 생성 유틸 표·차트·서명란 포함 문서 렌더링
관리자 액션 로그 누가 언제 소명자료를 요청했는지 감사 이력

화면은 단순해 보여도 안에서 처리할 게 많다. 대상자를 선택하면 기간 범위에 맞는 활동 내역을 집계해야 하고, 집계 결과를 PDF 레이아웃에 맞게 렌더링해야 하고, 그 결과물을 서버 디스크에 남기지 않고 스트리밍으로 클라이언트에 내려보내야 한다. 각 단계 중 하나에서 터지면 사용자는 빈 파일을 받거나 에러 화면을 본다. 금융·결제 도메인은 그냥 "에러 났어요" 정도로 끝나지 않고 어디서 왜 틀렸는지까지 추적해야 하기 때문에 단계 경계를 명확히 두고 각각 예외를 잡았다.

AOP로 감사 로그 분리하기

관리자가 소명자료를 요청하는 행위 자체를 기록해야 한다는 요구가 있었다. "누가 언제 어떤 대상의 자료를 뽑아갔는가"가 나중에 내부 감사나 이의제기 시 증거가 되기 때문이다. 처음에는 서비스 메서드 안에 로그 저장 코드를 직접 넣는 방식을 생각했다. 빠르게 할 수 있는데, 문제는 관리자 기능이 하나가 아니라는 점이다. 권한별로 접근 가능한 액션이 여럿이고, 앞으로도 기능이 추가될 가능성이 높다. 매번 서비스 레이어에 로그 코드를 끼워넣으면 비즈니스 로직이 오염되고, 로그 스펙이 바뀌면 여기저기 수정이 생긴다.

그래서 AOP(Aspect-Oriented Programming)를 택했다.

// 커스텀 어노테이션 @AdminAudit 을 정의하고
// @Around 어드바이스로 해당 어노테이션이 붙은 메서드를 감싼다.
// 호출자(관리자 ID), 입력 파라미터, 실행 결과를 audit 테이블에 저장.
// try-catch 로 감싸서 예외가 발생해도 로그는 반드시 남김.
// 성공/실패 여부를 status 컬럼에 구분해 기록.

어드바이스 안에서 메서드 인자, 반환값, 예외를 모두 잡을 수 있어서 비즈니스 로직 코드에는 손을 댈 필요가 없다. 나중에 로그 형식이 바뀌거나 저장 위치가 달라져도 어드바이스 쪽만 수정하면 된다.

트레이드오프도 있다. AOP는 처음엔 "어디서 어떻게 동작하는지" 추적이 어렵다. 디버깅할 때 로그가 왜 안 남는지 찾으려면 포인트컷 표현식부터 확인해야 하는데, 익숙하지 않은 사람에게는 시간을 잡아먹는 지점이다. 포인트컷 범위를 너무 넓게 잡으면 의도하지 않은 메서드까지 걸리고, 너무 좁게 잡으면 로그가 빠진다. 명시적 어노테이션 방식을 택한 건 그 이유에서다. 어느 메서드가 감사 대상인지 코드 레벨에서 바로 보이고, 범위 실수가 줄어든다.

PDF 스트리밍과 보안

생성된 PDF를 서버에 저장하지 않고 스트리밍 다운로드로 처리한 건 두 가지 이유다.

하나는 개인정보 보호. 민감한 활동 내역이 담긴 파일이 서버 어딘가에 쌓이면 파일 관리 정책을 따로 잡아야 하고, 삭제 시점이나 접근 권한도 별도로 관리해야 한다. 요청 즉시 생성해서 바로 클라이언트로 흘려보내고 끝내면 서버에 잔재가 남지 않는다.

다른 하나는 구현 단순화. 파일을 저장하는 방식은 임시 파일 경로 관리, 만료 정책, 정리 배치 같은 부가 작업이 붙는다. 스트리밍 방식이 관리 포인트가 적고, 그 단순함 자체가 버그 노출 면적을 줄인다.

보안 측면에서 챙긴 것들:

  • 권한 검증은 컨트롤러 진입 전과 AOP 어드바이스 두 군데서 확인
  • 파라미터 유효성 검사: 기간 범위 최대값, 대상자 ID 형식
  • 응답 헤더에 Content-Disposition, no-cache 설정
  • 감사 로그에 성공/실패 status와 예외 메시지 포함 - 실패한 요청도 기록

마지막 항목이 중요한데, 에러 케이스를 로그에서 누락시키면 나중에 이상 접근 여부를 확인할 때 불완전한 기록이 된다. "성공한 요청만 감사 이력에 남는" 구조는 감사 이력이라고 부르기 어렵다.


기능 하나 만들다 보면 버튼 하나가 아니라는 걸 또 체감했다. SQL 집계, 렌더링, 스트리밍, 권한 체크, 감사 로그가 전부 맞물려 있어서 어느 하나라도 어긋나면 숫자가 틀리거나 의도하지 않은 화면이 나온다. 특히 금융·결제 도메인은 숫자 하나가 틀리면 신뢰가 무너질 수 있어서, "대충 맞는 것 같다"로 넘기면 반드시 나중에 돌아온다.

작업 방식은 늘 같다. 변경 전 현재 동작 수치를 메모해두고, 수정 후 같은 케이스로 확인하고, 관련 화면의 숫자를 cross-check하는 순서. 커밋도 PDF 생성 유틸, AOP 어드바이스, 감사 로그 스키마, 화면 연동을 각각 분리했다. 나중에 어느 변경에서 뭔가 깨졌는지 추적하기 훨씬 쉽고, 커밋 메시지도 "무엇을"보다 "왜 이 구조를 택했는지"를 담으려고 신경 썼다. 작은 커밋을 자주 하는 게 결국 디버깅 비용을 줄인다는 걸, 일이 생길 때마다 다시 확인하게 된다.

댓글 0

첫 댓글 달아줘.