TIL - 20260901

juni·2026년 9월 1일

TIL

목록 보기
446/468

0901 백엔드 아키텍처 고도화 (9/N): 테스트 가능한 백엔드 구조와 Mock 전략


✅ 1. 테스트 가능한 백엔드 구조란 무엇인가?

  • 테스트 가능한 백엔드 구조는 기능을 만든 뒤 “직접 눌러봐야만” 정상 여부를 알 수 있는 구조가 아니라, 코드 단위로 핵심 동작을 검증할 수 있는 구조입니다.
  • 테스트는 단순히 버그를 찾기 위한 것이 아니라, 나중에 리팩토링하거나 AI/Codex에게 수정을 맡길 때 기존 기능이 깨졌는지 확인하는 안전장치입니다.
  • 특히 상담 상태 변경, 중복 신청, 알림톡 Job 생성, 엑셀 Export 요청, 권한 체크, Audit Log 저장 같은 기능은 테스트 가치가 높습니다.
테스트하기 어려운 구조:
Controller → Service 안에 모든 로직 → Prisma 직접 호출 → 외부 API 직접 호출

테스트하기 쉬운 구조:
Controller → Use Case → Domain Service / Repository / Adapter

➕ 1-1. 테스트가 어려운 구조의 문제

Service 파일이 너무 큼
DB query와 비즈니스 규칙이 섞임
외부 API 호출이 직접 들어감
transaction 범위가 숨겨져 있음
권한 체크가 여러 곳에 흩어짐
Mock으로 대체하기 어려움
  • 테스트가 어렵다는 것은 구조가 복잡하게 얽혀 있다는 신호일 수 있습니다.
  • 좋은 테스트를 만들려면 먼저 코드 책임이 나뉘어 있어야 합니다.

✅ 2. 테스트의 종류

  • 백엔드 테스트는 보통 단위 테스트, 통합 테스트, E2E 테스트로 나눌 수 있습니다.
  • 모든 것을 E2E로만 테스트하면 느리고 관리가 어렵습니다.
  • 반대로 모든 것을 단위 테스트로만 하면 실제 DB/HTTP 흐름에서 생기는 문제를 놓칠 수 있습니다.
종류목적예시
Unit Test작은 로직 검증상태 전이 규칙
Integration Test여러 계층/DB 연동 검증상태 변경 Use Case
E2E Test실제 API 흐름 검증로그인 후 상태 변경 요청
Contract Test외부 API/응답 계약 검증알림톡 Adapter 응답 타입

➕ 2-1. 현재 프로젝트 기준 추천

Unit Test:
Domain Service, Policy, Mapper

Integration Test:
Use Case + Repository + Test DB

E2E Test:
Controller + Guard + Use Case + DB

Mock Test:
외부 API Adapter, S3, 알림톡, Webhook
  • 핵심 규칙은 Unit Test로 빠르게 검증합니다.
  • DB 정합성이 중요한 흐름은 Integration Test로 확인합니다.
  • API 권한/응답 구조는 E2E Test로 확인합니다.

✅ 3. 테스트 가능한 구조의 핵심

  • 테스트를 쉽게 만들려면 의존성을 분리해야 합니다.
  • 특히 외부 API, DB, 현재 시간, 랜덤값, 파일 시스템, S3 같은 요소는 테스트를 어렵게 만듭니다.
1. 비즈니스 규칙은 Domain Service로 분리
2. 업무 흐름은 Use Case로 분리
3. DB 접근은 Repository로 분리
4. 외부 API는 Adapter로 분리
5. 현재 시간은 Clock으로 분리
6. 랜덤 ID 생성은 IdGenerator로 분리
7. 응답 변환은 Mapper로 분리

➕ 3-1. 테스트를 어렵게 만드는 의존성

new Date()
randomUUID()
process.env 직접 참조
axios 직접 호출
S3 SDK 직접 호출
Prisma 직접 호출
파일 시스템 직접 접근

➕ 3-2. 개선 방향

직접 호출:
Use Case 안에서 axios.post()

분리:
Use Case → NotificationAdapter 인터페이스 → 실제 AlimtalkAdapter
  • 테스트에서 제어하기 어려운 것들은 외부로 빼야 합니다.
  • 그래야 Mock으로 대체할 수 있습니다.

✅ 4. Unit Test

  • Unit Test는 가장 작은 단위의 로직을 빠르게 검증하는 테스트입니다.
  • DB나 외부 API 없이 실행되는 것이 좋습니다.
  • 상태 전이 규칙, 권한 Policy, 중복 판단 규칙, Mapper, 날짜 계산 함수 같은 코드가 좋은 대상입니다.

➕ 4-1. 상태 전이 테스트 예시

describe('ConsultStatusService', () => {
  const service = new ConsultStatusService();

  it('NEW 상태에서 CALLING으로 변경할 수 있다', () => {
    expect(() =>
      service.validateTransition('NEW', 'CALLING'),
    ).not.toThrow();
  });

  it('CANCELED 상태에서 CONVERTED로 변경할 수 없다', () => {
    expect(() =>
      service.validateTransition('CANCELED', 'CONVERTED'),
    ).toThrow();
  });
});

➕ 4-2. 권한 Policy 테스트 예시

describe('ConsultPermissionPolicy', () => {
  const policy = new ConsultPermissionPolicy();

  it('CONSULT_UPDATE_STATUS 권한이 없으면 상태 변경할 수 없다', () => {
    const result = policy.canChangeStatus({
      admin: {
        id: 1,
        role: 'STAFF',
        permissions: ['CONSULT_READ'],
      },
      currentStatus: 'NEW',
      nextStatus: 'CALLING',
      assignedAdminId: null,
    });

    expect(result).toBe(false);
  });

  it('SUPER_ADMIN은 상태 변경할 수 있다', () => {
    const result = policy.canChangeStatus({
      admin: {
        id: 1,
        role: 'SUPER_ADMIN',
        permissions: ['*'],
      },
      currentStatus: 'NEW',
      nextStatus: 'CALLING',
      assignedAdminId: null,
    });

    expect(result).toBe(true);
  });
});
  • Unit Test는 빨라야 합니다.
  • DB가 없어도 검증 가능한 규칙부터 테스트하면 부담이 적습니다.

✅ 5. Integration Test

  • Integration Test는 실제 DB 또는 테스트 DB와 함께 여러 계층이 제대로 연결되는지 확인합니다.
  • 상담 상태 변경처럼 transaction, Repository, Audit Log, 상태 이력이 함께 동작해야 하는 기능에 적합합니다.
Test DB 준비
  ↓
상담 row 생성
  ↓
Use Case 실행
  ↓
consults.status 확인
  ↓
status_history 확인
  ↓
audit_log 확인

➕ 5-1. 상태 변경 Integration Test 후보

NEW → CALLING 성공
상태 변경 시 status_history 생성
상태 변경 시 audit_log 생성
CANCELED → CONVERTED 실패
동시 수정 충돌 시 409
Audit Log 생성 실패 시 전체 rollback

➕ 5-2. 테스트 흐름 예시

it('상담 상태 변경 시 상태, 이력, Audit Log가 함께 저장된다', async () => {
  const consult = await seedConsult({
    status: 'NEW',
  });

  await updateConsultStatusUseCase.execute({
    consultId: consult.id,
    nextStatus: 'CALLING',
    adminId: 1,
    requestId: 'test-request-id',
  });

  const updated = await prisma.consult.findUnique({
    where: { id: consult.id },
  });

  const histories = await prisma.consultStatusHistory.findMany({
    where: { consultId: consult.id },
  });

  const auditLogs = await prisma.auditLog.findMany({
    where: {
      targetType: 'CONSULT',
      targetId: String(consult.id),
      action: 'CONSULT_STATUS_UPDATE',
    },
  });

  expect(updated?.status).toBe('CALLING');
  expect(histories).toHaveLength(1);
  expect(auditLogs).toHaveLength(1);
});
  • Integration Test는 Unit Test보다 느리지만 실무 신뢰도가 높습니다.
  • DB 정합성이 중요한 기능은 Integration Test가 더 의미 있습니다.

✅ 6. E2E Test

  • E2E Test는 실제 HTTP 요청부터 응답까지 전체 흐름을 검증합니다.
  • 인증, 권한 Guard, DTO validation, Controller, Use Case, Repository, 응답 구조를 함께 확인할 수 있습니다.
HTTP 요청
  ↓
AuthGuard
  ↓
PermissionGuard
  ↓
Controller
  ↓
Use Case
  ↓
DB
  ↓
표준 응답

➕ 6-1. E2E 테스트 후보

관리자 로그인
상담 목록 조회
상담 상태 변경
권한 없는 엑셀 Export 요청
Validation Error 응답 구조
403 권한 없음 응답 구조
409 동시 수정 충돌 응답 구조

➕ 6-2. E2E 예시

it('권한 있는 관리자는 상담 상태를 변경할 수 있다', async () => {
  const token = await loginAsAdmin({
    permissions: ['CONSULT_UPDATE_STATUS'],
  });

  const consult = await seedConsult({
    status: 'NEW',
  });

  const response = await request(app.getHttpServer())
    .post(`/admin/consults/${consult.id}/status`)
    .set('Authorization', `Bearer ${token}`)
    .send({
      status: 'CALLING',
    })
    .expect(200);

  expect(response.body.success).toBe(true);
  expect(response.body.data.status).toBe('CALLING');
});

➕ 6-3. 표준 에러 응답 테스트

it('권한이 없으면 표준 403 에러 응답을 반환한다', async () => {
  const token = await loginAsAdmin({
    permissions: ['CONSULT_READ'],
  });

  const consult = await seedConsult({
    status: 'NEW',
  });

  const response = await request(app.getHttpServer())
    .post(`/admin/consults/${consult.id}/status`)
    .set('Authorization', `Bearer ${token}`)
    .send({
      status: 'CALLING',
    })
    .expect(403);

  expect(response.body).toMatchObject({
    success: false,
    error: {
      code: 'FORBIDDEN',
    },
  });

  expect(response.body.requestId).toBeDefined();
});
  • E2E Test는 너무 많이 만들면 유지보수가 어렵습니다.
  • 핵심 API와 권한/에러 응답 중심으로 잡는 것이 좋습니다.

✅ 7. Mock이란 무엇인가?

  • Mock은 테스트에서 실제 의존성을 가짜 객체로 대체하는 것입니다.
  • 외부 API, S3, 알림톡, 현재 시간, 랜덤값처럼 테스트에서 실제로 실행하면 안 되거나 불안정한 요소를 대체합니다.
실제:
AlimtalkAdapter → 외부 알림톡 API 호출

테스트:
MockAlimtalkAdapter → 정해진 응답 반환

➕ 7-1. Mock이 필요한 이유

외부 API 비용 방지
테스트 속도 향상
외부 장애에 테스트가 흔들리지 않음
실패 상황을 의도적으로 만들 수 있음
개인정보/Secret 없이 테스트 가능

➕ 7-2. Mock 대상

AlimtalkAdapter
SmsAdapter
S3Adapter
ExcelService
Clock
IdGenerator
WebhookSignatureVerifier
ExternalCrmAdapter
  • Mock은 외부 세계를 통제하기 위한 도구입니다.
  • 비즈니스 규칙 자체를 무리하게 Mock하면 테스트 의미가 약해질 수 있습니다.

✅ 8. Mock 전략의 기준

  • 무엇을 Mock하고 무엇을 실제로 사용할지 기준이 필요합니다.
  • 모든 것을 Mock하면 실제 연결 문제를 놓치고, 아무것도 Mock하지 않으면 테스트가 느리고 불안정해집니다.

➕ 8-1. Mock하는 것이 좋은 것

외부 API
파일 업로드
이메일/알림톡/SMS 발송
현재 시간
랜덤 ID
결제/인증 Provider
느리고 비용이 드는 작업

➕ 8-2. 실제로 테스트하는 것이 좋은 것

Domain Service 규칙
Use Case 흐름
Repository query
DB transaction
권한 Guard
표준 에러 응답

➕ 8-3. 기준

내부 핵심 로직:
실제로 테스트

외부 의존성:
Mock으로 대체

DB 정합성:
Test DB로 확인

외부 API 계약:
Adapter 단위 테스트 또는 별도 Contract Test
  • 상담 상태 변경의 핵심은 Mock하면 안 됩니다.
  • 알림톡 API 호출은 Mock하는 것이 맞습니다.

✅ 9. Repository Mock과 Test DB

  • Use Case 테스트에서 Repository를 Mock할 수도 있고, 실제 Test DB를 사용할 수도 있습니다.
  • 각각 장단점이 있습니다.

➕ 9-1. Repository Mock

장점:
빠름
DB 필요 없음
특정 상황 만들기 쉬움

단점:
실제 query 문제를 못 잡음
transaction 문제를 놓칠 수 있음
Mock이 실제 동작과 달라질 수 있음

➕ 9-2. Test DB

장점:
실제 query 검증 가능
transaction 검증 가능
Prisma schema 문제 확인 가능

단점:
느림
테스트 환경 준비 필요
데이터 초기화 필요

➕ 9-3. 현재 프로젝트 기준 추천

Domain Service:
Mock/DB 없이 Unit Test

Use Case:
중요한 것은 Test DB 기반 Integration Test

단순 흐름:
Repository Mock도 가능

Repository 자체:
Test DB로 확인
  • 상태 변경, 중복 신청, ExportJob 생성처럼 DB 정합성이 중요한 기능은 Test DB가 낫습니다.
  • 단순히 “Repository 메서드를 호출했다”만 확인하는 테스트는 실무 가치가 낮습니다.

✅ 10. Test DB 전략

  • Integration/E2E 테스트에는 실제 운영 DB가 아니라 테스트 전용 DB를 사용해야 합니다.
  • 로컬에서는 OrbStack PostgreSQL에 테스트 DB를 따로 만들 수 있습니다.
개발 DB:
togethermall_dev

테스트 DB:
togethermall_test

운영 DB:
togethermall_prod

➕ 10-1. 테스트 DB 원칙

운영 DB 절대 사용 금지
테스트마다 데이터 초기화
seed 데이터 최소화
테스트 실행 후 정리
migration과 schema 일치

➕ 10-2. package script 예시

{
  "scripts": {
    "test": "jest",
    "test:unit": "jest --config jest.unit.config.js",
    "test:integration": "dotenv -e .env.test -- jest --config jest.integration.config.js",
    "test:e2e": "dotenv -e .env.test -- jest --config jest-e2e.json"
  }
}

➕ 10-3. 주의

DATABASE_URL이 테스트 DB인지 반드시 확인
test 환경에서 production URL 차단
테스트 실행 전 db reset 범위 확인
운영 Secret 사용 금지
  • 테스트 DB와 운영 DB가 섞이면 큰 사고가 납니다.
  • 테스트 실행 시 NODE_ENV=test와 DB URL 검증을 강하게 해두는 것이 좋습니다.

✅ 11. 테스트 데이터 초기화

  • 테스트는 서로 영향을 주면 안 됩니다.
  • 이전 테스트가 만든 데이터 때문에 다음 테스트가 실패하면 신뢰도가 떨어집니다.

➕ 11-1. 초기화 방식

방법 1:
각 테스트 후 deleteMany

방법 2:
테스트 파일마다 transaction rollback

방법 3:
DB schema reset 후 seed

방법 4:
테스트 컨테이너 사용

➕ 11-2. 간단한 초기화 예시

beforeEach(async () => {
  await prisma.auditLog.deleteMany();
  await prisma.consultStatusHistory.deleteMany();
  await prisma.consult.deleteMany();
  await prisma.product.deleteMany();
  await prisma.adminUser.deleteMany();
});

➕ 11-3. 삭제 순서 주의

FK 관계가 있는 테이블은 자식 테이블부터 삭제
status_history → consult
role_permission → role/permission
export_job → admin_user
  • 테스트 초기화는 귀찮지만 중요합니다.
  • 데이터가 섞인 테스트는 실패 원인을 찾기 어렵습니다.

✅ 12. Seed Factory

  • 테스트마다 row를 직접 만드는 코드를 반복하면 지저분해집니다.
  • seedConsult, seedAdmin, seedProduct 같은 Factory를 만들면 좋습니다.

➕ 12-1. Seed Factory 예시

export async function seedConsult(
  prisma: PrismaService,
  overrides: Partial<Prisma.ConsultCreateInput> = {},
) {
  return prisma.consult.create({
    data: {
      customerName: '테스트 고객',
      phoneMasked: '010****1234',
      phoneNormalized: '01012341234',
      status: 'NEW',
      source: 'TEST',
      ...overrides,
    },
  });
}

➕ 12-2. 관리자 Seed 예시

export async function seedAdmin(
  prisma: PrismaService,
  overrides: Partial<Prisma.AdminUserCreateInput> = {},
) {
  return prisma.adminUser.create({
    data: {
      email: `admin-${randomUUID()}@example.com`,
      name: '테스트 관리자',
      role: 'MANAGER',
      isActive: true,
      ...overrides,
    },
  });
}

➕ 12-3. 장점

테스트 코드 중복 감소
기본값 통일
필요한 값만 override 가능
테스트 가독성 향상
  • Seed Factory는 테스트 유지보수성을 크게 높입니다.
  • 특히 상담/상품/관리자처럼 반복해서 필요한 데이터에 유용합니다.

✅ 13. Clock Mock

  • new Date()를 코드 곳곳에서 직접 쓰면 테스트가 불안정해질 수 있습니다.
  • 상태 변경 시간, Export 만료 시간, retry 시간 계산은 Clock을 분리하면 테스트하기 쉽습니다.

➕ 13-1. 나쁜 예시

const changedAt = new Date();

➕ 13-2. Clock 서비스

@Injectable()
export class Clock {
  now() {
    return new Date();
  }
}

➕ 13-3. Use Case에서 사용

const changedAt = this.clock.now();

➕ 13-4. 테스트 Mock

const fixedDate = new Date('2026-09-01T00:00:00.000Z');

const clockMock = {
  now: jest.fn(() => fixedDate),
};
  • 시간은 테스트에서 고정할 수 있어야 합니다.
  • ExportJob expiresAt, Retry nextRetryAt, Audit Log 시각 검증에 특히 유용합니다.

✅ 14. IdGenerator Mock

  • requestId, 파일명, idempotency key처럼 랜덤값이 필요한 곳은 테스트에서 고정하기 어렵습니다.
  • IdGenerator를 분리하면 테스트가 쉬워집니다.

➕ 14-1. IdGenerator 예시

@Injectable()
export class IdGenerator {
  uuid() {
    return randomUUID();
  }

  requestId() {
    return `req_${randomUUID()}`;
  }
}

➕ 14-2. 테스트 Mock

const idGeneratorMock = {
  uuid: jest.fn(() => 'fixed-uuid'),
  requestId: jest.fn(() => 'req_test_123'),
};

➕ 14-3. 활용

requestId 생성
Webhook event 처리 key
Export 파일 key
idempotency key
테스트 snapshot 안정화
  • 랜덤값은 프로덕션에서는 유용하지만 테스트에서는 불안정성입니다.
  • 분리해두면 테스트 결과가 일정해집니다.

✅ 15. 외부 API Adapter Mock

  • 알림톡/SMS/S3 같은 외부 연동은 테스트에서 실제 호출하면 안 됩니다.
  • Adapter를 Mock으로 대체해 성공/실패 상황을 만들 수 있어야 합니다.

➕ 15-1. AlimtalkAdapter Mock

const alimtalkAdapterMock = {
  sendTemplate: jest.fn().mockResolvedValue({
    ok: true,
    data: {
      providerMessageId: 'msg_123',
      status: 'SENT',
    },
  }),
};

➕ 15-2. 실패 Mock

const alimtalkAdapterMock = {
  sendTemplate: jest.fn().mockResolvedValue({
    ok: false,
    errorKind: 'RETRYABLE',
    errorCode: 'TIMEOUT',
    safeMessage: '알림톡 발송 요청 시간이 초과되었습니다.',
  }),
};

➕ 15-3. 테스트 후보

알림톡 성공 시 NotificationJob DONE
timeout 시 RETRYING
잘못된 템플릿이면 FAILED
providerMessageId 저장
전화번호 원본 로그 미출력
  • 외부 API 테스트의 핵심은 성공보다 실패 상황입니다.
  • retryable/non-retryable 분기가 제대로 되는지 확인해야 합니다.

✅ 16. S3Adapter Mock

  • 엑셀 Export Worker는 S3 업로드가 필요할 수 있습니다.
  • 테스트에서는 실제 S3에 업로드하지 않고 Mock으로 대체해야 합니다.

➕ 16-1. Mock 예시

const s3AdapterMock = {
  upload: jest.fn().mockResolvedValue({
    key: 'exports/consults/test.xlsx',
  }),

  createPresignedUrl: jest.fn().mockResolvedValue(
    'https://signed-url.example.com',
  ),

  deleteObject: jest.fn().mockResolvedValue(undefined),
};

➕ 16-2. 테스트 후보

ExportWorker 성공 시 fileKey 저장
S3 업로드 실패 시 ExportJob FAILED
pre-signed URL 발급 전 권한 확인
만료된 Export 파일 다운로드 차단
CleanupJob에서 deleteObject 호출
  • 파일/S3 관련 테스트는 실제 외부 저장소를 쓰지 않는 것이 기본입니다.
  • 파일 생성 자체는 작은 샘플로 별도 테스트할 수 있습니다.

✅ 17. Worker 테스트 전략

  • Worker는 Job 상태 전환이 핵심입니다.
  • 실제 외부 API 호출보다 Job을 어떻게 claim하고, 처리하고, 실패 기록을 남기는지가 중요합니다.

➕ 17-1. Worker 테스트 후보

PENDING Job을 PROCESSING으로 claim
성공 시 DONE
재시도 가능한 실패 시 RETRYING
재시도 불가능한 실패 시 FAILED
maxRetry 초과 시 FAILED
PROCESSING stuck job 감지
같은 Job 중복 처리 방지

➕ 17-2. NotificationWorker 예시

it('알림톡 timeout이면 NotificationJob을 RETRYING으로 변경한다', async () => {
  const job = await seedNotificationJob(prisma, {
    status: 'PENDING',
    retryCount: 0,
  });

  alimtalkAdapterMock.sendTemplate.mockResolvedValue({
    ok: false,
    errorKind: 'RETRYABLE',
    errorCode: 'TIMEOUT',
    safeMessage: '요청 시간이 초과되었습니다.',
  });

  await notificationWorker.processNext();

  const updated = await prisma.notificationJob.findUnique({
    where: { id: job.id },
  });

  expect(updated?.status).toBe('RETRYING');
  expect(updated?.retryCount).toBe(1);
  expect(updated?.errorCode).toBe('TIMEOUT');
});
  • Worker 테스트는 Job 상태 변화가 핵심입니다.
  • 외부 API가 실제로 호출됐는지보다 “실패를 어떻게 기록했는지”가 더 중요합니다.

✅ 18. Webhook 테스트 전략

  • Webhook은 보안 검증, 중복 처리, 빠른 응답이 중요합니다.
  • 실제 처리는 Worker로 넘기고, Controller는 수신과 저장 중심으로 테스트할 수 있습니다.

➕ 18-1. 테스트 후보

signature가 올바르면 WebhookEvent 저장
signature가 틀리면 401/400
providerEventId 중복이면 비즈니스 처리 중복 안 함
중복 이벤트도 200 반환
WebhookEvent 저장 후 빠르게 응답
payload에 Secret이 저장되지 않음

➕ 18-2. Signature Verifier Mock

const signatureVerifierMock = {
  verify: jest.fn(() => true),
};

➕ 18-3. 중복 테스트 예시

it('중복 Webhook은 다시 처리하지 않고 200을 반환한다', async () => {
  await seedWebhookEvent(prisma, {
    provider: 'ALIMTALK',
    providerEventId: 'evt_123',
    status: 'PROCESSED',
  });

  const response = await request(app.getHttpServer())
    .post('/webhooks/alimtalk')
    .set('x-signature', 'valid-signature')
    .send({
      eventId: 'evt_123',
      type: 'ALIMTALK_DELIVERED',
    })
    .expect(200);

  expect(response.body.duplicated).toBe(true);
});
  • Webhook은 중복 수신이 정상 상황입니다.
  • 중복을 에러로 처리하면 외부 서비스가 계속 재전송할 수 있습니다.

✅ 19. Error Handling 테스트

  • 0831에서 만든 표준 에러 응답은 테스트로 고정해야 합니다.
  • 그래야 리팩토링 후에도 프론트 에러 처리 구조가 깨지지 않습니다.

➕ 19-1. 테스트 후보

Validation Error는 VALIDATION_ERROR 반환
권한 없음은 403 + FORBIDDEN 반환
상담 없음은 404 + CONSULT_NOT_FOUND 반환
상태 충돌은 409 + CONSULT_STATUS_CONFLICT 반환
Unknown Error는 500 + INTERNAL_SERVER_ERROR 반환
응답에 requestId 포함
응답에 stack trace 미포함

➕ 19-2. Unknown Error 테스트 기준

강제로 예상 못한 에러 발생
  ↓
응답은 안전한 500
  ↓
stack trace 없음
  ↓
requestId 있음

➕ 19-3. 민감정보 노출 테스트

에러 message/details/log에 아래 값이 없는지 확인:
phone
phoneNormalized
Authorization
DATABASE_URL
API key
passwordHash
  • 에러 테스트는 기능 테스트만큼 중요합니다.
  • 특히 표준 응답 구조와 민감정보 노출 방지는 반드시 확인해야 합니다.

✅ 20. 테스트 폴더 구조 추천

src/
  modules/
    consult/
      domain/
        consult-status.service.spec.ts
      application/
        update-consult-status.use-case.spec.ts
      infra/
        consult.repository.spec.ts

test/
  e2e/
    consult.e2e-spec.ts
    auth.e2e-spec.ts
    export-job.e2e-spec.ts

  factories/
    admin.factory.ts
    consult.factory.ts
    product.factory.ts
    export-job.factory.ts

  mocks/
    alimtalk-adapter.mock.ts
    s3-adapter.mock.ts
    clock.mock.ts

  helpers/
    reset-test-db.ts
    login-as-admin.ts

➕ 20-1. 기준

Unit Test:
소스 파일 가까이에 배치

E2E Test:
test/e2e에 분리

Factory/Mock:
test 공통 유틸로 분리

DB Reset:
공통 helper로 관리
  • 테스트 파일 위치는 팀 컨벤션에 따라 달라도 됩니다.
  • 중요한 것은 Factory, Mock, Reset helper를 중복 없이 관리하는 것입니다.

✅ 21. 테스트할 가치가 높은 기능

  • 모든 기능을 처음부터 테스트하려고 하면 부담이 큽니다.
  • 장애가 나면 큰일 나는 흐름부터 테스트해야 합니다.

➕ 21-1. 1순위

상담 상태 변경
상담 중복 신청
권한 Guard
엑셀 Export 요청
Audit Log 저장
표준 에러 응답

➕ 21-2. 2순위

NotificationJob Worker
ExportJob Worker
Webhook 수신
Soft Delete/Restore
상품 가격/지원금 수정

➕ 21-3. 3순위

목록 검색/필터/정렬
Mapper
Dashboard 통계
CleanupJob
  • 테스트는 많이 만드는 것보다 중요한 곳에 제대로 만드는 것이 우선입니다.
  • 현재 프로젝트에서는 상담 상태 변경과 Export/알림 Job부터 잡는 것이 좋습니다.

✅ 22. AI/Codex와 테스트

  • AI/Codex에게 코드를 맡길수록 테스트가 더 중요해집니다.
  • 테스트가 있으면 AI가 바꾼 코드가 기존 기능을 깨뜨렸는지 바로 확인할 수 있습니다.

➕ 22-1. AI에게 테스트를 함께 요구해야 하는 이유

AI가 의도치 않게 응답 구조를 바꿀 수 있음
권한 체크를 빠뜨릴 수 있음
transaction을 깨뜨릴 수 있음
민감정보 노출을 만들 수 있음
기존 API 호환성을 깨뜨릴 수 있음

➕ 22-2. Codex 요청 예시

상담 상태 변경 Use Case를 수정하면서 테스트도 같이 추가해줘.

조건:
1. ConsultStatusService 단위 테스트를 추가해줘
2. UpdateConsultStatusUseCase 통합 테스트를 추가해줘
3. 상태 변경 성공 시 consults.status, consult_status_histories, audit_logs가 모두 저장되는지 확인해줘
4. 잘못된 상태 전이는 실패해야 해
5. 권한이 없으면 403이 나야 해
6. 동시 수정 충돌은 409로 처리해야 해
7. 응답에는 phoneNormalized가 없어야 해
8. 에러 응답에는 requestId가 있어야 해
9. 외부 알림톡 호출은 Mock 처리하고 실제 호출하지 마
10. 테스트 실행 방법과 변경 파일 목록을 정리해줘

➕ 22-3. 리뷰 기준

테스트가 실제 핵심 동작을 검증하는가?
단순히 함수 호출 여부만 확인하지 않는가?
외부 API가 Mock 처리되었는가?
테스트 DB와 운영 DB가 분리되어 있는가?
권한/에러/민감정보 노출 테스트가 포함되어 있는가?
  • AI에게 테스트를 맡길 때는 기대 동작을 구체적으로 써야 합니다.
  • “테스트 추가해줘”만 쓰면 형식적인 테스트가 나올 가능성이 큽니다.

✅ 23. 테스트 실행 자동화

  • 테스트는 로컬에서만 돌리는 것보다 CI에서 자동으로 돌리는 것이 좋습니다.
  • GitHub Actions에 최소한 unit test와 lint/typecheck를 넣으면 안전성이 올라갑니다.

➕ 23-1. 추천 단계

1단계:
로컬 unit test

2단계:
lint + typecheck

3단계:
GitHub Actions에서 unit test

4단계:
integration test용 PostgreSQL service 추가

5단계:
배포 전 smoke test

➕ 23-2. GitHub Actions 개념

push 또는 pull request
  ↓
pnpm install
  ↓
typecheck
  ↓
lint
  ↓
unit test
  ↓
integration test

➕ 23-3. 주의

CI에 운영 Secret 넣지 않기
테스트용 DATABASE_URL 사용
외부 API 키 없이 Mock 사용
테스트 데이터가 운영과 연결되지 않게 하기
  • CI 테스트는 배포 전 최소 안전장치입니다.
  • 처음에는 unit test + typecheck만 넣어도 도움이 됩니다.

✅ 24. 테스트 커버리지에 대한 현실적인 기준

  • 테스트 커버리지는 높으면 좋지만, 숫자만 목표로 삼으면 의미 없는 테스트가 늘어납니다.
  • 1인 개발자 프로젝트에서는 핵심 흐름 중심으로 현실적인 커버리지를 잡는 것이 좋습니다.

➕ 24-1. 나쁜 목표

무조건 90% 이상
모든 getter/setter 테스트
Mock만 잔뜩 있는 테스트
실제 버그를 못 잡는 테스트

➕ 24-2. 좋은 목표

상담 상태 변경 핵심 흐름 테스트
중복 신청 방지 테스트
권한 없는 요청 차단 테스트
ExportJob 생성/실패 테스트
NotificationJob retry 테스트
표준 에러 응답 테스트

➕ 24-3. 기준

위험한 기능:
테스트 필수

자주 바뀌는 기능:
테스트 우선

단순 CRUD:
필요한 경우만

외부 연동:
Mock 기반 실패 테스트 필수
  • 커버리지 숫자보다 “어떤 사고를 막을 수 있는가”가 더 중요합니다.
  • 테스트는 보험이므로 비싼 사고가 날 부분에 먼저 걸어야 합니다.

✅ 25. 현재 프로젝트 적용 우선순위

➕ 25-1. 1순위: Domain Service/Policy Unit Test

ConsultStatusService
ConsultPermissionPolicy
DuplicateConsultPolicy
ExportPermissionPolicy
Mapper

완료 기준:

DB 없이 빠르게 실행 가능
상태 전이/권한 규칙이 테스트로 고정됨
정책 변경 시 실패 테스트로 확인 가능

➕ 25-2. 2순위: 상태 변경 Use Case Integration Test

UpdateConsultStatusUseCase
consults.status 변경
consult_status_histories 생성
audit_logs 생성
동시성 충돌
rollback 검증

완료 기준:

상태 변경 정합성을 DB 기준으로 확인 가능
리팩토링 후에도 이력/Audit Log 누락을 잡을 수 있음

➕ 25-3. 3순위: 권한/에러 E2E Test

403 권한 없음
409 상태 충돌
400 validation error
404 상담 없음
표준 에러 응답
requestId 포함

완료 기준:

프론트 공통 에러 처리와 API 응답 계약이 깨지지 않음
권한 없는 요청이 백엔드에서 차단됨

➕ 25-4. 4순위: Worker Mock Test

NotificationWorker
ExportWorker
S3Adapter Mock
AlimtalkAdapter Mock
retry/failed/done 상태 전환

완료 기준:

외부 API 실패 시 Job 상태가 올바르게 저장됨
실제 외부 API 호출 없이 테스트 가능
중복 처리/재시도 로직 검증 가능
  • 지금은 모든 API를 테스트하려고 하지 말고, 운영 사고가 클 기능부터 테스트하는 것이 좋습니다.
  • 상담 상태 변경, 권한, Export, 알림 Job이 우선입니다.

✅ 26. 실무 체크리스트

➕ 26-1. 테스트 구조 체크리스트

  • Domain Service가 DB 없이 테스트 가능한가?
  • Use Case가 Controller 없이 테스트 가능한가?
  • Repository가 Test DB에서 검증 가능한가?
  • 외부 API가 Adapter로 분리되어 Mock 가능한가?
  • 현재 시간과 랜덤값을 Mock할 수 있는가?
  • 테스트 DB와 운영 DB가 완전히 분리되어 있는가?
  • Seed Factory가 있는가?
  • 테스트 데이터 초기화 기준이 있는가?

➕ 26-2. Unit Test 체크리스트

  • 상태 전이 규칙을 테스트하는가?
  • 권한 Policy를 테스트하는가?
  • 중복 신청 정책을 테스트하는가?
  • Mapper가 민감정보를 제외하는지 테스트하는가?
  • 날짜 계산/만료 계산을 테스트하는가?
  • retry/backoff 계산을 테스트하는가?
  • DB 없이 빠르게 실행되는가?
  • 테스트 이름이 업무 규칙을 설명하는가?

➕ 26-3. Integration/E2E 체크리스트

  • 상태 변경 시 이력과 Audit Log가 함께 저장되는가?
  • 실패 시 transaction rollback이 되는가?
  • 권한 없는 요청이 403으로 막히는가?
  • Validation Error 구조가 표준화되어 있는가?
  • 409 충돌 응답이 정상인가?
  • 목록/상세 API 응답 구조가 깨지지 않는가?
  • Test DB가 초기화되는가?
  • 운영 DB URL로 테스트가 실행되지 않도록 막는가?

➕ 26-4. Mock/Worker 체크리스트

  • 알림톡 API가 실제 호출되지 않는가?
  • S3 업로드가 Mock 처리되는가?
  • 외부 API timeout 실패를 재현할 수 있는가?
  • retryable/non-retryable 실패를 테스트하는가?
  • Job 상태가 DONE/FAILED/RETRYING으로 정확히 바뀌는가?
  • providerMessageId가 저장되는가?
  • 실패 로그에 민감정보가 없는가?
  • 중복 처리 방어를 테스트하는가?

✅ 27. AI에게 테스트 구조를 물어볼 때 좋은 질문법

NestJS + Prisma + PostgreSQL 기반 온라인 휴대폰 판매몰 백엔드에 테스트 구조를 만들려고 해.

서비스 상황:
1. 상담 신청, 상담 상태 변경, 상품 수정, 엑셀 Export, 알림톡 Worker, Webhook 수신, 관리자 권한 기능이 있음
2. 상담 상태 변경은 consults.status 업데이트, consult_status_histories 생성, audit_logs 생성을 하나의 transaction으로 묶음
3. 권한은 PermissionGuard와 ConsultPermissionPolicy로 확인함
4. 에러 응답은 success=false, error.code, error.message, requestId 구조로 표준화함
5. 알림톡/SMS/S3 같은 외부 API는 Adapter로 분리되어야 하고 테스트에서는 Mock해야 함
6. Test DB와 운영 DB는 절대 섞이면 안 됨
7. 현재 시간, requestId, randomUUID는 테스트에서 고정할 수 있으면 좋겠음
8. AI/Codex가 코드를 수정할 때 기존 기능이 깨졌는지 확인할 안전장치가 필요함

요청:
- Unit/Integration/E2E 테스트 구분
- 테스트 가능한 폴더 구조
- Domain Service 테스트 예시
- Use Case Integration Test 예시
- PermissionGuard E2E Test 예시
- GlobalExceptionFilter 표준 에러 응답 테스트 예시
- Prisma Test DB 전략
- Seed Factory 구조
- Clock/IdGenerator Mock 전략
- AlimtalkAdapter/S3Adapter Mock 전략
- Worker 테스트 전략
- CI에서 테스트 실행하는 단계
- 현재 프로젝트 우선순위
를 실무 기준으로 정리해줘.

➕ 27-1. AI 답변 검증 기준

모든 테스트를 E2E로만 만들라고 하지 않는가?
Domain Service는 Unit Test로, DB 정합성은 Integration Test로 나누는가?
운영 DB와 테스트 DB 분리를 강하게 강조하는가?
외부 API/S3/알림톡을 Mock하라고 하는가?
상담 상태 변경의 이력/Audit Log 저장을 테스트 대상으로 보는가?
권한 없는 요청과 표준 에러 응답 테스트를 포함하는가?
시간/랜덤값 Mock 필요성을 설명하는가?
커버리지 숫자보다 핵심 리스크 중심 테스트를 권장하는가?

📌 요약

  • 테스트 가능한 백엔드 구조는 Controller, Use Case, Domain Service, Repository, Adapter의 책임이 분리되어 있어 핵심 로직을 작은 단위로 검증할 수 있는 구조입니다.
  • Unit Test는 상태 전이 규칙, 권한 Policy, 중복 신청 정책, Mapper처럼 DB 없이 검증 가능한 로직에 적합합니다.
  • Integration Test는 상담 상태 변경처럼 transaction, Repository, 상태 이력, Audit Log가 함께 동작해야 하는 흐름에 적합합니다.
  • E2E Test는 실제 HTTP 요청 기준으로 인증, 권한 Guard, DTO validation, Controller, 표준 응답 구조를 검증하는 데 유용합니다.
  • Mock은 외부 API, S3, 알림톡/SMS, 현재 시간, 랜덤값처럼 테스트에서 실제로 실행하면 안 되거나 불안정한 의존성을 대체하는 도구입니다.
  • 상담 상태 변경, 중복 신청, 권한 Guard, 엑셀 Export 요청, Audit Log 저장, 표준 에러 응답은 현재 프로젝트에서 우선 테스트할 가치가 높습니다.
  • Integration/E2E 테스트에는 운영 DB가 아니라 테스트 전용 DB를 사용해야 하며, 테스트 실행 전후 데이터 초기화 기준이 필요합니다.
  • Seed Factory를 만들면 상담, 상품, 관리자, ExportJob 같은 테스트 데이터를 일관되게 생성할 수 있어 테스트 유지보수가 쉬워집니다.
  • new Date(), randomUUID() 같은 시간/랜덤 의존성은 Clock, IdGenerator로 분리하면 테스트에서 고정할 수 있습니다.
  • Worker 테스트는 외부 API 호출 자체보다 Job 상태가 DONE, FAILED, RETRYING으로 올바르게 바뀌는지 확인하는 것이 핵심입니다.
  • AI/Codex에게 코드를 맡길수록 테스트는 더 중요하며, 단순 “테스트 추가”가 아니라 권한, transaction, Audit Log, 표준 에러 응답, 민감정보 노출 방지까지 구체적으로 요구해야 합니다.

0개의 댓글