Chapter 14. 프로젝트 회고와 아키텍처 리뷰
개요
구현을 완료한 지금, 가장 중요한 작업이 남았습니다. 바로 깊이 있는 회고입니다.
회고는 단순히 "무엇을 만들었는가"를 되돌아보는 것이 아닙니다. "왜 그렇게 설계했는가", "어떤 트레이드오프가 있었는가", "무엇을 다르게 할 수 있었는가"를 비판적으로 분석하는 과정입니다. 이것이 바로 일반 개발자와 시니어 엔지니어를 구분하는 메타인지(Metacognition) 능력입니다.
실패한 결정도, 우연히 성공한 선택도, 모두 학습의 재료입니다. 중요한 것은 그 이유를 이해하고 다음 프로젝트에 적용하는 것입니다.
학습 목표
1. 아키텍처 의사결정 심층 분석
- 각 설계 결정의 근거 재평가
- 실제로 직면한 문제와 예상의 차이
- 트레이드오프의 실제 영향 측정
- 대안 시나리오 분석 ("만약 ~했다면?")
2. 컴퓨팅 사고 적용 효과 검증
- 분해 전략의 실효성 평가
- 패턴 인식이 재사용성에 기여한 정도
- 추상화 레벨의 적절성 판단
- 알고리즘 선택의 성능 영향
3. AI 협업의 한계와 인간 판단
- Copilot이 탁월했던 영역
- AI가 실패했거나 오도한 순간들
- 인간의 비판적 사고가 필수였던 결정들
- 프롬프트 엔지니어링의 진화 과정
4. 기술 부채와 완성도의 균형
- 의도적으로 미룬 기술 부채 vs 우연히 생긴 부채
- 비즈니스 가치와 코드 품질의 균형점
- 리팩토링이 필요한 부분과 우선순위
- "완벽"과 "충분히 좋음"의 경계
5. 실패에서 배우는 지혜
- 아키텍처 안티패턴 경험
- 성능 병목의 원인과 해결
- 과도한 엔지니어링 vs 과소 설계
- 다음 프로젝트를 위한 교훈
이번 챕터는 여러분을 "실행자"에서 "사색하는 엔지니어"로 전환시킵니다. 코드를 넘어 사고를 돌아보는 시간입니다.
1. 아키텍처 의사결정 재평가
구현을 마친 지금, 설계 당시의 결정들을 냉정하게 돌아봅니다.
1.1 Clean Architecture 선택의 실제 효과
Chapter 12에 Clean Architecture를 선택했을 때, 세 가지 기대 효과가 있었습니다:
- 비즈니스 로직과 기술 구현 분리
- 테스트 용이성
- 기술 스택 변경 유연성
실제로는 어땠을까요?
기대했던 효과 vs 실제:
// 기대: Domain 레이어는 완전히 독립적이다
// 실제: 일부 도메인 로직이 Application 레이어로 새어나갔다
// 예시: Task 엔티티
export class Task {
private constructor(
public readonly id: string,
public readonly title: string,
public status: TaskStatus,
public readonly createdAt: Date
) {}
// ✅ 잘된 부분: 순수한 비즈니스 로직
start(): Result<void> {
if (this.status !== TaskStatus.TODO) {
return Result.fail('이미 시작된 작업입니다');
}
this.status = TaskStatus.IN_PROGRESS;
return Result.ok();
}
// ❌ 문제 발생: AI 분해 로직의 위치
// 처음에는 Task.decompose()를 만들려 했으나,
// AI 서비스 의존성 때문에 Use Case로 이동
// 이것이 올바른 결정이었을까?
}
// Use Case로 이동한 분해 로직
export class DecomposeTaskUseCase {
constructor(
private taskRepo: ITaskRepository,
private aiService: IAIService // 외부 의존성
) {}
async execute(taskId: string): Promise<Result<Task[]>> {
const task = await this.taskRepo.findById(taskId);
// AI 호출은 여기서
const subtasks = await this.aiService.decompose(task.title);
// 하위 작업 생성은 Domain 로직인가, Application 로직인가?
return subtasks.map(title => Task.create(title));
}
}
회고 질문:
- AI 분해가 "비즈니스 로직"인가, "기술적 구현"인가?
- Domain 레이어에
decompose(strategy: IDecompositionStrategy)형태로 남기고 AI 구현은 Infrastructure로 위임하는 것이 더 나았을까?
실제 효과 평가:
| 기대 효과 | 실제 달성도 | 예상 밖의 결과 |
|---|---|---|
| 비즈니스 로직 분리 | 70% | 일부 로직이 경계에서 모호해짐 |
| 테스트 용이성 | 90% | Domain 테스트는 쉬웠으나, Use Case 테스트는 Mock이 많아짐 |
| 기술 스택 변경 | 40% | TypeORM 의존성이 Repository에 깊이 침투 |
| 코드 복잡도 관리 | 60% | 레이어가 많아 파일 이동이 잦음 |
| 팀 협업 | 80% | 역할 분리가 명확해져 좋았음 |
교훈:
- Clean Architecture는 "순수한 이론"이 아니라 "실용적 가이드"로 접근해야 합니다
- 100% 순수한 분리보다는 80%의 분리 + 20%의 실용성이 현실적입니다
- AI 같은 새로운 요소는 전통적인 레이어 구분을 흐립니다
1.2 NestJS vs Express.js: 선택의 트레이드오프
NestJS를 선택한 이유:
- TypeScript 네이티브
- DI 컨테이너 내장
- Decorator 기반 라우팅
- 구조화된 모듈 시스템
실제 경험:
// ✅ 좋았던 점: DI가 Clean Architecture와 잘 맞음
@Injectable()
export class CreateTaskUseCase {
constructor(
@Inject('ITaskRepository') private taskRepo: ITaskRepository,
@Inject('IAIService') private aiService: IAIService,
@Inject('IEventBus') private eventBus: IEventBus
) {}
}
// ✅ Decorator로 깔끔한 API 정의
@Controller('tasks')
@UseGuards(JwtAuthGuard)
export class TaskController {
@Post()
@ApiOperation({ summary: '작업 생성' })
async create(@Body() dto: CreateTaskDto) {
return this.createTaskUseCase.execute(dto);
}
}
// ❌ 문제점: 러닝 커브와 "마법" 같은 동작
// Copilot이 생성한 코드가 왜 작동하는지 이해하기 어려웠던 순간들
@Module({
imports: [
TypeOrmModule.forFeature([TaskEntity]),
// 이 import가 어디까지 영향을 미치는가?
],
providers: [
{
provide: 'ITaskRepository',
useClass: TypeOrmTaskRepository,
// 이 바인딩은 언제 일어나는가?
}
]
})
export class TaskModule {}
성능 비교 (실측):
| 메트릭 | NestJS | Express.js (예상) |
|---|---|---|
| 시작 시간 | 2.3초 | 0.5초 |
| 메모리 사용 | 85MB | 35MB |
| Hello World RPS | 15,000 | 35,000 |
| 복잡한 API RPS | 3,000 | 3,500 |
| 번들 크기 | 12MB | 3MB |
트레이드오프 분석:
✅ NestJS를 선택하길 잘했다고 생각하는 이유:
- Clean Architecture 구현이 자연스러웠음
- GitHub Copilot이 NestJS 패턴을 잘 이해함 (많은 예제 학습)
- Swagger 통합이 쉬워 API 문서 자동화
- 모듈 시스템 덕분에 코드베이스 구조화가 명확
❌ Express.js가 나았을 것 같은 순간:
- 성능 최적화가 필요한 부분 (WebSocket 처리)
- 러닝 커브로 인한 초기 생산성 저하
- 디버깅이 어려웠던 순간들 (DI 관련 에러)
결론: 3주 프로젝트에서 NestJS는 "적절한 선택"이었습니다. 만약 1주 프로토타입이었다면 Express.js, 6개월 이상 장기 프로젝트라면 NestJS가 더 적합했을 것입니다.
1.3 PostgreSQL + TypeORM: 성능과 생산성
선택 근거:
- ACID 보장이 필요한 작업 관리
- 관계형 데이터 모델 (User-Task-Tag)
- TypeORM으로 빠른 개발
실제 병목 지점:
// ❌ 초기 구현: N+1 쿼리 문제
async findAllWithSubtasks(userId: string): Promise<Task[]> {
const tasks = await this.taskRepo.find({ userId });
// 각 작업마다 하위 작업 조회 → N번의 쿼리
for (const task of tasks) {
task.subtasks = await this.taskRepo.find({ parentId: task.id });
}
return tasks;
}
// 성능 테스트: 100개 작업 = 101번의 쿼리 = 450ms
// ✅ 개선 후: JOIN 활용
async findAllWithSubtasks(userId: string): Promise<Task[]> {
return this.taskRepo
.createQueryBuilder('task')
.leftJoinAndSelect('task.subtasks', 'subtasks')
.where('task.userId = :userId', { userId })
.getMany();
}
// 성능 개선: 1번의 쿼리 = 35ms (12배 빠름)
Copilot이 놓친 최적화:
Copilot은 기본적인 CRUD 코드를 빠르게 생성했지만, 다음을 놓쳤습니다:
- 인덱스 설정 (created_at, status 컬럼)
- N+1 쿼리 문제
- 트랜잭션 범위
- Connection Pool 설정
이것은 인간의 비판적 검토가 여전히 필수임을 보여줍니다.
만약 MongoDB를 선택했다면?
// MongoDB의 경우
{
_id: "task-1",
title: "사용자 인증 구현",
subtasks: [
{ title: "JWT 토큰 생성", status: "done" },
{ title: "Refresh Token", status: "todo" }
]
}
// 장점: N+1 문제 없음 (임베딩)
// 단점: 트랜잭션이 복잡함, 정규화가 어려움
결론: PostgreSQL은 "올바른 선택"이었습니다. 하지만 성능 최적화를 "나중에"가 아니라 "처음부터" 고려했어야 했습니다.
1.4 OpenAI API 통합: 비용과 신뢰성
초기 구현:
async decomposeTask(title: string): Promise<string[]> {
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: title }
]
});
return JSON.parse(response.choices[0].message.content).tasks;
}
실제 문제들:
-
Rate Limit (분당 3회)
// ❌ 초기: 에러 처리 없음 // 사용자 5명이 동시에 요청 → 실패 // ✅ 개선: Queue + Retry @Injectable() export class AIService { private queue = new PQueue({ concurrency: 1, interval: 20000, intervalCap: 3 }); async decompose(title: string): Promise<string[]> { return this.queue.add(() => this.callOpenAI(title), { retry: { retries: 3, factor: 2 } }); } } -
비용 (예상 vs 실제)
- 예상: 월 $20 (사용자 10명 기준)
- 실제: 월 $45 (테스트 과정의 반복 호출)
- 교훈: 개발 환경에서 Mock 사용 필요
-
응답 신뢰성
// AI가 JSON 형식을 지키지 않은 경우 { "tasks": [ "Task 1", "Task 2", // 갑자기 설명을 추가함 "참고: 이 작업은 3-4시간 소요됩니다." ] } // ✅ 검증 로직 추가 validateAIResponse(response: any): string[] { if (!Array.isArray(response.tasks)) { throw new InvalidAIResponseError(); } return response.tasks .filter(task => typeof task === 'string') .map(task => task.trim()); }
대안 검토:
| 옵션 | 비용 | 속도 | 품질 | 결론 |
|---|---|---|---|---|
| GPT-4 | $$ | 느림 | 최고 | 현재 선택 |
| GPT-3.5 | $ | 빠름 | 중간 | 프로토타입용 |
| 자체 모델 | $$$$ | 빠름 | ? | 장기적 옵션 |
| 규칙 기반 | 무료 | 빠름 | 낮음 | Fallback용 |
교훈:
- AI API는 항상 실패할 수 있다고 가정하라
- 비용을 실시간으로 모니터링하라
- Fallback 전략을 준비하라
// 이미지로 교체되어야 함 : 아키텍처 의사결정 트레이드오프 맵 - 4가지 주요 결정(Clean Architecture, NestJS, PostgreSQL, OpenAI)별로 기대효과 vs 실제결과 비교 차트 프롬프트: A 2x2 comparison grid showing 4 major architectural decisions (Clean Architecture, NestJS, PostgreSQL, OpenAI API). Each cell contains: decision name, expected benefits (light blue bars), actual results (green bars), and key trade-offs. Side-by-side bar charts for metrics like "Maintainability", "Performance", "Learning Curve", "Cost". Professional software architecture visualization style, clean layout, blue and green color scheme, white background.
2. 컴퓨팅 사고 4대 원리의 실전 검증
Chapter 2에 배운 컴퓨팅 사고 4대 원리가 실제 프로젝트에서 얼마나 효과적이었을까요?
2.1 분해(Decomposition): 효과적이었던 전략
초기 분해: "Smart TODO 시스템"
// Chapter 12 기획 단계에서의 분해
Smart TODO 시스템
├── 1. 사용자 관리
│ ├── 1.1 회원가입/로그인
│ ├── 1.2 프로필 관리
│ └── 1.3 권한 관리
├── 2. 작업 관리
│ ├── 2.1 작업 CRUD
│ ├── 2.2 AI 자동 분해
│ ├── 2.3 시간 예측
│ └── 2.4 상태 관리
├── 3. 실시간 협업
│ ├── 3.1 WebSocket 연결
│ ├── 3.2 이벤트 브로드캐스팅
│ └── 3.3 충돌 해결
└── 4. 대시보드
├── 4.1 통계 계산
├── 4.2 차트 렌더링
└── 4.3 필터링
실제 구현 순서는 달랐습니다:
// 실제로 구현한 순서
Chapter 1: 1.1, 2.1 (기본 CRUD)
Chapter 2: 2.2 (AI 분해 - 핵심 기능)
Chapter 3: 3.1, 3.2 (실시간 협업)
// 4.1, 4.2는 시간 부족으로 미완성
// 예상과 다른 점:
// - 2.2 (AI 분해)가 가장 오래 걸림 (3일 → 5일)
// - 3.3 (충돌 해결)은 단순화하여 1일로 단축
// - 1.3 (권한 관리)는 생략 (단일 사용자로 제한)
분해 전략의 효과 평가:
✅ 잘된 분해:
- Clean Architecture 레이어별 분해 (Domain → Application → Infrastructure → Presentation)
- Use Case 단위 분해 (CreateTask, UpdateTask, DecomposeTask 등)
- 각 작업이 독립적으로 테스트 가능
❌ 실패한 분해:
- AI 분해 기능을 너무 단순하게 봄 (3일 → 5일 소요)
- 실시간 협업의 복잡도 과소평가
- 대시보드를 "나중에" 미뤘다가 시간 부족
교훈:
- 핵심 기능(AI 분해)부터 구현하고 검증하는 것이 중요
- 불확실한 부분은 버퍼 시간 50% 추가
- "Nice to Have"는 과감히 포기
2.2 패턴 인식(Pattern Recognition): 재사용의 힘
발견한 패턴들:
// 패턴 1: Result 패턴 (함수형 에러 처리)
export class Result<T> {
private constructor(
public readonly isSuccess: boolean,
public readonly value?: T,
public readonly error?: string
) {}
static ok<T>(value: T): Result<T> {
return new Result(true, value);
}
static fail<T>(error: string): Result<T> {
return new Result(false, undefined, error);
}
}
// Use Case에서 일관되게 사용
export class CreateTaskUseCase {
async execute(input: CreateTaskInput): Promise<Result<Task>> {
// 검증 실패
if (!input.title) {
return Result.fail('제목은 필수입니다');
}
// 비즈니스 로직 실행
const task = Task.create(input);
await this.taskRepo.save(task);
return Result.ok(task);
}
}
이 패턴을 12번의 Use Case에 재사용했습니다. GitHub Copilot도 이 패턴을 학습하여 자동으로 적용했습니다.
// 패턴 2: Repository 인터페이스 패턴
interface IRepository<T> {
findById(id: string): Promise<T | null>;
findAll(filter?: Filter): Promise<T[]>;
save(entity: T): Promise<void>;
delete(id: string): Promise<void>;
}
// TaskRepository, UserRepository, ProjectRepository에 재사용
Copilot과 패턴 인식:
// 첫 번째 Repository 구현 후
// Copilot에게: "UserRepository도 같은 패턴으로 만들어줘"
// → 5분 만에 완성 (수동 작성 시 30분)
// 프롬프트:
// "IRepository<User>를 구현하는 TypeOrmUserRepository를 만들어줘.
// TaskRepository와 같은 패턴으로, QueryBuilder를 사용해서."
패턴 재사용의 효과:
| 작업 | 첫 구현 | 패턴 재사용 | 시간 절감 |
|---|---|---|---|
| Repository 구현 | 45분 | 10분 | 78% |
| Use Case 구현 | 30분 | 12분 | 60% |
| Controller 구현 | 20분 | 8분 | 60% |
| 테스트 작성 | 40분 | 15분 | 62% |
발견하지 못한 패턴 (아쉬운 점):
// 각 Use Case마다 반복되는 인증 체크
export class CreateTaskUseCase {
async execute(userId: string, input: CreateTaskInput) {
const user = await this.userRepo.findById(userId);
if (!user) throw new UnauthorizedException();
// 실제 로직...
}
}
// Decorator 패턴으로 추상화할 수 있었음
@Authorized()
export class CreateTaskUseCase {
async execute(input: CreateTaskInput) {
// 인증은 자동으로 처리됨
}
}
2.3 추상화(Abstraction): 적절한 레벨 찾기
추상화 레벨 결정의 딜레마:
// 레벨 1: 구체적 (추상화 없음)
class TaskService {
async createTask(title: string) {
const task = new Task();
task.title = title;
await db.query('INSERT INTO tasks ...');
await openai.chat.completions.create(...);
}
}
// 레벨 2: 적절한 추상화 (실제 선택)
class CreateTaskUseCase {
constructor(
private taskRepo: ITaskRepository,
private aiService: IAIService
) {}
async execute(input: CreateTaskInput): Promise<Result<Task>> {
const task = Task.create(input);
await this.taskRepo.save(task);
return Result.ok(task);
}
}
// 레벨 3: 과도한 추상화
class CreateTaskUseCase {
constructor(
private entityFactory: IEntityFactory,
private persistenceStrategy: IPersistenceStrategy,
private validationPipeline: IValidationPipeline,
private eventDispatcher: IEventDispatcher
) {}
// 너무 복잡해짐
}
실제 경험:
우리가 선택한 "레벨 2"는 대부분 적절했습니다. 하지만 일부 과도한 추상화도 있었습니다:
// ❌ 과도한 추상화 사례
interface ITimeEstimator {
estimate(task: Task): Promise<Duration>;
}
class AITimeEstimator implements ITimeEstimator {
async estimate(task: Task): Promise<Duration> {
// AI 호출
}
}
class HistoricalTimeEstimator implements ITimeEstimator {
async estimate(task: Task): Promise<Duration> {
// 과거 데이터 분석
}
}
// 실제로는 AI만 사용함 → 불필요한 인터페이스
// 3개월 후에도 다른 구현체를 만들지 않음
YAGNI (You Aren't Gonna Need It) 위반
"나중에 필요할 것 같아서" 만든 추상화들:
- 여러 시간 예측 전략 (실제로는 AI만 사용)
- 다양한 알림 채널 (실제로는 WebSocket만 사용)
- 여러 AI 모델 지원 (실제로는 GPT-4만 사용)
교훈:
- 현재 필요한 추상화만 만들어라
- 두 번째 구현체가 생길 때 추상화하라
- "미래를 위한 설계"는 대부분 낭비
2.4 알고리즘적 사고: 성능 최적화
작업 우선순위 계산 알고리즘:
// 초기 구현: 단순 계산
function calculatePriority(task: Task): number {
return task.urgency + task.importance;
}
// 문제: 모든 작업이 비슷한 점수
// 20개 작업의 우선순위가 5~7 사이에 몰림
// 개선: 가중치 + 마감일 고려
function calculatePriority(task: Task): number {
const urgencyWeight = 0.4;
const importanceWeight = 0.3;
const deadlineWeight = 0.3;
const daysUntilDeadline = differenceInDays(task.deadline, new Date());
const deadlineScore = Math.max(0, 10 - daysUntilDeadline / 3);
return (
task.urgency * urgencyWeight +
task.importance * importanceWeight +
deadlineScore * deadlineWeight
);
}
// 결과: 우선순위가 0~10 사이에 골고루 분포
작업 스케줄링 알고리즘:
// 목표: 의존성을 고려한 작업 순서 결정
// "Task B는 Task A가 완료된 후에만 시작 가능"
// 위상 정렬(Topological Sort) 적용
function scheduletasks(tasks: Task[]): Task[] {
const graph = new Map<string, string[]>();
const inDegree = new Map<string, number>();
// 그래프 구성
for (const task of tasks) {
graph.set(task.id, task.dependencies);
inDegree.set(task.id, task.dependencies.length);
}
// 진입 차수가 0인 작업부터 시작
const queue: Task[] = [];
for (const task of tasks) {
if (inDegree.get(task.id) === 0) {
queue.push(task);
}
}
const result: Task[] = [];
while (queue.length > 0) {
const current = queue.shift()!;
result.push(current);
// 의존 작업 업데이트
for (const dependent of getDependents(current.id)) {
const degree = inDegree.get(dependent.id)! - 1;
inDegree.set(dependent.id, degree);
if (degree === 0) {
queue.push(dependent);
}
}
}
return result;
}
// 복잡도: O(V + E) → 100개 작업도 1ms 이내 처리
GitHub Copilot과 알고리즘:
흥미로운 점은 Copilot이 "위상 정렬"이라는 명확한 알고리즘 이름을 주면 정확한 구현을 생성했다는 것입니다:
// 프롬프트:
// "작업 간 의존성을 고려하여 실행 순서를 결정하는 함수를 작성해줘.
// 위상 정렬(Topological Sort) 알고리즘을 사용해."
// Copilot이 생성한 코드는 거의 완벽했음
// 수정한 부분: 순환 의존성 체크 추가
교훈:
- 알고리즘 이름을 알면 Copilot 활용도가 10배 증가
- 자료구조/알고리즘 기초 지식은 여전히 중요
- 복잡도 분석 능력은 인간의 몫
// 이미지로 교체되어야 함 : 컴퓨팅 사고 4대 원리 적용 효과 - 각 원리별 기대효과 vs 실제효과 비교 레이더 차트 (분해, 패턴인식, 추상화, 알고리즘) 프롬프트: A radar chart showing the effectiveness of 4 computational thinking principles: Decomposition, Pattern Recognition, Abstraction, and Algorithmic Thinking. Each axis shows "Expected Effect" (dashed blue line) vs "Actual Effect" (solid green line) on a scale of 0-10. Include small icons for each principle. Professional data visualization style, clean layout, blue and green color scheme, white background.
3. AI 협업의 명암: 성공과 실패 사례
GitHub Copilot과의 3주간 협업을 돌아보며, 무엇이 효과적이었고 무엇이 실패했는지 분석합니다.
3.1 대성공 사례: Copilot이 탁월했던 순간들
사례 1: 보일러플레이트 코드 생성
// 프롬프트: "ITaskRepository 인터페이스를 구현하는 TypeOrmTaskRepository를 만들어줘"
// 소요 시간: 2분 (수동 작성 시 30분)
@Injectable()
export class TypeOrmTaskRepository implements ITaskRepository {
constructor(
@InjectRepository(TaskEntity)
private readonly taskRepo: Repository<TaskEntity>
) {}
async findById(id: string): Promise<Task | null> {
const entity = await this.taskRepo.findOne({ where: { id } });
return entity ? this.toDomain(entity) : null;
}
async findAll(filter?: TaskFilter): Promise<Task[]> {
const qb = this.taskRepo.createQueryBuilder('task');
if (filter?.userId) {
qb.where('task.userId = :userId', { userId: filter.userId });
}
if (filter?.status) {
qb.andWhere('task.status = :status', { status: filter.status });
}
const entities = await qb.getMany();
return entities.map(e => this.toDomain(e));
}
// ... 나머지 메서드들도 완벽하게 생성됨
}
효과:
- 시간 절약: 93%
- 오타/실수: 0건
- 테스트 통과율: 100%
사례 2: 테스트 코드 자동 생성
// `/tests` 명령어 사용
// CreateTaskUseCase 테스트 → 15개 테스트 케이스 자동 생성
describe('CreateTaskUseCase', () => {
let useCase: CreateTaskUseCase;
let mockTaskRepo: jest.Mocked<ITaskRepository>;
let mockEventBus: jest.Mocked<IEventBus>;
beforeEach(() => {
mockTaskRepo = {
save: jest.fn(),
findById: jest.fn()
} as any;
mockEventBus = {
publish: jest.fn()
} as any;
useCase = new CreateTaskUseCase(mockTaskRepo, mockEventBus);
});
it('should create a task successfully', async () => {
// Arrange
const input = { title: 'New Task', userId: 'user-1' };
mockTaskRepo.save.mockResolvedValue(undefined);
// Act
const result = await useCase.execute(input);
// Assert
expect(result.isSuccess).toBe(true);
expect(result.value).toBeDefined();
expect(mockTaskRepo.save).toHaveBeenCalledTimes(1);
expect(mockEventBus.publish).toHaveBeenCalled();
});
it('should fail when title is empty', async () => {
// Arrange
const input = { title: '', userId: 'user-1' };
// Act
const result = await useCase.execute(input);
// Assert
expect(result.isSuccess).toBe(false);
expect(result.error).toContain('제목');
});
// 13개의 테스트 케이스 더...
});
효과:
- 테스트 작성 시간: 50% 절감
- 엣지 케이스 커버리지: 향상 (인간이 놓치기 쉬운 부분 발견)
사례 3: 리팩토링
// `/simplify` 명령어로 복잡한 코드 단순화
// Before (복잡한 조건문)
function canStartTask(task: Task, user: User): boolean {
if (task.status === TaskStatus.TODO) {
if (task.assignedTo === user.id) {
if (task.dependencies.every(dep => dep.status === TaskStatus.DONE)) {
if (user.availableHours > task.estimatedHours) {
return true;
}
}
}
}
return false;
}
// After (Copilot의 리팩토링)
function canStartTask(task: Task, user: User): boolean {
return (
task.status === TaskStatus.TODO &&
task.assignedTo === user.id &&
task.dependencies.every(dep => dep.status === TaskStatus.DONE) &&
user.availableHours > task.estimatedHours
);
}
3.2 실패 사례: Copilot이 오도했던 순간들
실패 1: N+1 쿼리 생성
// Copilot이 생성한 코드
async getAllTasksWithSubtasks(userId: string): Promise<Task[]> {
const tasks = await this.taskRepo.find({ where: { userId } });
// 각 작업마다 하위 작업을 별도로 조회 (N+1 문제!)
for (const task of tasks) {
task.subtasks = await this.taskRepo.find({
where: { parentId: task.id }
});
}
return tasks;
}
// 문제: 100개 작업 = 101번의 쿼리 = 450ms
// Copilot은 이 문제를 지적하지 않음
교훈: Copilot은 "작동하는 코드"를 생성하지만, "최적화된 코드"는 인간의 몫입니다.
실패 2: 보안 취약점
// Copilot이 생성한 코드
@Post('search')
async search(@Body() body: any) {
const { query } = body;
// SQL Injection 취약점!
const tasks = await this.dataSource.query(
`SELECT * FROM tasks WHERE title LIKE '%${query}%'`
);
return tasks;
}
// Copilot은 편리한 raw query를 생성했지만,
// 입력 검증과 파라미터화를 누락함
올바른 코드 (인간이 수정):
@Post('search')
async search(@Body() dto: SearchTaskDto) {
// DTO로 타입 검증
const { query } = dto;
// 파라미터화된 쿼리
const tasks = await this.taskRepo
.createQueryBuilder('task')
.where('task.title LIKE :query', { query: `%${query}%` })
.getMany();
return tasks;
}
실패 3: 과도한 추상화
// 프롬프트: "확장 가능한 알림 시스템을 만들어줘"
// Copilot이 생성한 구조
interface INotificationChannel {
send(message: Notification): Promise<void>;
}
class EmailNotificationChannel implements INotificationChannel { }
class SMSNotificationChannel implements INotificationChannel { }
class PushNotificationChannel implements INotificationChannel { }
class SlackNotificationChannel implements INotificationChannel { }
class NotificationService {
constructor(private channels: INotificationChannel[]) {}
async notifyAll(message: Notification) {
await Promise.all(
this.channels.map(ch => ch.send(message))
);
}
}
// 문제: 현재 WebSocket만 사용하는데 4개 채널 구현체 생성
// YAGNI 위반 → 불필요한 복잡도
교훈: "확장 가능한"이라는 프롬프트는 Copilot을 과도한 엔지니어링으로 유도합니다.
3.3 Copilot의 한계: 인간 판단이 필수였던 순간
한계 1: 비즈니스 로직 설계
// 질문: "작업이 완료되면 하위 작업도 자동으로 완료되어야 하는가?"
// Copilot은 이 질문에 답할 수 없음
// 선택 1: 자동 완료 (Cascade)
complete(): Result<void> {
this.status = TaskStatus.DONE;
this.subtasks.forEach(sub => sub.complete());
return Result.ok();
}
// 선택 2: 검증만 (현재 선택)
complete(): Result<void> {
if (this.subtasks.some(sub => sub.status !== TaskStatus.DONE)) {
return Result.fail('하위 작업을 먼저 완료해주세요');
}
this.status = TaskStatus.DONE;
return Result.ok();
}
// 이 결정은 순전히 비즈니스 요구사항에 달려 있음
// AI가 할 수 없는 영역
한계 2: 트레이드오프 판단
// 질문: "실시간 협업에서 충돌을 어떻게 처리할 것인가?"
// 옵션 A: Last-Write-Wins (단순, 데이터 손실 가능)
// 옵션 B: Operational Transform (복잡, 데이터 보존)
// 옵션 C: CRDT (매우 복잡, 완벽한 병합)
// Copilot은 세 가지 모두 구현할 수 있지만,
// "어느 것을 선택해야 하는가"는 인간의 판단
우리는 A를 선택했습니다 (3주 프로젝트, 단순성 우선).
한계 3: 아키텍처 일관성 유지
// Copilot은 각 파일을 독립적으로 생성하므로,
// 프로젝트 전체의 일관성을 보장하지 못함
// 파일 A (Copilot 생성)
export class CreateTaskUseCase {
async execute(input: CreateTaskInput): Promise<Task> {
// Result 패턴 사용 안 함
}
}
// 파일 B (Copilot 생성, 같은 날)
export class UpdateTaskUseCase {
async execute(input: UpdateTaskInput): Promise<Result<Task>> {
// Result 패턴 사용함
}
}
// 해결: .github/copilot-instructions.md로 일관성 강제
3.4 효과적인 Copilot 활용 패턴
3주간 발견한 베스트 프랙티스:
// 패턴 1: 명확한 컨텍스트 제공
// ❌ 나쁜 프롬프트
// "Task 리포지토리 만들어줘"
// ✅ 좋은 프롬프트
// "NestJS Clean Architecture에서 ITaskRepository 인터페이스를 구현하는
// TypeOrmTaskRepository를 만들어줘. QueryBuilder를 사용하고,
// N+1 문제를 피하기 위해 eager loading을 사용해."
// 패턴 2: 예시 기반 학습
// 첫 번째 구현을 완벽하게 작성한 후
// "TaskRepository와 같은 패턴으로 UserRepository를 만들어줘"
// 패턴 3: 점진적 개선
// 1단계: "기본 CRUD만 구현해줘"
// 2단계: "에러 처리 추가해줘"
// 3단계: "트랜잭션 추가해줘"
// 4단계: "테스트 작성해줘"
// 패턴 4: Agent 모드 활용
// @workspace /new TaskModule 생성해줘
// → 관련된 모든 파일 자동 생성 (Controller, Service, Repository, DTO, Test)
시간 절약 효과 측정:
| 작업 유형 | Copilot 없이 | Copilot 활용 | 절감률 |
|---|---|---|---|
| 보일러플레이트 | 100% | 20% | 80% |
| 비즈니스 로직 | 100% | 60% | 40% |
| 테스트 코드 | 100% | 50% | 50% |
| 리팩토링 | 100% | 40% | 60% |
| 문서화 | 100% | 30% | 70% |
| 전체 평균 | 100% | 40% | 60% |
3주 프로젝트 = 1.2주 실제 작업 시간
4. 기술 부채 관리와 완벽주의의 함정
3주라는 짧은 기간 동안, "완벽한 코드"와 "작동하는 제품" 사이에서 균형을 찾아야 했습니다.
4.1 의도적 기술 부채 vs 우연한 기술 부채
의도적으로 미룬 부채 (전략적 결정):
// 부채 1: 대시보드 통계 최적화
// 현재: 매번 DB에서 계산 (200ms)
async getDashboardStats(userId: string): Promise<Stats> {
const totalTasks = await this.taskRepo.count({ userId });
const completedTasks = await this.taskRepo.count({
userId,
status: TaskStatus.DONE
});
const inProgressTasks = await this.taskRepo.count({
userId,
status: TaskStatus.IN_PROGRESS
});
return { totalTasks, completedTasks, inProgressTasks };
}
// 이상적: Redis 캐싱 (5ms)
// 결정: 3주 프로젝트에서는 우선순위 낮음
// 사용자 50명 이하에서는 문제없음
기록한 기술 부채 목록:
# Technical Debt Log
## High Priority (다음 Sprint에서 해결)
- [ ] N+1 쿼리 최적화 (작업 목록 조회)
- [ ] AI API Rate Limit 관리 개선
## Medium Priority (1개월 내)
- [ ] 대시보드 통계 캐싱
- [ ] 로깅 시스템 구조화 (Winston 도입)
- [ ] 에러 추적 (Sentry 통합)
## Low Priority (필요시)
- [ ] 다국어 지원
- [ ] 테마 커스터마이징
- [ ] CSV 내보내기
우연히 생긴 부채 (놓친 문제):
// 부채 2: 하드코딩된 상수들
const MAX_SUBTASKS = 10; // TaskController에
const MAX_TITLE_LENGTH = 200; // Task 엔티티에
const MAX_DESCRIPTION_LENGTH = 2000; // CreateTaskDto에
// 문제: 여러 곳에 분산, 변경 시 일관성 깨짐
// 발견 시점: Chapter 3 (너무 늦음)
// 해결: 중앙화된 상수 관리
// constants/task.constants.ts
export const TASK_CONSTRAINTS = {
MAX_SUBTASKS: 10,
MAX_TITLE_LENGTH: 200,
MAX_DESCRIPTION_LENGTH: 2000,
MIN_ESTIMATED_HOURS: 0.5,
MAX_ESTIMATED_HOURS: 40
} as const;
4.2 완벽주의의 함정: 과도한 엔지니어링
함정 1: 조기 최적화
// Chapter 1에 작성한 코드 (과도한 최적화)
class TaskCache {
private cache = new Map<string, { data: Task; timestamp: number }>();
private readonly TTL = 60000; // 1분
get(id: string): Task | null {
const cached = this.cache.get(id);
if (!cached) return null;
if (Date.now() - cached.timestamp > this.TTL) {
this.cache.delete(id);
return null;
}
return cached.data;
}
set(id: string, task: Task): void {
this.cache.set(id, { data: task, timestamp: Date.now() });
}
}
// 문제: 사용자 5명인데 메모리 캐싱을 구현함
// 3주 내내 cache hit rate = 0%
// 낭비된 시간: 3시간
교훈: "나중에 필요할 것 같아서"는 금물. 병목이 확인된 후 최적화하라.
함정 2: 과도한 테스트
// Task 엔티티의 getter를 테스트
describe('Task', () => {
it('should return correct title', () => {
const task = Task.create({ title: 'Test' });
expect(task.title).toBe('Test');
});
it('should return correct id', () => {
const task = Task.create({ title: 'Test' });
expect(task.id).toBeDefined();
});
// 20개의 trivial 테스트...
});
// 문제: 가치 없는 테스트에 시간 낭비
// 낭비된 시간: 2시간
의미 있는 테스트:
describe('Task', () => {
it('should not allow starting a task with incomplete dependencies', () => {
const mainTask = Task.create({ title: 'Main' });
const dependency = Task.create({ title: 'Dependency' });
mainTask.addDependency(dependency);
const result = mainTask.start();
expect(result.isSuccess).toBe(false);
expect(result.error).toContain('의존성');
});
});
함정 3: 불필요한 추상화
// Chapter 1에 만든 추상화 레이어
interface ILogger {
log(message: string): void;
error(message: string): void;
}
class ConsoleLogger implements ILogger { }
class FileLogger implements ILogger { }
class RemoteLogger implements ILogger { }
class LoggerFactory {
static create(type: LoggerType): ILogger { }
}
// 문제: 3주 내내 console.log만 사용함
// 낭비된 시간: 2시간
4.3 "충분히 좋음"의 정의
우리의 기준:
# Definition of "Good Enough"
## 기능적 요구사항
- [x] 핵심 기능이 작동함
- [x] 사용자가 목표를 달성할 수 있음
- [x] 치명적 버그 없음
## 코드 품질
- [x] 테스트 커버리지 > 70% (핵심 로직)
- [x] 린트 에러 없음
- [x] 타입 에러 없음
- [ ] 100% 커버리지는 불필요 ❌
## 성능
- [x] 일반적 시나리오에서 < 500ms 응답
- [x] 동시 사용자 50명 처리 가능
- [ ] 1000 RPS는 당장 불필요 ❌
## 확장성
- [x] 새 기능 추가가 어렵지 않음
- [x] 코드베이스 구조가 명확함
- [ ] 마이크로서비스는 과함 ❌
## 문서화
- [x] README에 시작 가이드
- [x] API 엔드포인트 목록 (Swagger)
- [ ] 상세 아키텍처 문서는 나중에 ❌
4.4 기술 부채 상환 전략
우선순위 결정 프레임워크:
interface TechnicalDebt {
description: string;
impact: 'high' | 'medium' | 'low'; // 사용자/개발자에게 미치는 영향
effort: 'small' | 'medium' | 'large'; // 해결에 필요한 시간
urgency: 'critical' | 'important' | 'nice-to-have';
}
// 우선순위 점수 계산
function calculatePriority(debt: TechnicalDebt): number {
const impactScore = { high: 10, medium: 5, low: 2 };
const effortScore = { small: 1, medium: 3, large: 5 };
const urgencyMultiplier = { critical: 3, important: 2, 'nice-to-have': 1 };
return (
(impactScore[debt.impact] / effortScore[debt.effort]) *
urgencyMultiplier[debt.urgency]
);
}
// 예시
const debts: TechnicalDebt[] = [
{
description: 'N+1 쿼리 해결',
impact: 'high', // 성능에 직접 영향
effort: 'small', // 1일이면 해결
urgency: 'important'
},
// Priority = (10 / 1) * 2 = 20 (최우선)
{
description: '다국어 지원',
impact: 'medium',
effort: 'large',
urgency: 'nice-to-have'
}
// Priority = (5 / 5) * 1 = 1 (낮음)
];
실제 적용 결과:
챕터별 부채 상환:
- Chapter 1: 의도적 부채 9건 발생 (빠른 개발)
- Chapter 2: 우연한 부채 3건 발견 + 수정
- Chapter 3: 고우선순위 부채 5건 해결
- 남은 부채: 4건 (모두 Low Priority)
교훈: 기술 부채는 "악"이 아니라 "전략적 도구"입니다.
5. 실패에서 배운 교훈과 다음 프로젝트를 위한 원칙
15주 여정의 마지막 회고입니다. 무엇을 잘했고, 무엇을 개선할 수 있을까요?
5.1 아키텍처 안티패턴 경험
안티패턴 1: God Object
// TaskService가 점점 비대해짐
@Injectable()
export class TaskService {
// 처음에는 간단했지만...
async createTask(input) { }
async updateTask(id, input) { }
async deleteTask(id) { }
// 점점 책임이 늘어남
async decomposeTask(id) { }
async estimateTime(id) { }
async calculatePriority(id) { }
async assignUser(taskId, userId) { }
async addComment(taskId, comment) { }
async shareTask(taskId, email) { }
// 결국 1000줄의 거대한 클래스가 됨
}
// 해결: Use Case 패턴으로 분리
CreateTaskUseCase
UpdateTaskUseCase
DecomposeTaskUseCase
// ... 각각 독립적인 클래스
안티패턴 2: Leaky Abstraction
// ITaskRepository 인터페이스 (추상)
interface ITaskRepository {
findAll(filter?: TaskFilter): Promise<Task[]>;
}
// 하지만 구현체 (TypeORM)의 세부사항이 새어나옴
const tasks = await taskRepo.findAll({
relations: ['subtasks', 'assignedUser'], // TypeORM 전용
skip: 10,
take: 20
});
// 다른 DB로 전환 시 이 코드도 수정 필요
// 추상화가 완전하지 않음
안티패턴 3: Premature Generalization
// "미래를 위한" 추상화
interface IAIProvider {
complete(prompt: string): Promise<string>;
}
class OpenAIProvider implements IAIProvider { }
class AnthropicProvider implements IAIProvider { }
class LocalLLMProvider implements IAIProvider { }
// 실제: 3주 내내 OpenAI만 사용
// 다른 구현체는 한 번도 만들지 않음
5.2 성능 병목의 원인과 해결
병목 1: 대시보드 로딩 지연
// 문제 코드 (5초 소요)
async getDashboard(userId: string) {
const tasks = await this.taskRepo.findAll({ userId }); // 1s
const stats = await this.calculateStats(tasks); // 2s
const chart = await this.generateChart(stats); // 2s
return { tasks, stats, chart };
}
// 해결: 병렬 처리
async getDashboard(userId: string) {
const [tasks, stats, chart] = await Promise.all([
this.taskRepo.findAll({ userId }),
this.statsService.calculate(userId),
this.chartService.generate(userId)
]);
return { tasks, stats, chart };
}
// 결과: 5s → 2s (60% 개선)
병목 2: AI API 호출
// 문제: 순차 호출
for (const task of tasks) {
const subtasks = await aiService.decompose(task);
await taskRepo.saveAll(subtasks);
}
// 10개 작업 = 100초
// 해결: 배치 처리 + 큐
const queue = new PQueue({ concurrency: 3 });
await Promise.all(
tasks.map(task =>
queue.add(() => this.decomposeAndSave(task))
)
);
// 10개 작업 = 35초
5.3 다음 프로젝트를 위한 10가지 원칙
1. 아키텍처 원칙
- Clean Architecture는 가이드이지 교리가 아니다
- 80%의 분리 + 20%의 실용성
- YAGNI: 두 번째 구현체가 생길 때 추상화하라
2. AI 협업 원칙
- Copilot은 초안 작성자, 인간은 편집자
- 생성된 코드는 항상 검증하라 (보안, 성능, 정확성)
- .github/copilot-instructions.md로 일관성 유지
3. 테스트 원칙
- 비즈니스 로직에 집중 (getter 테스트하지 마라)
- 테스트 커버리지 70-80%가 실용적
- 100% 커버리지는 ROI가 낮음
4. 성능 원칙
- 측정하기 전에는 최적화하지 마라
- N+1 쿼리는 처음부터 방지하라
- 병렬화 가능한 작업은 Promise.all
5. 기술 부채 원칙
- 의도적 부채는 문서화하라
- High Impact + Small Effort 부터 상환
- 3주마다 부채 상환 sprint 배정
6. 코드 품질 원칙
- 린트 에러 = 0 (타협 없음)
- 타입 안정성 > 타이핑 편의성
- 코드 리뷰는 학습 기회
7. 프로젝트 관리 원칙
- 핵심 기능 먼저 (Vertical Slice)
- 불확실한 작업에 50% 버퍼
- "Nice to Have"는 과감히 포기
8. 학습 원칙
- 매주 회고 (무엇을 배웠는가?)
- 실패를 문서화하라 (같은 실수 방지)
- 새 기술 도입은 1개씩
9. 협업 원칙
- README는 프로젝트의 얼굴
- API 문서는 자동화 (Swagger)
- 의사결정은 기록 (ADR)
10. 완성도 원칙
- "완벽"보다 "작동"이 우선
- 80% 완성도로 출시, 피드백 반영
- 마지막 20%는 ROI가 낮음
5.4 성장의 증거: Before & After
15주 전 (Chapter 1):
// GitHub Copilot은 자동완성 도구
// 코드를 빠르게 작성하는 것이 목표
// 아키텍처는 나중에 생각
// 테스트는 시간이 남으면
function createTask(title) {
const task = { id: Date.now(), title };
tasks.push(task);
return task;
}
15주 후 (Chapter 14):
// GitHub Copilot은 협업 파트너
// 문제를 올바르게 정의하고 설계하는 것이 핵심
// 좋은 아키텍처는 빠른 개발의 기반
// 테스트는 자신감의 근거
export class CreateTaskUseCase {
constructor(
@Inject('ITaskRepository') private taskRepo: ITaskRepository,
@Inject('IEventBus') private eventBus: IEventBus
) {}
async execute(input: CreateTaskInput): Promise<Result<Task>> {
// 입력 검증
const validation = this.validate(input);
if (!validation.isValid) {
return Result.fail(validation.error);
}
// 도메인 로직
const task = Task.create(input);
const result = await task.validate();
if (!result.isSuccess) {
return result;
}
// 영속화
await this.taskRepo.save(task);
// 이벤트 발행
await this.eventBus.publish(new TaskCreatedEvent(task));
return Result.ok(task);
}
}
핵심 변화:
- 코더 → 엔지니어 → 문제 설계자
- 코드 중심 → 사고 중심
- 혼자 작성 → AI와 협업
- 작동하는 코드 → 유지보수 가능한 시스템
// 이미지로 교체되어야 함 : 15주 성장 여정 - 5가지 역량(컴퓨팅사고, 아키텍처설계, AI협업, 코드품질, 문제해결)의 챕터별 성장 곡선 그래프 프롬프트: A line chart showing growth over 15 chapters across 5 competencies: Computational Thinking (blue), Architecture Design (green), AI Collaboration (purple), Code Quality (orange), and Problem Solving (red). X-axis: Chapter 1-15, Y-axis: Proficiency 0-10. Each line shows gradual upward trend with steeper growth in chapters 2-6 and 10-13. Professional data visualization style, smooth curves, legend, white background.
실습 결과 요약
Chapter 14는 지난 3주간의 프로젝트를 깊이 있게 회고하는 시간이었습니다.
핵심 학습 내용
1. 아키텍처 의사결정 재평가
- Clean Architecture 실제 효과 분석
- 기대: 비즈니스 로직 100% 분리
- 실제: 80% 분리 + 20% 실용성 타협
- 교훈: 이론은 가이드이지 교리가 아님
- NestJS vs Express.js 트레이드오프
- 구조화된 개발 vs 성능
- 3주 프로젝트에는 NestJS가 적합
- 1주 프로토타입이었다면 Express.js
- PostgreSQL + TypeORM 성능 병목
- N+1 쿼리 문제 경험 및 해결
- 인덱싱, QueryBuilder 최적화
- Copilot이 놓치는 최적화 영역
- OpenAI API 통합의 현실
- Rate Limit, 비용, 신뢰성 문제
- Queue + Retry 전략 필요
- Fallback 전략의 중요성
2. 컴퓨팅 사고 4대 원리 실전 검증
- 분해(Decomposition): 70% 효과
- 초기 분해와 실제 구현 순서의 차이
- 핵심 기능 우선, 불확실성에 50% 버퍼
- "Nice to Have"는 과감히 포기
- 패턴 인식(Pattern Recognition): 90% 효과
- Result 패턴, Repository 패턴 재사용
- Copilot이 패턴을 학습하여 자동 적용
- 시간 절약 60-78%
- 추상화(Abstraction): 60% 효과
- 적절한 레벨 찾기의 어려움
- YAGNI 위반 (불필요한 추상화)
- 두 번째 구현체가 생길 때 추상화하라
- 알고리즘적 사고: 80% 효과
- 위상 정렬로 작업 스케줄링
- 알고리즘 이름을 알면 Copilot 활용도 10배
- 복잡도 분석은 여전히 인간의 몫
3. AI 협업의 명암
- 대성공 사례 (60% 시간 절약)
- 보일러플레이트: 80% 절감
- 테스트 코드: 50% 절감
- 리팩토링: 60% 절감
/tests,/simplify명령어 활용
- 실패 사례 (인간 검증 필수)
- N+1 쿼리 생성 (최적화 누락)
- 보안 취약점 (SQL Injection)
- 과도한 추상화 ("확장 가능한" 프롬프트)
- Copilot의 한계
- 비즈니스 로직 설계는 인간의 몫
- 트레이드오프 판단 불가
- 아키텍처 일관성 유지 어려움
- 효과적인 활용 패턴
- 명확한 컨텍스트 제공
- 예시 기반 학습
- 점진적 개선
- Agent 모드 (
@workspace /new)
4. 기술 부채 관리
- 의도적 부채 vs 우연한 부채
- 전략적 결정으로 미룬 최적화
- 문서화 및 우선순위 관리
- Impact / Effort 프레임워크
- 완벽주의의 함정
- 조기 최적화 (캐싱, 불필요한 추상화)
- 과도한 테스트 (trivial 테스트)
- ROI 낮은 작업에 시간 낭비
- "충분히 좋음"의 정의
- 기능: 작동함, 목표 달성 가능, 치명적 버그 없음
- 품질: 70% 테스트 커버리지, 린트/타입 에러 0
- 성능: 일반 시나리오 <500ms, 동시 사용자 50명
- 확장성: 새 기능 추가 용이, 구조 명확
5. 다음 프로젝트를 위한 10가지 원칙
- Clean Architecture는 가이드, 80/20 균형
- Copilot은 초안 작성자, 인간은 편집자
- 비즈니스 로직 테스트 집중, 70-80% 커버리지
- 측정 전 최적화 금지, N+1은 방지
- 의도적 부채 문서화, Impact/Effort 우선순위
- 린트 0, 타입 안정성 우선
- 핵심 기능 먼저, 불확실성 50% 버퍼
- 매주 회고, 실패 문서화
- README/API 문서 자동화, ADR 작성
- "완벽"보다 "작동", 80% 완성도 출시
성장 지표
아키텍처 이해도:
- Before: 단순 MVC, 모든 로직을 Service에
- After: Clean Architecture 4-Layer, SOLID 원칙 적용
- 성장: Layer 간 의존성 방향, 인터페이스 활용 이해
컴퓨팅 사고 적용:
- Before: 직관적 구현, 패턴 인식 부족
- After: 4대 원리 자연스럽게 적용, 패턴 라이브러리 구축
- 성장: 복잡한 문제를 체계적으로 분해
AI 협업 숙련도:
- Before: 단순 자동완성 사용
- After: Agent 모드, 프롬프트 엔지니어링, 비판적 검증
- 성장: Copilot 생산성 60% 향상, 검증 능력 확보
메타인지 능력:
- Before: 코드 작성에만 집중
- After: 설계 결정 근거 분석, 트레이드오프 평가, 회고 습관화
- 성장: "왜?"를 묻는 습관, 실패에서 배우는 능력
회고 체크리스트
아키텍처 의사결정
- Clean Architecture 효과 측정
- 기술 스택 선택 근거 재평가
- 성능 병목 분석 및 해결
- 대안 시나리오 검토
컴퓨팅 사고 검증
- 분해 전략 효과성 평가
- 패턴 재사용 횟수 측정
- 추상화 레벨 적절성 판단
- 알고리즘 성능 영향 확인
AI 협업 분석
- 성공 사례 3개 이상 문서화
- 실패 사례 3개 이상 분석
- 시간 절약 효과 측정
- 효과적 패턴 정리
기술 부채 관리
- 의도적/우연한 부채 구분
- 우선순위 매트릭스 작성
- 완벽주의 함정 경험 정리
- "충분히 좋음" 기준 정의
교훈 정리
- 10가지 원칙 수립
- 안티패턴 경험 문서화
- Before/After 비교
- 다음 프로젝트 적용 계획
다음 주 예고: 종합평가 및 미래 전망
Chapter 15는 전체 여정의 마무리입니다:
- 15주간의 학습 총정리
- 사고의 깊이가 만드는 차별성
- AI 시대의 경쟁력: 무엇이 나를 특별하게 만드는가?
- AI와 함께하는 미래 커리어 로드맵
- 지속적인 학습을 위한 리소스
- 여정의 완료와 새로운 시작
여러분은 이제 단순히 코드를 작성하는 사람이 아닙니다. 문제를 정의하고, 설계하고, AI와 협업하여 구현하며, 비판적으로 회고하는 사색하는 엔지니어입니다.
축하합니다. 여러분의 사고는 한 단계 더 깊어졌습니다.