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
  }

  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)

이제 코드 독자는 외부 데이터가 어떻게 생겼는지, 내부 데이터가 어떻게 생겼는지, 그리고 변환이 어떻게 일어나는지 명확히 알 수 있다. 같은 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
  }
]

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: 'save', repository: userRepo }
  ]
}

async function executeWorkflow(data, workflow) {
  let result = data
  for (const step of workflow.steps) {
    result = await executeStep(step, result)
  }
  return result
}

비즈니스 로직은 설정 데이터로 표현된다. 코드는 그 데이터를 해석하는 엔진일 뿐이다.

The LLM Era: Representation Matters More Than Ever

LLM 시대에, representation의 중요성은 더욱 커졌다.

Code as Communication

과거에는 코드가 컴퓨터를 위한 명령이었지만, 현재 코드는 인간, AI, 그리고 컴퓨터 사이의 대화이다. 잘 구조화된 데이터는 LLM이 이해하고 생성하며 수정하기 쉽다.

// LLM이 이해하기 쉬운 코드
const activeUsers = users.filter(isActive)
const displayNames = activeUsers.map(toDisplayName)

두 번째 코드는 의미론적(semantic)으로 명확하다. LLM은 즉시 이해하고 정확하게 수정할 수 있다.

Language Beyond Code: Data as Universal Representation

LLM은 Large "Language" Model이다. 여기서 "언어"는 구조화된 데이터로 표현되는 도메인 지식을 포함한다. 90년대 말 XML(MathML, MusicXML 등)이 시도했던 것처럼, 도메인 지식을 구조화된 데이터로 표현하는 언어를 만드는 것이 핵심이다.

핵심 통찰: 데이터 구조를 설계하는 것은 곧 도메인 언어를 설계하는 것이다. 그리고 잘 설계된 도메인 언어는 인간과 LLM, 도메인 전문가 사이의 소통을 원활하게 한다.

Practical Examples

Example 1: API Response Transformation

Data-Driven Approach:

// 1. Define representations
type ApiUser = { user_id: string, first_name: string, status: string }
type User = { id: string, name: string, active: boolean }

// 2. Explicit transformations
const toUser = (apiUser: ApiUser): User => ({
  id: apiUser.user_id,
  name: apiUser.first_name,
  active: apiUser.status === 'active'
})

// 3. Usage
const users = apiResponse.data.map(toUser)

Example 2: Business Rules as Data

규칙을 리스트로 관리하면 새로운 규칙을 추가할 때 코드를 건드리지 않고 데이터만 추가하면 된다. 이는 테스트와 유지보수성을 극대화한다.

Common Pitfalls

"But Loops are Faster!"

성능 최적화는 나중에 하라. 먼저 명확성이 우선이다. 명확한 코드는 버그가 적고 나중에 최적화하기도 훨씬 쉽다.

"But Data Structures are Hard!"

데이터 구조를 설계하는 시간은 나중에 디버깅하는 시간을 획기적으로 절약한다.

Connection to Other Patterns

Signs of Success

The Ultimate Insight

프로그래밍의 본질은 데이터 + 변환 = 프로그램이다.

로직이 복잡해 보이면 데이터를 보라. 올바른 데이터 구조는 로직을 단순하게 만든다. 이것이 모든 깨끗한 코드의 시작이다.


CategoryPatternLanguage CategoryProgramming CategoryDesign CategoryDataStructure CategoryFunctionalProgramming

DataAsFoundation (last edited 2025-12-30 08:23:37 by 정수)