개발 slecs

supplier 포탈에 첨부파일 유형

목차

supplier-portal 영역에 첨부 파일 유형 지원을 추가하고, 이미지 갤러리에 다운로드/저장 기능을 확장했다. 변경 파일 자체는 뷰/스타일 1개인데, 그게 전부가 아니었다.

왜 이 기능이 필요했냐

supplier 포탈은 협력사가 직접 들어와서 데이터를 조회하고 자료를 확인하는 영역이다. 내부 시스템과 달리 외부 사용자가 쓰는 화면이다 보니, 파일 첨부·이미지 확인·다운로드 같은 기본 UX 요소가 생각보다 훨씬 크게 작용한다. 기존 화면에서는 특정 파일 유형이 렌더링되지 않거나, 갤러리에서 이미지를 내려받는 경로 자체가 없었다. 그게 실제 사용 흐름에서 막힘을 만들고 있었다.

"없으면 요청하면 되지"가 아니라 포탈에 들어와서 작업을 마칠 수 있어야 한다는 게 이번 추가의 출발점이었음. 단순 UI 추가처럼 보이지만 쿼리 레벨부터 설계를 다시 봐야 했고, 기존 API에서 제공하지 않던 동작을 새로 뚫어야 해서 결국 백엔드 엔드포인트 구현까지 이어졌다.

구현 흐름과 챙긴 것들

백엔드 API 엔드포인트를 새로 만들고, JSP에 화면을 붙이고, AJAX로 연동했다. 순서대로 나열하면 단순해 보이지만 중간에 챙길 게 꽤 있었다.

가장 신경 쓴 건 권한 인터셉터 통과 확인이었다. supplier 포탈은 협력사 계정으로만 접근 가능해야 하는데, 신규 엔드포인트가 인터셉터 필터 체인 밖으로 새어나가면 인증 없이 파일 경로가 노출될 수 있다. 구현 후 실제로 권한 없는 상태에서 직접 URL을 찍어서 차단되는지 확인했다. 이게 테스트로 잡히면 다행이고 운영에서 발견되면 곤란한 케이스라 꼼꼼하게 봤음.

사이드바 메뉴 활성화도 빠뜨리기 쉬운 부분이다. 새 경로가 생기면 현재 메뉴 하이라이트가 깨지는 경우가 종종 있어서, URL 패턴 매칭 로직이 신규 경로를 커버하는지 확인했다.

이미지 갤러리 다운로드는 AJAX로 blob을 받아서 브라우저에서 직접 내려받도록 처리했다. <a> 태그에 download 속성만 쓰는 방식보다 서버 응답 헤더에서 Content-Disposition을 제어하는 쪽이 파일명이나 유형 처리에서 유연하다.

// 이미지 다운로드 AJAX 처리 흐름
$.ajax({
    url: '/supplier/gallery/download',
    type: 'POST',
    data: { fileId: fileId },
    xhrFields: { responseType: 'blob' },
    success: function(blob, status, xhr) {
        var disposition = xhr.getResponseHeader('Content-Disposition');
        var filename = disposition.split('filename=')[1];
        var url = window.URL.createObjectURL(blob);
        var a = document.createElement('a');
        a.href = url;
        a.download = filename;
        a.click();
        window.URL.revokeObjectURL(url);  // 메모리 해제 빠뜨리지 말 것
    }
});

revokeObjectURL을 빠뜨리면 메모리 누수가 생긴다. 눈에 안 보이는 종류의 버그라서 습관적으로 챙기는 게 낫다.

첨부 파일 유형 추가는 허용 확장자 리스트와 MIME 타입 매핑 두 곳을 동시에 건드려야 했다. 프론트에서만 막으면 직접 HTTP 요청으로 우회할 수 있어서 백엔드에서도 유형 검증을 이중으로 달았다.

검증 위치 처리 방식 비고
프론트 (JSP) accept 속성 + 확장자 체크 UX용, 1차 필터
백엔드 (API) MIME 타입 + 확장자 매핑 검증 실질적 보안 게이트
저장 시 파일명 sanitize + 저장 경로 고정 경로 traversal 방지

이중 검증이 귀찮아 보여도 어느 한쪽만 하면 결국 구멍이 생긴다. 파일 업로드 관련해서는 프론트 검증을 신뢰하면 안 된다는 게 기본 전제.

설계에서 결정한 것들

파일 관련 기능에서 항상 나오는 트레이드오프가 있다. 이미지를 서버에서 스트리밍할 것이냐, 정적 URL을 직접 내려줄 것이냐. URL 직접 노출은 구현이 간단하지만 인증 없이 접근 가능한 경로가 생길 수 있다. 이번엔 서버 스트리밍 방식을 택했음. 응답 헤더에서 캐시 정책이나 Content-Type을 제어할 수 있고, 파일 경로 자체가 클라이언트에 노출되지 않는다는 게 결정적이었다.

실시간 갱신 여부도 짧게 고민했다. 갤러리 목록이 실시간으로 바뀌는 시나리오인지, 페이지 로딩 시 한 번만 가져오면 충분한지. 이 화면에서는 폴링이나 웹소켓까지 갈 이유가 없어서 초기 로딩 방식으로 확정했다. 기능 요건에 비해 복잡도를 키울 필요가 없다고 판단했고, 나중에 필요해지면 그때 붙이는 게 맞다.

NULL 처리는 파일 쪽에서 특히 꼼꼼하게 봐야 한다. 파일이 없는 협력사, 갤러리가 비어 있는 케이스, 이미 삭제된 파일을 조회하는 케이스. 이 상태들을 빠뜨리면 화면에 에러나 빈 영역이 그대로 노출된다. 각 케이스마다 fallback 메시지를 잡아두고, 쿼리 실행 계획도 데이터가 없는 상황에서 불필요한 full scan이 없는지 확인했다.

구현 후 검증은 직접 화면에서 했다. 실제 파일을 올려보고, 허용하지 않는 유형으로도 시도해보고, 권한 없는 접근 차단도 확인. 기존 데이터가 깨지지 않았는지, 관련 화면 숫자가 일치하는지 cross-check하는 건 기본이다.

사내 서비스를 만들다 보면 기능 하나가 버튼 하나 추가로 끝나지 않는다는 걸 계속 체감한다. SQL, 권한 체크, 파일 유형 검증, 예외 처리, 화면 렌더링이 모두 엮여 있어서 하나를 빠뜨리면 반드시 어딘가에서 나타난다. 특히 파일 처리는 보안과 UX가 동시에 얽히는 영역이라, "대충 동작하는 것 같다"로 넘어가면 나중에 반드시 다시 돌아온다.

커밋은 논리적으로 독립된 단위로 쪼개는 습관을 유지 중이다. 뷰/스타일 1개 파일만 바뀐 것처럼 보여도, 실제로는 백엔드 엔드포인트 추가, 권한 설정, 화면 연동이 단계별로 이어지는 작업이었음. 작은 커밋을 자주 하면 문제가 생겼을 때 어느 변경에서 깨졌는지 찾기 훨씬 쉽고, 커밋 메시지에 "무엇을"보다 "왜"를 담으려고 노력하는 것도 같은 이유다.

댓글 0

첫 댓글 달아줘.