DataAsFoundation

주니어 개발자들을 위한 패턴 언어 - 복잡한 로직을 단순한 데이터 구조로 변환하여 명확성을 달성하는 방법

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 응답을 처리하거나, 비즈니스 규칙을 적용하거나, 다양한 조건에 따라 다르게 동작하는 코드를 작성한다. 코드가 작동하지만, 읽기 어렵고, 수정하기 어렵고, 버그가 숨어있기 쉽다.

일상적인 상황:

당신의 코드는 "어떻게 하는지" (how)로 가득하지만, "무엇을 하는지" (what)는 보이지 않는다.

Problem

로직에 집중하면, 복잡성이 로직에 쌓인다. 주니어 개발자들은 절차적 사고에 익숙하다: "이것을 하고, 그다음 저것을 하고, 그러면..." 하지만 이런 접근은 여러 문제를 만든다:

읽을 수 없는 Loop

const result = []
for (const item of items) {
  // 10-20 lines of complex logic
  // What is this loop actually doing?
}

루프 안의 로직을 다 읽어야만 "아, 이게 필터링이었구나" 또는 "이게 변환이었구나"를 알 수 있다. 의도가 구현 속에 묻혀있다.

변환의 부재

외부 API의 응답을 받아서 내부 로직에서 사용할 때, 많은 주니어들은:

  1. 빈 변수를 만들고
  2. for loop을 돌면서
  3. 하나씩 요소를 추가한다

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)

이제 코드 독자는:

  1. 외부 데이터가 어떻게 생겼는지 안다 (ApiUser)

  2. 내부 데이터가 어떻게 생겼는지 안다 (User)
  3. 변환이 어떻게 일어나는지 안다 (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)
}

개선점:

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

Signs of Success

데이터 중심 사고를 효과적으로 사용하고 있다는 신호:

For Teachers and Mentors

주니어들에게 데이터 중심 사고를 가르칠 때:

질문하라:

리팩토링하라:

"이 for loop을 보자.
실제로 이것은 필터링이야 - 조건에 맞는 것만 선택하고 있어.
그럼 filter를 사용하면 어떨까?"

대조하라: 같은 기능을 두 가지 방식으로 보여주라:

어느 것이 더 명확한지 물어보라.

The Ultimate Insight

프로그래밍의 본질:

데이터 + 변환 = 프로그램

좋은 프로그램:

명확한 데이터 + 명시적 변환 = 이해하기 쉬운 프로그램

LLM 시대의 프로그래밍:

의미론적 데이터 + 명확한 구조 = 인간과 AI가 협업하기 쉬운 프로그램

로직이 복잡해 보이면, 데이터를 보라. 올바른 데이터 구조는 로직을 단순하게 만든다. 그리고 단순한 로직은 버그가 적고, 이해하기 쉽고, 수정하기 쉽다.

이것이 DataAsFoundation이다. 모든 깨끗한 코드의 기반은 잘 정리된 데이터에서 시작된다.


CategoryPatternLanguage CategoryProgramming CategoryDesign CategoryDataStructure CategoryFunctionalProgramming