당신은 코드를 사람들보다 더 신뢰할 수 있게 해야 합니다

발행: (2025년 12월 11일 오전 06:35 GMT+9)
10 min read
원문: Dev.to

Source: Dev.to

사용자 행동의 현실

사용자는 문서를 읽지 않습니다. 오류 메시지를 꼼꼼히 검토하지도 않죠. 그들은 산만하고, 급하거나, 단순히 애플리케이션이 기대하는 바를 이해할 기술적 배경이 없을 수도 있습니다. 그리고 그것은 전혀 문제가 되지 않습니다—사용자가 여러분의 코드를 맞추는 것이 그들의 일은 아니니까요. 여러분의 코드는 사용자를 맞추는 역할을 해야 합니다.

실제 상황을 생각해 보세요:

  • 사용자가 보이지 않는 유니코드 문자를 포함한 PDF에서 텍스트를 복사함
  • 누군가의 인터넷 연결이 트랜잭션 도중 끊김
  • 비동기 작업이 아직 진행 중인 상태에서 사용자가 페이지를 떠남
  • 사용자가 폼을 제출한 뒤 “뒤로 가기”를 클릭함
  • 사용자가 세션을 3일 동안 그대로 두고 떠남
  • 애플리케이션의 여러 탭을 동시에 열어 둠

이러한 상황 중 어느 하나라도 애플리케이션을 깨뜨리거나 데이터를 손상시킬 수 있다면, 그것은 사용자 문제가 아니라 코드 신뢰성 문제입니다.

신뢰성이 실제 의미하는 바

신뢰할 수 있는 코드는 이상적인 조건에서만 동작하는 코드가 아닙니다. 신뢰할 수 있는 코드는 다음을 의미합니다:

  • 철저한 검증 – 입력을 절대 신뢰하지 않음, 심지어 자체 프론트엔드에서도
  • 우아한 실패 – 문제가 발생하면 기능을 축소하고, 크래시를 방지함
  • 에지 케이스 처리 – 99 % 상황은 중요하지만, 1 % 상황에 버그가 숨어 있음
  • 데이터 무결성 유지 – 사용자가 무엇을 하든 데이터는 일관성을 유지함
  • 자동 복구 – 가능하면 사용자 개입 없이 문제를 해결함

신뢰할 수 없는 코드의 비용

한 번은 금융 애플리케이션에서 레이스 컨디션 때문에 사용자가 전신 송금을 두 번 제출할 수 있는 버그가 있었습니다. 이 버그는 드물었는데, 200 ms 이내에 두 번 클릭해야 발생했습니다. “사용자는 그렇게 하지 않을 거야”라고 생각했죠. 하지만 수천 명의 사용자가 있으면 “드물다”는 매일 발생했습니다. 각 사고마다 수동 개입, 고객 서비스 전화, 그리고 잠재적인 재정적 책임이 따랐습니다.

수정은 두 시간 만에 구현되었습니다: 첫 클릭 시 버튼을 비활성화하고 멱등성 키(idempotency key)를 추가함. 처음부터 구현하지 않은 비용은? 수백 시간에 달하는 지원 시간과 손상된 고객 신뢰였습니다.

신뢰할 수 있는 코드를 만들기 위한 전략

1. 모든 곳에서 입력 검증

// Bad: Trust the frontend
function createUser(data) {
  return database.users.insert(data);
}

// Good: Validate everything
function createUser(data) {
  const validated = userSchema.parse(data); // Throws on invalid data
  const sanitized = sanitizeInput(validated);
  return database.users.insert(sanitized);
}

자체 UI라 하더라도 입력이 깨끗하다고 가정하지 마세요. 브라우저는 조작될 수 있고, API는 직접 호출될 수 있으며, 미들웨어가 실패할 수도 있습니다.

2. 모든 상태 변경에 멱등성 적용

# Bad: Can create duplicates
def submit_order(user_id, items):
    order = Order.create(user_id=user_id, items=items)
    return order

# Good: Uses idempotency key
def submit_order(user_id, items, idempotency_key):
    existing = Order.find_by_idempotency_key(idempotency_key)
    if existing:
        return existing

    order = Order.create(
        user_id=user_id,
        items=items,
        idempotency_key=idempotency_key
    )
    return order

상태를 변경하는 모든 작업은 멱등해야 합니다—여러 번 실행해도 한 번 실행한 것과 동일한 결과가 나와야 합니다.

3. 방어적인 데이터베이스 연산

-- Bad: Assumes the record exists
UPDATE users SET balance = balance - 100 WHERE id = ?;

-- Good: Ensures constraints are maintained
UPDATE users 
SET balance = balance - 100 
WHERE id = ? AND balance >= 100;

-- Then check affected rows to ensure it succeeded

4. 타임아웃과 서킷 브레이커

// Bad: Wait forever for a response
const response = await fetch(externalAPI);

// Good: Fail fast with timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch(externalAPI, { signal: controller.signal });
  return response;
} catch (error) {
  if (error.name === 'AbortError') {
    // Handle timeout gracefully
    return fallbackResponse();
  }
  throw error;
} finally {
  clearTimeout(timeout);
}

5. 레이트 리밋 및 자원 보호

from functools import wraps
from flask import request
import time

def rate_limit(max_calls, time_window):
    calls = {}

    def decorator(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            user_id = get_current_user_id()
            now = time.time()

            # Clean old entries
            calls[user_id] = [t for t in calls.get(user_id, [])
                              if now - t = max_calls:
                return {"error": "Rate limit exceeded"}, 429

            calls[user_id].append(now)
            return f(*args, **kwargs)
        return wrapped
    return decorator

사용자는 실수로(또는 의도적으로) 엔드포인트를 과도하게 호출할 수 있습니다. 코드가 스스로를 보호해야 합니다.

사고 방식의 전환

신뢰할 수 있는 코드를 만들려면 “이건 작동해야 한다”는 사고에서 “이게 어떻게 실패할 수 있을까?”라는 사고로 전환해야 합니다. 다음과 같은 질문을 스스로에게 던져 보세요:

  • 이 작업이 100 ms가 아니라 10 초가 걸리면 어떻게 될까?
  • 이 함수가 null과 함께 호출되면 어떻게 될까?
  • 두 사용자가 정확히 같은 순간에 이 작업을 하면 어떻게 될까?
  • 네트워크가 중간에 끊어지면 어떻게 될까?
  • 외부 서비스가 다운되면 어떻게 될까?
  • 누군가 100 MB 문자열을 보내면 어떻게 될까?

신뢰성을 위한 테스트

단위 테스트는 훌륭하지만 신뢰성 문제를 거의 잡아내지 못합니다. 다음이 필요합니다:

  • Chaos testing – 프로세스를 무작위로 종료하고, 네트워크 장애를 시뮬레이션
  • Load testing – 압박 하에서 무엇이 깨지는지 확인
  • Fuzzing – 무작위 쓰레기 입력을 보내고 결과 확인
  • Concurrent testing – 여러 인스턴스를 동시에 실행
  • Time‑travel testing – 시스템 시계를 엣지 케이스로 설정해 테스트

보상

예, 신뢰할 수 있는 코드를 만들려면 초기 작업이 더 많이 필요합니다. 더 많은 검증 로직, 더 많은 오류 처리, 더 많은 방어적 체크를 작성하게 되죠. 하지만 그 보상은 엄청납니다:

  • 프로덕션 사고와 새벽 3시 호출 감소
  • 지원 부담 감소
  • 사용자 신뢰 증가
  • 유지보수 비용 절감
  • 밤에 더 편안한 잠

결론

사용자는 실수를 저지릅니다. 네트워크 문제, 브라우저 특이점, 타이밍 문제 등을 겪게 됩니다. 사용자는 여러분이 예상하지 못한 방식으로 애플리케이션을 사용할 것입니다. 이는 사용자의 버그가 아니라 인간을 위한 소프트웨어를 만드는 현실입니다.

문제는 사용자가 예상치 못한 행동을 할지 여부가 아니라, 그들이 그렇게 할 때 여러분의 코드가 우아하게 처리할지, 아니면 모든 것이 붕괴될지 입니다.

코드를 사용자보다 더 신뢰할 수 있게 만드세요. 미래의 여러분(그리고 온‑콜 로테이션 담당자)에게 큰 감사가 돌아올 것입니다.

Back to Blog

관련 글

더 보기 »