성급한 추상화 지양하기

김준엽·2023년 5월 8일
0

클린코드

목록 보기
1/1

성급한 추상화란? 말 그대로 추상화를 미리 과도하게 적용한 것을 의미합니다. 복잡성을 줄이고 재사용성을 높여서 버그나 오타를 한 곳에서 해결하기 위해 추상화를 했지만, 오히려 안 하는 것이 더 나은 상황이 있습니다. 이런 성급한 추상화는 코드 이해와 관리를 더 어렵게 만듭니다.

성급한 추상화의 발생과 그로 인한 문제점

성급한 추상화가 어떤 경우에 발생하며, 그로 인한 문제점에 대해 알아보겠습니다.

  1. 애플리케이션에 'phil' 객체가 있습니다. 이 객체는 애플리케이션에서 UI에 이름을 표시하는 navDisplayName, profileDisplayName, cardDIsplayName 총 3군데에서 사용됩니다.
const phil = {
  name: { honorific: "Dr.", first: "Philp", last: "Radriquez" },
  username: "philpr",
};

// navigation.js
// ...
const navDisplayName = `${phil.name.first} ${phil.name.lest}`;
console.log(navDisplayName); // Philp undefined

// profile.js
// ...
const profileDisplayName = `${phil.name.first} ${phil.name.lest}`;
console.log(profileDisplayName); // Philp undefined

// card.js
// ...
const cardDisplayName = `${phil.name.first} ${phil.name.lest}`;
console.log(cardDisplayName); // Philp undefined

위의 예에서 name.lest 오타가 발견되었습니다. 중복이 발생했고 앞으로 버그가 발생하면 한 군데에서 코드를 수정할 수 있으니깐 추상화를 고려해볼 수 있습니다.

오타를 수정한 후(lest → last) 이름을 표시하는 로직을 추상화했습니다.

const phil = {
  name: { honorific: "Dr.", first: "Philp", last: "Radriquez" },
  username: "philpr",
};

function getDisplayName(user) {
  return `${user.name.first} ${user.name.last}`; // lest -> last
}

// navigation.js
// ...
const navDisplayName = getDisplayName(phil);
console.log(navDisplayName); // Philp Radriquez

// profile.js
// ...
const profileDisplayName = getDisplayName(phil);
console.log(profileDisplayName); // Philp Radriquez

// card.js
// ...
const cardDisplayName = getDisplayName(phil);
console.log(cardDisplayName); // Philp Radriquez

이 경우에는 추상화가 적절하게 적용되어 중복을 제거하고 코드의 유지보수를 용이합니다.

  1. 기획자가 profile 페이지에 'Dr. Philip Rodriguez'처럼 직위(honorific)를 같이 보여주기를 요청합니다. 그러면 우리는 선택할 수 있습니다. 이전에 추상화한 getDisplayName에 이 유스케이스를 적용하는 방법과 추상함수를 제거하는 방법이 있습니다. 보통 재사용성과 편리함으로 인해 추상함수 로직을 조금 개선하는 쪽으로 택합니다.
function getDisplayName(user, { includeHonorific = false } = {}) {
  let displayName = `${user.name.first} ${user.name.last}`;
  
	if (includeHonorific) {
    displayName = `${user.name.honorific} ${displayName}`;
  }

  return displayName;
}

// navigation.js
// ...
const navDisplayName = getDisplayName(phil);
console.log(navDisplayName); // Philp Radriquez

// profile.js
// ...
const profileDisplayName = getDisplayName(phil, { includeHonorific: true });
console.log(profileDisplayName); // Dr. Philp Radriquez

// card.js
// ...
const cardDisplayName = getDisplayName(phil);
console.log(cardDisplayName); // Philp Radriquez

이정도 조건문 추가는 괜찮습니다. 아직까지 봐줄만합니다. 하지만 이제부터 문제가 발생합니다.

  1. 기획자가 추가 변경사항을 요청합니다.
    • card에 사용자 아이디(username)를 괄호에 넣어 표시하기
    • navigation에 이름의 맨 앞글자(이니셜)만 표시하기

이러한 요구사항을 적용하기 위해 추상함수를 수정합니다.

const phil = {
  name: { honorific: "Dr.", first: "Philp", last: "Radriquez" },
  username: "philpr",
};

function getDisplayName(
  user,
  {
    includeHonorific = false,
    includeUserName = false,
    firstInitial = false,
  } = {}
) {
  let first = user.name.first;

  if (firstInitial) {
    first = `${first.slice(0, 1)}.`;
  }

  let displayName = `${first} ${user.name.last}`;

  if (includeHonorific) {
    displayName = `${user.name.honorific} ${displayName}`;
  }

  if (includeUserName) {
    displayName = `${displayName} (${user.username})`;
  }

  return displayName;
}

// navigation.js
// ...
const navDisplayName = getDisplayName(phil, { firstInitial: true });
console.log(navDisplayName); // Philp Radriquez

// profile.js
// ...
const profileDisplayName = getDisplayName(phil, { includeHonorific: true });
console.log(profileDisplayName); // Dr. Philp Radriquez

// card.js
// ...
const cardDisplayName = getDisplayName(phil, { includeUserName: true });
console.log(cardDisplayName); // Philp Radriquez

유지 보수의 편리함과 가독성을 위해 추상함수를 만들었는데, 어느 순간 조건문이 많아져 복잡해졌습니다. 이건 간단한 예제라서 복잡해 보이지 않을 수 있지만, 실제 비즈니스 로직을 담고 있는 코드에서는 더욱 복잡할 것입니다. 다른사람이나 미래의 자신이 이런 추상화 함수를 수정해야 하는 상황이 발생할 때 어려움을 겪습니다.

  1. 이번에는 profile에서 더 이상 honorific 표시를 빼달라는 요청이 들어옵니다.
// navigation.js
// ...
const navDisplayName = getDisplayName(phil, { firstInitial: true });
console.log(navDisplayName); // P. Radriquez

// profile.js
// ...
const profileDisplayName = getDisplayName(phil);
console.log(profileDisplayName); // Philp Radriquez

// card.js
// ...
const cardDisplayName = getDisplayName(phil, { onlyUsername: true });
console.log(cardDisplayName); // Philp Radriquez (philpr)

profileDisplayName에서 넣어준 includeHonorific 옵션을 지웁니다. 하지만 추상함수의 includeHonorific 로직을 지우기에는 조금 꺼려집니다. 그 이유는 다음과 같습니다.

  • 코드를 유지하는 비용은 작고 이 코드를 제거하여 버그를 발생시킬 수 있는 리스크는 크게 느껴집니다.
    (특히 css에서 이런 생각을 많이 하셨을겁니다.)
  • ‘언제 또 includeHonorific 옵션을 쓸 일이 있을거야’하는 생각에 남겨두기도 합니다.
    (이건 git을 활용하면 됩니다.)

지금까지 과정에서 성급한 추상화의 문제점을 2가지로 볼 수 있습니다.

  • 변경사항에 유연하지 못합니다. 그래서 변경사항에 생길 때마다 추상함수의 유스 케이스는 늘어나서 유지보수가 어려워집니다. 뚜렷한 차이가 있는 유스케이스들을 담고 있는 함수를 잘 추상화되었다고 보기에는 어렵습니다.
  • 불필요한 테스트 코드를 작성해야 합니다. 실제로 사용하지 않을 유스 케이스를 테스트해야 합니다. profileDisplayName에서 세 가지 옵션이 true인 경우는 실제로 사용하고 있지 않는데 테스트 코드는 그 쓸데 없는 경우를 생각해서 작성합니다.

그러면 해결책은 무엇일까요?

추상화를 안하면 됩니다. 중복이 발생해도 추상화 하지말고 그대로 놔둡니다. 여기저기 추상화할 부분이 발견되더라도 복사 붙여넣기하고 중복되도록 내버려 둡니다. 그러면 처음 추상화하고 싶었을 때 생각했던 것만큼 비슷하지 않기 때문에 별개의 것으로 내버려 두길 잘했다는 생각이 들 수 있습니다. ‘중복은 잘못된 추상화보다 비용이 낮다.’ 이 말을 기억하시길 바랍니다.

만약, 비슷한 로직의 코드가 두 군데서 사용되고 코드의 양이 많다면, 함수를 두 개 따로 만듭니다. 이렇게 하면 각 함수의 기능이 명확해지고 해당 함수를 사용하는 컴포넌트에서 코드의 양이 줄어 가독성이 증가합니다.

profile
프론트엔드 개발자

0개의 댓글