아키텍처와 설계 철학
🎯 설계 철학
1. "실패는 예외가 아닌 일상" 철학
기존의 접근 방식에서는 에러가 발생하면 사용자 경험이 망가지는 경우가 많았습니다. 다음 코드를 보시면 문제점을 쉽게 이해할 수 있습니다.
try {
const data = await api.getData();
// 런타임에 data.someField가 null이면 UI 크래시
return data.someField.toFixed(2);
} catch (error) {
// 에러가 발생해도 개발자는 모름
return "에러 발생";
}
const { data, isValidated } = await validatedApi.getData();
// 검증이 실패해도 UI는 정상 동작하고, 문제는 백그라운드에서 수집됨
return data?.someField?.toFixed(2) ?? "데이터 없음";
왜 이런 철학을 택했을까요?
- 실제 운영 환경의 현실: 외부 API는 언제든 예상과 다른 데이터를 보낼 수 있습니다
- 사용자 경험 우선: 데이터 문제가 있어도 앱이 멈추면 안됩니다
- 개발자 경험 향상: 문제를 사전에 감지하고 자동으로 리포트합니다
2. "타입 안전성 = 런타임 안전성" 원칙
TypeScript의 컴파일 타임 체크만으로는 런타임 안전성을 보장할 수 없습니다. 다음 예시를 통해 이 문제를 명확히 이해할 수 있습니다.
// TypeScript 컴파일 타임 체크만으로는 부족
interface ApiResponse {
value: number; // 컴파일러는 이게 항상 number라고 믿음
}
// 실제 런타임에는...
const response: ApiResponse = {
value: null // 💥 실제로는 null이 올 수 있음
};
// 해결책: 런타임 검증을 통한 타입 가드
const schema = z.object({
value: z.coerce.number() // 런타임에 실제로 검증하고 변환
});
- 컴파일 타임 + 런타임 이중 안전장치
- 자동 타입 추론: Zod에서 TypeScript 타입 자동 생성
- 문서화 효과: 스키마 자체가 API 명세 역할
- 초기 개발 시간 증가: 스키마 작성 필요
- 런타임 오버헤드: 검증 과정에서 성능 비용
- 학습 곡선: Zod 문법 숙지 필요
🏛️ 아키텍처 레이어
전체 시스템 구조
(컴포넌트)
(Hook)
(검증된 API)
(로깅)
레이어별 책임과 설계 근거
1. Presentation Layer (표현 계층)
표현 계층은 UI 렌더링에만 집중하며, 데이터 검증 실패 여부와 무관하게 안정적으로 동작합니다.
<template>
<div class="data-widget">
<!-- 데이터 검증 실패해도 UI는 정상 동작 -->
<div v-if="isLoading">로딩중...</div>
<div v-else-if="hasValidationErrors">
⚠️ 일부 데이터에 문제가 있지만 표시 가능한 데이터로 표시
</div>
<div v-else>
{{ data.value }}
</div>
</div>
</template>
<script setup lang="ts">
// 컴포넌트는 검증 여부를 몰라도 됨 (관심사 분리)
const { data, isLoading, hasValidationErrors } = useApiData();
</script>
- 관심사 분리: 컴포넌트는 UI 렌더링에만 집중합니다
- 선언적 접근: 데이터 상태에 따른 UI 상태 자동 결정합니다
- 에러 경계: 검증 실패가 컴포넌트 렌더링을 방해하지 않습니다
2. Business Logic Layer (비즈니스 로직 계층)
비즈니스 로직 계층은 도메인 지식을 캡슐화하고, 검증 실패 시 기본값을 제공하는 역할을 담당합니다.
// src/hooks/useApiData.ts
export const useApiData = () => {
// 비즈니스 로직: "데이터 조회"
const { data, isLoading, error } = useDataQuery();
// 비즈니스 규칙: 검증 실패 시 기본값 제공
const processedData = computed(() => {
if (!data.value) return getDefaultData();
return {
value: data.value.apiValue ?? 0,
hasValidationErrors: data.value._validationErrors?.length > 0,
lastUpdated: data.value.timestamp
};
});
return {
data: processedData,
isLoading,
hasValidationErrors: computed(() => processedData.value.hasValidationErrors)
};
};
- 비즈니스 로직 캡슐화: 도메인 지식을 Hook에 집중합니다
- 재사용성: 여러 컴포넌트에서 동일한 비즈니스 로직을 사용합니다
- 테스트 용이성: 비즈니스 로직을 독립적으로 테스트할 수 있습니다
3. Data Access Layer (데이터 접근 계층)
데이터 접근 계층에서는 투명한 검증 기능을 제공하며, 기존 API 호출 방식과 100% 호환됩니다.
// src/utilities/validation/createValidatedApiCall.ts
export const createValidatedApiCall = <TParams, TResponse>(
originalApiCall: (params: TParams) => Promise<TResponse>,
validator: (data: TResponse, params?: TParams) => Promise<ValidationResult<TResponse>>
) => {
return async (params: TParams): Promise<TResponse> => {
try {
// 1. 원본 API 호출
const response = await originalApiCall(params);
// 2. 백그라운드 검증 (UI 블로킹 없음)
validator(response, params).catch(error => {
// 검증 실패는 로깅만 하고 사용자 경험에 영향 없음
console.debug('Validation failed but continuing:', error);
});
// 3. 원본 데이터 반환 (검증 실패와 무관)
return response;
} catch (error) {
// API 호출 실패는 상위로 전파
throw error;
}
};
};
- 투명성: 기존 API 호출 방식과 100% 호환됩니다
- 비침습성: 검증 로직이 비즈니스 로직을 방해하지 않습니다
- 점진적 적용: 기존 코드 수정 없이 검증 기능을 추가할 수 있습니다
🔄 데이터 플로우와 의사결정 지점
일반적인 API 호출 플로우
사용자 액션 또는 라이프사이클에 따라 데이터를 요청합니다.
비즈니스 로직을 포함한 Hook에서 검증된 API를 호출합니다.
실제 서버와 통신하여 데이터를 받아옵니다.
받아온 데이터를 백그라운드에서 검증하고, 실패 시 로그를 수집합니다.
검증 결과와 무관하게 원본 데이터를 반환하여 사용자 경험을 보장합니다.
핵심 의사결정 지점들
Decision 1: 검증 실패 시 어떻게 할 것인가?
검증 실패 시 처리 방법에 대한 두 가지 접근법을 비교해보겠습니다.
// Option A: 검증 실패 시 에러 throw (기존 방식)
if (!isValid) {
throw new Error('Data validation failed');
}
// Option B: 검증 실패해도 원본 데이터 반환 (채택한 방식)
// 장점: 사용자 경험 중단 없음, 점진적 개선 가능
// 단점: 잘못된 데이터로 계속 동작할 수 있음
- 사용자 경험 우선: 데이터 품질 문제로 앱이 멈추면 안됩니다
- 점진적 개선: 검증 로직을 하나씩 추가하면서 개선할 수 있습니다
- 레거시 호환: 기존 코드를 대폭 수정하지 않고도 적용할 수 있습니다
Decision 2: 언제 검증을 수행할 것인가?
// Option A: 동기적 검증 (UI 블로킹)
const data = await api.getData();
const validatedData = await validate(data); // UI 대기
return validatedData;
// Option B: 비동기적 검증 (채택한 방식)
const data = await api.getData();
validate(data); // 백그라운드에서 실행, UI는 즉시 진행
return data;
- 성능: 검증 시간이 UI 응답성에 영향을 주지 않습니다
- 사용자 경험: 데이터가 즉시 표시됩니다
- 확장성: 복잡한 검증 로직도 성능 영향 없이 적용할 수 있습니다
🎭 Design Patterns 적용
1. Decorator Pattern: createValidatedApiCall
Decorator 패턴을 활용하여 기존 API 함수에 검증 기능을 투명하게 추가했습니다.
// 기본 API 함수
const getApiData = (params) => axios.get('/api/data', { params });
// Decorator로 검증 기능 추가
const validatedGetApiData = createValidatedApiCall(
getApiData,
validateApiData
);
// 사용법은 동일하지만 내부적으로 검증 기능 추가됨
const data = await validatedGetApiData({ date: '2025-01-01' });
- 기존 코드 보존: 원본 함수 수정 없이 기능을 확장할 수 있습니다
- 조합 가능: 여러 Decorator를 체인으로 연결할 수 있습니다
- 선택적 적용: 필요한 API에만 선택적으로 적용할 수 있습니다
2. Strategy Pattern: Zod 스키마 전략
환경이나 상황에 따라 다른 검증 전략을 적용할 수 있도록 Strategy 패턴을 사용했습니다.
// 검증 전략 인터페이스
interface ValidationStrategy<T> {
validate: (data: unknown) => ValidationResult<T>;
}
// 구체적인 전략들
const strictStrategy = z.object({
value: z.number() // null 허용 안함
});
const lenientStrategy = z.object({
value: z.number().nullable() // null 허용
});
const coerciveStrategy = z.object({
value: z.coerce.number() // 강제 변환
});
// 상황에 따라 전략 선택
const validator = isDevelopment ? strictStrategy : lenientStrategy;
- 유연성: 환경이나 상황에 따라 검증 방식을 변경할 수 있습니다
- 점진적 적용: 엄격한 검증에서 느슨한 검증으로 단계적 적용이 가능합니다
- A/B 테스트: 다른 검증 전략의 효과를 비교할 수 있습니다
3. Observer Pattern: BatchLogger
로깅 시스템의 상태를 실시간으로 관찰할 수 있도록 Observer 패턴을 적용했습니다.
class BatchLogger {
private observers: LogObserver[] = [];
addObserver(observer: LogObserver) {
this.observers.push(observer);
}
private notifyObservers(event: LogEvent) {
this.observers.forEach(observer => observer.notify(event));
}
async flush() {
// 로그 전송 시도
this.notifyObservers({ type: 'FLUSH_START', queueSize: this.queue.length });
try {
await this.sendLogs();
this.notifyObservers({ type: 'FLUSH_SUCCESS' });
} catch (error) {
this.notifyObservers({ type: 'FLUSH_FAILED', error });
}
}
}
// 개발자 도구에서 로그 상태 모니터링
batchLogger.addObserver({
notify: (event) => {
if (isDevelopment) {
console.log(`BatchLogger: ${event.type}`, event);
}
}
});
🎯 핵심 설계 원칙
- Fail-Safe Design (안전 실패)
문제가 발생해도 시스템이 안전한 상태를 유지합니다 - Progressive Enhancement (점진적 향상)
기본 기능은 보장하면서 추가 기능을 점진적으로 개선합니다 - Separation of Concerns (관심사 분리)
각 레이어는 자신의 책임에만 집중합니다 - Observability (관찰 가능성)
시스템의 내부 상태를 외부에서 관찰하고 디버깅할 수 있습니다 - Composability (조합성)
작은 단위들을 조합해서 복잡한 기능을 구현합니다
📊 성능과 트레이드오프
성능 비용 분석
| 구성 요소 | 성능 비용 | 이유 | 최적화 방안 |
|---|---|---|---|
| Zod 검증 | +20~50ms | 스키마 파싱, 객체 순회 | 스키마 캐싱, 필수 필드만 검증 |
| 백그라운드 로깅 | +5~10ms | 큐 추가, 메모리 할당 | 배치 크기 조정, 메모리 풀 사용 |
| API 래퍼 | +1~3ms | 함수 호출 오버헤드 | 프록시 패턴 대신 직접 호출 |
메모리 사용량 최적화
메모리 효율성을 위해 큐 크기를 제한하고 FIFO 방식으로 오래된 로그를 제거합니다.
// 메모리 효율적인 배치 로거 설계
class BatchLogger {
private queue: ValidationErrorLog[] = [];
private readonly maxQueueSize = 100; // 메모리 사용량 제한
addLog(log: ValidationErrorLog): void {
// 큐가 가득 차면 오래된 로그 제거 (FIFO)
if (this.queue.length >= this.maxQueueSize) {
this.queue.shift(); // 메모리 누수 방지
}
this.queue.push(log);
}
}
네트워크 효율성
배치 전송과 압축을 통해 네트워크 효율성을 확보했습니다.
// 배치 전송으로 네트워크 효율성 확보
const config = {
maxBatchSize: 20, // 한 번에 20개씩 전송
flushInterval: 30000, // 30초마다 전송
compressionEnabled: true // gzip 압축 사용
};
🔮 확장성과 미래 고려사항
1. 다중 환경 지원
환경별로 다른 검증 전략을 적용하여 개발, 스테이징, 프로덕션 환경에 최적화된 검증을 수행합니다.
// 환경별 다른 검증 전략
const getValidationStrategy = () => {
switch (process.env.NODE_ENV) {
case 'development': return 'strict';
case 'staging': return 'lenient';
case 'production': return 'monitoring-only';
}
};
2. A/B 테스트 지원
// 사용자 그룹별 다른 검증 활성화
const shouldEnableValidation = (userId: string) => {
return hashUserId(userId) % 100 < 50; // 50% 사용자만 활성화
};
3. 실시간 설정 변경
// 원격에서 검증 설정 동적 변경
const validationConfig = reactive({
enabled: true,
batchSize: 20,
apiFilters: ['critical-apis-only']
});
// 서버에서 설정 업데이트 받음
watchEffect(() => {
batchLogger.updateConfig(validationConfig);
});
'je개발 회고' 카테고리의 다른 글
| [ 데이터 품질 관리 ] (4) Frontend : Batching (4) | 2025.08.19 |
|---|---|
| [ 데이터 품질 관리 ] (3) Frontend : Zod 스키마와 API 검증 (3) | 2025.08.18 |
| [ 데이터 품질 관리 ] (1) 기획 : 페이지 시나리오 (1) | 2025.08.18 |
| [ 데이터 품질 관리 ] (0) 프롤로그 : 탄생 배경 (2) | 2025.08.18 |
| [ E2E ] SSL 중간 인증서 문제 (2) | 2025.07.16 |