DataAsFoundation
주니어 개발자들을 위한 패턴 언어 - 복잡한 로직을 단순한 데이터 구조로 변환하여 명확성을 달성하는 방법
Contents
The Story: The Tangled Loop
한 주니어 개발자가 API에서 받은 사용자 목록을 화면에 표시하는 코드를 작성했다. 코드는 작동했지만, 시니어 개발자가 리뷰하면서 멈췄다.
"이 for loop 안에서 무슨 일이 일어나고 있지?" 시니어가 물었다.
주니어는 자신있게 대답했다. "API에서 받은 데이터를 우리 형식으로 바꾸고 있어요. 먼저 빈 배열을 만들고, 각 사용자를 순회하면서, 이름이 있으면 포맷을 바꾸고, 권한을 체크해서, 활성 사용자만 추가하고..."
시니어가 고개를 끄덕였다. "그런데 이 loop을 읽는 사람이 그걸 알 수 있을까? 10줄의 코드를 다 읽어야만 '아, 이게 활성 사용자 필터링이었구나'를 알 수 있어."
"그럼 주석을 달까요?" 주니어가 제안했다.
"주석 말고," 시니어가 미소 지으며 말했다. "데이터가 말하게 하자."
시니어는 코드를 다시 썼다:
// Before (Procedural)
const result = []
for (const user of apiUsers) {
if (user.status === 'active') {
const formatted = {
name: user.firstName + ' ' + user.lastName,
role: user.permissions.role
}
result.push(formatted)
}
}
// After (Data-Driven)
const activeUsers = apiUsers.filter(isActive)
const displayUsers = activeUsers.map(toDisplayFormat)
// Data transformations are explicit
const isActive = user => user.status === 'active'
const toDisplayFormat = user => ({
name: `${user.firstName} ${user.lastName}`,
role: user.permissions.role
})"보이지?" 시니어가 설명했다. "로직이 복잡해 보일 때, 사실은 데이터가 복잡한 거야. 로직을 단순하게 만들려면, 먼저 데이터를 올바르게 구조화해야 해. 그러면 로직 은 저절로 단순해져."
Context
당신은 기능을 구현하고 있다. API 응답을 처리하거나, 비즈니스 규칙을 적용하거나, 다양한 조건에 따라 다르게 동작하는 코드를 작성한다. 코드가 작동하지만, 읽기 어렵고, 수정하기 어렵고, 버그가 숨어있기 쉽다.
일상적인 상황:
- 외부 API 응답을 내부 형식으로 변환해야 한다
- 비즈니스 규칙이 여러 if-else로 얽혀있다
- 같은 로직이 조금씩 다른 형태로 여러 곳에 반복된다
- 새로운 케이스를 추가할 때마다 코드가 더 복잡해진다
당신의 코드는 "어떻게 하는지" (how)로 가득하지만, "무엇을 하는지" (what)는 보이지 않는다.
Problem
로직에 집중하면, 복잡성이 로직에 쌓인다. 주니어 개발자들은 절차적 사고에 익숙하다: "이것을 하고, 그다음 저것을 하고, 그러면..." 하지만 이런 접근은 여러 문제를 만든다:
읽을 수 없는 Loop
const result = []
for (const item of items) {
// 10-20 lines of complex logic
// What is this loop actually doing?
}루프 안의 로직을 다 읽어야만 "아, 이게 필터링이었구나" 또는 "이게 변환이었구나"를 알 수 있다. 의도가 구현 속에 묻혀있다.
변환의 부재
외부 API의 응답을 받아서 내부 로직에서 사용할 때, 많은 주니어들은:
- 빈 변수를 만들고
- for loop을 돌면서
- 하나씩 요소를 추가한다
const users = []
for (const apiUser of apiResponse.data) {
users.push({
id: apiUser.userId,
name: apiUser.fullName,
active: apiUser.status === 'ACTIVE'
})
}문제: 이것은 데이터 변환(transformation)이지만, 코드는 그렇게 말하지 않는다. 독자는 loop을 읽어야만 "아, 변환이구나"를 알 수 있다.
같은 의미(semantic)를 가진 데이터가 다른 표현(representation)을 가질 때, 그 변환을 명시적으로 만들지 않으면, 코드의 의도가 숨겨진다.
로직의 폭발
비즈니스 규칙이 증가하면, if-else가 증가한다:
function calculatePrice(item, user) {
let price = item.basePrice
if (user.type === 'premium') {
price = price * 0.9
} else if (user.type === 'vip') {
price = price * 0.8
}
if (item.category === 'electronics') {
if (user.country === 'US') {
price = price * 1.1 // tax
}
}
if (isHoliday() && item.onSale) {
price = price * 0.95
}
// ... more rules ...
return price
}문제: 로직이 코드로 하드코딩되어 있다. 새로운 규칙을 추가하려면 코드를 수정해야 한다. 규칙들이 얽혀있어서 하나를 바꾸면 다른 것이 깨질 수 있다.
이 모든 복잡성의 근본 원인: 데이터를 데이터로 다루지 않고, 로직으로 다루고 있다.
Solution
로직을 단순하게 만들고 싶다면, 먼저 데이터를 올바르게 구조화하라. 많은 프로그래밍은 실제로 데이터를 변환하는 것이다. 이것을 명시적으로 만들면, 로직은 저절로 단순해진다.
Principle 1: Make Data Transformations Explicit
절차적 loop 대신, 데이터 변환을 명시적으로 표현하라.
Before:
const result = []
for (const item of items) {
if (item.active) {
result.push(transform(item))
}
}After:
const result = items .filter(isActive) .map(transform)
차이점: 두 번째는 "무엇을 하는지"가 즉시 보인다. 필터링하고, 변환한다. 의도가 구조에 드러난다.
Principle 2: Separate External and Internal Representations
외부 API의 데이터는 외부의 representation이다. 내부 로직에 맞는 representation으로 명시적으로 변환하라.
// External representation (from API)
type ApiUser = {
userId: string
fullName: string
status: 'ACTIVE' | 'INACTIVE'
permissions: { role: string }
}
// Internal representation (for our domain)
type User = {
id: string
name: string
active: boolean
role: string
}
// Explicit transformation
function fromApi(apiUser: ApiUser): User {
return {
id: apiUser.userId,
name: apiUser.fullName,
active: apiUser.status === 'ACTIVE',
role: apiUser.permissions.role
}
}
// Usage is clear
const users = apiResponse.data.map(fromApi)이제 코드 독자는:
외부 데이터가 어떻게 생겼는지 안다 (ApiUser)
- 내부 데이터가 어떻게 생겼는지 안다 (User)
- 변환이 어떻게 일어나는지 안다 (fromApi)
같은 semantic, 다른 representation - 그리고 그 변환이 명시적이다.
Principle 3: Encode Logic as Data
로직이 복잡해지면, 그것을 데이터로 표현할 수 있는지 물어보라.
Before (Logic as Code):
function calculatePrice(item, user) {
let price = item.basePrice
if (user.type === 'premium') {
price = price * 0.9
} else if (user.type === 'vip') {
price = price * 0.8
}
return price
}After (Logic as Data):
const discountRules = {
premium: 0.9,
vip: 0.8,
regular: 1.0
}
function calculatePrice(item, user) {
const discount = discountRules[user.type] || 1.0
return item.basePrice * discount
}더 나아가서, 규칙이 더 복잡해지면:
const pricingRules = [
{
condition: user => user.type === 'vip',
apply: price => price * 0.8
},
{
condition: user => user.points > 1000,
apply: price => price * 0.95
},
{
condition: user => user.firstPurchase,
apply: price => price * 0.9
}
]
function calculatePrice(item, user) {
return pricingRules
.filter(rule => rule.condition(user))
.reduce((price, rule) => rule.apply(price), item.basePrice)
}이제 새로운 규칙을 추가하는 것은 데이터를 추가하는 것이다. 코드를 수정하지 않는다.
Principle 4: Build Domain-Specific Languages
데이터 구조가 충분히 복잡해지면, 그것은 언어가 된다. 개념을 언어로, 즉 데이터로 표현하라.
// Configuration as DSL
const workflow = {
steps: [
{ type: 'validate', schema: userSchema },
{ type: 'transform', mapper: toInternalFormat },
{ type: 'enrich', fetcher: getUserDetails },
{ type: 'save', repository: userRepo }
]
}
// Engine executes the workflow
async function executeWorkflow(data, workflow) {
let result = data
for (const step of workflow.steps) {
result = await executeStep(step, result)
}
return result
}이제 비즈니스 로직은 설정 데이터로 표현된다. 코드는 그 데이터를 해석하는 엔진일 뿐이다.
Practical Examples
Example 1: API Response Transformation
Procedural Approach (Junior):
async function getUsers() {
const response = await fetch('/api/users')
const data = await response.json()
const result = []
for (const user of data.users) {
if (user.status === 'active') {
const formatted = {
id: user.user_id,
name: user.first_name + ' ' + user.last_name,
email: user.email_address,
admin: user.role === 'administrator'
}
result.push(formatted)
}
}
return result
}문제:
- 무엇을 하는지 코드를 다 읽어야 안다
- 외부 형식과 내부 형식이 섞여있다
- 재사용할 수 없다
Data-Driven Approach (Better):
// 1. Define representations
type ApiUser = {
user_id: string
first_name: string
last_name: string
email_address: string
status: string
role: string
}
type User = {
id: string
name: string
email: string
admin: boolean
}
// 2. Explicit transformations
const isActive = (user: ApiUser) => user.status === 'active'
const toUser = (apiUser: ApiUser): User => ({
id: apiUser.user_id,
name: `${apiUser.first_name} ${apiUser.last_name}`,
email: apiUser.email_address,
admin: apiUser.role === 'administrator'
})
// 3. Compose transformations
async function getUsers(): Promise<User[]> {
const response = await fetch('/api/users')
const data = await response.json()
return data.users
.filter(isActive)
.map(toUser)
}개선점:
- 의도가 즉시 보인다: filter then map
- 변환이 재사용 가능하다
- 타입이 명확하다
- LLM이 쉽게 이해하고 수정할 수 있다
Example 2: Business Rules as Data
Logic as Code (Hard to Maintain):
function processOrder(order, customer) {
let discount = 0
if (customer.type === 'vip') {
discount = 0.2
} else if (customer.type === 'premium') {
discount = 0.1
}
if (order.total > 1000) {
discount += 0.05
}
if (customer.firstOrder) {
discount += 0.1
}
// ... more rules ...
return order.total * (1 - discount)
}Logic as Data (Easy to Maintain):
// Rules are data
const discountRules = [
{
name: 'VIP customer discount',
condition: ({ customer }) => customer.type === 'vip',
discount: 0.2
},
{
name: 'Premium customer discount',
condition: ({ customer }) => customer.type === 'premium',
discount: 0.1
},
{
name: 'Large order discount',
condition: ({ order }) => order.total > 1000,
discount: 0.05
},
{
name: 'First order discount',
condition: ({ customer }) => customer.firstOrder,
discount: 0.1
}
]
// Engine applies rules
function calculateDiscount(order, customer) {
return discountRules
.filter(rule => rule.condition({ order, customer }))
.reduce((total, rule) => total + rule.discount, 0)
}
function processOrder(order, customer) {
const discount = calculateDiscount(order, customer)
return order.total * (1 - discount)
}
Example 3: Workflow as DSL
Imperative Code (Inflexible):
async function processUser(userData) {
// Step 1: Validate
if (!userData.email || !userData.name) {
throw new Error('Invalid user data')
}
// Step 2: Transform
const user = {
id: generateId(),
email: userData.email.toLowerCase(),
name: userData.name.trim(),
createdAt: new Date()
}
// Step 3: Enrich
const profile = await fetchUserProfile(user.email)
user.profile = profile
// Step 4: Save
await db.users.insert(user)
// Step 5: Notify
await sendWelcomeEmail(user.email)
return user
}Declarative DSL (Flexible):
// Define workflow as data
const userRegistrationWorkflow = {
name: 'User Registration',
steps: [
{
name: 'validate',
handler: validateUserData,
required: ['email', 'name']
},
{
name: 'transform',
handler: transformUserData
},
{
name: 'enrich',
handler: enrichWithProfile,
async: true
},
{
name: 'save',
handler: saveToDatabase,
async: true
},
{
name: 'notify',
handler: sendWelcomeEmail,
async: true,
optional: true // Don't fail if notification fails
}
]
}
// Generic workflow engine
async function executeWorkflow(workflow, data) {
let result = data
for (const step of workflow.steps) {
try {
result = step.async
? await step.handler(result)
: step.handler(result)
} catch (error) {
if (!step.optional) throw error
console.warn(`Optional step ${step.name} failed:`, error)
}
}
return result
}
// Usage
const user = await executeWorkflow(userRegistrationWorkflow, userData)
Connection to Other Patterns
LanguageBuilding - 데이터 구조가 충분히 풍부해지면 그것은 도메인을 표현하는 언어가 됩니다. 자연스러운 확장
ComplexityTaming - 복잡한 로직을 데이터 구조로 옮기는 것은 복잡성을 길들이는 가장 강력한 도구 중 하나입니다. 직접 적용
TwoWorlds - 외부와 내부의 데이터 표현을 분리하는 것은 문제 공간과 해결 공간을 나누는 것과 같습니다. 원리
NamesAsDesign - 데이터의 필드 이름을 짓는 행위 자체가 시스템의 기초 설계를 결정합니다. 기반
Signs of Success
데이터 중심 사고를 효과적으로 사용하고 있다는 신호:
읽기 쉽다 - 코드를 보면 무엇을 하는지 즉시 안다
변환이 명시적이다 - Before와 After가 명확하다
재사용 가능하다 - 같은 변환을 여러 곳에서 쓸 수 있다
확장 가능하다 - 새로운 케이스를 추가하기 쉽다
타입이 안내한다 - 타입을 보면 무엇을 해야 하는지 안다
LLM이 이해한다 - AI가 코드를 쉽게 이해하고 수정할 수 있다
For Teachers and Mentors
주니어들에게 데이터 중심 사고를 가르칠 때:
질문하라:
- "이 loop이 실제로 무엇을 하고 있지?"
- "이것을 데이터 변환으로 표현할 수 있을까?"
- "Before와 After 데이터는 무엇이지?"
- "이 로직을 데이터로 표현할 수 있을까?"
리팩토링하라:
"이 for loop을 보자. 실제로 이것은 필터링이야 - 조건에 맞는 것만 선택하고 있어. 그럼 filter를 사용하면 어떨까?"
대조하라: 같은 기능을 두 가지 방식으로 보여주라:
- 절차적 방식 (how)
- 데이터 변환 방식 (what)
어느 것이 더 명확한지 물어보라.
The Ultimate Insight
프로그래밍의 본질:
데이터 + 변환 = 프로그램
좋은 프로그램:
명확한 데이터 + 명시적 변환 = 이해하기 쉬운 프로그램
LLM 시대의 프로그래밍:
의미론적 데이터 + 명확한 구조 = 인간과 AI가 협업하기 쉬운 프로그램
로직이 복잡해 보이면, 데이터를 보라. 올바른 데이터 구조는 로직을 단순하게 만든다. 그리고 단순한 로직은 버그가 적고, 이해하기 쉽고, 수정하기 쉽다.
이것이 DataAsFoundation이다. 모든 깨끗한 코드의 기반은 잘 정리된 데이터에서 시작된다.
CategoryPatternLanguage CategoryProgramming CategoryDesign CategoryDataStructure CategoryFunctionalProgramming
