|
Size: 33021
Comment:
|
← Revision 6 as of 2025-12-30 10:00:30 ⇥
Size: 7675
Comment:
|
| Deletions are marked like this. | Additions are marked like this. |
| Line 2: | Line 2: |
| Line 5: | Line 4: |
| ''주니어 개발자들을 위한 패턴 언어 - 먼저 작동하게 만들고, 이해한 다음, 올바르게 만드는 방법'' | ''주니어 개발자들을 위한 패턴 언어 - 우아함보다 먼저 작동하는 실체를 만들어 진실을 드러내는 법'' |
| Line 9: | Line 8: |
| == The Story: Two Weeks of Perfect Design == | == The Story 1: The Perfectionist vs. The Snowballer (Programming) == |
| Line 11: | Line 10: |
| 주니어 개발자 민수는 사용자 인증 시스템을 구축하는 임무를 받았다. 그는 첫날, 노트북을 열고 생각에 잠겼다. | 두 명의 개발자, 민수와 하나가 '새로운 알림 엔진'을 만들고 있다. |
| Line 13: | Line 12: |
| "완벽하게 만들어야 해. 어떤 암호화 알고리즘을 써야 할까? bcrypt? Argon2? 토큰은 어떻게 관리하지? JWT? Session? 세션 저장소는 Redis? Memcached? 확장성은 어떻게 보장하지? 마이크로서비스 아키텍처가 필요할까?" | '''민수의 방식 (The Perfectionist):''' 민수는 처음부터 확장 가능하고 우아한 설계를 꿈꾼다. 그는 인터페이스를 설계하고, 추상 클래스를 만들고, 디자인 패턴을 적용하느라 사흘을 보냈다. 하지만 사흘째 되는 날에도 알림은 단 한 건도 발송되지 않았다. 설계도만 거창할 뿐, 실제로 무엇이 문제일지는 여전히 미지수였다. |
| Line 15: | Line 15: |
| 2주가 지났다. 민수는 수많은 기술 문서를 읽었다. 아키텍처 다이어그램을 그렸다. 보안 베스트 프랙티스를 연구했다. 하지만 '''한 줄의 코드도 작성하지 않았다'''. | '''하나의 방식 (The Snowballer):''' 하나는 다르게 시작했다. 일단 `print("알림 발송!")`이라는 한 줄의 코드로 시작했다. 그 다음, 하드코딩된 이메일 주소로 진짜 메일이 가는지 확인했다. "일단 돌아가네. 이제 여기서부터 하나씩 붙여보자." 하나는 한 시간 만에 '작동하는 실체'를 만들었다. 그 실체는 하나에게 어디가 병목인지, 어떤 예외 처리가 진짜 필요한지 알려주었다. 작은 눈덩이(Snowball)는 구를수록 커져서 저녁 무렵엔 견고한 엔진이 되었다. |
| Line 17: | Line 20: |
| "아직 완벽한 설계를 찾지 못했어요," 민수가 말했다. "뭔가 놓친 게 있을 것 같아서... 시작할 수가 없어요." | |
| Line 19: | Line 21: |
| 시니어 개발자 지혜는 같은 문제를 다르게 접근했다. 첫날 오전, 그녀는 20줄짜리 코드를 작성했다: | == The Story 2: The Signature Dish (Ordinary Life) == |
| Line 21: | Line 23: |
| {{{ def login(username, password): if username == "admin" and password == "secret": return {"token": "simple_token"} return None }}} |
민수와 하나는 친구들을 초대해 저녁 식사를 대접하기로 했다. 메뉴는 한 번도 안 해본 복잡한 '프랑스 요리'다. |
| Line 28: | Line 25: |
| "이건 틀렸어요!" 민수가 말했다. "보안도 없고, 확장성도 없고, 데이터베이스도 없어요!" | '''민수의 요리 (The Ideal Recipe):''' 민수는 요리책을 수십 번 읽으며 완벽한 타이밍과 도구를 준비했다. 하지만 정작 요리를 시작하자 생각지 못한 변수가 발생했다. 화력이 책과 달랐고, 재료의 수분기도 달랐다. 민수는 당황했고 저녁 식사는 엉망이 되었다. |
| Line 30: | Line 28: |
| 지혜가 미소 지었다. "맞아. 하지만 '''작동하지'''. 이제 우리는 세 가지를 확인했어:" * 인증이 가능하다는 것을 * 어떻게 만드는지 기본 구조를 * 어디서부터 개선해야 할지를 "이제부터 점진적으로 개선하면 돼." 다음 이틀 동안, 지혜는 그 작동하는 코드를 단계별로 개선했다: {{{ # Day 1 오후: 하드코딩을 데이터베이스로 def login(username, password): user = db.get_user(username) if user and user.password == password: return {"token": generate_token(user)} return None # Day 2: 암호화 추가 def login(username, password): user = db.get_user(username) if user and verify_password(password, user.password_hash): return {"token": generate_secure_token(user)} return None # Day 3: 세션 관리 def login(username, password): user = db.get_user(username) if user and verify_password(password, user.password_hash): session = create_session(user) return {"token": session.token, "expires": session.expires_at} return None }}} 일주일 후, 시스템은 production-ready였다. 각 단계마다 코드는 '''작동했다'''. 테스트할 수 있었고, 검증할 수 있었고, 개선할 수 있었다. 민수는 여전히 완벽한 설계를 찾고 있었다. |
'''하나의 요리 (The Prototype Meal):''' 하나는 전날 밤, 아주 적은 양의 재료로 핵심 소스만 미리 만들어보았다. "아, 이 소스는 생각보다 빨리 타는구나. 설탕을 좀 줄여야겠어." 하나는 아주 간단한 버전으로 '맛'을 먼저 확인했다. 이 작은 실험 덕분에 하나는 실제 요리에서 불 조절을 어떻게 해야 할지 정확히 알게 되었고, 친구들에게 완벽한 식사를 대접할 수 있었다. 기예는 이론이 아니라 '작동하는 경험'에서 시작됨을 하나는 알고 있었다. |
| Line 70: | Line 36: |
| 당신은 새로운 기능을 구현하거나 복잡한 문제를 해결해야 한다. 여러 가지 방법이 가능해 보이고, 어떤 접근이 "올바른지" 확신할 수 없다. 시간은 제한되어 있고, 요구사항은 완전히 명확하지 않을 수 있다. 당신은 '''[[TwoWorlds]]'''를 통해 문제와 해결책을 분리하는 법을 배웠고, '''[[DataAsFoundation]]'''을 통해 데이터 구조의 중요성을 이해하고 있다. 이제 '''어떻게 만들 것인가'''의 단계에 있다. |
새로운 기능을 구현하거나 복잡한 문제를 해결해야 한다. 머릿속에는 거창한 설계가 떠오르지만, 어디서부터 손을 대야 할지 막막한 상황이다. |
| Line 75: | Line 39: |
| * 새로운 API를 통합해야 하는데 어디서부터 시작할지 모르겠다 * 복잡한 알고리즘을 구현해야 하는데 한 번에 완벽하게 만들고 싶다 * 리팩토링을 해야 하는데 최종 구조를 미리 설계하려 한다 * 프로토타입을 만들어야 하는데 "제대로" 만들지 않으면 안 될 것 같다 |
* 완벽한 설계를 하려다 시작도 못 하고 시간을 보낸다. * "나중에 고생 안 하려면 처음부터 제대로 짜야 해"라는 압박을 느낀다. * 코드를 짜는 시간보다 화이트보드 앞에서 고민하는 시간이 훨씬 길다. * 구현 도중 예상치 못한 기술적 난관에 부딪혀 전체 설계를 뒤엎는다. |
| Line 83: | Line 47: |
| '''초보 프로그래머는 실행하기 전에 완벽하게 설계하려는 경향이 있다.''' 그들은 도전적인 과제를 만들 수 있을지 두려워하여 너무 신중하게, 너무 많이 설계한다. 실행 없이 설계만 한다. | '''실행 없는 완벽한 설계는 가설일 뿐이며, 실제 구현 단계에서 나타나는 수많은 변수를 예측하지 못한다.''' |
| Line 85: | Line 49: |
| === The Fear of Starting === 주니어 개발자의 내면의 목소리: * "내가 이걸 만들 수 있을까?" * "완벽하게 설계하지 않으면 나중에 다시 만들어야 할 거야" * "처음부터 제대로 만들어야 해" * "실수하면 어떡하지?" 이 두려움은 '''분석 마비(Analysis Paralysis)'''로 이어진다. 결정을 내리지 못하고, 계속 더 많은 정보를 찾고, 더 많은 대안을 고려하고, 결국 아무것도 만들지 못한다. === The Premature Optimization Trap === Donald Knuth가 말했다: '''"Premature optimization is the root of all evil."''' 하지만 주니어는 종종 이것을 무시한다. 작동하기도 전에: * 성능 최적화를 고민한다 * 확장성 아키텍처를 설계한다 * 모든 엣지 케이스를 처리하려 한다 * 미래의 요구사항을 예측하려 한다 {{{ # ❌ 작동도 안 하는데 최적화부터 def process_data(items): # 캐싱 레이어? # 병렬 처리? # 메모리 최적화? # 아직 한 줄도 작성 안 함... pass }}} === YAGNI and KISS Violations === 두 가지 기본 원칙을 위반한다: '''YAGNI (You Aren't Gonna Need It)''' * 실제로 필요하지 않을 기능들을 미리 설계한다 * "나중에 필요할 수도 있어"라고 추측한다 * 결과: 사용되지 않는 복잡한 코드 '''KISS (Keep It Simple, Stupid)''' * 단순한 해결책을 복잡한 아키텍처로 만든다 * "제대로 된" 해결책은 복잡해야 한다고 생각한다 * 결과: 이해하기 어려운 over-engineered 코드 {{{ # ❌ YAGNI 위반 class UserAuthenticator: def __init__(self, strategy_factory, token_manager, session_store, cache_layer, audit_logger): # 5개의 의존성... 정말 다 필요한가? pass # ✅ KISS: 필요한 것만 def authenticate(username, password): user = get_user(username) return user and check_password(password, user.password_hash) }}} === The Wrong Priority === 초기 단계에서 우선순위가 뒤바뀐다: '''주니어의 우선순위:''' 1. 완벽한 아키텍처 2. 최적화된 성능 3. 모든 엣지 케이스 처리 4. 확장 가능한 설계 5. ...그리고 작동하는지? '''시니어의 우선순위:''' 1. '''작동하는가?''' ← 가장 먼저 2. 올바른가? 3. 깔끔한가? 4. 빠른가? Kent Beck이 표현했듯: '''"Make it work, make it right, make it fast."''' 순서가 중요하다. === The Epistemological Problem === 더 근본적인 문제가 있다: '''만들어보기 전에는 알 수 없다'''. ChrisArgyris가 말했듯이, 의사결정은 '''과정(process)'''이다. 우리는 해결책을 '''탐색'''하고 '''실험'''해야 한다. 올바른 방법을 미리 아는 것은 불가능하다. 특히 초기 단계에서: * '''이론적 지식 ≠ 실천적 지식''' - 문서를 읽는 것 ≠ 코드를 작성하는 것 * '''문제는 해결하면서 드러난다''' - 미리 모든 문제를 예측할 수 없다 * '''해결책은 진화한다''' - 최종 형태는 과정을 통해 나타난다 {{{ 계획: "인증 시스템은 OAuth2를 사용하고, JWT 토큰을 발급하고..." 실제로 만들어보니: - OAuth2는 우리 use case에 과도했다 - 간단한 세션 기반 인증으로 충분했다 - 요구사항을 잘못 이해하고 있었다 }}} === The Cost of Wrong Starting Point === 완벽한 설계부터 시작하면: * '''2주 설계 + 3주 구현 + 2주 리팩토링 = 7주''' * 왜? 실제 문제를 이해하지 못한 상태에서 설계했기 때문 작동하는 것부터 시작하면: * '''1일 프로토타입 + 1주 개선 + 1주 정제 = 2.5주''' * 왜? 실제 문제를 빨리 발견하고 올바른 방향으로 진화했기 때문 '''4.5주의 차이'''. 이것이 WorkingFirst의 가치다. |
* '''피드백 지연:''' 실제로 돌아가는 것을 보기 전까지는 내 생각이 맞는지 틀린지 알 수 없다. * '''심리적 부담:''' 완벽해야 한다는 생각은 행동을 느리게 만들고 창의성을 억누른다. * '''매몰 비용:''' 거창하게 설계한 뒤에는 나중에 결함을 발견해도 설계를 바꾸기가 아까워진다. |
| Line 197: | Line 55: |
| '''먼저 작동하게 만들어라. 우아하거나 효율적으로 만들기 전에.''' | '''우아하거나 효율적으로 만들기 전에, 가장 단순한 형태로 "먼저 작동하게" 만들어라.''' |
| Line 199: | Line 57: |
| 작동하는 해결책은 문제의 진정한 본질을 드러내고 더 나은 설계 결정을 안내한다. 이것은 단순한 기법이 아니라 '''탐색의 철학'''이다. | 작동하는 해결책은 문제의 진정한 본질을 드러내고, 당신이 가야 할 다음 길을 안내한다. |
| Line 201: | Line 59: |
| === Principle 1: Reachable Working State ASAP === '''가능한 한 빨리 작동 상태에 도달하라(Reach to Working State ASAP).''' 이것은 세 가지를 확인한다: 1. '''가능하다''' - 이것은 만들 수 있다 2. '''어떻게''' - 기본 구조와 접근법 3. '''무엇이''' - 진짜 어려운 부분이 무엇인지 {{{ # Day 1 목표: "작동" def api_call(endpoint): response = requests.get(f"https://api.example.com/{endpoint}") return response.json() # 이제 확인했다: # ✓ API가 작동한다 # ✓ 인증이 필요하다 (에러를 통해 발견) # ✓ rate limit이 있다 (에러를 통해 발견) # ✓ 응답 형식이 문서와 다르다 (실행을 통해 발견) }}} 작동하는 코드는 '''학습 도구'''다. 이론이 아닌 실제를 가르쳐준다. |
=== Principle 1: Make it Work, Then Make it Right === Kent Beck의 조언을 기억하라. 1. 작동하게 만들어라 (Make it Work). 2. 올바르게 만들어라 (Make it Right - 리팩토링). 3. 빠르게 만들어라 (Make it Fast - 최적화). 대부분의 주니어는 1단계를 건너뛰고 2단계나 3단계부터 하려다 길을 잃는다. |
| Line 229: | Line 70: |
| * 작동하는 실체가 당신에게 "이게 가능하다"는 용기를 줄 것이다. | |
| Line 231: | Line 71: |
| === Principle 3: Make It Work, Make It Right, Make It Fast === Kent Beck의 3단계 접근: '''Phase 1: Make It Work (작동하게)''' * 목표: 기능이 실행됨 * 허용: 하드코딩, 중복, 비효율 * 금지: 최적화, 추상화, 일반화 '''Phase 2: Make It Right (올바르게)''' * 목표: 깔끔하고 이해하기 쉬움 * 허용: 리팩토링, 추상화, 테스트 추가 * 금지: 기능 추가, 성능 최적화 '''Phase 3: Make It Fast (빠르게)''' * 목표: 성능 요구사항 충족 * 허용: 최적화, 캐싱, 병렬화 * 전제: Phase 1, 2 완료됨 {{{ # Phase 1: Work def calculate_total(items): total = 0 for item in items: total += item['price'] * item['quantity'] return total # Phase 2: Right def calculate_total(items): return sum(item.price * item.quantity for item in items) # Phase 3: Fast (필요한 경우만) @lru_cache(maxsize=128) def calculate_total(items_tuple): return sum(item.price * item.quantity for item in items_tuple) }}} 대부분의 경우, Phase 2까지만 필요하다. Phase 3는 '''측정된 성능 문제'''가 있을 때만. === Principle 3: Exploration First, Optimization Later === 올바른 방법은 '''여러 가지'''다. WorkingFirst는 그 중 '''하나'''에 빠르게 도달하게 돕는다. George Polya의 "How to Solve It"에서: 1. '''문제 이해''' - 작동하는 것을 만들면서 진짜 문제 이해 2. '''계획 수립''' - 첫 작동 버전이 더 나은 계획의 기초 3. '''계획 실행''' - 작동 → 개선의 반복 4. '''되돌아보기''' - 작동하는 것에서 패턴 발견 {{{ 문제: "사용자 데이터를 캐싱해야 한다" 탐색적 접근: 1. 먼저 캐싱 없이 작동하게 (Day 1) 2. 성능 측정 (Day 1) 3. 병목이 확인되면 단순 dict 캐싱 (Day 2) 4. 필요하면 Redis로 (Day 3) 설계 우선 접근: 1. Redis 아키텍처 연구 (Week 1) 2. 캐시 무효화 전략 설계 (Week 1) 3. 구현 시작 (Week 2) 4. 실제로는 단순 캐싱으로 충분했다는 것을 발견 (Week 2) 5. 1.5주 낭비 }}} === Principle 4: Prototype as Learning Tool === 초기 프로토타입은 '''버릴 것'''을 전제로 만들어라. Fred Brooks는 "The Mythical Man-Month"에서 말했다: '''"Plan to throw one away; you will, anyhow."''' 프로토타입의 목적: * '''학습''' - 문제와 해결 공간 이해 * '''검증''' - 접근법이 작동하는지 확인 * '''발견''' - 예상하지 못한 문제 찾기 * '''의사소통''' - 팀과 구체적으로 논의 프로토타입은 실패해도 된다. 그것이 목적이다. {{{ # Prototype (버릴 것) def quick_search(text): return [item for item in all_items if text in item['name']] # 배운 것: # - 대소문자 구분 필요 # - 부분 매칭 필요 # - 성능 문제 (all_items가 크면) # - 정렬 필요 # 실제 구현 (배운 것 기반) def search(text, options=None): text = text.lower() results = [item for item in indexed_items if text in item.name.lower()] return sorted(results, key=lambda x: x.relevance) }}} 프로토타입을 만들고 나면, '''어떻게 제대로 만들어야 할지''' 안다. === Principle 5: Working State as Safe Refuge === '''[[GreenRefuge]]'''와 연결: 작동하는 상태는 안전한 피난처다. 각 단계에서 "작동하는" 상태를 유지하면: * 언제든 데모할 수 있다 * 언제든 배포할 수 있다 (feature flag 뒤에) * 언제든 중단할 수 있다 * 막혔을 때 되돌아갈 곳이 있다 {{{ # 나쁜 흐름 Week 1: 50% 구현, 작동 안 함 Week 2: 80% 구현, 작동 안 함 Week 3: 100% 구현, 버그 투성이 Week 4: 디버깅... # 좋은 흐름 (WorkingFirst) Day 1: 작동 (기본 기능) Day 3: 작동 (개선된 기능) Day 5: 작동 (더 개선됨) Day 7: 작동 (production-ready) }}} 작동하는 상태 → 작동하는 상태 → 작동하는 상태. 이것이 '''[[BabySteps]]'''다. === Principle 6: Snowballing Growth === 시니어는 '''작은 작동하는 것'''을 만들고, 거기서부터 '''눈덩이처럼 굴려간다(snowballing)'''. {{{ # Snowballing 과정 V1 (1시간): 단순 로그인 체크 V2 (2시간): + 데이터베이스 연결 V3 (1시간): + 암호화 V4 (2시간): + 세션 관리 V5 (1시간): + 토큰 발급 V6 (2시간): + 권한 체크 V7 (1시간): + 로깅 V8 (1시간): + 에러 처리 총 11시간, 8개의 작동하는 버전 }}} 각 버전은 이전 버전 위에 '''유기적으로 성장'''한다. '''[[OrganicGrowth]]'''. |
=== Principle 3: Snowballing from a Small Core === 작고 초라한 시작을 부끄러워하지 마라. * `Hello World`가 찍히는 순간 당신은 불확실성의 50%를 제거한 것이다. * 작은 성공의 경험은 눈덩이처럼 불어나 당신에게 설계를 완성할 자신감과 데이터를 제공한다. |
| Line 381: | Line 79: |
| === Example 1: API Integration === | === Example 1: Sketching first === 화가들은 눈동자의 디테일을 그리기 전에 전체 구도를 연필로 슥쓱 그린다. 엉성한 선들이 모여 전체적인 균형을 잡은 뒤에야 세밀한 묘사에 들어간다. |
| Line 383: | Line 82: |
| '''주니어 접근 (3일):''' {{{ Day 1-2: API 문서 연구, 에러 처리 설계, 재시도 로직 계획 Day 3: 구현 시작... 하지만 API가 문서와 다르게 작동 }}} '''시니어 접근 (3시간):''' {{{ # 15분: 가장 단순한 호출 import requests response = requests.get("https://api.example.com/users/1") print(response.json()) # → 작동 확인! # 30분: 함수로 추출 def get_user(user_id): response = requests.get(f"https://api.example.com/users/{user_id}") return response.json() # 1시간: 에러 처리 추가 (실제 에러를 본 후) def get_user(user_id): try: response = requests.get( f"https://api.example.com/users/{user_id}", timeout=5 ) response.raise_for_status() return response.json() except requests.RequestException as e: logger.error(f"API call failed: {e}") return None # 2시간: 재시도 로직 (필요하다고 판단한 후) @retry(max_attempts=3, backoff=2) def get_user(user_id): # ... # 3시간: production-ready }}} '''차이점:''' * 시니어는 15분 만에 "작동"을 확인 * 실제 문제를 보고 나서 해결책 추가 * 이론이 아닌 실제에 기반한 설계 === Example 2: Data Transformation === '''주니어 접근:''' {{{ # 완벽한 클래스 계층을 설계하려고 시도 class DataTransformer: def __init__(self, config, validators, formatters): # 어떤 변환이 필요한지도 아직 모름 self.config = config self.validators = validators self.formatters = formatters def transform(self, data): # TODO: 구현... pass }}} '''시니어 접근:''' {{{ # Phase 1: Work (30분) def transform_user_data(raw_data): return { 'name': raw_data['user_name'], 'age': int(raw_data['user_age']), 'email': raw_data['email'].lower() } # 사용하면서 패턴 발견... # Phase 2: Right (1시간) def transform_user_data(raw_data): return { 'name': extract_name(raw_data), 'age': parse_age(raw_data), 'email': normalize_email(raw_data) } def extract_name(data): return data.get('user_name', data.get('name', 'Unknown')) def parse_age(data): age_str = data.get('user_age', data.get('age', '0')) return int(age_str) if age_str.isdigit() else 0 def normalize_email(data): email = data.get('email', '') return email.lower().strip() # 필요하면 나중에 클래스로 }}} === Example 3: Search Feature === '''주니어 접근 (2주):''' {{{ Week 1: Elasticsearch 연구, 인덱싱 전략 설계 Week 2: 구현 시작 결과: 실제로는 간단한 SQL LIKE로 충분했음 }}} '''시니어 접근 (2일):''' {{{ # Day 1 아침: 작동하는 것 def search(query): return [item for item in items if query.lower() in item.name.lower()] # Day 1 오후: DB로 이동 def search(query): return db.query(Item).filter( Item.name.ilike(f'%{query}%') ).all() # Day 2: 성능 측정 후 인덱스 추가 # 인덱스로 충분함을 확인 # Elasticsearch 불필요 # 2주 대신 2일, Elasticsearch 대신 PostgreSQL 인덱스 }}} '''차이:''' 작동하는 것부터 만들면서 '''실제 요구사항'''을 발견했다. === Example 4: Real Story - File Upload === 실제 경험한 사례: '''요구사항:''' "파일 업로드 기능이 필요합니다" '''주니어의 계획:''' * S3 통합 * 청크 업로드 * 재시도 로직 * 바이러스 스캔 * 썸네일 생성 * 예상 시간: 2주 '''시니어의 접근:''' {{{ # Day 1: 로컬 파일 시스템에 저장 @app.route('/upload', methods=['POST']) def upload(): file = request.files['file'] filename = secure_filename(file.filename) file.save(os.path.join('/uploads', filename)) return {'file': filename} # 작동 확인! # 사용자 테스트... }}} '''놀라운 발견:''' * 사용자는 주로 작은 이미지만 업로드 (< 1MB) * 평균 하루 10개 파일 * 로컬 파일 시스템으로 충분했음 '''최종 솔루션 (Day 2):''' * 로컬 저장 + 간단한 validation * S3, 청크 업로드, 바이러스 스캔 모두 YAGNI * '''2주 대신 2일''', 훨씬 단순한 코드 이것이 WorkingFirst의 힘이다: '''실제 요구사항을 발견'''한다. === Example 5: Authentication Evolution === 앞의 스토리를 코드로: {{{ # Day 1 오전: Work (20줄, 30분) users = {"admin": "secret"} def login(username, password): if users.get(username) == password: return {"token": username} # 극도로 단순 return None # Day 1 오후: Database (30줄, 1시간) def login(username, password): user = db.query(User).filter_by(username=username).first() if user and user.password == password: return {"token": generate_token(user.id)} return None # Day 2: Hashing (40줄, 1시간) def login(username, password): user = db.query(User).filter_by(username=username).first() if user and check_password_hash(user.password_hash, password): return {"token": generate_token(user.id)} return None # Day 3: Sessions (60줄, 2시간) def login(username, password): user = db.query(User).filter_by(username=username).first() if user and check_password_hash(user.password_hash, password): session = Session.create(user.id, expires_in=3600) return {"token": session.token, "expires": session.expires_at} return None # Day 4: Error handling, logging, rate limiting... # Day 5: Production-ready # 5일, 각 단계마다 작동했음 }}} |
=== Example 2: Tracer Bullets === 어두운 밤, 목표를 맞추기 위해 계산기만 두드리는 대신 '예광탄'을 쏜다. 탄환이 날아가는 궤적을 눈으로 확인하면, 조준경을 어떻게 수정해야 할지 즉시 알 수 있다. |
| Line 593: | Line 88: |
| === Pitfall 1: "Working" Becomes "Done" === | === "Dirty Code Guilt" (더러운 코드에 대한 죄책감) === 일단 작동하게 만들 때 나오는 지저분한 코드에 괴로워하는 것. * 이것은 최종 결과물이 아니라 '탐색을 위한 도구'다. [[BabySteps]]와 [[GreenRefuge]]가 있다면 언제든 깨끗하게 고칠 수 있다. |
| Line 595: | Line 92: |
| 가장 위험한 함정: 작동하면 끝이라고 생각하는 것. {{{ # ❌ "작동하니까 됐어!" def process_payment(amount): # 작동하지만... 에러 처리 없음, 로깅 없음, 트랜잭션 없음 api.charge(card, amount) return True }}} '''해결:''' Kent Beck의 3단계를 기억하라: Work → Right → Fast. "Work"는 시작일 뿐이다. === Pitfall 2: "Simple" First Version is Still Complex === "단순한" 첫 버전이 여전히 너무 복잡하다. {{{ # ❌ 이게 "단순"? class SimpleAuthenticator: def __init__(self, db, cache, logger, config): self.db = db self.cache = cache self.logger = logger self.config = config def authenticate(self, credentials): # 100줄의 "단순한" 코드... }}} '''해결:''' 정말 최소한만. '''[[TinyExperiment]]''' - 가장 작은 것부터. === Pitfall 3: No Tests Along the Way === 각 단계마다 테스트 없이 진행한다. '''문제:''' "작동"을 검증할 방법이 없다. 다음 단계에서 이전 단계가 깨질 수 있다. '''해결:''' 각 단계마다 테스트 추가. '''[[DesignThroughTest]]'''. {{{ # Phase 1: Work + Test def login(username, password): return username == "admin" and password == "secret" def test_login(): assert login("admin", "secret") == True assert login("admin", "wrong") == False # Phase 2: Improve + Update Test def login(username, password): user = db.get_user(username) return user and user.password == password def test_login(): user = User.create(username="admin", password="secret") assert login("admin", "secret") == True assert login("admin", "wrong") == False }}} === Pitfall 4: Prototype Becomes Production === Fred Brooks의 경고를 무시하고, 프로토타입을 그대로 프로덕션에 넣는다. '''문제:''' * 프로토타입은 학습용이다 * 에러 처리, 보안, 성능을 고려하지 않았다 * 기술 부채가 쌓인다 '''해결:''' {{{ # 프로토타입을 명확히 표시 def login_PROTOTYPE(username, password): """ PROTOTYPE: For testing only TODO: Add error handling, hashing, sessions """ return username == "admin" and password == "secret" # 배운 것을 기반으로 새로 작성 def login(username, password): # 프로토타입에서 배운 것을 적용 # 하지만 제대로 구현 ... }}} === Pitfall 5: Skipping the "Right" Phase === Work → Fast로 바로 점프한다. "Right" 단계를 건너뛴다. {{{ # ❌ Work에서 Fast로 직행 def ugly_but_working_code(): # 작동하지만 난장판 pass @cache def ugly_but_working_code(): # 여전히 난장판, 이제 캐싱까지 pass }}} '''해결:''' 최적화 전에 정리하라. Clean code first, then optimize. === Pitfall 6: Wrong "Working" Definition === "작동"의 정의가 잘못되었다. {{{ # ❌ 이게 "작동"? def api_call(endpoint): # 성공 케이스만 작동 return requests.get(endpoint).json() # 에러나면? 네트워크 끊기면? 타임아웃이면? }}} '''해결:''' "작동"은 핵심 flow가 end-to-end로 실행되는 것. 완벽할 필요는 없지만, 실제 사용 가능해야 한다. |
=== Stopping at "Working" (작동만 한다고 멈추는 것) === 작동하게 만든 뒤 '올바르게' 만드는 과정을 생략하는 것. * 그것은 성장이 아니라 부채를 쌓는 일이다. 반드시 리팩토링 과정을 거쳐야 한다. |
| Line 715: | Line 99: |
| '''[[TinyExperiment]]''' - WorkingFirst를 위해 작은 실험들을 수행한다. 가장 작은 것이 작동하는지 확인하라. ''도구'' '''[[BabySteps]]''' - 작동 상태에서 다음 작동 상태로 작은 단계들로 이동한다. ''실행 방법'' '''[[OrganicGrowth]]''' - 작동하는 핵심에서 시스템을 유기적으로 성장시킨다. ''성장 방식'' '''[[TwoWorlds]]''' - 문제 공간을 이해하기 위해 해결 공간에서 작동하는 것을 만들어 실험한다. ''탐색 도구'' '''[[GreenRefuge]]''' - 작동하는 상태가 안전한 피난처다. 막히면 돌아갈 곳. ''안전망'' '''[[DetectiveWork]]''' - 작동하는 것을 만들면서 진짜 문제를 발견한다. ''발견 과정'' '''[[DesignThroughTest]]''' - 테스트로 "작동"을 정의하고 검증한다. ''검증 방법'' '''[[TightLoop]]''' - 빠르게 작동 상태에 도달해서 빠른 피드백을 받는다. ''피드백 루프'' '''[[ArtisanMind]]''' - 작동하는 실체를 만드는 것은 장인의 가장 기본이 되는 태도다. ''정신'' '''[[The95PercentRule]]''' - 일단 95%라도 작동하게 만들어라. 완벽을 기하다가 0%에 머물지 마라. ''순서'' '''AtomicCommit''' - 각 작동 상태를 커밋한다. 되돌릴 수 있게. ''버전 관리'' === Contrasts With (대조) === '''Big Design Up Front''' - 실행 전 완벽한 설계 시도. WorkingFirst는 반대다. '''Waterfall''' - 설계 → 구현 → 테스트의 순차적 진행. WorkingFirst는 반복적이다. '''Analysis Paralysis''' - 결정하지 못하고 분석만 계속. WorkingFirst는 빠른 실행이다. === Prevents (예방) === '''YAGNI violations''' - 작동하는 것을 만들면서 실제 필요를 발견한다. '''Over-engineering''' - 실제 복잡성을 안 후에 설계한다. '''Premature Optimization''' - 작동 후 측정, 그다음 최적화. '''Analysis Paralysis''' - 빠른 실행이 결정을 만든다. === Enables (가능하게 함) === '''DataAsFoundation''' - 작동하는 것을 만들면서 올바른 데이터 구조를 발견한다. '''LanguageBuilding''' - 작동하는 코드에서 패턴을 보고 언어를 추출한다. '''StrongCenter''' - 작동하는 핵심을 먼저 만들고 그것이 중심이 된다. |
* '''[[TinyExperiment]]''' - WorkingFirst를 실천하는 가장 구체적인 행동이 작은 실험입니다. ''수단'' * '''[[BabySteps]]''' - 일단 작동시킨 후에는 작은 보폭으로 안전하게 전진하십시오. ''과정'' * '''[[OrganicGrowth]]''' - 작동하는 작은 핵심이 유기적 성장의 씨앗이 됩니다. ''출발점'' * '''[[RoughWholeFirst]]''' - 엉성하게나마 전체가 돌아가게 만드는 것이 WorkingFirst의 지향점입니다. ''관점'' |
| Line 766: | Line 106: |
| WorkingFirst를 효과적으로 사용하고 있다는 신호: '''빠른 초기 피드백''' - 프로젝트 시작 후 하루 이내에 "뭔가 작동하는 것"을 볼 수 있다. '''자신감 증가''' - "이것은 가능하다"는 확신이 일찍 생긴다. 두려움이 줄어든다. '''진짜 문제 발견''' - 설계 단계에서 예상하지 못한 문제를 실행을 통해 발견한다. '''덜 복잡한 최종 솔루션''' - 처음 계획했던 것보다 훨씬 단순한 솔루션으로 끝난다. '''빠른 iteration''' - 작동 → 피드백 → 개선의 사이클이 하루 단위로 돌아간다. '''낮은 스트레스''' - "막혔다"는 느낌이 적다. 항상 작동하는 상태에서 시작하기 때문에. '''더 나은 설계''' - 역설적으로, 설계를 먼저 하지 않았는데 더 나은 설계가 나온다. '''팀 신뢰''' - 매일 데모할 수 있는 것이 있어서 팀과 stakeholder가 진행을 본다. == For Teachers and Mentors == 주니어가 바로 완벽한 설계를 하려 할 때: '''멈추게 하라:''' "잠깐, 먼저 가장 단순한 버전이 작동하는지 확인해보자. 10분만 투자해서." '''작은 목표를 제시하라:''' * "지금은 하드코딩해도 돼. 일단 화면에 뭔가 나오게 해보자." * "에러 처리는 나중에. 먼저 happy path만 작동하게." * "최적화는 측정 후에. 먼저 결과가 맞는지 확인하자." '''3단계를 가르쳐라:''' {{{ 1. Make it work - "작동만 하면 돼" 2. Make it right - "이제 깔끔하게 만들자" 3. Make it fast - "성능이 문제면 최적화하자" }}} '''성공 경험을 만들어라:''' "봐, 30분 만에 작동하는 걸 만들었잖아! 이제 여기서부터 개선하면 돼." '''실패의 안전성을 강조하라:''' "이건 프로토타입이야. 나중에 버려도 돼. 지금은 배우는 게 목표야." '''점진적 개선을 시연하라:''' {{{ # "이렇게 단계적으로 발전시키는 거야" V1: 하드코딩 (5분) V2: 변수로 추출 (5분) V3: 함수로 (10분) V4: 에러 처리 (10분) V5: 테스트 추가 (15분) V6: 리팩토링 (15분) 총 1시간, 6개의 작동하는 버전 }}} |
* 프로젝트 시작 30분 만에 무언가 실행되는 것을 동료에게 보여줄 수 있다. * "아, 해보니까 이게 문제네요"라는 실질적인 통찰이 쏟아진다. * 설계에 대한 불필요한 논쟁이 줄어들고, 실행 결과에 기반한 대화가 늘어난다. * 막연한 두려움이 사라지고 개발 리듬이 경쾌해진다. |
| Line 826: | Line 113: |
| Kent Beck이 말했다: '''"Make it work, make it right, make it fast."''' | '''이론은 당신을 생각하게 만들지만, 실행은 당신을 눈뜨게 만든다.''' |
| Line 828: | Line 115: |
| Donald Knuth가 경고했다: '''"Premature optimization is the root of all evil."''' Fred Brooks가 통찰했다: '''"Plan to throw one away; you will, anyhow."''' Chris Argyris가 가르쳤다: '''의사결정은 과정이다. 탐색하고 실험하라.''' George Polya가 보여주었다: '''문제를 이해하는 것은 해결하면서 온다.''' 이 모든 지혜는 같은 진리를 가리킨다: {{{ 우리는 만들어보기 전에는 알 수 없다. 작동하는 것을 만드는 것은 이해의 도구다. 탐색이 설계보다 먼저다. }}} === The Epistemology of Making === 프로그래밍에서 지식은 '''실천을 통해''' 온다: '''이론적 지식:''' "OAuth2는 인증 프로토콜이다" * 책에서 배울 수 있다 * 이해할 수 있다 * 하지만 '''적용'''할 수는 없다 '''실천적 지식:''' "이 상황에서 OAuth2를 어떻게 구현하는가" * 만들어봐야 안다 * 시도해봐야 이해한다 * '''경험'''을 통해서만 얻을 수 있다 Michael Polanyi의 말: '''"We know more than we can tell."''' 암묵적 지식(tacit knowledge)은 실천에서 온다. === The Paradox of Perfection === '''역설:''' 완벽을 추구하면 완성하지 못한다. 완성을 추구하면 완벽에 도달할 수 있다. 처음부터 완벽을 만들려는 시도는: * 실제 문제를 이해하지 못한 상태에서의 최적화 * 필요하지 않을 기능에 대한 과도한 설계 * 실제 제약을 모르는 상태에서의 추상화 작동하는 것부터 만들면: * 실제 문제에 기반한 설계 * 필요한 것만 구현 * 실제 제약을 고려한 추상화 '''결과:''' 역설적으로 더 나은 설계. === The Psychology of Momentum === Newton의 제1법칙은 프로그래밍에도 적용된다: * '''정지한 물체는 정지해 있으려 한다''' - 시작하기가 가장 어렵다 * '''움직이는 물체는 움직이려 한다''' - 일단 시작하면 계속하기 쉽다 WorkingFirst는 '''momentum을 만든다''': 1. 작은 작동 → 성취감 2. 성취감 → 자신감 3. 자신감 → 다음 단계 시도 4. 다음 단계 → 또 작동 5. 반복 → momentum '''두려움을 극복하는 방법:''' 작은 작동하는 것을 만드는 것. === The Path to Mastery === 시니어와 주니어의 진짜 차이는 '''지식의 양'''이 아니다. '''접근 방식'''이다: '''주니어:''' 완벽한 설계 → 구현 → 테스트 → 디버그 → 리팩토링 * 선형적 * 각 단계가 길다 * 막히면 멈춘다 '''시니어:''' 작동 → 개선 → 작동 → 개선 → 작동 → ... * 순환적 * 각 단계가 짧다 * 항상 진행 중 WorkingFirst는 '''시니어의 사고방식'''이다. == Summary == WorkingFirst는 '''탐색의 철학'''이다: '''핵심 원칙:''' * '''가능성 먼저, 완벽은 나중에''' - ASAP로 working state에 도달 * '''3단계 접근''' - Work → Right → Fast, 순서가 중요 * '''작동은 학습 도구''' - 만들면서 이해한다 * '''프로토타입은 버릴 것''' - 배우기 위해 만든다 * '''Snowballing''' - 작은 작동에서 유기적으로 성장 '''왜 작동하는가:''' * 만들어보기 전에는 알 수 없다 * 문제는 해결하면서 드러난다 * 해결책은 진화한다 * Momentum을 만든다 '''주니어 vs 시니어:''' * 주니어: 실행 없이 설계한다 → 분석 마비, 잘못된 설계 * 시니어: 작은 작동하는 것을 만들고 snowball한다 → 빠른 학습, 더 나은 설계 '''궁극의 통찰:''' {{{ 작동하는 것을 만들면: - 가능하다는 것을 안다 - 어떻게 만드는지 안다 - 진짜 문제가 무엇인지 안다 - 초기 프로토타입을 버리고 올바르게 다시 만들 수도 있다 하지만 그때는 머릿속에 어떻게 작동하게 만드는지 알고 있다. }}} '''WorkingFirst는 두려움을 극복하고, momentum을 만들고, 올바른 문제를 해결하게 하는 방법이다.''' 올바른 방법은 여러 가지다. WorkingFirst는 그 중 하나에 빠르게 도달하게 돕고, 거기서부터 올바른 방향으로 진화하게 한다. '''"The best way to get a project done faster is to start sooner." - Jim Highsmith''' '''지금 시작하라. 작게. 작동하게. 그다음 개선하라.''' |
완벽한 설계도는 책상 위에만 존재합니다. 진짜 진실은 실행되는 런타임 속에 숨어 있습니다. 거창한 건축을 꿈꾸기 전에 먼저 작은 말뚝 하나를 박으십시오. 그 말뚝이 당신의 설계를 현실로 이끌어줄 것입니다. |
| Line 949: | Line 118: |
| CategoryPatternLanguage CategoryProgramming CategoryTDD CategoryLearning CategoryWorkflow | CategoryPatternLanguage CategoryProgramming CategoryAgile CategoryMindset |
WorkingFirst
주니어 개발자들을 위한 패턴 언어 - 우아함보다 먼저 작동하는 실체를 만들어 진실을 드러내는 법
Contents
The Story 1: The Perfectionist vs. The Snowballer (Programming)
두 명의 개발자, 민수와 하나가 '새로운 알림 엔진'을 만들고 있다.
민수의 방식 (The Perfectionist): 민수는 처음부터 확장 가능하고 우아한 설계를 꿈꾼다. 그는 인터페이스를 설계하고, 추상 클래스를 만들고, 디자인 패턴을 적용하느라 사흘을 보냈다. 하지만 사흘째 되는 날에도 알림은 단 한 건도 발송되지 않았다. 설계도만 거창할 뿐, 실제로 무엇이 문제일지는 여전히 미지수였다.
하나의 방식 (The Snowballer): 하나는 다르게 시작했다. 일단 print("알림 발송!")이라는 한 줄의 코드로 시작했다. 그 다음, 하드코딩된 이메일 주소로 진짜 메일이 가는지 확인했다. "일단 돌아가네. 이제 여기서부터 하나씩 붙여보자." 하나는 한 시간 만에 '작동하는 실체'를 만들었다. 그 실체는 하나에게 어디가 병목인지, 어떤 예외 처리가 진짜 필요한지 알려주었다. 작은 눈덩이(Snowball)는 구를수록 커져서 저녁 무렵엔 견고한 엔진이 되었다.
The Story 2: The Signature Dish (Ordinary Life)
민수와 하나는 친구들을 초대해 저녁 식사를 대접하기로 했다. 메뉴는 한 번도 안 해본 복잡한 '프랑스 요리'다.
민수의 요리 (The Ideal Recipe): 민수는 요리책을 수십 번 읽으며 완벽한 타이밍과 도구를 준비했다. 하지만 정작 요리를 시작하자 생각지 못한 변수가 발생했다. 화력이 책과 달랐고, 재료의 수분기도 달랐다. 민수는 당황했고 저녁 식사는 엉망이 되었다.
하나의 요리 (The Prototype Meal): 하나는 전날 밤, 아주 적은 양의 재료로 핵심 소스만 미리 만들어보았다. "아, 이 소스는 생각보다 빨리 타는구나. 설탕을 좀 줄여야겠어." 하나는 아주 간단한 버전으로 '맛'을 먼저 확인했다. 이 작은 실험 덕분에 하나는 실제 요리에서 불 조절을 어떻게 해야 할지 정확히 알게 되었고, 친구들에게 완벽한 식사를 대접할 수 있었다. 기예는 이론이 아니라 '작동하는 경험'에서 시작됨을 하나는 알고 있었다.
Context
새로운 기능을 구현하거나 복잡한 문제를 해결해야 한다. 머릿속에는 거창한 설계가 떠오르지만, 어디서부터 손을 대야 할지 막막한 상황이다.
일상적인 상황:
- 완벽한 설계를 하려다 시작도 못 하고 시간을 보낸다.
- "나중에 고생 안 하려면 처음부터 제대로 짜야 해"라는 압박을 느낀다.
- 코드를 짜는 시간보다 화이트보드 앞에서 고민하는 시간이 훨씬 길다.
- 구현 도중 예상치 못한 기술적 난관에 부딪혀 전체 설계를 뒤엎는다.
Problem
실행 없는 완벽한 설계는 가설일 뿐이며, 실제 구현 단계에서 나타나는 수많은 변수를 예측하지 못한다.
피드백 지연: 실제로 돌아가는 것을 보기 전까지는 내 생각이 맞는지 틀린지 알 수 없다.
심리적 부담: 완벽해야 한다는 생각은 행동을 느리게 만들고 창의성을 억누른다.
매몰 비용: 거창하게 설계한 뒤에는 나중에 결함을 발견해도 설계를 바꾸기가 아까워진다.
Solution
우아하거나 효율적으로 만들기 전에, 가장 단순한 형태로 "먼저 작동하게" 만들어라.
작동하는 해결책은 문제의 진정한 본질을 드러내고, 당신이 가야 할 다음 길을 안내한다.
Principle 1: Make it Work, Then Make it Right
Kent Beck의 조언을 기억하라.
- 작동하게 만들어라 (Make it Work).
- 올바르게 만들어라 (Make it Right - 리팩토링).
- 빠르게 만들어라 (Make it Fast - 최적화).
대부분의 주니어는 1단계를 건너뛰고 2단계나 3단계부터 하려다 길을 잃는다.
Principle 2: Use Familiar Tools First (익숙한 도구로 뚫기)
새로운 기술 스택이나 과제 조건에 얽매여 전진을 멈추지 마라.
Tailwind가 익숙하지 않다면 Custom CSS로, HTMX가 헷갈린다면 jQuery나 순수 JavaScript로 일단 동작하게 만들어라.
- 통 페이지 새로고침(Web 1.0 방식)으로라도 기능을 먼저 완성한 뒤, 시간이 남으면 그때 요구된 기술 스택으로 리팩토링하라.
Principle 3: Snowballing from a Small Core
작고 초라한 시작을 부끄러워하지 마라.
Hello World가 찍히는 순간 당신은 불확실성의 50%를 제거한 것이다.
- 작은 성공의 경험은 눈덩이처럼 불어나 당신에게 설계를 완성할 자신감과 데이터를 제공한다.
Real Examples
Example 1: Sketching first
화가들은 눈동자의 디테일을 그리기 전에 전체 구도를 연필로 슥쓱 그린다. 엉성한 선들이 모여 전체적인 균형을 잡은 뒤에야 세밀한 묘사에 들어간다.
Example 2: Tracer Bullets
어두운 밤, 목표를 맞추기 위해 계산기만 두드리는 대신 '예광탄'을 쏜다. 탄환이 날아가는 궤적을 눈으로 확인하면, 조준경을 어떻게 수정해야 할지 즉시 알 수 있다.
Common Pitfalls
"Dirty Code Guilt" (더러운 코드에 대한 죄책감)
일단 작동하게 만들 때 나오는 지저분한 코드에 괴로워하는 것.
이것은 최종 결과물이 아니라 '탐색을 위한 도구'다. BabySteps와 GreenRefuge가 있다면 언제든 깨끗하게 고칠 수 있다.
Stopping at "Working" (작동만 한다고 멈추는 것)
작동하게 만든 뒤 '올바르게' 만드는 과정을 생략하는 것.
- 그것은 성장이 아니라 부채를 쌓는 일이다. 반드시 리팩토링 과정을 거쳐야 한다.
Connection to Other Patterns
TinyExperiment - WorkingFirst를 실천하는 가장 구체적인 행동이 작은 실험입니다. 수단
BabySteps - 일단 작동시킨 후에는 작은 보폭으로 안전하게 전진하십시오. 과정
OrganicGrowth - 작동하는 작은 핵심이 유기적 성장의 씨앗이 됩니다. 출발점
RoughWholeFirst - 엉성하게나마 전체가 돌아가게 만드는 것이 WorkingFirst의 지향점입니다. 관점
Signs of Success
- 프로젝트 시작 30분 만에 무언가 실행되는 것을 동료에게 보여줄 수 있다.
- "아, 해보니까 이게 문제네요"라는 실질적인 통찰이 쏟아진다.
- 설계에 대한 불필요한 논쟁이 줄어들고, 실행 결과에 기반한 대화가 늘어난다.
- 막연한 두려움이 사라지고 개발 리듬이 경쾌해진다.
The Ultimate Insight
이론은 당신을 생각하게 만들지만, 실행은 당신을 눈뜨게 만든다.
완벽한 설계도는 책상 위에만 존재합니다. 진짜 진실은 실행되는 런타임 속에 숨어 있습니다. 거창한 건축을 꿈꾸기 전에 먼저 작은 말뚝 하나를 박으십시오. 그 말뚝이 당신의 설계를 현실로 이끌어줄 것입니다.
CategoryPatternLanguage CategoryProgramming CategoryAgile CategoryMindset
