들어가며
우리 인간이 가진 능력 중에서 미친(positive) 능력을 하나 뽑자면 '추상화'일 것입니다. 우리는 눈에 보이지도 않는 '사랑', '진리'같은 개념을 마음이나 상상으로 그리고, 비슷한 일이 반복되면 거기서 일반적인 법칙을 찾아내곤 합니다. 이런 추상화 능력은 일상부터 예술, 과학, 철학까지 정말 다양한 분야에 적용되고 있죠. 그러나 우리가 말하는 '추상적'이라는 표현은 종종(꽤나 자주) 부정적인 의미로 사용됩니다. "야 그건 너무 추상적인데?" 라거나, "너무 추상적이라서 이해가 안 돼" 라는 말을 들어보신 적 있으실 겁니다.
현실이 아니라 프로그래밍에서 '추상화'란 좋은 코드를 위한 필수 요소이자 복잡성을 다루는 방법입니다. 그러나 프로그래밍에서도 '추상화'는 항상 좋은 것이라고 단정지을 수는 없는데요, '성급한 추상화', '잘못된 추상화'라는 말을 들어보셨거나 실제로 코드를 작성하며 경험하셨을 거라 생각합니다. 그렇다면 어떤 추상화가 좋은 것일까요? 그리고 어떤 추상화가 '성급'하고 '잘못된' 것일까요?
현실 세계에서 '추상'이라는 말이 어떻게 사용되는지, 어떤 뉘앙스인지를 정리해보고, 그걸 바탕으로 프로그래밍의 추상화를 이해해보려 합니다.
현실 세계에서 말하는 '추상'
사물이나 표상(表象)을 어떤 성질·공통성·본질에 착안하여 그것을 추출(抽出)하여 파악하는 것. 그 때 다른 성질을 배제하는 작용인 사상(捨象)을 수반하므로 추상과 사상은 동일 작용의 두 측면을 형성함.
출처: Oxford Languages
음... 네, 사전에서 정의한 내용은 항상 어렵고 복잡하니까, 일단 캐주얼하게 생각해보겠습니다. '추상'이라는 단어는 사용되는 맥락이 참 다양합니다. 그리고 맥락에 따라 의미가 달라질 수 있죠. 우리가 사용하는 어휘가 항상 사전적 정의와 일치하는 것은 아니지만, 많은 사람들이 사용하다 보면 큰 수의 법칙에 따라 어느 정도는 사전적 의미에 수렴해가기 마련이라고 생각합니다. 그럼 일상적으로 자주 사용되는 사례를 통해 '추상'을 어떤 의미로 사용하고 있었나 살펴보겠습니다.
구체적이지 않고 막연하거나 일반적인 것
일상 대화에서 가장 흔하게 접하는 '추상'의 의미는 '구체적이지 않고 막연한 것'입니다.
설명이 너무 추상적이라 구체적으로 뭘 해야 할지 모르겠다.
이런 말에서 '추상적'이라는 표현은 명확한 지시나 구체적인 예시 없이 너무 일반적이고 모호하게 표현된 것을 의미합니다. 헐레벌떡 달려온 PO가 다짜고짜 "UX을 개선해야 합니다!" 라고 말한다면 이건 추상적 표현의 전형적 예시라고 할 수 있겠죠. 누가, 언제, 어떻게, 무엇을 해야 하는지 명확하지 않기 때문입니다.
약속이나 규칙에서도 추상성은 문제가 됩니다. 길을 가다 우연히 동창을 만나면 "언제 밥 한끼 하자~" 라는 말을 나누곤 합니다. 이는 시간과 장소가 특정되지 않은 추상적 약속입니다. 사실상 다시 만나지 않겠다는 의미와 같죠.
그래서 필요하다면 명확하고, 구체적으로 표현해야 합니다. 예를 들어, 옛날에는 고등학교에서 두발을 단속 했습니다. 만약 교칙이 '두발이 단정해야 한다'라면 '단정함'이 구체적으로 정의되지 않은 추상적 교칙입니다. 학생들은 이 부분을 악용(?)하여 머리를 기르며 반항할 수 있겠죠. 그래서 '귀에 닿지 않는 길이', '길이가 5cm를 넘지 않는다'와 같이 구체적으로 규정하는 경우가 많습니다. 군대도 그랬던 것 같네요.
이처럼 일상적 맥락에서 '추상적'이라는 표현은 종종 '불명확함', '모호함', '실천하기 어려움'이라는 부정적인 의미를 가집니다. 그러나 이러한 추상성이 항상 나쁜 것만은 아닙니다. 때로는 의도적인 추상성이 필요할 때도 있죠. 예를 들어, 정치나 외교에서는 의도적으로 추상적 표현을 사용하여 다양한 해석의 여지를 남겨둡니다. 이해가 다른 사람들이 자의적으로 해석하며 합의에 도달할 수 있게 해주는 전략입니다.
실체가 없는 감정, 성격, 가치, 개념
일상에서 '추상'은 눈에 보이지 않고 만질 수 없는 물리적 실체가 없는 개념을 가리키기도 합니다.
자유, 정의, 진리, 사랑, 평화, 행복... 다양한 가치들은 모두 추상적 개념입니다. 이것들은 구체적인 형태나 색깔, 무게를 가지고 있지 않습니다. 비록 보거나 만질 수 없지만 우리는 그 의미와 중요성을 잘 알고 있습니다.
철학자들은 이러한 추상적 개념에 대해 수천 년 동안 사유해왔습니다. 우리가 도덕책에서 들어본 플라톤, 칸트 등의 유명한 철학자들이 고민해온 것이 바로 이데아, 실천 이성 등의 추상적 개념이죠. 저는 사실 철학자들이 하는 이야기를 잘 이해하지 못합니다. 그리고 정말 많은 분들이 저와 비슷할 거라 생각합니다. 이런 추상적 개념에 대한 지나치게 깊은 사유는 오히려 사람들을 혼란스럽게 만들고, 실질적인 문제 해결에는 그닥 도움이 되지 않는 경우가 있습니다.
문학과 예술은 이러한 추상적 개념을 표현하는 가장 대중적인 수단이라고 할 수 있습니다. 예를 들면, '너의 이름은'이라는 영화는 사랑, 운명, 그리고 시간이라는 추상적 개념에 대해 다루고 있습니다.(칸트에 따르면 시간 역시 추상적인 개념이라고 하네요) 또한, '운수 좋은 날'이라는 소설은 인간의 고통과 절망을 다루죠. '왜 먹지를 못하니' 다들 기억하시죠?
종교에서도 추상적 개념은 핵심적입니다. 신, 영혼, 천국, 지옥 등은 모두 물리적 실체가 없는 추상적 개념입니다. 그러나 이러한 개념들은 수많은 사람들의 삶에 깊은 의미와 방향을 제공합니다.
이러한 추상적 개념들은 현실에서 물리적으로 확인할 수는 없지만, 삶에 큰 영향을 줍니다. 우리는 사랑을 직접 볼 수는 없지만, 사랑의 영향력을 느끼고 경험합니다. 정의를 만질 수는 없지만, 정의가 실현될 때와 무시될 때의 차이를 분명히 인식하죠. 물론 이런 가치들은 좋다, 나쁘다를 논하기 어렵습니다. 좋은 가치도 있고, 나쁜 가치도 있고... 심지어는 같은 가치를 판단하는 사람에 따라 다르게 해석할 수 있죠. 그 이유는 '추상적' 개념이기 때문입니다. '정의'가 무엇인지 아주 구체적으로 명시한다면 데스노트의 L과 키라의 싸움도 없었을 것이고, '정의란 무엇인가'가 하버드대에서 강의되지 않았을 것입니다.
실체가 없다는 것 자체로는 부정적으로 보기 어렵습니다. 하지만, 추상적 개념을 본인이 해석한 대로 다른 사람에게 믿음을 강요하는 경우도 있을 것이고, 혹은 '뭔지는 몰라도 좋은거니까' 믿어버리는 경우도 있을 것입니다. "내가 정의를 위해 싸우고 있다" 라고 주장하는 사람들은 많지만, 그들이 말하는 '정의'는 모두 다릅니다. 심지어는 본인이 말하는 정의가 무엇인지 설명하지 못하는 사람도 있죠. 즉, '추상'이 실체가 없기 때문에 발생하는 부정적인 영향이 분명히 존재한다고 생각합니다.
구체적 대상을 단순화, 요약한 것
세 번째 의미의 '추상'은 복잡한 현실을 단순화하거나 요약하는 것을 말합니다.
최근에 일본의 나오시마라는 섬으로 여행을 다녀왔는데요, 한국에서 손꼽히는 추상화가인 이우환의 '이우환 미술관'이 있어 관람하고 왔습니다.
추상 미술(抽象美術, abstract art)은 대상의 구체적인 형상을 나타낸 것이 아니라 점, 선, 면, 색과 같은 순수한 조형 요소로 표현한 미술의 한가지 흐름이다.
출처: Wikipedia
이우환의 작품도 '점'과 '선'으로 표현된 추상화입니다. 보시다시피 구체적인 형상은 없고, 점과 선으로만 이루어져 있습니다. 솔직히 저는 이게 뭘 의미하는지는 전혀 모르겠는데, 이렇게 의도나 감정을 극한으로 단순화하여 표현한게 추상화라고 합니다.
디자인에서도 추상화는 핵심적 과정입니다. 우리가 흔히 접하는 아이콘이나 로고는 복잡한 개념이나 형태를 단순하게 나타낸 추상화의 결과물이죠. 또 재미있는 예시로, 우리가 보는 지하철 노선도는 현실과 차이가 심합니다. 역간 거리도 다르고, 실제로는 곡선으로 연결된 선들이 직선으로 그려져 있죠. 그래서 강남-역삼은 가까운데, 역삼-선릉은 똑같은 한 정거장이지만 걸어가기 힘들 정도로 멉니다. 어떻게 아냐구요? 저도 알고싶지 않았습니다... 하지만 우리는 이 노선도를 통해 복잡한 지하철 시스템을 이해하고, 목적지까지 가는 경로를 쉽게 파악할 수 있습니다. 이처럼 추상화는 복잡한 현실을 단순화하여 이해하기 쉽게 만들어줍니다.
우리가 사용하는 어휘도 현실을 추상화한 것이라고 생각합니다. '책상'라는 단어가 어떤 사물을 뜻하는지 대충 생각해 보자면, '다리 네개로 상판을 지지해서 위에 뭔가를 올릴 수 있는 가구'를 말하죠. 하지만 다리를 네개 대신 세개로 만든다면요? 혹은 가운데 큰 다리를 하나만 두는 책상은요? 그렇다면 책상의 의미를 너무 성급하게 정의해버린 것이 아닐까요? 수많은 다양한 형태의 책상들의 공통 특성을 계속해서 추상화하며 의미를 만든 것이 '책상'이라는 단어입니다. 즉, 우리가 사용하는 일반명사는 개별 사물들의 공통점을 추출하여 만든 추상적 범주라고 할 수 있습니다.
MBTI야 말로 추상화의 장단점을 아주 잘 보여주는 예시라고 할 수 있습니다. 복잡한 인간의 성격을 16가지 유형으로 추상화하여 설명합니다. 우리는 INTP라면 조용하고, 상상하고, 논리적이고, 계획적이지 않은 사람일 것이라고 생각합니다. 이렇게 네자리 알파벳만으로 사람의 성격을 쉽고 빠르게 판단할 수 있게 되었습니다. 하지만 '고작 16가지 유형으로 사람을 어떻게 설명하냐'라는 말을 하기도 합니다. 복잡한 것을 추상화하는 과정에서 세부적인 차이를 간과할 위험이 있다는 것이죠.
이처럼 우리는 모든 세부 사항을 다 기억하고 처리하는 것은 불가능하므로 핵심적인 특성만을 추출하여 단순화된 모델을 만듭니다. 이 과정에서 일부 정보는 손실되지만, 대신 전체 패턴을 더 명확하게 인식할 수 있게 됩니다.
추상은 왜 부정적인 의미를 가질까?
위 내용을 바탕으로 현실 세계에서 '추상적'이라는 표현이 종종 부정적으로 사용되는 이유를 정리해 보겠습니다.
먼저, 추상적 표현은 구체적이지 않고 막연하여 실용성과 명확성이 부족합니다. "UX을 개선해야 합니다!" 라고 이야기한다면 누가 그걸 반대할까요? 개선하고 싶어도 구체적으로 뭘 어떡해야 개선되는지를 모르니까 문제인 것 아닐까요? 즉, 추상적 선언은 구체적 행동 지침이 없어 실천으로 이어지기 어렵습니다. 현실에서는 구체적인 '무엇을, 어떻게'가 없는 추상적 개념은 비실용적이며 다소 허황된 것으로 받아들여질 수도 있겠죠.
둘째, 추상적 개념은 실체가 없는 개념이라 주관적 해석의 여지가 큽니다. '정의', '자유', '평등'과 같은 추상적 가치는 사람마다 다르게 해석될 수 있어, 오히려 소통을 방해하는 요소가 되기도 합니다. 100분 토론에 나오는 정치인들을 보면 패널들이 모두 '정의', '평등', '자유'를 위한다고 주장하지만, 그 구체적 내용은 완전히 다른 경우가 많습니다.
셋째, 단순화, 요약하기 위해 추상화하는 과정에서 세부 사항이 손실될 수 있습니다. 예를 들어, '평균적으로 만족도가 높다'는 추상적 결론은 일부 심각하게 불만족한 사례들을 숨기고, 꼭 해결해야 하는 구체적 문제를 놓치게 만들 수 있습니다. 또한 MBTI가 개인을 제대로 설명하지 못하는 등 지나치게 단순화된 모델은 현실을 제대로 반영하지 못할 수도 있습니다.
프로그래밍의 잘못된 추상화
지금까지 현실 세계에서 '추상'이라는 개념이 어떻게 이해되고, 왜 부정적으로 인식되는지 살펴보았습니다. 이제 현실 세계의 추상화를 프로그래밍에 대입하여 '잘못된 추상화'에 대해 정리해보겠습니다.
'구체적이지 않고 막연한' 추상화
현실 세계에서 '추상'이 '구체적이지 않고 막연한 것'이라서 부정적으로 인식되었던 것처럼, 프로그래밍에서도 유사한 문제가 존재합니다.
모호한 네이밍, 의도가 불분명한 인터페이스
프로그래밍에서 가장 흔한 '잘못된 추상화'는 모호한 네이밍이라고 생각합니다. 'UX를 개선하자' 라는 추상적인 말처럼, 모호한 네이밍은 코드를 읽는 사람에게 실질적인 정보를 제공하지 못합니다. 따라서 함수나 변수의 이름은 그것이 무엇을 하는지, 왜 존재하는지, 어떻게 사용되는지를 명확히 알려줘야 합니다.
예를 들어, processData()
, handleEvent()
, getData()
같은 이름들은 구체적인 의도나 기능을 명확히 전달하지 못합니다. calculateTotalPriceWithTax()
, validateUserCredentials()
와 같이 구체적인 이름이 더 유용합니다. 만약 이름을 구체적으로 짓자니 너무 길어지고, 이해하기 어려운 영단어를 사용해야 한다면 한글 변수명도 좋은 방법이라고 생각합니다. 예를 들어, calculateTotalPriceWithTax()
대신 세금포함총가격계산하기()
와 같이 작성하면 훨씬 이해하기 쉽죠.
마찬가지로 의도가 불분명한 인터페이스도 문제가 됩니다. 예를 들어, 어떤 함수가 특정 플래그를 사용하여 동작을 제어한다고 가정해보겠습니다.
// 나쁜 예
function fetchData(id: string, flag: boolean) {
// flag가 무슨 의미인지 알 수 없음
}
// 좋은 예
function fetchData(id: string, includeDeleted: boolean) {
// 삭제된 항목을 포함할지 여부를 명확히 알 수 있음
}
flag
보다 includeDeleted
가 외부와 더 원활하게 소통할 수 있다고 생각합니다. 즉, 모호한 네이밍과 의도가 불분명한 인터페이스는 코드의 가독성을 떨어뜨리고, 유지보수를 어렵게 만듭니다.
문서화되지 않은 암묵적 규칙
현실에서 구체적으로 정의되지 않은 규칙이 자의적으로 해석될 수 있는 것처럼, 프로그래밍에서도 명시적으로 문서화되지 않은 규칙이나 가정은 문제를 일으킬 수 있습니다.
예를 들어, 어떤 함수가 특정 순서로 호출되어야 한다거나, 특정 상태에서만 동작한다거나, 특정 형식의 입력만 처리할 수 있다는 제약 조건이 문서화되지 않으면 잘못된 추상화라고 생각합니다. '두발이 단정해야 한다'는 모호한 교칙이 학교를 무법지대로 만드는 것처럼, 프로그래밍에서도 명확한 규칙이 없으면 개발자 간의 오해와 버그를 초래할 수 있습니다.
제약 조건이 명확히 문서화(테스트)되고, 타입 시스템을 통해 강제되어야 추상화가 효과적으로 작동할 수 있습니다. 즉, '귀에 닿지 않는 길이'처럼 구체적인 규칙을 만들어두면 좋겠죠.
'실체가 없는 개념'으로서의 추상화
현실 세계에서 추상적 개념은 실체가 없기 때문에 부정적으로 인식될 수 있었습니다. 프로그래밍에서도 비슷하게 실체가 없는(주로 과도한 추상화로 인해) 추상화 문제가 발생할 수 있습니다.
과도한 인터페이스 분리
인터페이스 분리 원리(Interface Segregation Principle)은 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙이라고 합니다. 즉, 큰 인터페이스보다는 작고 구체적인 인터페이스가 불필요한 의존성을 줄이기 좋다는 것이죠. 스탬프 결합도(Stamp Coupling) 개념과도 어느정도 유사성이 느껴집니다. 스탬프 결합도는 함수가 실제로 필요한 것보다 더 많은 데이터를 매개변수로 전달받는 상황을 말하며, 이 역시 꼭 필요한 데이터만 의존해야 한다는 원칙입니다. 그런데 이런 원칙을 제대로 고민하지 않고 마구잡이로 적용하다 보면 '실체 없는 추상화' 문제가 생깁니다.
예시를 들어보겠습니다. 단순한 사용자 정보를 표시하는 기능이 필요한 상황입니다.
// 과도하게 분리된 인터페이스들
interface Name {
name: string;
}
interface Age {
age: number;
}
interface Email {
email: string;
}
const UserProfile = ({ user }: { user: Name & Age & Email }) => {
return (
<div>
<h2>{user.name}</h2>
<p>나이: {user.age}세</p>
<p>이메일: {user.email}</p>
</div>
);
};
- 불필요한 복잡성:
Name
,Age
,Email
처럼 작은 인터페이스를 여러 개 만들었지만, 실제로는 항상 같이 사용됩니다. 즉, 무의미하게 인터페이스를 나눠 복잡도만 높아졌습니다. - 실용성 부족: 이런 작고 구체적인 인터페이스가 실제로 개별적으로 재사용되거나 확장될 계획이 없다면 과도한 추상화입니다. 나중에 필요해지면 그때 나눠도 되지 않을까요?
더 간단하고 직관적인 방식은 하나의 인터페이스로 묶는 것입니다.
// 실제 필요에 맞는 간단한 인터페이스
interface User {
name: string;
age: number;
email: string;
}
목적이나 분명한 가치 없이 그저 '원칙이니까 지키면 좋지 뭐...'라는 마인드, 근거없는 미래 확장성, 디자인 패턴을 위한 패턴 등으로 인한 과도한 추상화는 코드를 복잡하게 만들고, 작업할 개발자의 부담만 늘리는 경우가 많습니다. 좋은 추상화는 코드가 갖는 문제를 해결하고, 더 이해하기 쉽게 만드는 것이 목표입니다.
과도한 도메인 모델
리팩터링 책에는 '프리미티브 집착'이라는 안티패턴이 등장합니다. 복잡한 개념을 객체 대신 기본 타입(문자열, 숫자 등)으로만 표현하려는 패턴을 말하는데요, 반대로 지나치게 복잡한 도메인 모델을 구축하는 것도 문제가 될 수 있습니다.
예를 들어, 현재 시스템에서는 단순히 사용자 이름과 이메일만 필요한데 전체 사용자 프로필, 권한 관리, 활동 내역 등을 포함하는 복잡한 사용자 모델을 구축하는 경우입니다.
불필요한 기능과 데이터를 포함하고, '이상적인' 모델이라는 추상적 개념에 집중하다 보니 현실에서의 필요성을 간과하게 된거죠. 이로 인해 코드가 복잡해지고 유지보수성이 떨어지며, 실제 사용자가 필요로 하는 기능을 구현하는 데 방해가 될 수 있습니다.
철학자들이 정의나 진리와 같은 추상적 개념을 과도하게 이론화하여 현실과는 동떨어진 느낌을 받는 것처럼, 이런 과도한 도메인 모델도 실질적인 문제 해결보다는 '이상적 설계'라는 추상적 개념에 집착한 결과물라고 생각합니다.
'단순화, 요약된 것'으로서의 추상화
현실 세계에서 '추상'이 구체적 대상을 단순화, 요약하는 과정에서 문제가 발생할 수 있는 것과 같이, 프로그래밍에서도 추상화 과정에서 여러 문제가 발생할 수 있습니다.
누수 추상화(Leaky Abstractions)
누수 추상화는 복잡한 것을 감추고 단순화하기 위해서 추상화했지만 역설적이게도 추상화된 부분을 잘 활용하려면 복잡한 내부 구현을 이해해야 하는 상황을 말합니다. 위키에 적혀있는 예시 중 하나를 요약하자면, SQL 언어가 데이터 접근 방식의 복잡성을 숨기지만, 성능을 최적화하려면 결국 내부 작동 방식을 이해해야 한다는 이야기입니다.
누설되는 추상화는 추상화가 단순화해야 할 복잡성을 충분히 단순화하지 못할 때 문제가 됩니다. 사용자가 추상화 계층 아래에 숨겨진 세부 사항을 이해해야 하므로, 추상화의 주요 목적인 '복잡성 감소'가 제대로 이루어지지 않게 됩니다. 아무리 단순화하려 해봐도 대상이 너무 복잡하다면 완벽하게 단순화할 수는 없겠죠. MBTI처럼요.
세부 사항의 과도한 은닉
바로 위에서는 추상화에 가려진 세부 사항이 누수되는 것이 문제였다면 이번에는 너무 많은 세부 사항을 숨길때 발생하는 문제를 생각해보겠습니다.
예를 들어, tsconfig.json
에서 skipLibCheck
옵션을 사용하여 라이브러리의 타입 검사를 건너뛰는 경우입니다. 이 옵션을 사용하면 타입스크립트가 라이브러리의 타입 정의를 검사하지 않으므로, 컴파일 속도가 빨라진다고 알려져 있습니다. 네이밍도 뭔가 lib(라이브러리)의 check를 skip하는 것으로 보이죠. 하지만 사실 이 옵션은 d.ts
파일을 검사하지 않도록 설정하는 것입니다. 즉, 라이브러리 뿐만 아니라 프로젝트 내의 모든 d.ts
파일을 검사하지 않게 되죠. 이로 인해 타입 오류를 놓치게 될 수 있습니다.
이런 상황은 지하철 노선도가 실제 역 간 거리를 무시하고 어떻게 연결되어 있는지만 표시하는 것과 유사합니다. 노선도는 지하철 이용에는 유용하지만, 역 사이를 걸어가려면 완전히 다른 정보가 필요합니다.
과도한 일반화
아직은 잘 모르겠지만, 미래에는 이것도 필요할 수 있다는 불확실한 믿음으로 인해 지나치게 일반화된 추상화를 만드는 것도 문제가 될 수 있습니다. 예를 들자면, 특정 컴포넌트가 모든 상황에 대응할 수 있도록 수많은 prop을 받는 경우가 있습니다.
interface SomeComponentProps {
type?: 'default' | 'primary' | 'secondary' ... ;
size?: 'small' | 'medium' | 'large' ... ;
variant?: 'filled' | 'outlined' | 'ghost' ... ;
rounded?: boolean | 'small' | 'medium' | 'large' | 'full' ... ;
shadow?: boolean | 'small' | 'medium' | 'large' ... ;
padding?: string | number;
margin?: string | number;
onClick?: () => void;
children: React.ReactNode;
// ... 수많은 prop들
}
const SomeComponent = ({
type = 'default',
size = 'medium',
variant = 'filled',
rounded = false,
shadow = false,
padding = 0,
margin = 0,
onClick,
children,
}: SomeComponentProps) => {
// ...
};
컴포넌트 하나로 모든 상황을 처리하면 좋겠지만, 오히려 개발자가 어떻게 사용해야 할지 혼란스러워질 수 있습니다. 이런 경우에는 컴포넌트를 여러 개로 나누는 것이 더 나은 선택일 수 있습니다.
interface BaseComponentProps {
type?: 'default' | 'primary' | 'secondary';
variant?: 'filled' | 'outlined' | 'ghost';
onClick?: () => void;
children: React.ReactNode;
}
interface AComponentProps extends BaseComponentProps {
size?: 'small' | 'medium' | 'large';
}
const AComponent = (props: AComponentProps) => {
// ...
};
interface BComponentProps extends BaseComponentProps {
rounded?: boolean | 'small' | 'medium' | 'large' | 'full' ... ;
shadow?: boolean | 'small' | 'medium' | 'large' ... ;
}
const BComponent = (props: BComponentProps) => {
// ...
};
과도한 일반화는 불필요하게 복잡도를 높이고, 코드의 가독성과 유지보수성을 해칩니다. 꼭 추상화에 적용되는 말은 아니지만, YAGNI(You Ain't Gonna Need It)라는 말이 있습니다. 지금 당장 필요하지 않다면 만들지 말라는 뜻이죠.
잘못된 추상화에서 나는 냄새
현실 세계의 추상에 대한 관점을 프로그래밍에 적용해보면, 이게 '잘못된 추상화'인지 아닌지 판단할 때 도움을 받을 수 있습니다.
구체적 사례가 충분하지 않은 상태에서의 반복되는 로직 추상화
이런 경우를 '성급한 추상화'라고 부르죠. 너무 이른 시점에 추상화하면 어디까지가 공통적인 로직일지 판단하기 어렵습니다. 리팩터링 책에서는 'Rule of three'라고 하여 반복되는 코드가 최소 세 번 이상 나타나고 패턴이 명확해진 후에 추상화를 고려하라고 말합니다.
'Rule of three'는 언제 추상화를 도입할지 결정하는 가이드라인으로 볼 수 있겠습니다. 처음 비슷한 코드를 작성할 때는 그냥 작성하고, 두 번째로 유사한 것을 발견하면 중복을 허용하되 주의를 기울이고, 세 번째로 같은 패턴이 발견되면 그때 리팩토링을 통해 추상화를 도입하라는 것입니다.
이처럼 '성급한 추상화'는 반복되는 구체적 사례에 기반해야 한다는 원칙을 따르지 않아 발생하는 문제입니다. 위에서 '책상'이라는 단어를 성급하게 정의하면 다리가 세개인 책상은 책상이 아니냐는 이야기를 했었는데요, 이처럼 구체적 사례에서 공통점을 찾아내는 것이 중요합니다.
추상화 비용이 이득보다 클 때
모든 추상화에는 비용이 따르기 마련입니다. 일단 코드를 작성하는 것 자체도 그렇고, 추상화 계층으로 인해 모듈을 간접적으로 의존하게 되죠. 이는 학습 곡선, 디버깅 복잡성, 때로는 성능 오버헤드로 이어질 수 있습니다. 추상화로 얻는 이득이 비용보다 작다면 '잘못된 추상화'를 의심해볼 수 있겠습니다.
예를 들어, 단 한 번만 사용될 기능을 위해 복잡한 추상화 계층을 도입하는 것은 전혀 효율적이지 않습니다. 또는 몇 번 반복된다고 하더라도 아주 간단한 PoC이고, 지속적으로 운영 및 유지하지 않는 프로젝트라면 굳이 정교한 추상화를 위해 리소스를 쏟는 것보다 중복 코드를 만들어서라도 빠르게 구현하는 것이 효율적이겠죠.
본인만 아는 복잡한 디자인 패턴을 위해 추상화 계층을 만드는 것도 마찬가지입니다. 개인적으로 동료들에게 학습을 강요하는 복잡한 디자인 패턴보다는 보편적이고 이해하기 쉬운 코드를 작성하는 것이 더 좋다고 생각합니다. 물론 디자인 패턴이 필요할 때도 있지만, 그 패턴이 정말 필요한지, 아니면 단순히 '그럴듯한' 디자인 패턴을 사용하고 싶은 것인지 고민해봐야 합니다.
유연성과 복잡성의 균형
모든 가능한 상황에 대응하기 위한 과도한 유연성은 불필요한 복잡성을 초래할 수 있습니다. 함수나 컴포넌트가 너무 많은 매개변수, 구성 옵션을 요구한다면 잘못된 추상화를 의심해볼 때라고 생각합니다.
너무 많은 조합 가능성과 구성 옵션을 제공하는 지나치게 추상화된 시스템은 이해하기 어렵고 잘못 사용되거나, 혹은 아예 사용을 포기하게 될 수도 있습니다. 현실에서 너무 많은 해석의 여지가 있는 추상적 표현이 의사소통을 방해하는 것처럼, 지나치게 유연한 API는 사용자에게 혼란을 줄 수 있습니다.
좋은 추상화는 필요한 유연성을 제공하면서도, 불필요한 복잡성은 피하는 균형을 찾아야 합니다. 물론 그건 쉽지 않다고 생각합니다. 저는 보통 셀프 리뷰 -> 동료 리뷰를 거치면서 균형을 맞추려고 노력하는 편입니다.
특수한 경우가 계속 추가되는 경우
처음에는 깔끔하게 잘 추상화 했다고 생각했지만 시간이 지나면서 특별한 경우나 예외 처리가 계속해서 추가되는 경우가 있습니다. 이 경우 역시 잘못된 추상화일 가능성이 있습니다.
기존 추상화를 계속 건드리는 것보다는 다시 추상화 해보거나, 아예 추상화 계층을 없애고 중복 코드를 사용하면서 다시 기회를 노리는 것도 방법이라고 생각합니다. 혹은 하나의 추상화가 아니라 여러 개의 추상화가 필요했을 수도 있겠죠.
이런 점에서는 비즈니스 맥락을 이해하는 것이 꽤나 중요하다고 생각합니다. 비즈니스가 바뀌면 요구사항이 바뀌고, 그에 따라 추상화도 바뀌어야 하니까요. 결국 개발자도 비즈니스를 이해하고, 도메인 지식을 갖출 필요가 있다고 생각합니다.
네이밍을 자주 바꾸는 경우
추상화된 함수명을 계속 고민하게 된다면 추상화가 제대로 정의되지 않았거나, 너무 많은 책임을 지고 있지는 않은지 의심해볼 필요가 있습니다. 우리가 현실에서 정의내린 추상적인 개념들은 자주 바뀌지 않습니다. 정의가 자주 바뀌는 것은 개념 자체가 정의내릴 수 있을 정도로 명확하지 않거나, 너무 많은 것을 포함하고 있다는 것이 아닐까 싶습니다.
예를 들어, User
라는 추상화가 사용자 정보, 권한, 활동 내역 등을 모두 포함하고 있다면, 이 추상화는 너무 많은 책임을 지고 있는 것입니다. 이런 경우에는 UserProfile
, UserPermissions
, UserActivity
와 같이 더 구체적인 추상화로 나누는 것이 좋습니다. 혹은, 여러 도메인을 포함하는 프로젝트라면 ADomainUser
, BDomainUser
와 같이 구분해야 할 수도 있겠죠.
나가며: 좋은 추상화란?
추상화에 대해서는 언젠가 한 번쯤 글을 써보고 싶다고 생각했는데요, 사실 제 머리 속에서 '추상화'라는 개념이 너무 추상적이라 글로 정리하기가 어려웠습니다. 이번 기회에 느낌적인 느낌으로만 알고 있던 추상화에 대해 정리해볼 수 있어 좋았습니다. 물론 이 글은 제 개인적인 생각이 묻어있으니, 다른 분들은 또 다르게 생각하실 수도 있습니다.
제가 생각하는 좋은 추상화의 조건은 다음과 같습니다.
- 구체적 사례에서 출발하기
- 명확한 의도와 책임 정의하고, 좋은 네이밍하기
- 옆자리 동료의 관점에서 설계하기
- 비즈니스 맥락을 염두에 두기
- 적절한 추상화 수준 선택하기
제가 멋지게 추상화한 코드가 오히려 다른 사람에게는 와닿지 않거나, 복잡하게 느껴질 수도 있고, 다른 사람이 추상화해둔 코드가 제가 가져다 쓰기에는 애매하게 느껴질 때도 있는 것 같습니다. 추상화가 복잡도를 다루는 기술이라는 점을 염두에 두고, 좋은 동료들과 의견을 나누다 보면 점점 좋은 추상화라는 것에 가까워지지 않을까 생각합니다.