if문을 잘쓰면 아저씨들이 좋아해준다.

※주의: 아래 글은 Javascript 코드 기준으로 작성하였습니다.

이 세상에 if문이 없는 프로덕트는 존재하지 않을겁니다. 개발을 함에 있어서 if문이란 너무나도 중요한 존재입니다. 하지만 우리는 그 if문을 정말로 합리적으로 작성했을까요? 어쩌면 필요없는 if문 일 수도 있고, 잘못 작성하여 로직상 1~2초에 손해를 볼 수도 있습니다.

어떤 시니어 개발자는 말합니다.

"요즘..@@@ 개발자들은...@@@ if문..@@ 최적화도...@@ 제대로 못한다....@@@"

위 말을 들은 주니어 개발자인 당신... 너무 서운하고 자괴감들고 괴롭고 개발을 접어야할 거 같은 생각을 할지도 모릅니다.

하지만~! 이 글을 읽은 당신. 저도 잘 모르지만 함께 if문 최적화의 세계를 여행하며 한층 성장해봅시다.

그래서 if문 최적화 뭔데?

if문 최적화란 조건문(if)을 작성할 때 불필요한 비교나 연산을 줄이고, 실행 속도를 개선하거나 가독성을 높이는 기법을 의미합니다.

위 정의에서 말했듯 if문 최적화는 단순히 성능 관점에서만이 아니라 가독성을 높이는 작업도 의미합니다.

저는 이 글에서 if문 최적화를 단순히 성능 관점에서만 다루지 않으려고 합니다. 실제 프로덕션 환경에서는 성능만큼 이를 유지보수할 수 있도록 가독성 있는 코드를 작성하는 것이 중요하기 때문입니다.

근데 저는 if문 최적화를 왜 해야하는지 모르겠어요.

우린 위 질문에 대한 해답을 찾기 위해서는 "if문이 실제로 어떻게 작동하는가?"를 알아야합니다. 그래야 공학적인, 논리적인 이유를 기반으로 최적화를 진행할 수 있기 때문입니다.

하지만 여기서 if문이 CPU에서 어떻게 동작하는지 딥다이브한 설명을 하진 않겠습니다.
(설명이 너무 길어져 if문 최적화라는 주제에 벗어날 수 있기 때문입니다. 나중에 글로 따로 빼겠습니다.)

우선 CPU에 "조건문" 자체는 존재하지 않습니다. 대신 비교 -> 분기 하는 방식으로 처리합니다. 간략하게 말해서 값을 "비교" 해서 상태(플래그 레지스터)를 설정하고 그 상태에 따라 "분기"하여 다른 주소로 실행을 이어갑니다. 즉 참이면 이 위치에 코드를 실행하고 거짓이면 다른 주소에 있는 코드를 실행하도록 IF문 동작 원리를 구현합니다. 그래서 점프한다고 표현하기도 합니다.

근데 이 과정에서 CPU는 효율적인 연산을 위해 분기 예측을 합니다. 말 그대로 CPU는 성능 향상을 위해 분기 예측기를 사용하여 다음 실행할 명령어의 주소를 예측합니다. 그럼 이런 반론이 들어올 수 있습니다. "아니 예측을 해버리면 예측이 틀리면 개손해아닌가요?" 팩트입니다. 그래서 if문을 최적화를 더욱 권장합니다.

Image

성능 관점에서 if문 최적화

성능 관점에서 if문 최적화란 주로 계산량을 줄이기 위한 목적으로 진행합니다.
여기서 계산량을 줄인다는 것은 CPU가 계산을 적게하도록 유도하는 것 입니다. 즉 평균적으로 덜 계산하고, 덜 분기하고, 분기 예측을 더 맞추게 만듬으로써 수행시간이 빨라집니다.

1. "좌에서 우로"를 기억합시다.

많은 언어들은 if문 안에 AND 연산(&&)이나 OR 연산(||)에 경우 좌측에 위치한 조건을 먼저 검사 또는 실행 시킵니다. 이 때 만약 좌측에 위치한 조건이 True라면 우측에 위치한 조건은 검사하지 않고 스킵할 수 있습니다. 즉 좌측에 상대적으로 가벼운 연산이면서 예측 가능한 조건을 두는 것이 유리합니다.

// Bad
if(굉장히무거운로직() || 상대적으로가벼운로직()) {...}

// Good
if(상대적으로가벼운로직() || 굉장히무거운로직()) {...}

2. switch문을 적극 할용합시다.

다수의 상수 비교에는 swtich문이 더 가독성이 좋고 컴파일러의 의해 점프 테이블로 최적화될 가능성이 높습니다.

3. 가끔은 if문 자체를 없앨 수도 있습니다.

if문을 생각보다 많은 수학적 연산으로 대체할 수 있습니다. 이 경우 분기 예측이 실패하는 경우가 사라집니다. 분기 자체가 없어지기 때문입니다. 하지만 이 경우 가독성을 해칠 우려도 있습니다.

// Before
let 현재값 = 입력값;
if (현재값 < 최소값) 현재값 = 최소값;
else if (현재값 > 최대값) 현재값 = 최대값;

// After
const 현재값 = Math.min(최대값, Math.max(최소값, 입력값));

4. 카르노맵 기반 if문 최적화 기법도 있습니다.

복잡한 if문을 최적화할 때 카르노맵을 기반으로 최적화할 수 있습니다. 카르노맵 이란 불리안일 때 논리식을 간소화하는 방법 중 하나입니다. 세명컴퓨터고등학교 1학년 컴퓨터시스템일반 교과서에 나오기 때문에 자세한 설명은 스킵하겠습니다.

// Before
if ((한국배송 && VIP회원) || (한국배송 && 장바구니충족) || (한국배송 && 무료배송프로모션)) {
  무료배송적용();
}

//After
if (한국배송 && (VIP회원 || 장바구니충족 || 무료배송프로모션)) {
  무료배송적용();
}

코드 관점에서 if문 최적화

코드 관점에서의 if문 최적화는 언어적 특성에 따라 변할 수 있습니다.

코드 관점에서 if문 최적화란 주로 가독성을 높히기 위한 목적으로 진행합니다.
여기서 가독성이란 글이나 코드를 얼마나 쉽게 읽히고 이해되도록 작성하는가를 뜻합니다. 즉 그만큼 유지보수를 하는데 용이하게합니다.

1. 적절한 부정 연산자 사용과 가드 패턴으로 조기 반환을 사용합시다.

들여쓰기를 최소화하고, 조건 흐름을 직관적으로 보여줄 수 있습니다. if문 분기를 일찍 탈출하기 때문에 때에 따라서 추가 검사를 덜할 수 있습니다.

// Before
function 처리(사용자) {
  if (사용자) {
    if (사용자.활성화됨) {
      return "처리 중...";
    } else {
      return "비활성화됨";
    }
  } else {
    return "사용자 없음";
  }
}

// After
function 처리(사용자) {
  if (!사용자) return "사용자 없음";
  if (!사용자.활성화됨) return "비활성화됨";
  return "처리 중...";
}

2. 불필요한 if문을 줄입시다.

단순한 if문의 경우 if문을 중첩하여 사용하는 것 보다 언어적 특성을 이용해서 다양한 연산자로 치환할 수 있습니다.

// Before
if (사용자) {
  if (사용자.활성화됨) {
    console.log("활성 사용자");
  }
}

// After
if (사용자?.활성화됨) {
  console.log("활성 사용자");
}

// Before
if (로그인됨) {
  결과 = "환영합니다";
} else {
  결과 = "로그인해주세요";
}

// After
const 결과 = 로그인됨 ? "환영합니다" : "로그인해주세요";

마무리하며...

솔직히 if문 최적화 기법 더 적기 귀찮아서 조금만 적었습니다. 검색하시거나 AI에게 물어보면 이 글보다 더 고급진 방식을 찾아볼 수 있을겁니다.

요즘에는 if문 최적화가 꼭 필요한가라는 의견도 있습니다. if문 최적화도 컴파일러의 역할이라고 생각하는 사람도 존재하고, 언어적 특성과 도메인 로직에 따라 대중적인 if문 최적화 패턴을 적용하는 것이 오히려 악영향을 줄 수도 있기 때문입니다.

엄청난 안티패턴을 사용하지 않는 이상 if문 최적화를 통해 얻을 수 있는 이점은 보통 1ms 단축하기 정도밖에 되지 않습니다. 막상 글을 적고보니 성능을 중점으로 if문 최적화에 대해 다룬 것 같습니다.

사실 대부분의 상황에서 성능만을 위한 if문보다 가독성 있는 if문이 트레이드오프상 유지보수나 협업 관점에서 더 큰 이득을 주는 경우가 많습니다. 이를 인지하시고 너무 성능에만 신경쓰며 코드를 작성하면 좋긴 하지만 너무 매몰되진 않았으면 좋겠습니다. 이런 것도 있구나~ 라고 읽어주셨으면 좋겠습니다. 감사합니다.