사용자 요청
↓
백엔드 처리
↓
DB 조회/수정
↓
외부 API 호출
↓
응답 반환
각 단계에서 필요한 정보를 로그로 기록
장애 원인 분석
운영 추적
보안 감시
외부 API 문제 확인
커리어/성과 증명
| 구분 | 목적 | 예시 |
|---|---|---|
| 애플리케이션 로그 | 서버 동작과 에러 확인 | API 요청, 에러 stack, DB 오류 |
| 접근 로그 | 누가 어떤 요청을 했는지 확인 | Nginx access.log, API request log |
| 감사 로그 | 중요한 운영 행위 추적 | 관리자 상태 변경, 엑셀 다운로드 |
| 보안 로그 | 비정상 접근 감지 | 로그인 실패, 권한 없는 요청 |
| 외부 연동 로그 | 외부 API 호출 결과 추적 | 알림톡, SMS, 결제, Webhook |
| 배치 로그 | 정기 작업 성공/실패 확인 | 통계 집계, EP 생성, 파일 정리 |
상담 상태 변경:
김관리자
2026-07-04 14:10
PENDING → DONE
메모: 고객 상담 완료
IP: 123.123.123.123
method=GET
path=/api/admin/consults
status=200
durationMs=124
adminId=3
ip=123.123.123.123
level=ERROR
message=PrismaClientKnownRequestError
path=/api/admin/consults/export
adminId=3
errorCode=P2002
stack=...
상담 상태 변경
상담 메모 수정
주문 상태 변경
상품 등록/수정/삭제
배너 등록/수정/삭제
관리자 계정 생성/권한 변경
엑셀 다운로드
사전예약 상태 변경
시스템 설정 변경
provider=KAKAO
action=SEND_CONSULT_COMPLETE
status=FAILED
relatedType=CONSULT
relatedId=123
errorCode=TIMEOUT
attempts=3
jobName=DAILY_CONSULT_SUMMARY
status=SUCCESS
targetDate=2026-07-03
processedCount=428
durationMs=1530
나쁜 로그:
Error occurred
좋은 로그:
Failed to export consults
adminId=3
query=status=PENDING,source=NAVER,startDate=2026-07-01
errorCode=DB_TIMEOUT
durationMs=30200
로그에 남기면 위험한 것:
비밀번호
JWT
Refresh Token
API Key
Secret
전체 전화번호
주민등록번호
결제 카드 정보
인증번호
고객 상담 메모 원문
level=INFO
event=CONSULT_STATUS_UPDATED
adminId=3
consultId=123
fromStatus=PENDING
toStatus=DONE
durationMs=42
adminId, consultId, event, status 같은 값을 구조화해두면 나중에 검색하기 쉽습니다.| 레벨 | 의미 | 예시 |
|---|---|---|
| DEBUG | 개발/디버깅용 상세 정보 | SQL 조건, 내부 변수 |
| INFO | 정상적인 주요 이벤트 | 서버 시작, 상태 변경 |
| WARN | 주의가 필요한 상황 | 재시도 예정, 느린 요청 |
| ERROR | 실패한 작업 | API 오류, 외부 API 실패 |
| FATAL | 서비스 중단급 오류 | 서버 시작 실패, DB 연결 불가 |
개발 환경:
DEBUG까지 자세히 출력
운영 환경:
INFO, WARN, ERROR 중심
민감정보:
어떤 환경에서도 출력 금지
관리자 엑셀 다운로드 요청
↓
API 서버 로그
↓
DB 조회 로그
↓
Queue Job 등록 로그
↓
Worker 처리 로그
↓
S3 업로드 로그
같은 requestId로 묶으면 전체 흐름 추적 가능
requestId=req_20260704_abcd1234
event=EXPORT_JOB_CREATED
adminId=3
exportJobId=55
requestId=req_20260704_abcd1234
event=EXPORT_FILE_UPLOADED
exportJobId=55
fileKey=private/exports/consults/2026/07/04/file.xlsx
@Injectable()
export class RequestLogInterceptor implements NestInterceptor {
private readonly logger = new Logger(RequestLogInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
const { method, originalUrl } = request;
const start = Date.now();
return next.handle().pipe(
tap(() => {
const durationMs = Date.now() - start;
this.logger.log({
event: 'HTTP_REQUEST',
method,
path: originalUrl,
statusCode: context.switchToHttp().getResponse().statusCode,
durationMs,
userId: request.user?.id,
ip: request.ip,
});
}),
);
}
}
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest();
const response = ctx.getResponse();
const status =
exception instanceof HttpException
? exception.getStatus()
: 500;
this.logger.error({
event: 'HTTP_EXCEPTION',
status,
path: request.originalUrl,
method: request.method,
userId: request.user?.id,
ip: request.ip,
message:
exception instanceof Error
? exception.message
: 'Unknown error',
});
response.status(status).json({
success: false,
message:
status >= 500
? '서버 오류가 발생했습니다.'
: '요청을 처리할 수 없습니다.',
});
}
}
model AdminActionLog {
id Int @id @default(autoincrement())
adminId Int?
action String
resource String
resourceId Int?
beforeData Json?
afterData Json?
reason String?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
@@index([adminId, createdAt])
@@index([resource, resourceId])
@@index([action, createdAt])
}
| 필드 | 설명 |
|---|---|
adminId | 작업한 관리자 |
action | 수행한 작업 |
resource | 대상 리소스 |
resourceId | 대상 ID |
beforeData | 변경 전 데이터 |
afterData | 변경 후 데이터 |
reason | 변경 사유 |
ipAddress | 요청 IP |
userAgent | 브라우저/클라이언트 정보 |
createdAt | 작업 일시 |
beforeData, afterData에 모든 데이터를 넣으면 용량과 개인정보 문제가 생길 수 있습니다.{
"adminId": 3,
"action": "CONSULT_STATUS_UPDATED",
"resource": "CONSULT",
"resourceId": 123,
"beforeData": {
"status": "PENDING"
},
"afterData": {
"status": "DONE"
},
"reason": "고객 상담 완료"
}
{
"adminId": 3,
"action": "CONSULT_EXPORT_REQUESTED",
"resource": "EXPORT_JOB",
"resourceId": 55,
"afterData": {
"type": "CONSULT",
"query": {
"status": "PENDING",
"source": "NAVER",
"startDate": "2026-07-01",
"endDate": "2026-07-04"
},
"rowCount": 1200
}
}
{
"adminId": 2,
"action": "PRODUCT_UPDATED",
"resource": "PRODUCT",
"resourceId": 88,
"beforeData": {
"isVisible": false,
"price": 1200000
},
"afterData": {
"isVisible": true,
"price": 1150000
}
}
전화번호:
01012345678 → 010****5678
이름:
홍길동 → 홍*동
이메일:
test@example.com → te**@example.com
토큰:
앞 6자리만 표시 후 나머지 마스킹
export function maskPhone(phone?: string) {
if (!phone) {
return phone;
}
return phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1****$3');
}
export function maskEmail(email?: string) {
if (!email) {
return email;
}
const [local, domain] = email.split('@');
return `${local.slice(0, 2)}**@${domain}`;
}
model ExternalApiLog {
id Int @id @default(autoincrement())
provider String
action String
status String
relatedType String?
relatedId Int?
requestId String?
requestPayload Json?
responseBody Json?
errorCode String?
errorMessage String?
attempts Int @default(0)
createdAt DateTime @default(now())
completedAt DateTime?
@@index([provider, action])
@@index([status, createdAt])
@@index([relatedType, relatedId])
}
알림톡 발송 실패:
ExternalApiLog에 FAILED 저장
관리자 페이지:
실패 건 조회
재발송:
Queue에 재처리 Job 등록
성공:
ExternalApiLog 상태 SUCCESS 변경
model BatchJobLog {
id Int @id @default(autoincrement())
jobName String
status String
targetDate DateTime?
startedAt DateTime
endedAt DateTime?
durationMs Int?
processedCount Int?
successCount Int?
failCount Int?
errorMessage String?
createdAt DateTime @default(now())
@@index([jobName, startedAt])
@@index([status, startedAt])
}
{
"jobName": "DAILY_CONSULT_SUMMARY",
"status": "SUCCESS",
"targetDate": "2026-07-03",
"durationMs": 1450,
"processedCount": 428,
"successCount": 428,
"failCount": 0
}
관리자 로그인 성공
관리자 로그인 실패
비밀번호 변경
비밀번호 재설정 요청
권한 없는 API 접근
관리자 계정 생성
관리자 권한 변경
관리자 계정 비활성화
비정상적으로 많은 요청
IP 차단 또는 Rate Limit 발생
model SecurityLog {
id Int @id @default(autoincrement())
adminId Int?
event String
ipAddress String?
userAgent String?
result String
reason String?
createdAt DateTime @default(now())
@@index([adminId, createdAt])
@@index([event, createdAt])
@@index([ipAddress, createdAt])
}
| 저장 위치 | 용도 |
|---|---|
| 콘솔 로그 | 개발/간단한 운영 확인 |
| 파일 로그 | 서버 내부 로그 |
| DB 테이블 | 관리자 작업 이력, 외부 API 이력 |
| CloudWatch Logs | AWS 운영 로그 중앙화 |
| Sentry | 애플리케이션 에러 추적 |
| Slack/Email 알림 | 중요한 장애/실패 알림 |
일반 서버 요청/에러:
CloudWatch Logs 또는 파일 로그
관리자 작업 이력:
DB 테이블
외부 API 발송/실패 이력:
DB 테이블
심각한 장애:
Slack/Email 알림
프론트/백엔드 에러 추적:
Sentry 같은 도구
| 로그 종류 | 보관 기간 예시 |
|---|---|
| API 요청 로그 | 7일 ~ 30일 |
| 에러 로그 | 30일 ~ 90일 |
| 관리자 작업 이력 | 6개월 ~ 1년 이상 |
| 엑셀 다운로드 이력 | 1년 이상 검토 |
| 보안 로그 | 6개월 ~ 1년 이상 |
| 배치 로그 | 3개월 ~ 1년 |
| 외부 API 로그 | 3개월 ~ 1년 |
예시:
togethermall-prod-api
togethermall-prod-worker
togethermall-prod-batch
togethermall-prod-nginx
API 500 에러 급증
외부 API 실패 급증
배치 작업 실패
Webhook 처리 실패
관리자 로그인 실패 반복
RDS 연결 실패
Queue 적체 증가
Export 생성 실패
[API ERROR] 상담 Export 생성 실패
환경: production
requestId: req_20260704_abcd1234
adminId: 3
path: /api/admin/consults/export
errorCode: DB_TIMEOUT
durationMs: 30000
관리자 작업 이력
엑셀 다운로드 이력
알림톡/SMS 발송 이력
Webhook 수신 이력
배치 작업 이력
로그인/보안 이력
Export 생성 이력
예시:
2026-07-01 ~ 2026-07-04
action=CONSULT_EXPORT_REQUESTED
adminId=3
status=SUCCESS
문제 발생:
상담 상태가 바뀜
확인 불가:
누가 바꿨는지
언제 바꿨는지
이전 상태가 무엇이었는지
console.log(dto)
console.log(headers)
console.log(token)
console.log(apiKey)
매 요청마다 전체 body 출력
매 DB 조회마다 전체 결과 출력
외부 API 응답 전체 저장
console.log를 어디에 넣을지 묻는 문제가 아닙니다.NestJS + Prisma + AWS 기반 관리자 서비스에서 로그와 감사 이력 구조를 설계하고 싶어.
상황:
1. 관리자 페이지에는 상담 관리, 주문 관리, 상품 관리, 배너 관리, 엑셀 다운로드 기능이 있음
2. 상담 상태 변경, 주문 상태 변경, 상품 수정, 배너 수정은 이력을 남겨야 함
3. 엑셀 다운로드에는 개인정보가 포함될 수 있어 다운로드 이력이 필요함
4. 알림톡/SMS 외부 API 발송 성공/실패 이력을 남기고 싶음
5. Webhook 수신과 배치 작업 성공/실패도 추적하고 싶음
6. AWS에서는 CloudWatch Logs를 사용할 예정
7. 로그에 전화번호, 토큰, API Key 같은 민감정보는 남기면 안 됨
8. requestId로 요청 흐름을 추적하고 싶음
요청:
- 로그 종류별 저장 위치
- AdminActionLog 모델
- ExternalApiLog 모델
- BatchJobLog 모델
- SecurityLog 모델
- NestJS Interceptor/Filter 구조
- 개인정보 마스킹 기준
- CloudWatch Logs 운영 기준
- 관리자 페이지에서 보여줄 이력 메뉴
를 실무 기준으로 설명해줘.