본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 25. · 8 Views
API 표준과 규정 준수 완벽 가이드
현대 웹 서비스에서 필수적인 개인정보 보호와 API 보안에 대해 알아봅니다. GDPR부터 데이터 암호화, 감사 로그까지 실무에서 반드시 알아야 할 규정 준수 방법을 초보자도 이해할 수 있게 설명합니다.
목차
1. GDPR과 개인정보 보호
김개발 씨는 유럽 시장 진출을 앞둔 스타트업에서 백엔드 개발을 담당하고 있습니다. 어느 날 기획팀에서 급한 연락이 왔습니다.
"개발팀, 우리 서비스 GDPR 준수하고 있는 거 맞죠?" 김개발 씨는 GDPR이라는 단어를 처음 들어봤습니다.
GDPR(General Data Protection Regulation)은 유럽연합에서 제정한 개인정보 보호 규정입니다. 마치 아파트 경비원이 외부인의 출입을 철저히 관리하듯, 사용자의 개인정보를 수집하고 처리하는 모든 과정을 엄격하게 규제합니다.
이를 위반하면 전 세계 매출의 4%까지 과징금을 물 수 있어 모든 개발자가 반드시 알아야 합니다.
다음 코드를 살펴봅시다.
// GDPR 준수를 위한 사용자 데이터 처리 인터페이스
interface UserData {
id: string;
email: string;
name: string;
consentGiven: boolean; // 동의 여부 필수 기록
consentTimestamp: Date; // 동의 시점 기록
dataProcessingPurpose: string; // 처리 목적 명시
}
// 개인정보 수집 전 동의 확인
function collectUserData(userData: UserData): boolean {
// 핵심: 동의 없이는 데이터 수집 불가
if (!userData.consentGiven) {
throw new Error('사용자 동의가 필요합니다');
}
// 수집 목적이 명시되어야 함
if (!userData.dataProcessingPurpose) {
throw new Error('데이터 처리 목적을 명시해야 합니다');
}
return true;
}
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 그동안 열심히 기능 개발에만 집중해왔는데, 갑자기 GDPR이라는 생소한 용어를 마주하게 되었습니다.
도대체 이게 뭐길래 회사 전체가 긴장하는 걸까요? 선배 개발자 박시니어 씨가 커피 한 잔을 건네며 설명을 시작합니다.
"GDPR은 2018년부터 시행된 유럽의 개인정보 보호법이야. 우리 서비스가 유럽 사용자를 대상으로 한다면 반드시 지켜야 해." 그렇다면 GDPR이란 정확히 무엇일까요?
쉽게 비유하자면, GDPR은 마치 개인 금고의 열쇠와 같습니다. 은행에 맡긴 귀중품은 반드시 본인만 열 수 있어야 하고, 은행은 그 귀중품을 왜 보관하는지 명확히 알려줘야 합니다.
마찬가지로 사용자의 개인정보도 본인의 동의 없이는 절대 수집하거나 사용할 수 없습니다. GDPR이 없던 시절에는 어땠을까요?
기업들은 사용자 데이터를 마음대로 수집하고 활용했습니다. 이메일 주소를 수집해 동의 없이 광고 메일을 보내는 일이 흔했습니다.
더 심각한 문제는 수집한 데이터가 어디에 어떻게 사용되는지 사용자가 전혀 알 수 없었다는 점입니다. 바로 이런 문제를 해결하기 위해 GDPR이 등장했습니다.
GDPR의 핵심 원칙은 크게 세 가지입니다. 첫째, 투명성입니다.
어떤 데이터를 왜 수집하는지 명확히 알려야 합니다. 둘째, 목적 제한입니다.
수집 목적 외에 다른 용도로 사용할 수 없습니다. 셋째, 최소 수집입니다.
꼭 필요한 데이터만 수집해야 합니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 UserData 인터페이스를 보면 consentGiven과 consentTimestamp 필드가 있습니다. 이것이 핵심입니다.
사용자가 언제 동의했는지 반드시 기록해야 합니다. dataProcessingPurpose 필드는 이 데이터를 왜 수집하는지 명시합니다.
collectUserData 함수에서는 동의 여부를 가장 먼저 확인합니다. 동의가 없으면 아예 데이터 수집 자체를 거부합니다.
이것이 바로 GDPR의 핵심 정신입니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 뉴스레터 서비스를 운영한다고 가정해봅시다. 사용자가 이메일을 입력할 때 "마케팅 메일 수신에 동의합니다"라는 체크박스를 반드시 보여줘야 합니다.
그리고 이 동의는 선택사항이어야 합니다. 체크하지 않아도 서비스 이용이 가능해야 합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 동의 체크박스를 미리 체크해두는 것입니다.
이건 GDPR 위반입니다. 사용자가 직접 명시적으로 동의해야 합니다.
또한 "모든 약관에 동의"처럼 포괄적인 동의도 인정되지 않습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 곧바로 기존 회원가입 로직을 점검했습니다. 동의 체크박스가 있긴 했지만, 동의 시점을 기록하지 않고 있었습니다.
GDPR을 제대로 이해하면 사용자의 신뢰를 얻는 서비스를 만들 수 있습니다. 규정 준수는 귀찮은 일이 아니라 더 나은 서비스를 만드는 기회입니다.
실전 팁
💡 - 개인정보 수집 시 반드시 동의 시점과 동의 내용을 함께 기록하세요
- 동의 체크박스는 절대 미리 체크해두지 마세요
- 수집하는 모든 데이터의 목적을 문서화해두세요
2. 데이터 암호화 (전송/저장)
김개발 씨가 로그인 기능을 개발하고 있었습니다. 코드 리뷰 시간, 박시니어 씨가 화면을 보더니 미간을 찌푸렸습니다.
"비밀번호를 평문으로 저장하고 있네요. 이러면 큰일 납니다." 김개발 씨는 암호화라는 것이 왜 그렇게 중요한지 처음으로 깨달았습니다.
데이터 암호화는 정보를 알아볼 수 없는 형태로 변환하는 과정입니다. 마치 비밀 편지를 쓸 때 암호표를 사용하는 것처럼, 중요한 데이터는 전송 중에도(TLS/SSL), 저장할 때도(AES, bcrypt) 암호화되어야 합니다.
이를 통해 해커가 데이터를 탈취하더라도 내용을 알 수 없게 됩니다.
다음 코드를 살펴봅시다.
import bcrypt from 'bcrypt';
import crypto from 'crypto';
// 비밀번호 해싱 (저장용 - 단방향 암호화)
async function hashPassword(password: string): Promise<string> {
const saltRounds = 12; // 솔트 라운드: 높을수록 안전하지만 느림
return await bcrypt.hash(password, saltRounds);
}
// 비밀번호 검증
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return await bcrypt.compare(password, hash);
}
// 민감 데이터 암호화 (저장용 - 양방향 암호화)
function encryptData(data: string, secretKey: string): string {
const iv = crypto.randomBytes(16); // 초기화 벡터
const cipher = crypto.createCipheriv('aes-256-cbc', secretKey, iv);
const encrypted = cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
김개발 씨는 회원가입 기능을 열심히 만들었습니다. 이메일, 이름, 비밀번호를 받아서 데이터베이스에 저장하는 간단한 로직이었습니다.
그런데 왜 선배는 걱정스러운 표정을 지은 걸까요? 박시니어 씨가 화이트보드 앞으로 김개발 씨를 데려갔습니다.
"만약 해커가 우리 데이터베이스에 침입했다고 상상해봐. 비밀번호가 그대로 보인다면 어떻게 될까?" 그렇다면 데이터 암호화란 정확히 무엇일까요?
쉽게 비유하자면, 암호화는 마치 자물쇠가 달린 금고와 같습니다. 중요한 문서를 그냥 책상 위에 두면 누구나 볼 수 있지만, 금고에 넣으면 열쇠를 가진 사람만 볼 수 있습니다.
데이터 암호화도 마찬가지로 정보를 알아볼 수 없는 형태로 변환합니다. 암호화에는 크게 두 가지 상황이 있습니다.
전송 중 암호화와 저장 시 암호화입니다. 전송 중 암호화는 HTTPS를 생각하면 됩니다.
사용자가 로그인할 때 비밀번호가 네트워크를 통해 서버로 전달됩니다. 이때 누군가가 중간에서 데이터를 가로채면 어떻게 될까요?
HTTPS가 없다면 비밀번호가 그대로 노출됩니다. HTTPS는 TLS라는 프로토콜을 사용해 전송되는 모든 데이터를 암호화합니다.
저장 시 암호화는 데이터베이스에 저장할 때 적용합니다. 여기서 중요한 개념이 해싱과 양방향 암호화의 차이입니다.
비밀번호는 반드시 해싱을 사용해야 합니다. 해싱은 단방향 암호화입니다.
한번 변환하면 원래 값을 복원할 수 없습니다. 왜 복원할 수 없어야 할까요?
관리자조차 사용자의 비밀번호를 알 필요가 없기 때문입니다. 로그인할 때는 입력된 비밀번호를 같은 방식으로 해싱해서 저장된 해시값과 비교합니다.
위의 코드에서 bcrypt를 사용한 이유가 있습니다. bcrypt는 솔트라는 임의의 값을 추가해서 같은 비밀번호도 매번 다른 해시값을 만듭니다.
이렇게 하면 해커가 미리 계산해둔 해시 테이블(레인보우 테이블)로 공격하는 것을 막을 수 있습니다. 반면 나중에 원본 데이터가 필요한 경우도 있습니다.
예를 들어 사용자의 신용카드 번호를 저장해뒀다가 결제할 때 사용해야 한다면, 복호화가 가능한 양방향 암호화를 사용합니다. 코드에서 AES-256-CBC가 바로 이런 용도입니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 의료 정보 시스템을 개발한다고 가정해봅시다.
환자의 진료 기록은 나중에 조회해야 하므로 AES로 양방향 암호화합니다. 하지만 환자가 설정한 비밀번호는 bcrypt로 해싱합니다.
이렇게 데이터의 성격에 따라 적절한 암호화 방식을 선택해야 합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수는 MD5나 SHA1 같은 취약한 해시 알고리즘을 사용하는 것입니다. 이런 알고리즘은 너무 빨라서 오히려 공격에 취약합니다.
비밀번호 해싱에는 반드시 bcrypt, scrypt, 또는 Argon2처럼 의도적으로 느리게 설계된 알고리즘을 사용해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 후, 김개발 씨는 즉시 bcrypt를 도입했습니다. 기존 사용자들의 비밀번호도 다음 로그인 시 자동으로 bcrypt 해시로 마이그레이션되도록 처리했습니다.
데이터 암호화는 보안의 기본 중의 기본입니다. 이것 없이는 아무리 좋은 기능도 사상누각에 불과합니다.
실전 팁
💡 - 비밀번호는 절대 평문으로 저장하지 마세요. bcrypt 솔트 라운드는 최소 12 이상으로 설정하세요
- HTTPS는 선택이 아닌 필수입니다. 모든 API 통신에 적용하세요
- 암호화 키는 코드에 하드코딩하지 말고 환경변수나 비밀 관리 서비스를 사용하세요
3. 사용자 동의 관리
김개발 씨가 마케팅팀의 요청으로 푸시 알림 기능을 추가했습니다. 그런데 출시 후 고객센터에 항의 전화가 쏟아졌습니다.
"저는 광고 알림 받겠다고 한 적 없는데요?" 김개발 씨는 동의 관리의 중요성을 뼈저리게 느꼈습니다.
사용자 동의 관리는 개인정보 수집과 활용에 대한 사용자의 명시적 승인을 받고 관리하는 시스템입니다. 마치 계약서에 서명을 받는 것처럼, 어떤 목적으로 어떤 데이터를 사용하는지 명확히 알리고 동의를 얻어야 합니다.
사용자는 언제든 동의를 철회할 수 있어야 하며, 이 모든 이력을 기록해야 합니다.
다음 코드를 살펴봅시다.
// 동의 항목별 관리 인터페이스
interface ConsentRecord {
userId: string;
consentType: 'marketing' | 'analytics' | 'thirdParty' | 'essential';
isGranted: boolean;
grantedAt: Date | null;
revokedAt: Date | null;
ipAddress: string; // 동의 시점의 IP 기록
userAgent: string; // 동의 시점의 브라우저 정보
}
// 동의 상태 변경 함수
async function updateConsent(
userId: string,
consentType: string,
isGranted: boolean,
requestInfo: { ip: string; userAgent: string }
): Promise<ConsentRecord> {
const record: ConsentRecord = {
userId,
consentType: consentType as ConsentRecord['consentType'],
isGranted,
grantedAt: isGranted ? new Date() : null,
revokedAt: isGranted ? null : new Date(),
ipAddress: requestInfo.ip,
userAgent: requestInfo.userAgent
};
// 데이터베이스에 저장 (이력 보존)
await saveConsentHistory(record);
return record;
}
김개발 씨는 곤란한 상황에 처했습니다. 분명히 회원가입할 때 "이용약관 동의" 체크박스가 있었는데, 왜 사용자들은 광고 알림을 받겠다고 한 적 없다고 주장하는 걸까요?
박시니어 씨가 회원가입 화면을 열어보았습니다. "문제가 보이네.
이용약관 동의 하나로 마케팅 동의까지 포함시켰구나. 이건 명백한 위반이야." 그렇다면 올바른 동의 관리는 어떻게 해야 할까요?
쉽게 비유하자면, 동의 관리는 마치 뷔페 레스토랑의 메뉴 선택과 같습니다. 손님이 스테이크만 먹고 싶다고 했는데 강제로 해산물도 접시에 담아주면 안 됩니다.
각 메뉴를 개별적으로 선택할 수 있어야 합니다. 개인정보 동의도 마찬가지입니다.
동의는 크게 몇 가지 유형으로 나눌 수 있습니다. 첫째, 필수 동의입니다.
서비스 제공에 반드시 필요한 최소한의 정보 수집입니다. 예를 들어 배달 앱에서 배송지 주소는 필수입니다.
둘째, 선택 동의입니다. 마케팅 메일, 푸시 알림 같은 것들입니다.
사용자가 거부해도 서비스 이용에 지장이 없어야 합니다. 위의 코드를 살펴보면, ConsentRecord 인터페이스에 중요한 필드들이 있습니다.
consentType을 보세요. 마케팅, 분석, 제3자 제공, 필수 항목으로 세분화되어 있습니다.
이렇게 하면 사용자가 "마케팅 동의만 철회"할 수 있습니다. 모든 동의를 한 번에 관리하면 이런 세밀한 제어가 불가능합니다.
grantedAt과 revokedAt 필드도 중요합니다. 동의한 시점뿐만 아니라 철회한 시점도 기록합니다.
나중에 "이 사용자가 이 시점에 정말 동의했었나요?"라는 질문에 답할 수 있어야 합니다. ipAddress와 userAgent를 기록하는 이유는 무엇일까요?
법적 분쟁이 생겼을 때 "이 동의가 정말 해당 사용자로부터 온 것인가"를 증명해야 할 수 있습니다. 이런 메타 정보가 증거가 됩니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 이커머스 서비스를 운영한다고 가정해봅시다.
회원가입 시 필수 동의(이용약관, 개인정보처리방침)와 선택 동의(마케팅 수신, 맞춤형 광고)를 명확히 구분합니다. 선택 동의 체크박스는 미리 체크되어 있으면 안 됩니다.
그리고 마이페이지에서 언제든 동의 상태를 변경할 수 있는 화면을 제공해야 합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수는 동의 철회를 어렵게 만드는 것입니다. 동의할 때는 한 번의 클릭으로 쉽게 할 수 있으면서, 철회할 때는 복잡한 절차를 거치게 하면 안 됩니다.
GDPR에서는 이를 명시적으로 금지하고 있습니다. 철회는 동의만큼 쉬워야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언을 듣고, 김개발 씨는 동의 항목을 세분화하고 마이페이지에 동의 관리 화면을 추가했습니다.
기존 사용자들에게는 별도로 마케팅 동의를 다시 받는 팝업을 보여주기로 했습니다. 동의 관리를 제대로 하면 법적 리스크도 줄이고, 사용자 신뢰도 얻을 수 있습니다.
실전 팁
💡 - 동의 항목은 목적별로 세분화하고, 각각 개별적으로 동의/철회할 수 있게 하세요
- 동의 시점, IP, 브라우저 정보 등 메타 데이터를 함께 기록하세요
- 동의 철회는 동의만큼 쉬운 절차로 제공하세요
4. 데이터 삭제 요청 처리
어느 날 고객센터에서 개발팀으로 요청이 왔습니다. "한 고객이 자신의 모든 데이터를 삭제해달라고 합니다." 김개발 씨는 당황했습니다.
사용자 데이터가 여러 테이블에 분산되어 있었고, 어디까지 삭제해야 하는지 기준도 불명확했습니다.
데이터 삭제 요청 처리는 GDPR의 "잊힐 권리"에 해당하는 기능입니다. 마치 은행에서 계좌를 해지하면 관련 기록을 정리해주는 것처럼, 사용자가 요청하면 그 사람의 개인정보를 완전히 삭제해야 합니다.
다만 법적으로 보관해야 하는 데이터는 예외입니다. 이 모든 과정은 자동화되어야 합니다.
다음 코드를 살펴봅시다.
// 삭제 요청 상태 관리
interface DeletionRequest {
requestId: string;
userId: string;
requestedAt: Date;
status: 'pending' | 'processing' | 'completed' | 'partial';
completedAt: Date | null;
retainedDataReason: string | null; // 일부 보존 사유
}
// 사용자 데이터 삭제 처리
async function processDataDeletion(userId: string): Promise<DeletionRequest> {
const request: DeletionRequest = {
requestId: generateUUID(),
userId,
requestedAt: new Date(),
status: 'processing',
completedAt: null,
retainedDataReason: null
};
// 1. 삭제 가능한 데이터 목록 조회
const deletableData = await getDeletableUserData(userId);
// 2. 법적 보존 의무가 있는 데이터 분리
const { toDelete, toRetain } = separateByLegalRequirement(deletableData);
// 3. 삭제 실행 (소프트 삭제 또는 하드 삭제)
await deleteUserData(toDelete);
// 4. 보존 데이터가 있으면 사유 기록
if (toRetain.length > 0) {
request.retainedDataReason = '세금 관련 기록 5년 보존 의무';
request.status = 'partial';
} else {
request.status = 'completed';
}
request.completedAt = new Date();
return request;
}
김개발 씨는 데이터 삭제 요청을 처음 받아보았습니다. 단순히 users 테이블에서 한 줄 지우면 될 줄 알았는데, 생각보다 복잡했습니다.
주문 내역, 결제 기록, 상품 리뷰, 고객 문의 내역... 이 사용자와 관련된 데이터가 수십 개 테이블에 흩어져 있었습니다.
박시니어 씨가 심각한 표정으로 말했습니다. "이건 그냥 DELETE 쿼리 하나로 해결될 문제가 아니야.
체계적인 시스템이 필요해." 그렇다면 데이터 삭제 요청은 어떻게 처리해야 할까요? 쉽게 비유하자면, 이건 마치 이사할 때 집을 완전히 비우는 것과 같습니다.
거실만 비우고 창고에 짐을 두고 가면 안 됩니다. 모든 방, 모든 서랍을 확인해야 합니다.
하지만 임대차 계약서처럼 법적으로 보관해야 하는 서류는 따로 보관해야 합니다. GDPR에서는 이를 **잊힐 권리(Right to be Forgotten)**라고 부릅니다.
사용자가 요청하면 30일 이내에 처리해야 합니다. 위의 코드를 살펴보면, 삭제 처리가 단계별로 이루어집니다.
첫 번째 단계는 삭제 가능한 데이터를 전부 조회하는 것입니다. 이를 위해서는 사전에 "어떤 테이블에 사용자 데이터가 있는가"를 문서화해두어야 합니다.
데이터 맵핑이 되어 있지 않으면 이 단계에서 막힙니다. 두 번째 단계가 핵심입니다.
separateByLegalRequirement 함수를 보세요. 모든 데이터를 무조건 삭제하면 안 됩니다.
예를 들어 세금 관련 거래 기록은 5년간 보존해야 합니다. 의료 기록은 더 오래 보관해야 할 수도 있습니다.
이런 법적 의무가 있는 데이터는 삭제 대상에서 제외합니다. 세 번째 단계에서 실제 삭제가 일어납니다.
여기서 소프트 삭제와 하드 삭제의 차이를 알아야 합니다. 소프트 삭제는 deleted_at 같은 플래그를 설정해서 화면에 안 보이게 하는 것입니다.
하드 삭제는 데이터베이스에서 완전히 지우는 것입니다. GDPR 삭제 요청에는 하드 삭제가 필요합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 소셜 미디어 서비스를 운영한다고 가정해봅시다.
사용자가 탈퇴를 요청하면 프로필, 게시글, 댓글, 좋아요 기록을 모두 삭제해야 합니다. 하지만 다른 사용자가 해당 게시글을 신고해서 법적 분쟁 중이라면, 그 게시글은 분쟁이 해결될 때까지 보존해야 합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 백업 데이터를 잊는 것입니다.
데이터베이스에서 삭제했더라도 어제 만든 백업 파일에는 그 데이터가 남아 있습니다. 백업 데이터도 삭제 대상입니다.
또한 로그 파일, 분석 시스템, 캐시에 남아 있는 데이터도 확인해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨와 함께 모든 테이블을 점검한 결과, 사용자 데이터가 23개 테이블에 분산되어 있었습니다. 김개발 씨는 데이터 맵을 문서화하고, 자동화된 삭제 스크립트를 만들었습니다.
데이터 삭제 요청 처리는 사전 준비가 핵심입니다. 요청이 들어온 후에 허겁지겁 찾아다니면 30일 기한을 맞추기 어렵습니다.
실전 팁
💡 - 사용자 데이터가 저장된 모든 위치(테이블, 캐시, 백업)를 사전에 문서화하세요
- 법적 보존 의무가 있는 데이터 유형과 보존 기간을 명확히 정의하세요
- 삭제 요청 처리 상태를 사용자에게 투명하게 알려주세요
5. 감사 로그와 규정 준수
보안 감사를 앞둔 어느 날, 감사팀에서 질문이 왔습니다. "지난달 15일에 이 고객 데이터에 누가 접근했나요?" 김개발 씨는 식은땀이 났습니다.
그런 기록이 어디에도 없었기 때문입니다.
**감사 로그(Audit Log)**는 시스템에서 일어나는 모든 중요한 활동을 기록하는 것입니다. 마치 CCTV가 건물 출입을 녹화하듯, 누가 언제 어떤 데이터에 접근하고 변경했는지 추적할 수 있어야 합니다.
이는 보안 사고 대응뿐만 아니라 각종 규정 준수를 증명하는 데도 필수적입니다.
다음 코드를 살펴봅시다.
// 감사 로그 인터페이스
interface AuditLog {
logId: string;
timestamp: Date;
userId: string;
userRole: string;
action: 'CREATE' | 'READ' | 'UPDATE' | 'DELETE' | 'EXPORT';
resource: string; // 접근한 리소스 (테이블명, API 경로)
resourceId: string; // 해당 리소스의 ID
previousValue: object | null; // 변경 전 값 (UPDATE, DELETE 시)
newValue: object | null; // 변경 후 값 (CREATE, UPDATE 시)
ipAddress: string;
requestId: string; // 요청 추적용 ID
}
// 감사 로그 미들웨어
async function auditMiddleware(
req: Request,
action: string,
resource: string,
details: { previousValue?: object; newValue?: object }
): Promise<void> {
const log: AuditLog = {
logId: generateUUID(),
timestamp: new Date(),
userId: req.user.id,
userRole: req.user.role,
action: action as AuditLog['action'],
resource,
resourceId: req.params.id || 'N/A',
previousValue: details.previousValue || null,
newValue: details.newValue || null,
ipAddress: req.ip,
requestId: req.headers['x-request-id'] as string
};
// 별도의 감사 로그 저장소에 기록 (수정 불가능한 저장소 권장)
await writeToAuditStore(log);
}
김개발 씨는 감사팀의 질문에 당황했습니다. 서버 로그는 있었지만, "특정 고객 데이터에 누가 접근했는가"라는 질문에 답할 수 있는 형태가 아니었습니다.
그냥 HTTP 요청 로그만 덩그러니 남아 있었습니다. 박시니어 씨가 한숨을 쉬며 말했습니다.
"감사 로그는 나중에 추가하려면 정말 힘들어. 처음부터 설계에 포함했어야 했는데..." 그렇다면 감사 로그란 정확히 무엇일까요?
쉽게 비유하자면, 감사 로그는 마치 항공기의 블랙박스와 같습니다. 비행기에 무슨 일이 생기면 블랙박스를 분석해서 원인을 파악합니다.
마찬가지로 보안 사고가 발생하면 감사 로그를 분석해서 무슨 일이 있었는지 추적합니다. 감사 로그와 일반 서버 로그는 다릅니다.
일반 서버 로그는 "언제 어떤 요청이 왔고 응답 시간이 얼마였는가" 정도를 기록합니다. 성능 모니터링 목적입니다.
반면 감사 로그는 "누가 어떤 데이터를 조회/수정/삭제했는가"를 비즈니스 관점에서 기록합니다. 위의 코드를 살펴보면, AuditLog 인터페이스에 핵심 필드들이 있습니다.
previousValue와 newValue 필드를 주목하세요. 데이터가 변경될 때 "무엇이 무엇으로 바뀌었는가"를 기록합니다.
예를 들어 사용자의 이메일 주소가 변경되면, 이전 이메일과 새 이메일을 모두 기록합니다. 나중에 "이 변경이 정당했는가"를 검증할 수 있습니다.
requestId 필드도 중요합니다. 마이크로서비스 환경에서는 하나의 사용자 요청이 여러 서비스를 거칩니다.
모든 로그에 동일한 requestId를 붙이면 전체 흐름을 추적할 수 있습니다. 감사 로그는 어디에 저장해야 할까요?
일반 데이터베이스에 저장하면 관리자가 수정할 수 있습니다. 진정한 감사 목적이라면 수정 불가능한 저장소를 사용해야 합니다.
AWS의 CloudWatch Logs나 전용 SIEM 솔루션이 이런 용도로 사용됩니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 금융 서비스를 운영한다고 가정해봅시다. 고객의 계좌 잔액이 변경될 때마다 누가 변경했는지, 이전 잔액과 새 잔액이 얼마인지 기록합니다.
나중에 금융감독원 감사에서 "이 거래의 처리 과정을 보여주세요"라는 요청이 오면 감사 로그로 답할 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수는 너무 많은 것을 기록하거나 너무 적게 기록하는 것입니다. 모든 API 호출을 다 기록하면 저장 비용이 폭발하고 정작 중요한 로그를 찾기 어렵습니다.
반면 중요한 데이터 변경을 기록하지 않으면 감사 목적을 달성할 수 없습니다. 무엇을 기록할지 명확한 정책이 필요합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨와 함께 "감사 로그 정책 문서"를 만들었습니다.
개인정보 조회, 결제 처리, 권한 변경 같은 중요한 활동만 감사 로그로 남기기로 했습니다. 감사 로그는 보안과 규정 준수의 마지막 방어선입니다.
사고가 터진 후에 추가하려면 이미 늦습니다.
실전 팁
💡 - 감사 로그는 수정 불가능한 별도 저장소에 보관하세요
- 무엇을 기록하고 무엇을 기록하지 않을지 명확한 정책을 수립하세요
- 로그 보존 기간을 규정에 맞게 설정하세요 (보통 최소 1년)
6. API 보안 체크리스트
김개발 씨가 드디어 첫 번째 API를 운영 환경에 배포했습니다. 뿌듯해하던 것도 잠시, 다음 날 보안팀에서 연락이 왔습니다.
"취약점 스캔 결과가 나왔는데요, 좀 봐야 할 것 같아요." 김개발 씨는 보안이라는 것이 기능 개발과는 별개의 세계라는 것을 깨달았습니다.
API 보안 체크리스트는 API를 배포하기 전에 반드시 확인해야 할 보안 항목들의 목록입니다. 마치 비행기 이륙 전 조종사가 체크리스트를 확인하듯, 개발자도 배포 전에 보안 항목을 하나씩 점검해야 합니다.
인증, 권한, 입력값 검증, 속도 제한 등 다양한 영역을 포괄합니다.
다음 코드를 살펴봅시다.
// 보안 미들웨어 체인 예시
import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import { body, validationResult } from 'express-validator';
// 1. 보안 헤더 설정
app.use(helmet());
// 2. 속도 제한 (Rate Limiting)
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // IP당 최대 100 요청
message: '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.'
});
app.use('/api/', limiter);
// 3. 입력값 검증 미들웨어
const validateUserInput = [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }).trim(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
// 4. 인증 확인 미들웨어
const requireAuth = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '인증이 필요합니다' });
}
// 토큰 검증 로직
next();
};
김개발 씨는 보안팀에서 보내온 취약점 보고서를 열어보았습니다. SQL 인젝션 가능성, XSS 취약점, 무차별 대입 공격에 취약...
용어 하나하나가 낯설었습니다. 도대체 어디서부터 손을 대야 할까요?
박시니어 씨가 차분하게 설명을 시작했습니다. "보안은 한 번에 완벽하게 할 수 없어.
하지만 기본적인 체크리스트를 따르면 대부분의 일반적인 공격은 막을 수 있지." API 보안 체크리스트의 핵심 항목들을 하나씩 살펴보겠습니다. 첫 번째는 보안 헤더입니다.
HTTP 응답에 보안 관련 헤더를 추가하면 브라우저 수준에서 많은 공격을 막을 수 있습니다. 코드에서 helmet 라이브러리를 사용한 이유입니다.
이것 하나로 X-XSS-Protection, X-Content-Type-Options 같은 여러 보안 헤더가 자동으로 추가됩니다. 두 번째는 **속도 제한(Rate Limiting)**입니다.
이것이 없으면 공격자가 로그인 API에 비밀번호를 수백만 번 시도할 수 있습니다. 코드에서는 15분에 100번으로 제한했습니다.
실제 서비스에서는 API 종류에 따라 다르게 설정합니다. 로그인 같은 민감한 API는 더 엄격하게 제한합니다.
세 번째는 입력값 검증입니다. 사용자가 보내는 모든 데이터는 믿으면 안 됩니다.
이메일 필드에 SQL 쿼리를 넣어 보내는 공격자도 있습니다. 코드에서 express-validator를 사용해 이메일 형식을 검증하고, 비밀번호 길이를 확인합니다.
normalizeEmail과 trim 같은 정규화도 중요합니다. 네 번째는 **인증(Authentication)**과 **인가(Authorization)**입니다.
인증은 "당신이 누구인가"를 확인하는 것이고, 인가는 "당신이 이 작업을 할 권한이 있는가"를 확인하는 것입니다. 둘은 다릅니다.
로그인한 사용자라도 다른 사용자의 데이터에 접근하면 안 됩니다. 다섯 번째는 HTTPS 강제입니다.
모든 API 통신은 반드시 HTTPS를 통해야 합니다. HTTP로 접속하면 HTTPS로 리다이렉트하거나 아예 거부해야 합니다.
여섯 번째는 에러 메시지 관리입니다. 상세한 에러 메시지는 개발 중에는 유용하지만, 운영 환경에서는 공격자에게 힌트를 줍니다.
"비밀번호가 틀렸습니다"보다 "이메일 또는 비밀번호가 올바르지 않습니다"가 더 안전합니다. 일곱 번째는 의존성 취약점 관리입니다.
npm audit 같은 도구로 정기적으로 사용 중인 패키지의 알려진 취약점을 확인해야 합니다. 실제 현업에서는 어떻게 활용할까요?
많은 기업에서는 배포 파이프라인에 보안 검사를 통합합니다. 코드가 커밋되면 자동으로 정적 분석이 돌아가고, 취약점이 발견되면 배포가 차단됩니다.
이것을 DevSecOps라고 부릅니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수는 보안을 나중에 추가하려고 하는 것입니다. "일단 기능 먼저 개발하고 보안은 나중에"라는 생각은 위험합니다.
나중에 추가하려면 코드를 대대적으로 수정해야 할 수도 있습니다. 보안은 처음부터 설계에 포함되어야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨가 알려준 체크리스트를 바탕으로 하나씩 적용해나갔습니다.
며칠 후 다시 취약점 스캔을 돌렸더니, 심각한 취약점이 모두 사라졌습니다. API 보안은 끝이 없는 여정입니다.
하지만 기본 체크리스트부터 철저히 지키면 대부분의 일반적인 공격은 막을 수 있습니다.
실전 팁
💡 - 배포 전 보안 체크리스트를 팀 차원에서 문서화하고 필수로 확인하세요
- npm audit, Snyk 같은 도구로 정기적으로 의존성 취약점을 점검하세요
- 보안은 나중에가 아니라 처음부터 설계에 포함해야 합니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Docker 실전 프로젝트 완벽 가이드
프론트엔드, 백엔드, 데이터베이스를 Docker로 컨테이너화하고 Nginx 리버스 프록시를 구성하여 실제 프로덕션 환경에 배포하는 전 과정을 다룹니다. 실무에서 바로 적용할 수 있는 Docker Compose 기반 멀티 컨테이너 애플리케이션 구축법을 배웁니다.
Docker Compose 실전 활용 완벽 가이드
Docker Compose를 활용하여 복잡한 멀티 컨테이너 환경을 손쉽게 구축하고 관리하는 방법을 배웁니다. 개발 환경부터 운영 환경까지, 실무에서 바로 적용할 수 있는 핵심 기법들을 단계별로 알아봅니다.
API 아키텍처 패턴과 설계 완벽 가이드
모놀리식부터 마이크로서비스까지, 현대 백엔드 아키텍처의 핵심 패턴들을 초급 개발자도 이해할 수 있도록 실무 예제와 함께 설명합니다. API Gateway, BFF, 이벤트 기반 아키텍처 등 실전에서 바로 적용할 수 있는 설계 전략을 다룹니다.
효과적인 API 문서 작성법 완벽 가이드
API 문서는 개발자와 개발자 사이의 가장 중요한 소통 수단입니다. 이 가이드에서는 좋은 API 문서가 갖춰야 할 조건부터 Getting Started, 엔드포인트 설명, 에러 코드 문서화, 인증 가이드, 변경 이력 관리까지 체계적으로 배워봅니다.
OpenAPI/Swagger로 API 문서화 완벽 가이드
API 문서화의 표준인 OpenAPI와 Swagger를 활용하여 프론트엔드 개발자와 원활하게 협업하는 방법을 배웁니다. 스펙 작성부터 자동 문서 생성, 버전 관리까지 실무에서 바로 적용할 수 있는 내용을 다룹니다.