• About

우리는 왜 어떤 코드를 읽기 쉽다고 느낄까

개발자의 직관과 코드 가독성, 그리고 뇌과학


우리는 왜 어떤 코드를 읽기 쉽다고 느낄까

이전에 좋은 코드란 무엇일까? - 가독성이란 허상에 대하여라는 글에서, “좋은 코드 = 가독성이 좋은 코드”라는 공식이 얼마나 주관적이고 맥락 의존적인지에 대해 이야기한 적이 있다.

물론 사람마다 가독성이 좋다고 판단하는 결과는 주관적이라고 볼 수 있다. 하지만 가독성이 좋은 코드라는 것이 어떤 원리로 동작하는지, 그 감각은 대체 어디서 오는 것인지를 추적해보다보면 약간은 힌트를 얻을 수 있다.

그래서 이번 포스팅에서는 바로 그 이야기를 해보려 한다. 인간이 코드를 이해하는 방식, 그리고 어떤 형태의 정보가 이해하기 쉽다고 느끼는지에 대한 이야기이다.

코드를 읽을 때 뇌에서는 무슨 일이 일어날까

MIT에서 2020년에 진행한 기능적 MRI 연구에서 프로그래머들이 코드를 읽을 때 뇌의 어떤 영역이 활성화되는지를 관찰했는데, 재밌는 결과가 나왔다.

연구에 따르면 코드를 읽을 때는 언어 네트워크가 아니라 논리 추론과 복잡한 인지 작업을 담당하는 다중 수요 네트워크가 주로 활성화되었다고 한다. 언어 영역의 관여가 완전히 없었던 것은 아니지만, 다중 수요 네트워크의 반응이 지배적이었다.

쉽게 말해서, 우리 뇌는 코드를 읽을 때 언어 처리보다는 논리적 추론 쪽에 더 크게 의존한다는 뜻이다.

이 연구에서 확인된 것은 코드 이해 시 작업 기억, 주의 집중, 언어 처리와 관련된 다섯 개의 뇌 영역이 뚜렷하게 활성화된다는 점이다. 동시에 디폴트 모드 네트워크(뇌가 쉬고 있을 때 활성화되는 영역)의 활동은 감소했다. 즉, 코드를 읽는 건 뇌 입장에서 꽤 비싼 작업인 셈이다.

여기까지는 그러려니 할 수 있겠지만, 더 흥미로운 사실은 바로 전문가와 초보자의 차이에서 나온다. 숙련된 개발자는 코드를 읽을 때 뇌의 전반적인 활성화 수준이 낮았다. 덜 열심히 한 게 아니라, 더 효율적으로 처리한 것이다. 반면 초보 개발자는 뇌의 넓은 영역이 활성화됐는데, 이는 거의 모든 것을 의식적으로 하나씩 처리하고 있다는 뜻이다.

초보자는 코드를 자연어 텍스트처럼 위에서 아래로 읽는 경향이 있었고, 전문가는 프로그램의 실행 흐름을 따라 읽었다. 같은 코드를 보고 있지만, 뇌가 처리하는 방식 자체가 다른 것이다.

작업 기억: 4개의 슬롯

왜 어떤 코드는 쉽게 읽히는데 어떤 코드는 머릿속에서 정리가 안 되는지를 이해하려면, 작업 기억이라는 개념을 알아야 한다.

1956년 인지심리학자 조지 밀러(George A. Miller)가 발표한 유명한 논문 “The Magical Number Seven, Plus or Minus Two”에서, 인간의 단기 기억 용량이 약 7±2개의 항목이라는 사실이 밝혀졌다. 이후 연구에서는 이 숫자가 더 줄어들어, Cowan(2001) 등의 견해에 따르면 작업 기억에 동시에 유지할 수 있는 청크는 약 3~4개 수준으로 수렴한다는 주장이 유력하다. 다만 이 수치는 과제의 종류, 숙련도, 정보의 양식에 따라 달라질 수 있다.

코드를 읽을 때 우리가 머릿속에 올려놓는 것들을 떠올려보자. 변수의 현재 값, 제어 흐름의 방향, 함수 호출 순서, 현재 스코프의 상태 등 다양한 것들이 작업 기억의 슬롯을 차지한다. 그리고 이 슬롯이 꽉 차면, 뇌는 새로운 정보를 받아들이기 어려워는데, 바로 이때 “이 코드 뭔가 복잡한데”라는 느낌이 찾아오는 것이다.

이걸 개발랭이들이 이해하기 쉬운 프로그래밍적인 비유로 바꿔보면, 작업 기억은 일종의 고정 크기 스택이라고 볼 수 있다.

실제 작업 기억은 스택보다 훨씬 복잡한 시스템이지만, “용량에 한계가 있고 초과하면 처리가 무너진다”는 특성을 직관적으로 이해하기에는 나쁘지 않은 비유다. 스택 사이즈가 약 4인데 이걸 초과하면 스택 오버플로우가 나는 것이다. 다만 실제로는 슬롯에 담기는 항목의 크기가 균일하지 않고, 항목 간 간섭도 발생하기 때문에, 숫자 자체보다는 “용량에 한계가 있다”는 사실이 핵심이다. 그리고 이 용량이 한계에 부딪히는 순간이 바로 “이 코드 읽기 어렵다”는 감각이 발생하는 순간이다.

한번 작업 기억을 빠르게 소진하는 코드와 그렇지 않은 코드를 비교해보며 살펴보자.

// 작업 기억 슬롯 4개를 빠르게 소진하는 코드
function processOrder(order: Order) {
  if (order.status === 'pending' && order.items.length > 0) {
    const discount = order.customer.tier === 'premium'
      ? order.items.reduce((sum, item) => sum + item.price, 0) * 0.1
      : order.coupon?.discount ?? 0;

    const tax = (order.total - discount) * (order.shipping.domestic ? 0.1 : 0);
    const finalPrice = order.total - discount + tax + order.shipping.cost;

    return { ...order, finalPrice, status: 'processed' };
  }
  return order;
}

이 함수는 틀린 코드는 아니지만, 읽는 사람의 작업 기억에 동시에 올려야 할 것이 너무 많다.

order.status의 조건, order.items의 존재 여부, customer.tier에 따른 할인 분기, 쿠폰의 널 체크, 세금 계산의 국내/해외 분기, 최종 가격 산출 등 이 모든 맥락을 3~4개의 슬롯에 동시에 담으려 하니 뇌가 비명을 지르는 것이다.

만약 이 맥락을 적절한 단위와 크기로 나눠줄 수 있다면, 어떤 동작을 이해하기 위해 필요한 작업 슬롯을 아낄 수 있다.

// 작업 기억 부담을 줄인 버전
function calculateDiscount(order: Order): number {
  if (order.customer.tier === 'premium') {
    const subtotal = order.items.reduce((sum, item) => sum + item.price, 0);
    return subtotal * 0.1;
  }
  return order.coupon?.discount ?? 0;
}

function calculateTax(amount: number, shipping: ShippingInfo): number {
  return shipping.domestic ? amount * 0.1 : 0;
}

function processOrder(order: Order) {
  if (order.status !== 'pending' || order.items.length === 0) {
    return order;
  }

  const discount = calculateDiscount(order);
  const tax = calculateTax(order.total - discount, order.shipping);
  const finalPrice = order.total - discount + tax + order.shipping.cost;

  return { ...order, finalPrice, status: 'processed' };
}

이 버전에서는 각 단계에서 머릿속에 올려야 할 것이 확연하게 줄어든다. 동작 자체는 동일하지만, 정보를 일정한 단위로 패키징함으로서 작업 슬롯에 들어가는 맥락을 제어해주는 것이다.

calculateDiscount를 읽을 때는 할인 로직에만 집중하면 되며, processOrder를 읽을 때는 각 계산의 세부 구현을 몰라도 전체 흐름을 이해할 수 있다. 각 함수가 작업 기억의 용량 안에서 소화 가능한 크기이기 때문에 이해가 쉽다고 느껴진다. 그리고 이것이 우리가 설계를 할 때 적절한 단위로 추상화를 해야하는 이유라고도 할 수 있다. 물론 반대로, 지나치게 잘게 쪼개면 함수 간 점프와 맥락 추적에 작업 기억을 소모하게 된다. 분리 자체가 목적이 아니라, 한 번에 머릿속에 올려야 할 양을 줄이는 것이 목적이다.

청킹: 뇌의 데이터 압축 알고리즘

그런데 여기서 한 가지 의문이 생긴다. 작업 기억의 슬롯이 겨우 4개라면, 우리는 대체 어떻게 수백 줄의 코드를 이해할 수 있는 걸까?

답은 청킹이라는 인지 메커니즘에 있다. 청킹이란 여러 개의 작은 정보 단위를 하나의 의미 있는 덩어리로 묶어서 처리하는 것을 말한다. 취리히 대학교의 연구에 따르면, 청킹은 장기 기억에서 압축된 청크 표상을 불러와 개별 요소의 표상을 대체함으로써 작업 기억의 부하를 줄인다. 이로 인해 확보된 용량은 이후에 입력되는 새로운 정보를 처리하는 데 사용된다.

전화번호를 생각해보자. 01012345678이라는 11자리 숫자를 한 자리씩 기억하려면 작업 기억의 용량을 한참 초과한다. 그런데 010-1234-5678로 나누면 세 덩어리만 기억하면 된다. 더 나아가, 이미 익숙한 010은 “한국 휴대폰 번호 앞자리”라는 하나의 청크로 자동 처리되니, 실질적으로 두 덩어리만 새로 기억하면 되는 것이다.

코드에서도 같은 일이 일어난다. 숙련된 개발자가 아래 코드를 볼 때 일어나는 일을 보자.

const activeUsers = users.filter(u => u.isActive).map(u => u.name);

초보 개발자는 이걸 읽을 때 users라는 변수, .filter 메서드의 동작, 화살표 함수의 문법, u.isActive라는 속성 접근, .map 메서드의 동작, 또 다른 화살표 함수… 이런 개별 요소들을 하나씩 작업 기억에 올려야 한다.

반면 숙련된 개발자에게 users.filter(...).map(...)은 “배열 필터링 후 변환”이라는 하나의 청크로 인식된다. 이 패턴을 수백, 수천 번 봤기 때문에, 장기 기억에 저장된 청크를 꺼내와서 작업 기억의 슬롯을 하나만 쓰는 것이다. 나머지 슬롯은 “왜 필터링하는가”, “결과가 어디에 쓰이는가” 같은 더 상위 수준의 사고에 할당할 수 있다.

이것이 바로 체스 마스터가 체스판을 한 번 보고 전체 상황을 파악할 수 있는 원리와 같다. 체스 마스터는 개별 말의 위치를 하나씩 기억하는 게 아니라, 익숙한 포진 패턴을 하나의 청크로 인식한다. 그래서 의미 있는 배치의 체스판은 빠르게 기억하지만, 말을 무작위로 배치하면 초보자와 별 차이가 없다.

코드도 마찬가지다. 관용적인 코드가 읽기 쉬운 이유는 그게 “올바른” 코드여서가 아니라, 개발자의 장기 기억에 이미 저장된 청크와 일치하기 때문이다.

그리고 특정 패턴이 관용적이 되는 데는 언어의 설계 의도, 표준 라이브러리의 관례, 커뮤니티의 반복적 선택 같은 외부 요인이 작용한다. 단순히 “많이 써서 익숙한 것”이 아니라, 여러 이유로 수렴한 결과가 익숙함을 만든 것이다. 프로젝트만의 독특한 패턴이나 지나치게 창의적인 코드가 읽기 어려운 이유도 여기에 있다. 기존 청크와 매칭되지 않으면, 뇌는 모든 것을 개별 요소로 분해해서 처리해야 하고, 작업 기억은 순식간에 포화된다.

시스템 1과 시스템 2: 직관과 분석

여기서 다니엘 카너먼(Daniel Kahneman)의 이중 처리 이론을 꺼내야 한다. 카너먼은 인간의 사고를 두 가지 시스템으로 나눴다.


  • 시스템 1: 빠르고, 자동적이고, 직관적인 사고. 패턴 인식에 기반한다.
  • 시스템 2: 느리고, 의식적이고, 분석적인 사고. 논리적 추론에 기반한다.

우리가 일상적으로 하는 판단 대부분은 시스템 1이 담당한다. 운전을 하거나, 전화기 너머 여친의 기분을 한마디에 알아채는 것도 시스템 1의 영역이다. 시스템 1은 새로운 정보를 접했을 때, 완전히 새로운 패턴을 만들어내는 게 아니라 기존에 저장된 패턴과 대조하는 방식으로 작동한다.

시스템 1이 막히면 그때서야 시스템 2가 호출된다. “이게 뭐지?” 하고 의식적으로 분석을 시작하는 순간이 바로 시스템 2가 개입하는 시점이다. 그리고 이 프레임워크를 코드 읽기에 적용하면 정말 많은 것이 설명된다.

읽기 쉬운 코드란, 대부분 시스템 1의 패턴 인식으로 처리되고 시스템 2의 개입이 최소화되는 코드다.

숙련된 개발자가 코드를 읽을 때, 익숙한 패턴은 시스템 1이 자동으로 처리한다. for 루프, if-else 분기, map/filter/reduce 체이닝, try-catch 블록 등 이런 것들은 수천 번 봐온 패턴이기 때문에 의식적 노력 없이 처리된다. 시스템 2는 편안한 저전력 모드에 머물면서, 시스템 1이 올려주는 정보를 승인하기만 하면 된다.

그런데 갑자기 예상치 못한 패턴이 나타나면 상황이 달라진다.

// 시스템 1이 처리 가능한 코드
const canPurchase = user.age >= 18 && user.isVerified;

// 시스템 2를 호출하는 코드
const canPurchase = !(user.age < 18 || !user.isVerified) && (user.age !== undefined);

두 코드는 같은 의도를 표현한다. 하지만 두 번째 코드를 보는 순간 뇌에서는 시스템 1이 “모르겠다”는 신호를 보내고, 시스템 2가 비싼 비용을 들여 분석을 시작한다. 이중 부정을 풀고, 드모르간 법칙을 머릿속에서 돌리고, 마지막 undefined 체크가 왜 필요한지를 따져봐야 한다. 이러한 전환 자체가 인지적 비용이다.

카너먼은 이런 현상을 인지적 긴장이라 불렀다. 시스템 2가 개입할수록 뇌는 더 많은 에너지를 소비하고 피로감을 느끼게 된다. “읽기 어려운 코드”라는 주관적 느낌의 실체는 바로 패턴 매칭 실패로 인한 시스템 전환, 그리고 그에 따르는 인지적 비용이 발생하는 것이다.

이 관점에서 보면, 코드 리뷰에서 “이해하기 어렵다”는 피드백은 단순한 취향 불만이 아니라, 인지 시스템의 전환 비용이 실제로 발생하고 있다는 신호에 가깝다. 그 비용의 크기는 개인의 경험과 청크 구성에 따라 다르겠지만, 비용이 발생한다는 사실 자체는 실재한다.

게슈탈트 원리: 코드의 시각적 구조가 이해에 미치는 영향

읽기 쉬운 코드에는 논리적 구조뿐만 아니라 시각적 구조도 중요하다. 여기서 게슈탈트 심리학의 지각 원리가 등장한다.

게슈탈트 심리학은 인간의 뇌가 개별 요소가 아닌 전체 패턴과 구조를 우선적으로 인식한다는 점을 연구하는 분야다. 그 핵심 원리 몇 가지는 코드 가독성에 직접적으로 연결된다.

근접성의 원리

가까이 있는 요소들은 하나의 그룹으로 인식된다. 한번 두 가지 버전의 코드를 살펴보며 이 그룹이 왜 중요한지를 살펴보자.

// 근접성 원리가 적용되지 않은 코드
const name = user.firstName + ' ' + user.lastName;
const email = user.email.toLowerCase();
const isValid = email.includes('@') && email.includes('.');
const role = determineRole(user.permissions);
const dashboard = getDashboard(role);
const notifications = getNotifications(user.id, role);
// 근접성 원리가 적용된 코드
const name = user.firstName + ' ' + user.lastName;
const email = user.email.toLowerCase();
const isValid = email.includes('@') && email.includes('.');

const role = determineRole(user.permissions);
const dashboard = getDashboard(role);
const notifications = getNotifications(user.id, role);

두 번째 버전에서 빈 줄 하나가 추가됐을 뿐인데 뇌는 자동으로 “사용자 정보 처리”와 “권한 기반 데이터 조회”라는 두 그룹을 인식한다.

게슈탈트 연구에 따르면 근접성은 색상이나 형태의 유사성보다도 더 강력한 그룹핑 단서다. 이 원리는 UI 디자인에서도 그대로 사용되는데, 서로 연관이 있는 정보는 가까이 붙이고 연관이 없는 정보는 간격을 늘려 멀리 떨어트려놓는 것이 이 때문이다.

마찬가지로 코드에서 빈 줄과 들여쓰기가 중요한 이유는 단순히 미관 때문이 아니라, 뇌의 지각 시스템이 그걸 기반으로 구조를 파악하기 때문이다.

유사성의 원리

근접성과 마찬가지로 비슷하게 생긴 요소들도 같은 그룹으로 인식된다. 이것을 유사성의 원리라고 하는데, 이 원리는 코드에서 일관된 네이밍이 왜 중요한지를 설명한다.

// 유사성 원리가 깨진 네이밍
const userData = fetchUser(id);
const get_orders = retrieveOrderList(userId);
const pmtHistory = loadPayments(uid);
// 유사성 원리가 적용된 네이밍
const user = fetchUser(id);
const orders = fetchOrders(id);
const payments = fetchPayments(id);

첫 번째 버전에서는 데이터 페칭이라는 같은 종류의 작업임에도 네이밍 규칙이 제각각이다. userData, get_orders, pmtHistory라는 변수명은 각각 다른 형태를 가지고, fetchUser, retrieveOrderList, loadPayments라는 함수명도 일관성이 없다. 뇌는 이런 코드를 보면 이들을 같은 그룹으로 인식하지 못하고 각 라인을 개별 항목으로 처리하면서 작업 기억을 소진한다.

두 번째 버전에서는 패턴이 명확하다. fetch + 리소스명이라는 일관된 구조 덕분에, 세 줄이 “동일한 패턴의 데이터 패칭”이라는 하나의 청크로 인식된다.

연속성의 원리

연속성의 원리는 시선이 자연스럽게 흐르는 방향을 따라 요소들을 하나의 연속된 것으로 인식하는 원리다. 코드에서 이건 실행 흐름의 선형성과 관련된다.

우리 뇌는 위에서 아래로, 왼쪽에서 오른쪽으로 흐르는 것을 자연스럽게 느낀다. 깊은 중첩, 복잡한 콜백, 여기저기 점프하는 goto문 등이 읽기 어려운 이유는 연속성의 원리를 위반하기 때문이다.

우리가 얼리 리턴 패턴을 사용했을 때, 중첩된 if문보다 읽기 쉬운 이유도 마찬가지다. 예외 케이스를 먼저 걸러내고 나면 남은 코드는 위에서 아래로 한 방향으로 흐른다.

// 연속성이 깨지는 코드
function getPrice(user: User, product: Product): number {
  if (user.isActive) {
    if (product.inStock) {
      if (user.tier === 'premium') {
        return product.price * 0.8;
      } else {
        if (product.onSale) {
          return product.salePrice;
        } else {
          return product.price;
        }
      }
    } else {
      throw new Error('Out of stock');
    }
  } else {
    throw new Error('Inactive user');
  }
}
// 연속성이 유지되는 코드
function getPrice(user: User, product: Product): number {
  if (!user.isActive) throw new Error('Inactive user');
  if (!product.inStock) throw new Error('Out of stock');

  if (user.tier === 'premium') return product.price * 0.8;
  if (product.onSale) return product.salePrice;

  return product.price;
}

첫 번째 버전은 들여쓰기가 깊어지면서 시선이 오른쪽으로, 다시 왼쪽으로 지그재그를 그린다. 뇌는 각 중첩 수준에서 “지금 어느 조건 안에 있는가”를 작업 기억에 유지해야 한다.

반면 두 번째 버전은 예외를 먼저 걸러낸 뒤 위에서 아래로 자연스럽게 흘러내린다. 연속성의 원리에 부합하기 때문에 뇌가 구조를 파악하는 데 드는 비용이 크게 줄어든다.

인지 부하 이론: 세 가지 부하의 종류

여기까지의 이야기를 좀 더 체계적으로 정리해주는 프레임워크가 있다. 존 스웰러(John Sweller)의 인지 부하 이론이다.

이 이론에 따르면 학습이나 문제 해결 시 발생하는 인지 부하는 세 가지로 나뉜다.


  1. 내재적 부하: 과제 자체의 본질적 복잡성. 알고리즘의 난이도, 도메인의 복잡성 같은 것.
  2. 외재적 부하: 과제와 무관한, 표현 방식에서 오는 불필요한 복잡성. 일관되지 않은 네이밍, 불필요한 간접 참조, 혼란스러운 코드 구조 같은 것.
  3. 본유적 부하: 새로운 스키마를 형성하고 학습하는 데 드는 유익한 부하.

이 이론에 따르면 우리가 읽기 쉬운 코드를 작성하기 위해 추구해야하는 것은 외재적 부하를 최소화하는 것이다.

어차피 내재적 부하는 문제 자체에 내재된 것이니 줄일 수 없다. 예를 들어 분산 시스템의 합의 알고리즘을 구현하는 코드는 아무리 잘 써도 복잡할 수밖에 없는 것이다. 우리가 피해야할 문제는 “표현 방식”에서 오는 외재적 부하가 내재적 부하 위에 불필요하게 쌓일 때다.

이 관점으로 보면 코드 가독성을 높이는 일관된 네이밍, 적절한 함수 분리, 명확한 타입 선언, 의미 있는 빈 줄과 같은 패턴은 결국 외재적 인지 부하를 줄이는 행위다. 뇌의 제한된 자원을 외재적 부하에 낭비하지 않고, 내재적 부하(실제 문제)를 처리하는 데 집중할 수 있게 해주는 것이다.

2023년에 발표된 체계적 문헌 리뷰에서도 흥미로운 사실이 확인됐다. 소스 코드 메트릭과 실제 측정된 인지 부하 사이의 관계를 조사한 연구들에서 전통적 코드 메트릭 중 실제 인지 부하와 일관되게 높은 상관을 보인 것은 드물었고, 상관이 확인된 경우에도 과제 조건이나 측정 방식에 따라 결과가 달라지는 경향이 있었다. 즉, 우리가 “복잡도”라고 측정하는 것과 개발자의 뇌가 실제로 “복잡하다”고 느끼는 것은 다를 수 있다는 뜻이다.

이건 의미심장한 결과다. 기계적으로 측정 가능한 메트릭은 코드의 외형적 복잡도를 포착하지만, 개발자의 뇌가 실제로 겪는 인지 부하는 패턴의 익숙함, 청킹의 효율성, 시각적 구조의 명확성과 같이 주관적 인식에 영향을 받는다는 것이다.

경험이 뇌를 물리적으로 바꾼다

앞서 전문가와 초보자의 뇌 활성화 패턴이 다르다고 했다. 이건 단순히 개발 짬바가 쌓이면 더 잘한다는 이야기가 아니다. 2024년 Scientific Reports에 발표된 연구에서는 62명의 파이썬 프로그래머를 대상으로 뇌파를 측정하며, 코드에 의도적으로 삽입한 문법 오류와 의미 오류에 대한 뇌의 반응을 관찰했다.

결과는 놀라웠다. 숙련된 프로그래머는 코드의 문법적 위반과 의미적 위반에 대해 서로 다른 뇌파 패턴을 보였다. 이는 자연어를 읽을 때 문법 오류와 의미 오류에 대해 다른 뇌파가 나타나는 것과 유사한 패턴이다. 프로그래밍 경험이 뇌에 특정 언어와 패턴을 처리하기 위한 신경 회로를 형성한다는 뜻이다.

다시 말해, 코딩 경험은 뇌의 물리적 구조를 변화시킨다. 신경가소성에 의해, 반복적으로 노출된 코드 패턴은 장기 기억에 스키마로 저장되고, 이 스키마가 청킹의 기반이 된다. 시스템 1이 코드를 자동으로 처리할 수 있는 것은 이런 스키마가 축적된 결과다.

또한 스키마는 개인이 노출된 패턴에 따라 형성되기 때문에 같은 코드베이스에서 같은 패턴을 반복적으로 접한 사람들은 비슷한 스키마를 공유하게 된다. 이걸 뒤집어 생각하면 팀에서 일관된 코드 스타일을 유지하는 것은 취향의 문제가 아니라 팀원들의 뇌에 공유된 청크를 형성하는 과정인 셈이다. 코딩 컨벤션이 중요한 이유가 단순히 통일성이 아니라, 집단적 인지 효율성 때문인 것이다.

예측 부호화: 뇌는 코드를 읽는 게 아니라 예측한다

최근 인지과학에서 주목받는 이론 중 하나는 예측 부호화다. 이 이론에 따르면, 뇌는 수동적으로 정보를 수신하는 게 아니라, 끊임없이 다음에 올 정보를 예측하고, 실제 입력이 그 예측과 다를 때만 추가적인 처리를 한다.

코드를 읽을 때도 마찬가지다. 우리 뇌는 다음 줄에 뭐가 올지 끊임없이 예측한다.

async function fetchUserProfile(userId: string) {
  try {
    const response = await api.get(`/users/${userId}`);
    // 여기에 뭐가 올지, 이미 예측하고 있다

이 코드를 읽는 숙련된 개발자의 뇌는 이미 “뭐 이제 응답 데이터를 파싱해서 리턴하겠지”라고 예측하고 있다. 그리고 실제로 return response.data; 같은 코드가 나오면 그 예측이 맞았으므로 추가적인 인지 비용이 거의 발생하지 않는다.

하지만 만약 예측과 전혀 다른 쌩뚱맞은 코드가 나오면 어떻게 될까?

async function fetchUserProfile(userId: string) {
  try {
    const response = await api.get(`/users/${userId}`);
    globalEventBus.emit('user-fetched', response.data); // 엥 이거 뭐임?
    localStorage.setItem('lastUser', JSON.stringify(response.data)); // 어라?
    analytics.track('profile_view', { userId }); // 아 로깅을 왜 여기서 해
    return response.data;

fetchUserProfile이라는 이름에서 뇌가 예측한 것은 “사용자 프로필을 가져와서 돌려주는 함수”다. 그런데 이벤트 버스 발행, 로컬 스토리지 저장, 애널리틱스 추적이라는 예측 밖의 동작이 나타난다. 이때마다 뇌는 예측 오류 신호를 발생시키고, 시스템 2를 호출해서 이 코드가 왜 여기에 있는지를 분석하기 시작한다.

즉, 우리가 두 번째 버전의 코드를 읽기 어렵다고 느끼는 이유는 함수의 이름은 fetchUserProfile이지만, 내부에서는 그와 전혀 상관없는 동작들이 발생하고 있기 대문이다. 이름이 설정한 예측과 실제 동작이 일치할수록 예측 오류가 줄어들고 인지 비용이 절감된다.

이건 함수 내부뿐 아니라 인터페이스 설계에서도 마찬가지다. 필자와 같은 프론트엔드 개발자에게 익숙한 컴포넌트 인터페이스를 예시로 들어보자.

// 예측 가능한 인터페이스
<TextInput
  value={name}
  onChange={setName}
  placeholder="이름을 입력하세요"
/>
// 예측이 어려운 인터페이스
<UserNameInput
  user={name}
  setUser={setUser}
  blank="이름을 입력하세요"
/>

첫 번째 컴포넌트는 value, onChange, defaultValue라는 React 생태계에서 거의 모든 입력 컴포넌트가 공유하는 인터페이스를 따른다. 이 패턴을 수없이 접해온 개발자의 시스템 1은 “입력 컴포넌트구나”로 처리하고 넘어간다.

반면 두 번째는 user, setUser, blank라는 비즈니스 로직에 강하게 결합되었거나 의미를 알기 어려운 인터페이스를 가지고 있다. 이 컴포넌트가 내부에서 user 객체의 어떤 필드를 건드리는지, blank는 도대체 언제 사용되는 것인지 파악하기 전까지는 안심하고 사용할 수가 없다. 매번 내부 구현을 들여다봐야 하고, 그때마다 예측 오류가 발생한다.

즉 읽기 쉬운 코드를 작성하기 위해서 우리는 함수의 이름 뿐 아니라 변수명, 파일 구조, 디렉토리 구성, API 설계 등 코드베이스의 모든 수준에서 예측 가능성을 높여야한다.

그래서 “좋은 코드”란 결국

지금까지의 내용을 정리해보면, “읽기 쉬운 코드”라는 주관적 느낌의 실체는 다음과 같다.


  1. 작업 기억을 초과하지 않는 코드: 동시에 머릿속에 담아야 할 맥락이 3~4개 이내인 코드
  2. 기존 청크와 매칭되는 코드: 익숙한 패턴을 사용해서 장기 기억의 스키마를 활용할 수 있는 코드
  3. 시스템 1에서 처리 가능한 코드: 시스템 2를 불필요하게 호출하지 않는, 패턴 인식만으로 이해 가능한 코드
  4. 시각적 구조가 논리적 구조와 일치하는 코드: 게슈탈트 원리에 부합하여 뇌의 지각 시스템이 구조를 자동으로 파악하는 코드
  5. 예측 가능한 코드: 이름과 구조에서 설정한 기대를 위반하지 않는 코드
  6. 외재적 인지 부하가 낮은 코드: 문제 자체의 복잡성만 남기고, 표현에서 오는 불필요한 복잡성을 제거한 코드

이 목록을 보면 재밌는 사실을 하나 발견할 수 있다. 이 중 어느 것도 “정확한 코드”를 의미하지 않는다는 점이다.

가독성과 정확성은 별개의 축이다. 완벽하게 정확하지만 읽기 불가능한 코드도 있고, 읽기는 쉽지만 틀린 코드도 있다. 다만 읽기 쉬운 코드는 버그를 발견하기도 쉽다. 외재적 부하가 낮으니, 뇌의 자원을 논리적 오류를 찾는 데 집중할 수 있기 때문이다.

다만 한 가지 주의할 점이 있다. 카너먼은 시스템 1의 편향에 대해서도 경고했다. 시스템 1은 빠르지만, 그 속도의 대가로 편향이 존재한다. 대표적인 것이 익숙함 편향이다. 자신에게 익숙한 패턴을 “읽기 쉬운 코드”로 느끼는 것은 사실이지만, 그것이 객관적으로 최선인지는 별개의 문제다. 함수형 프로그래밍에 익숙한 개발자는 for 루프를 “읽기 어렵다”고 느낄 수 있고, 반대의 경우도 마찬가지다. 이때 “읽기 어렵다”는 느낌은 코드의 객관적 품질이 아니라, 자신의 시스템 1에 저장된 청크의 편향을 반영하는 것일 수 있다.

코드 리뷰에서 “이해하기 어렵다”는 피드백을 줄 때, 한 번쯤 자문해볼 가치가 있다. 이게 정말로 인지 부하가 높은 코드인가, 아니면 단순히 내 시스템 1에 등록되지 않은 패턴인가? 전자라면 리팩토링이 필요하고, 후자라면 오히려 내 청크 라이브러리를 확장할 기회다.

마무리

“읽기 쉬운 코드”는 그냥 느낌이나 취향의 문제가 아니다. 작업 기억의 용량 제한, 청킹 메커니즘, 이중 처리 시스템, 게슈탈트 지각 원리, 예측 부호화와 같은 인간의 인지 구조가 만들어내는 자연스러운 결과다.

필자가 이전 글에서 “가독성은 주관적이다”라고 했던 이야기의 이유도 바로 여기에 있다. 청킹은 개인의 경험에 의존하고 시스템 1에 등록된 패턴은 사람마다 다르기 때문이다.

같은 코드를 보고 누군가는 쉽다고 느끼고 누군가는 어렵다고 느끼는 건 그저 취향이 달라서가 아니라 뇌에 축적된 스키마가 다르기 때문이다. 가독성이 주관적일 수밖에 없는 이유 자체가 인지 구조에 내재되어 있는 셈이다.

이상으로 우리는 왜 어떤 코드를 읽기 쉽다고 느낄까 포스팅을 마친다.

관련 포스팅 보러가기

Dec 23, 2024

좋은 코드란 무엇일까? - 가독성이란 허상에 대하여

에세이