[번역] 잘못된 추상화

회사에서 thunk를 매번 사용하다 Redux-saga를 이용해보았다. Saga의 예제 중에 비동기로 동작하는 (API 통신 관련 로직을 잘 추상화해놓은 코드)[https://github.com/redux-saga/redux-saga/blob/master/examples/real-world/sagas/index.js#L23] 를 참고하여 회사 코드에 적용했다.

raceCondition을 피하고 다음 일련의 과정(API 요청 -> dispatch(성공 | 실패))의 중복을 제거해주는 추상화된 코드다.

이 추상화 코드가 굉장히 멋지다고 생각하여, 사이드 프로젝트에서 (saga 아닌 thunk사용) API 통신, SOCKET 통신의 일련의 로직에 대해 추상화를 시도해 보았다. 처음엔 굉장히 멋지다 생각했는데, 완성 후 다른 기능을 추가하려다보니 멋만 부리고 다른 사람이 이해하기 어렵고, 깨지기 쉬운 코드라고 느꼈다.

이후에 컴포넌트를 여러 컴포넌트로 나눠야 할 때 포스팅을 보던 중 잘못된 추상화라는 글을 보고 좋은 글이라 생각하여 번역해보았다.

추상화된 것을 어플리케이션이나 workflow에 적용하기 전에 알아야 하는 것

  1. 추상화 했을 때 이점은?
  2. 추상화의 비용은?

만약 이것에 대해 알지못한다면, 문제가 없는 것에 대한 해결책을 위해 비용에 대한 리스크를 가지고 된다. 이득이 없는 비용은 좋은 딜이 아니다.

이득과 비용을 이해하는 중요한 부분은 추상화가 해결해주는 문제에 대해 이해하는 것이다.

잘못된 추상화

The Wrong Abstraction

“잘못된 추상화”의 결론에 대해 생각했고, RailsConf 2014에서 주장한 것은 다음과 같다.

잘못된 추상화는 중복되는 것보다 비싸다.

요약하면,

잘못된 추상화보다 중복을 선호한다.


The strength of the reaction made me realize just how widespread and intractable the “wrong abstraction” problem is. 나는 질문을 하기 시작했고 다음 패턴을 보게되었다.

  1. 프로그래머 A는 중복된 것을 본다.

  2. A는 중복을 추출하여 이름을 지어준다.

  3. 이것은 새로운 추상화를 생성한다. 새로운 메서드이거나 새로운 클래스일 수 있다.

  4. A는 중복은 새로운 추상화로 대체한다.

  5. 코드는 완벽해. A는 행복하게 떠난다.

  6. 시간이 지난다.

  7. 현재 추상화가 거의 완벽해야 하는(?) 새로운 요구사항이 나타난다.

  8. B는 이 요구사항을 구현해야하는 상황이다.

  9. B는 기존의 추상화를 유지하기 위해 노력하고 있지만 모든 케이스에 정확히 동일하지 않다.

  10. 파라미터를 받기 위해 코드를 수정해야하고 파라미터의 값에 따라 조건부로 올바르게 동작하기해위해 로직을 추가해야 한다.

  11. 새로운 요구사항이 도착한다.

  12. 파라미터의 추가, 새로운 조건 추가

  13. 코드가 이해할 수 없을 때까지 반복..

  14. 당신은 이 이야기에 나타나며, 너의 삶은 극적으로 나쁜쪽으로 변한다.

기존 코드는 강력한 영향력을 가진다. 기존 코드의 존재는 정확하고 필요하다는 것을 주장한다. 코드는 노력을 나타내고, 이러한 노력의 가치를 보존하기 위해 동기부여를 한다. 불행하게도 슬픈 진실은 추상화 만들기에 시간을 투자할수록, 복잡하고 이해할 수 없는 코드를 만들어낸다. (일종의 매몰 비용의 오류를 범한다.)

위의 step 8에서 너가 나타났을 때, 기존 코드를 변경하여 새로운 요구사항을 구현한다.

그러나, 그렇게 시도할수록 코드는 더이상 단일 추상화를 나타내지 못한다. 대신에 추상화에 여러가지 모호하게 관련 있는 아이디어가 삽입되어 조건이 많은 과정을 가지게 된다.

이 상황을 발견하면, 매몰 비용에 저항해라. 잘못된 추상화를 처리할 때, 가장 빠른 방법은 되돌아가는되것이다. 다음을 따르자.

  1. 추상화된 코드를 모든 호출부에서 인라인화하여 중복을 다시 살펴보자.
  2. 각 호출자 내에서, 특정 호출자가 인라인으로 작성된 코드의 부분 집합을 결정하기 위해 전달된 파라미터를 사용하자.
  3. 특정 호출자에서 필요하지 않은 부분은 삭제하자.

이것은 추상화와 조건문 둘다 제거된다. 또한, 각 호출자가 필요한 코드로 축소된다.

사람들이 잘못된 추상화를 진행하는 것을 많이 보았다. (거의 성공하지 못했다) 추상화된 코드에 새로운 기능을 추가하는 것은 매우 힘들고, 추가되는 것이 성공할 때마다 코드를 더욱 복잡하게 만들었다. (이것은 추가 기능 구현할 때 더 어려워진다..). “나는 이 코드에 추가한 시간이 너무 아까워” 에서 “이 코드는 잠시나마 이해가 됐는데, 아마도 우리가 할 수 있는 모든 것을 배웠을지 몰라(?)”로 관점을 바꾸고, 현재의 요구사항에 맞게 추상화를 다시 생각할 수 있는 기회를 자신에게 주었을 때, 모든것은 쉬워진다. 일단 코드를 인라인화 하고 나면, 앞으로 나아가는 경로는 분명해지고 새로운 기능을 추가하는 것은 점점 더 빨라지고 쉬워졌다.

이 이야기의 교훈은.. 매몰 비용의 덫에 걸리지 말자. 파라미터를 전달하고 공유된 코드를 통해 조건문을 추가하고 있는 자신을 발견하다면, 추상화는 부정확하다. 처음엔 추상화가 맞았겠지만 그 날은 지나갔다.. 일단 추상화가 잘못된 것으로 판단되면, 가장 좋은 전략은 중복된 것을 다시 확인하고, 그것이 맞다는 것을 자신에게 다시 보여주는 것이다. 무슨일이 일어나고 있는지를 알기 위해, 몇가지 조건을 추가하는 것이 가끔 맞긴 한데, 잘못된 추상화를 늦기전에 빨리 버린다면 고통을 덜 겪을 것이다.


Summary

반복되는 로직에 대해 핵심 부분에 집중하여 추상화하는 것은 관련 로직에 대해 재사용성을 높이고 유지보수하기 쉽다고 생각한다. 하지만 잘 작성된 추상화 코드는 빠르게 변하는 서비스 스펙에 대응하기 힘들 수 있다. 처음엔 파라미터를 통한 조건문으로 쉽게 해결할 수 있다. 이후에 스펙을 반영하기 위해 조건문은 계속 늘어나고.. 이 때부터는 복잡하고 이해하기 힘든 코드로 변해간다. 매몰 비용의 오류 에 빠지지 말고.. 추상화 코드를 제거하고 다시 반복되는 코드를 작성하여 중복되는 것을 살펴보자. 그리고 다시 추상화 설계를 거치자.

핵심은 매몰 비용의 오류에 빠지지 말자이다. 잘못된 추상화를 늦기 전에 버리고 중복되는 것을 선택하거나(잘못된 추상화보다 코드가 이해하기 쉽고 변화에 대응하기 쉽다.) 중복된 것을 통해 다시 추상화 설계를 하자.