Leaky Abstraction

Engineering

우리는 왜 추상화를 할까?

추상화란

복잡한것을 감추고 사용자가 필요한것만 노출하는것

입니다.

다시 말해서 사용자가 관심가지지 않아도 되는 부분을 숨기는 일종의 단순화의 과정에 가깝습니다.

소프트웨어가 커지기 시작하는 동시에 개발자는 이해해야하는 대상이 많이 늘어나게 됩니다. 이 때 추상화는 크게 2가지 장점을 줍니다.

  • 눈에 보이는 복잡성을 줄여 인지 부하(Cognitive Load) 를 감소 시킨다
  • 변경의 파급 을 막아 수정에 용이하게 한다.

아주 대표적인 예시로 Java 의 List Interface 를 보겠습니다.

List Interface 는 그 대상이 Linked List 인지 Array List 인지 어떻게 구현했는지, 내부 구조가 어떻게되어있는지는 중요하지 않습니다. 무엇을 할 수 있는가는 보여주고 어떻게 구현했는가를 숨김으로서 눈에 보이는 복잡도를 줄인것입니다.

---
config:
  layout: elk
---
flowchart TB
    L["List<br/>무엇을 할 수 있는지<br/><br/>add() · get() · size() · remove()"]

    AL["ArrayList<br/>배열을 사용하여 구현"]
    LL["LinkedList<br/>노드를 연결하여 구현"]

    AL -->|implements| L
    LL -->|implements| L

    U["사용하는 입장에선 구체적인 구현 방식을 알 필요가 없음 <br/><br/> 구현체를 교체하더라도 코드변경의 파급이 적음"]

    L --> U

Leaky Abstraction

All non-trivial abstractions, to some degree, are leaky.

모든 사소하지 않은(non-trivial) 추상화는 어느 정도 새어나온다.

— Joel Spolsky (Joel on Software Blog, 2002)

Leaky Abstraction 은 우리에게

  • 추상화가 얼마나 잘 디자인 되었는지는 상관없고 내부 사항에 따라 예외적인 사례가 있다.
  • 개발자는 추상화 아래에 무슨 일이 일어나는지 알 필요 없는게 아니라, 최소한 기초적인 수준에서 이해해야한다.
  • 추상화를 할 때도 누수를 최소화 하려고 노력해야하고 그 추상화가 망가지는 케이스들을 문서화 해야한다.

라고 이야기합니다.

Joel Spolsky

Joel Spolsky

Joel Spolsky 는 뉴욕의 소프트웨어 개발자이며 Jeff Atwood 와 함께 Stack Overflow 를 만들었고 2010-2019년까지 CEO 를 역임했습니다.

Leaky Abstraction 은 그의 블로그 글인 the law of leaky abstraction 에서 전문을 확인할 수 있습니다.

사례

Leaky Abstraction 은 생각보다 우리 주변에 가까이에 있습니다.

Garbage Collector

저는 Java 와 C# 을 주로 사용합니다. C 와는 다르게 직접 메모리를 할당하고 해제하지 않습니다.

즉 메모리는 알아서 관리된다 라는 추상화를 제공합니다.

하지만 이 추상화과 완벽하지는 않습니다. 싱글턴 패턴에선 특히 조심해야합니다. 싱글턴은 애플리케이션이 실행되는 동안 계속 살아있는 경우가 많기 때문에 싱글턴이 다른 객체를 의도치않게 계속 참조하고 있다면 해당 객체 역시 GC의 대상이 되지 않을 수 있습니다.

이건 곧 메모리 누수로 이어집니다.

그리고 만약 GC 개입으로 인한 Latency 마저 민감한 극도의 퍼포먼스를 챙겨야하는 애플리케이션의 경우에는 GC의 동작 방식과 메모리 구조까지 이해해야 할 수 있습니다.

평소에는 메모리 관리를 신경쓰지 않아도 되지만 문제가 발생하는 순간 GC 라는 추상화 아래에 있던 복잡함이 모습을 드러내게 됩니다.

HTTP Request

HTTP Request 는 웹을 통한 인터페이스에선 빠질 수 없는 대표적인 추상화입니다.

const response = await fetch("/api/users");

작성한 코드 아래 수많은 네트워크 계층이나 복잡성을 당장 다 알 필요는 없습니다.

---
config:
  layout: elk
---
flowchart TB
    A["Application<br/><br/>fetch('/api/users')"]

    subgraph HTTP["HTTP Abstraction"]
        B["HTTP Request<br/>GET /api/users"]
        C["HTTP Response<br/>200 OK"]
    end

    subgraph Hidden["HTTP 아래에 존재하는 현실"]
        D["TLS<br/>Encryption"]
        E["TCP<br/>Handshake ⋅ Connection"]
        F["IP<br/>Routing"]
        G["DNS<br/>Domain → IP"]
        H["Network<br/>Packet"]
    end

    I["Server"]

    A --> B
    B --> D
    D --> E
    E --> F
    F --> H
    G --> F
    H --> I
    I --> C
    C --> A

개발할 땐 HTTP Request 와 Response 만 생각하면 됩니다.

하지만 네트워크가 끊기거나, 느려지거나 무언가 문제가 생기면 이야기가 달라집니다. 추상화 아래에 숨겨져 있던 복잡함이 드러나 영향을 미치기 시작합니다.

Over Abstraction

저는 여기에 과도한 추상화(Over Abstraction) 도 함께 이야기하고싶습니다.

실무를 뛰다 보면 추상화가 항상 복잡도를 감추어주는 것만은 아니라는걸 경험하게됩니다.

추상화에는 비용(Cost) 이 있습니다. 분명 이론적으론 추상화는 복잡성을 숨기기 위해 사용합니다. 그런데 아이러니하게도 추상화를 만드는 것 자체가 새로운 복잡성을 만들어내기도 합니다.

단순하게 이야기하면, 추상화 계층이 많아질수록 클래스가 많아지고 실제 동작하는 코드가 멀어집니다. ‘그래서 결국 이 코드가 뭘하는거지?’ 라는 질의에 여러 파일을 돌아다녀야합니다.

극단적으로 이야기하면 과도한 추상화는 개발자가 이해해야할 경로를 늘린것 이라고 생각합니다.

아마 이런 경험은 제가 이야기하지 않아도 많은 개발자들은 이미 경험과 몸으로 체득하였을겁니다.

문제는 그 다음입니다.

도대체 그럼 추상화는 어디까지 어느정도 해야할까요?

  • 추상화를 하지 않으면 결합도가 높아지고 변경에 취약해집니다.
  • 추상화를 지나치게 하면 코드의 구조를 이해하는 것 자체가 어려워집니다.

우리는 일종의 저울, Trade Off 를 고민하게됩니다. ‘적절히’ 라는게 정말 어렵기 때문입니다.

과도한 추상화를 경계하는것은 쉽지만 어디까지가 적절한 추상화인지 판단하는 것은 생각보다 훨씬 어렵기 때문입니다.

언제 추상화 하면 좋을까?

우리는 추상화엔 장단점이 있다는걸 알고있습니다. 그러면 대체 언제 추상화해야할지 기준이라곤 하기는 조심스럽지만 아래의 상황일 땐 고려해볼법 합니다.

반복되는 복잡성이 보이는가?

추상화의 본질은 복잡성을 감추는것입니다. 그것이 반복된다면 추상화 대상으로 고민해볼법 합니다.

예를 들어 API 요청이 왔을 때 다음과 같은 로직이 반복된다고 가정해보겠습니다.

---
config:
  layout: elk
---
flowchart LR
    USECASE1["내가 쓴 글 조회"]
    USECASE2["마이페이지 조회"]
    USECASE3["좋아요 누르기"]
    USECASE4["..."]

    AUTH["인증 및 권한 확인"]

    USECASE1 --> AUTH
    USECASE2 --> AUTH
    USECASE3 --> AUTH
    USECASE4 --> AUTH

이 로직이 여러곳에서 반복된다면 복잡성을 하나의 개념으로 묶어볼 수 있습니다.

authorization.check(user)

반복되는 코드를 단순히 줄인 것이 아니라 여러 곳에서 반복되는 복잡성을 하나의 추상화된 개념 뒤로 숨긴 것입니다.

변경의 영향을 제한하고 싶을 떄

추상화의 큰 장점중 하나는 변경의 영향을 제한하는 것입니다.

만약에 주문 처리 코드에서 결제를 직접 구현했다고 가정해봅시다.

---
config:
  layout: elk
---
flowchart LR
    ORDER["주문 처리"]

    CARD["카드 결제"]
    BANK["계좌 이체"]
    EASY["간편 결제"]

    ORDER --> CARD
    ORDER --> BANK
    ORDER --> EASY

이 구조에서는 결제 방식이 변경될 때 비즈니스 로직까지 영향을 받을 수 있습니다. 그러면 결제라는 개념으로 추상화를 한면 다음과 같은 구조를 만들 수 있습니다.

---
config:
  layout: elk
---
flowchart LR
    ORDER["주문 처리"]

    PAYMENT["Payment"]

    CARD["카드 결제"]
    BANK["계좌 이체"]
    EASY["간편 결제"]

    ORDER --> PAYMENT

    PAYMENT --> CARD
    PAYMENT --> BANK
    PAYMENT --> EASY

결제 방식이 바뀌거나 추가되더라도 비즈니스로직인 주문 로직까지 영향을 주지 않고 싶을 수 있습니다.

이러한 생각은 SOLID 원칙 중 DIP(Dependency Inversion Principle) 와도 연결됩니다.

마치며

추상화는 복잡성을 없애지 않고 감춥니다. 감춰진 복잡함은 예외 상황, 성능 문제, 장애 등 문제가 생기는 순간 언젠가는 모습을 드러냅니다. Leaky Abstraction 은 추상화가 실패했다는 뜻이 아니라 모든 추상화에는 한계가 있다는 사실을 알려줍니다.

반대로 추상화를 과도하게 하면 그 자체로 새로운 복잡성이 됩니다. 그래서 추상화는 많이 할수록 좋은 것이 아니라 반복되는 복잡성이나 변경의 영향을 줄여야 할 곳에 필요한 만큼만 해야 합니다.

쓰는 사람은 그 아래에서 무슨 일이 일어나는지 최소한의 수준에서는 알고 있어야 하고 만드는 사람은 그 아래를 들여다볼 수 있는 방법과 가이드를 남겨 두어야 합니다.

추상화는 실무에서 매우 자주 마주하는 개념입니다. 조직과 분야, 상황에 따라 적절한 방식은 달라지기에 하나의 정답을 말하기는 어렵습니다. 다만 공통적으로 중요한 것은 추상화가 왜 필요한지 무엇을 얻고 무엇을 감수하게 되는지를 충분히 고민하는 일이라고 생각합니다.