김우진

멋쟁이사자처럼 경희대학교는 올해 새로 만들어진 교내 지부입니다. 운영진과 부원을 합쳐 약 50명이 교육과 프로젝트에 참여하고 있습니다.
신설 조직이다 보니 외부에서 우리를 한 번에 파악할 수 있는 공식적인 접점이 없었습니다. 프로젝트와 활동, 멤버, 블로그, 모집 정보를 한곳에 모아 보여줄 홈페이지가 필요했습니다.
그렇게 시작한 홈페이지에는 점차 운영 기능이 붙었습니다.
멤버는 자기 프로필과 글, 프로젝트를 직접 관리합니다. 운영진은 계정과 콘텐츠, 모집 상태를 관리합니다. 모집 기간에는 지원서를 받고, 지원자의 이름과 연락처도 확인합니다.
어느 순간부터 이 사이트는 단순히 조직을 소개하는 페이지라고 부르기 어려워졌습니다. 외부에는 경희대 멋사의 공식 얼굴이지만, 내부에서는 여러 사람이 권한을 사용하고 개인정보를 다루는 운영 시스템이었습니다.
저는 이 프로젝트에서 제품 책임자이자 조직장으로 일했습니다. 감사 대상과 민감정보 처리 원칙, 기록의 불변성을 어디까지 보장할지, 기록을 실제 운영에서 어떻게 사용할지를 정했습니다.
백엔드에서는 감사 이벤트 모델과 저장·조회 구조를 만들고, 상태 변경과 인증, 민감한 정보의 열람을 기록하는 부분을 직접 구현했습니다. 프론트엔드 담당자와는 개인정보 열람 전 경고와 관리자 리뷰 화면을 함께 설계했습니다.
설계 과정에서 계속 기준으로 삼은 질문은 하나였습니다.
같은 권한을 가진 관리자가 여럿이고, 개인정보를 다루며, 담당자가 기수마다 바뀌는 서비스는 자신의 과거를 어떻게 설명할 수 있어야 하는가?
운영 중 문제가 생기면 보통 “누가 했습니까?”라는 질문부터 나옵니다.
기록이 없다면 답은 사람의 기억과 단체 채팅방, 현재 데이터의 마지막 상태에 흩어집니다. 사건 직후에는 기억을 맞춰 볼 수 있겠지만, 시간이 지나거나 담당자가 바뀌면 사실 확인은 금세 추측이 됩니다.
작은 조직에서는 이 문제가 조금 더 까다롭습니다. 서로 친한 사이에서 확인 질문을 던지는 것만으로도 특정인을 의심하는 것처럼 들릴 수 있기 때문입니다.
그래서 감사 로그의 목적을 사람을 감시하거나 처벌하는 데 두지 않았습니다. 대화의 시작점을 사람의 기억에서 시스템이 남긴 사실로 옮기는 것이 더 중요했습니다.
“누가 그런 것 같습니까?”가 아니라, “기록상 어떤 계정으로 언제 무슨 일이 있었습니까?”에서 시작할 수 있어야 했습니다.
저는 이를 조직의 설명 가능성이라고 보았습니다. 현재 상태만 갖고 있는 것이 아니라, 그 상태에 이른 과정을 나중에 다시 구*할 수 있는 능력입니다.
무엇을 기록할지 정하기 전에 먼저 보호해야 할 대상과 사각지대를 정리했습니다.
| 보호해야 할 것 | 발생 가능한 문제 | 기존의 사각지대 | 설계한 통제 |
|---|---|---|---|
| 지원자 개인정보 | 업무 목적과 무관한 열람 | 조회해도 원본 데이터가 바뀌지 않음 | 민감 열람 기록 |
| 콘텐츠와 계정 | 잘못된 수정·삭제 | 여러 관리자가 같은 권한을 사용함 | 행위자와 변경 내용 기록 |
| 관리자·멤버 계정 | 계정 탈취와 반복 로그인 시도 | 인증 시도의 흐름을 재구성하기 어려움 | 로그인 성공·실패 기록 |
| 운영 맥락 | 기수 교체 과정에서 소실 | 기억과 메신저에 의존함 | 관리자 리뷰 화면 |
| 감사 기록 | 기록 자체의 임의 수정 | 기록의 신뢰가 훼손될 수 있음 | 앱에서 수정·삭제 경로 제거 |
이 홈페이지에서는 멤버가 글이나 프로젝트를 올릴 때마다 관리자의 승인을 받지 않습니다.
게시 전에 모든 내용을 검사하기 시작하면 소수 운영진에게 일이 몰리고, 멤버가 자기 콘텐츠를 관리한다는 원칙도 흐려집니다. 그래서 로그인한 사용자가 자신의 권한 안에서 바로 게시하고, 문제가 발견되면 운영진이 숨기거나 수정하는 방식을 택했습니다.
다만 사전 승인을 없앴다고 통제까지 없어도 되는 것은 아닙니다.
콘텐츠와 계정은 권한 안에서 바로 변경할 수 있게 두되, 누가 무엇을 바꿨는지는 남겼습니다. 개인정보를 열 때마다 별도의 결재를 받게 하지는 않았지만, 열람한 사실은 기록하고 나중에 운영진이 확인할 수 있게 했습니다.
권한 행사를 매번 막기보다, 사용된 권한을 설명할 수 있게 만든 것입니다.
대기업의 다단계 승인 절차를 학생 조직에 그대로 가져오는 것은 맞지 않았습니다. 그렇다고 공식 홈페이지와 지원자 개인정보를 전적으로 신뢰에만 맡기는 것도 적절하지 않았습니다.
운영 속도는 유지하면서 문제가 생겼을 때 사실로 돌아갈 수 있는 정도. 그 사이에서 필요한 통제 수준을 정했습니다.
감사 범위는 크게 세 종류로 나눴습니다.
상태 변경과 인증을 기록하는 것은 비교적 자연스럽습니다. 반면 민감 열람은 별도로 의식하지 않으면 쉽게 빠집니다. 아무것도 바뀌지 않기 때문입니다.
지원자 명단을 열어 개인정보를 확인해도 원본에는 아무 변화가 없습니다. 기록을 만들지 않으면 ‘누가 보았다’는 사건 자체가 존재하지 않았던 것처럼 사라집니다.
감사 로그를 조회한 행위도 다시 기록했습니다. 기록을 검토하는 사람만 기록의 바깥에 남겨 두면 또 다른 사각지대가 생기기 때문입니다.
다만 감사 로그 조회와 일반 운영 사건은 서로 다른 유형으로 구분했습니다. 운영진이 로그를 확인할 때 생기는 기록이 정작 중요한 사건을 찾는 데 방해되지 않도록 하기 위해서였습니다.
일반적인 서버 로그에는 다음과 같은 정보가 남습니다.
PATCH /api/members/12
개발자가 장애를 분석할 때는 유용하지만, 다음 기수 운영진이 이 기록만 보고 무슨 일이 있었는지 알아내기는 어렵습니다.
그래서 요청 경로를 그대로 쌓는 대신 업무상 의미가 드러나는 사건을 기록했습니다.
멤버의 역할이 BACKEND에서 FRONTEND로 변경됨
각 사건에는 행위자와 시각, 행위 종류, 대상, 처리 결과가 포함됩니다. 수정이 일어난 경우에는 실제로 달라진 필드만 비교해 보여줍니다.
기술 로그가 시스템이 어떻게 움직였는지를 설명한다면, 감사 이벤트는 조직의 권한이 어떻게 사용됐는지를 설명해야 했습니다.
모든 요청을 하나의 필터에서 받아 URL만 보고 사건의 의미를 추측하게 만들지는 않았습니다.
멤버 역할 변경과 지원자 명단 조회는 모두 HTTP 요청이지만, 두 사건은 의미도 다르고 기록해야 하는 방식도 다릅니다. 그래서 사건을 가장 잘 이해하는 위치에서 기록하고, 실제 저장만 공통 감사 서비스에 맡겼습니다.
멤버 수정, 모집 열기, 프로젝트 숨김과 같은 사건은 해당 업무를 수행하는 서비스가 직접 기록합니다.
이 위치에서는 단순히 어느 API가 호출됐는지가 아니라, 실제로 어떤 업무가 수행됐는지를 알고 있습니다. 덕분에 ‘프로젝트 수정’이나 ‘멤버 오프보딩’처럼 운영자가 이해할 수 있는 이름을 붙일 수 있었습니다.
상태 변경과 성공 감사 이벤트는 같은 트랜잭션에 넣었습니다.
멤버 정보를 수정하다가 오류가 발생해 변경이 취소됐다면, 성공했다는 감사 기록도 남아서는 안 됩니다. 업무 데이터와 성공 기록이 함께 커밋되고 함께 롤백되게 해, 둘이 서로 다른 이야기를 하지 않도록 했습니다.
로그인 성공과 실패, 로그아웃은 인증 서비스가 직접 기록합니다.
요청 경로만으로는 어떤 계정을 대상으로 로그인했는지, 어떤 이유로 실패했는지를 정확히 알기 어렵기 때문입니다.
로그인 실패는 상태 변경과 반대로 처리했습니다. 인증 과정에서 예외가 발생하더라도 실패 횟수와 감사 이벤트는 사라지지 않아야 했습니다. 계정 탈취 시도를 나중에 살펴볼 때는 성공한 로그인 한 건보다, 그전에 반복된 실패 기록이 더 중요할 수 있습니다.
지원자 명단과 감사 로그 조회는 JWT 인증이 끝난 뒤 동작하는 서버 필터에서 기록했습니다.
응답이 끝나면 누가 요청했는지, 언제 어떤 경로를 조회했는지, 요청 결과가 무엇이었는지와 클라이언트 IP를 남깁니다.
열람은 업무 코드가 실수로 기록을 빼먹더라도 놓치지 않아야 했습니다. 그래서 개발자가 각 조회 기능마다 감사 코드를 잘 붙여 주기를 기대하는 대신, 요청이 반드시 지나가는 서버 경계에서 포착했습니다.
지원자 개인정보를 열기 직전에는 “이 열람은 기록됩니다”라는 안내가 나타납니다.
이 안내는 접근을 차단하는 보안 장치가 아닙니다. 지금 하려는 열람이 업무상 필요한 일인지 한 번 더 생각하게 만드는 장치입니다.
하지만 기록이 남는지는 프론트엔드에 맡기지 않았습니다. 사용자가 경고 화면을 거치지 않고 API를 직접 호출하더라도 서버가 열람 사건을 기록합니다.
프론트엔드는 사람의 판단에 개입하고, 서버는 기록을 남깁니다. 관리자 화면은 그 기록을 실제 운영 판단으로 연결합니다.
운영진은 관리자 화면에서 행위자와 행위 종류를 기준으로 사건을 검색하고, 수정이 있었다면 무엇이 달라졌는지 확인할 수 있습니다. 화면에는 기록을 고치거나 지우는 기능을 두지 않았습니다.
전체 흐름은 다음과 같습니다.
경고 → 기록 → 발견 → 판단 → 조치 → 운영 개선
경고만 있으면 우회할 수 있습니다. 기록만 있으면 데이터베이스 안에서 쌓이기만 합니다. 관리자 화면이 있어도 실제 조치로 이어지지 않으면 운영은 달라지지 않습니다.
감사 로그를 기능 하나가 아니라 이 흐름 전체로 보려고 했습니다.
감사 로그에 모든 변경 전후 값을 그대로 남기면 나중에 사건을 조사하기는 편할 수 있습니다. 대신 새로운 문제가 생깁니다.
전화번호나 지원자의 개인정보가 감사 테이블에 한 번 더 복제됩니다. 원본 데이터는 삭제했는데 감사 로그에는 그대로 남아 있을 수도 있습니다.
그래서 사건을 설명하는 데 필요한 정보와 원문을 복원하는 데 필요한 정보를 구분했습니다.
전화번호나 소개처럼 민감할 수 있는 필드는 이전 값과 새로운 값을 저장하지 않습니다. 해당 필드가 변경됐다는 사실만 남깁니다.
나중에 설명할 수 있을 만큼은 남기고, 새로운 위험을 만들 만큼은 남기지 않습니다.
감사 이벤트 객체에도 생성 이후 값을 바꾸는 공개 메서드를 두지 않았습니다. 저장소는 범용 CRUD 인터페이스 대신 저장과 검색만 제공하고, 관리자 API와 프론트엔드 화면도 조회 기능만 갖습니다.
백엔드 응답과 프론트엔드 화면은 같은 타입 계약을 코드로 공유했습니다. 서버와 화면이 각자 감사 이벤트 구조를 따로 관리하다가 서로 다른 형태로 변하는 문제를 줄이기 위한 선택이었습니다.
구현이 끝난 뒤 상태 변경과 인증, 민감 열람을 포함한 14개의 운영 시나리오를 정해 직접 검증했습니다.
아래는 그중 핵심적인 경계를 확인한 사례입니다.
| 검증한 상황 | 기대한 결과 | 실제 결과 |
|---|---|---|
| 경고 화면 없이 지원자 조회 API 직접 호출 | 민감 열람 기록 생성 | 관리자·시각·경로·응답 결과 기록 |
| 상태 변경 직후 예외 발생 | 변경과 성공 기록이 함께 롤백 | 상태와 성공 기록 모두 남지 않음 |
| 로그인 실패 후 인증 예외 발생 | 실패 기록 보존 | 실패 횟수와 감사 이벤트 유지 |
| 전화번호 등 민감 필드 변경 | 원문 비저장 | 해당 필드가 바뀌었다는 사실만 기록 |
| 감사 로그 조회 | 조회 행위도 기록 | 조회한 관리자와 시각 확인 |
| 관리자 화면에서 기록 변경 시도 | 수정·삭제 불가 | 조회 전용 API와 화면만 존재 |
| 비개발 운영자가 특정 사건 탐색 | 개발자 도움 없이 확인 | 필터와 상세 화면으로 사건 확인 |
가장 먼저 확인한 것은 프론트엔드 우회였습니다.
관리자 계정으로 지원자 명단 API를 직접 호출해 개인정보 열람 경고를 거치지 않았습니다. 그래도 서버에는 관리자 계정과 요청 시각, 경로, 응답 결과가 동일하게 남았습니다.
경고는 사람의 판단을 돕고, 기록의 성립은 서버가 책임진다는 구분이 실제로 지켜지는지 확인한 것입니다.
트랜잭션도 서로 반대 방향으로 검증했습니다.
멤버 정보를 변경한 직후 의도적으로 예외를 발생시키자 업무 데이터와 성공 감사 이벤트가 모두 롤백됐습니다. 반면 로그인 실패 이벤트는 인증 예외가 발생해도 남았습니다.
둘 다 실패한 요청이지만 남겨야 하는 이야기는 달랐습니다.
상태 변경에 실패했다면 성공 기록이 없어야 합니다. 로그인에 실패했다면 실패했다는 사실이 남아야 합니다. 같은 예외 처리 안에서도 사건의 의미에 따라 트랜잭션 정책을 다르게 가져갔습니다.
기존 기능의 회귀 여부는 회원과 콘텐츠, 모집, 인증 기능을 포함한 백엔드 자동 테스트 363개와 프론트엔드 타입 검사·정적 분석으로 확인했습니다.
다만 기존 테스트가 모두 통과한다는 사실만으로 감사 통제가 제대로 작동한다고 볼 수는 없었습니다. 기존 기능이 깨지지 않았는지는 자동 테스트로 확인하고, 감사 기능이 의도한 사건을 남기는지는 API와 관리자 화면에서 별도로 검증했습니다.
감사 로그를 사고가 발생했을 때만 여는 화면으로 두지는 않았습니다.
모집 기간에는 로그인 실패와 지원자 개인정보 열람 기록을 정기적으로 확인합니다. 짧은 시간에 반복된 로그인 실패, 업무 맥락을 알기 어려운 개인정보 열람, 설명되지 않는 계정과 콘텐츠 변경은 바로 사고로 단정하지 않고 추가 확인이 필요한 신호로 봅니다.
이상한 기록이 발견되면 먼저 계정 소유자와 실제 사용 여부를 확인합니다. 상황에 따라 비밀번호를 초기화하거나 기존 토큰을 폐기하고, 관리자 계정을 회수합니다.
같은 실수가 반복되면 특정 운영자를 탓하는 데서 끝내지 않고 화면이나 운영 절차를 바꿀 근거로 사용합니다.
기수 교체 때는 새 운영진을 관리자로 초대하고 로그인이 정상적으로 되는 것을 확인한 뒤 기존 운영진의 계정을 삭제합니다. 이 과정도 감사 이벤트로 남기 때문에 권한 이전이 실제로 끝났는지 기록으로 확인할 수 있습니다.
현재 감사 기록에는 자동 삭제 정책을 두지 않았습니다. 지원자 연락처 같은 원문은 복제하지 않고 사건과 최소한의 식별 정보만 저장합니다.
다만 기록이 계속 쌓였을 때 실제로 얼마나 활용되는지, 클라이언트 IP와 같은 부가 정보가 장기간에도 필요한지는 운영 이후 다시 검토해야 합니다. 자동 삭제 정책이 없다는 사실이 모든 정보를 영구히 보존해야 한다는 뜻은 아닙니다.
이 구조가 모든 변조와 보안 사고를 막는 것은 아닙니다.
| 항목 | 현재 보장하는 것 | 현재 보장하지 않는 것 |
|---|---|---|
| 애플리케이션 관리자 | 감사 이벤트 수정·삭제 차단 | 서버·DB 운영자의 직접 변조 |
| 직접 API 호출 | 지정된 민감 조회 기록 | 클라이언트 단말 내부 행동 |
| 업무 트랜잭션 | 상태 변경과 성공 기록의 일관성 | 외부 시스템을 포함한 완전한 원자성 |
| 인증 실패 | 예외가 발생해도 실패 기록 보존 | 별도 침입 탐지 시스템의 자동 판정 |
| 기록 보관 | 현재 데이터베이스에 사건 보존 | 외부 WORM 저장소나 해시 체인 |
| 운영 리뷰 | 관리자가 사건을 검색하고 비교 | 24시간 실시간 보안 관제 |
이번에 보장한 불변성의 경계는 애플리케이션 관리자까지입니다.
서버나 데이터베이스에 직접 접근할 수 있는 사람이 기록을 수정하는 상황까지 탐지하려면 외부 저장소나 해시 체인 같은 구조가 추가로 필요합니다.
현재 조직의 규모와 운영 여건에서는 애플리케이션을 통한 수정과 삭제를 먼저 차단했습니다. 더 강한 변조 증거성은 지금 당장 필요한 통제와 구분해 후속 범위로 남겼습니다.
이 작업은 단지 관리자 화면에 로그 목록 하나를 추가한 일이 아니었습니다.
사전 승인을 두지 않는 운영 원칙은 그대로 유지하면서, 개인정보와 관리자 권한이 어떻게 사용됐는지를 나중에 다시 설명할 수 있게 만든 일이었습니다.
50명 규모의 조직에 대기업의 복잡한 승인 체계를 그대로 가져올 필요는 없었으며 권한을 빠르게 사용하면서도, 문제가 생겼을 때 사람의 기억이 아니라 중립적인 사실로 돌아갈 수 있어야 했습니다.
감사 로그는 사람을 믿지 않기 때문에 만든 장치가 아니라 사람을 믿고 운영하되, 그 신뢰가 개인의 기억과 관계에만 의존하지 않도록 만든 장치입니다.