본문 바로가기
개발/개발지식

Early return은 무조건 좋은 코드가 아니야!

by 핸디(Handy) 2025. 7. 20.

들어가며

많은 개발자들이 'early return'을 클린 코드로 가는 가장 쉽게 적용할 수 있는 예시로 생각합니다. 저 또한 그렇구요.

하지만 모든 상황에서 early return이 좋은 선택일까요?

특히 객체지향 설계와 비즈니스 로직이 얽힌 상황에서는 ealry return이 좋은 코드는 아닐 수 있습니다. 

이번 글에서는 ealry return에 대한 얘기를 시작으로 이를 객체지향적으로 풀어내는 과정까지 살펴보도록 하겠습니다.

그럼 시작.

예시 시나리오 ㅣ 장바구니 할인 정책

대표적인 예시인 장바구니 시스템으로 설명해보겠습니다.

할인 정책이 있으면 할인된 가격을 사용해야하죠. 그게 이 로직의 전부입니다.

코드로 나타내보면 다음과 같을 겁니다.

interface 할인정책 {
  calculateDiscount(total: number): number;
}

class 장바구니 {
  constructor(
    private items: { name: string; price: number }[],
    private discount: 할인정책 | null,
  ) {}

  getTotal(): number {
    const total = this.items.reduce((sum, item) => sum + item.price, 0);
    return total - this.discount.calculateDiscount(total);
  }
}

코드는 깔쌈하죠.

하지만 이 코드를 본 클린코드 신봉자는 갑자기 생각을 합니다.
"할인 정책이 없으면 total를 빠르게 return하면 코드의 의도가 명확하게 드러나고 좋겠구나"

그리고 바로 리펙토링 커밋을 날립니다.

  getTotal(): number {
    const total = this.items.reduce((sum, item) => sum + item.price, 0);

    /**
     * 할인정책이 없으면 총 금액을 반환
     */
    if (this.discount === null) {
      return total;
    }

    return total - this.discount.calculateDiscount(total);
  }

그리고 pr를 날리면 저 멀리 우리 시니어 엔지니어 형님의 샤우팅과 나를 호출하는 소리가 들립니다. 대체 뭐가 문제일까요?

할인정책의 예외 상황과 변경에 대한 취약성

시니어 엔지니어 형님은 의견이 이렇습니다.

"할인정책이 없을 수도 있따는 예외 상황이 비즈니스 로직 내부에 분기로 들어있잖아"

즉 장바구니는 할인 정책이라는 협력 객체의 존재 여부까지 책임지고 있으며, 그 책임은 애매하게 섞여 있다는 설명이었습니다.

할인 정책이 어떤 방식이든 "할인된 금액을 돌려주는 객체"라는 본질이 있는데, 그 책임을 null 체크로 위반하고 있습니다.

이런 경우를 OOP기준으로 변경에 취약한 코드라고 하는데 제가 간단한 예시를 추가해보겠습니다.

새로운 비즈니스 요구사항이 추가된다면

예를 들어 “X마트 특별세일”이라는 조건이 생겼다고 해봅시다.

“기본 할인 정책이 없을 경우, 일괄적으로 1,000원을 할인해준다.”

이 요구사항은 기존의 할인정책 === null 분기 조건을 수정해야합니다.

    if (this.discount === null) {
      return total - 1000; // X마트 특별세일
    }

혹자는 로직에 -1000원만 추가하면 되는거 아니냐 하는데, 실제 필드에서는 이러한 요구사항이 더욱 복잡하고 다양합니다.

그래서 점차 조건문이 복잡해지고, 도메인 정책이 클래스 내부에 하드코딩되며, 향후 다른 특별세일 조건(VIP 할인, 시간대 할인)등이 추가될 경우 if문이 늘어날 수밖에 없습니다.

당연히 정책이 추가되면 if문이든 switch든 로직이 추가될 수밖에 없습니다. 하지만 문제는 그 역할이 단순하 가격만 가져오는 장바구니의 getTotal 메서드 내부에 있다는 것입니다.

그럼 좋은 코드 예시로 살펴보겠습니다.

할인정책은 할인정책의 책임이 있다.

class 장바구니 {
  constructor(
    private items: { name: string; price: number }[],
    private discount: 할인정책,
  ) {}

  getTotal(): number {
    const total = this.items.reduce((sum, item) => sum + item.price, 0);
    return total - this.discount.calculateDiscount(total);
  }
}

getTotal은 다시 원복되었고

interface 할인정책 {
  calculateDiscount(total: number): number;
}

class 할인정책클래스 implements 할인정책 {
  constructor(private type: 'percent' | null) {}

  calculateDiscount(total: number): number {
    if (this.type === 'percent') {
      return total * 0.2; // 20% 할인
    }

    // X마트 특별세일: 정책이 없으면 1,000원 고정 할인
    return 1000;
  }
}

할인정책클래스에는 calculateDiscount가 마트의 할인정책을 책임지며 로직에 따라 적절한 할인 값을 전달해줍니다. 

마지막줄에 특별세일도 return 1000을 주면서 로직을 잘 정리했네요.

만약 ealry return를 사용한다면 여기에서 사용해야할 것입니다.

class 할인정책클래스 implements 할인정책 {
  constructor(private type: 'percent' | null) {}

  calculateDiscount(total: number): number {
    if (this.type === null) {
      // Early Return X마트 특별세일: 정책이 없으면 1,000원 고정 할인
      return 1000;
    }

    if (this.type === 'percent') {
      return total * 0.2; // 20% 할인
    }

    throw new Error('Invalid discount type');
  }
}

할인정책이 없으면 1000으로 빨리 돌려주고, 그 아래에 정책에 따라 처리하고 마지막에는 처리안되는 예시를 위한 에러던지기까지 리펙토링했습니다.

하지만 이 클래스, 책임이 너무 크지 않나요?

이전 코드에서는 ‘percent 할인’과 ‘X마트 특별세일’이라는 두 정책을 한 클래스에서 처리하고 있습니다. 그 자체로 잘 작동하고 있긴 하지만, 다음과 같은 징후가 보입니다.

  • 한 메서드 안에서 두 개 이상의 비즈니스 로직 정책이 공존함
  • 형후 정책이 늘어나면 calculateDiscount 는 건들기 어려운 더러운 코드가 될 예정
  • 테스트 코드 작성할때다 type 조합을 만들고 넣어야함

즉, 할인정책클래스는 하나의 인터페이스를 구현하는 것처럼 보이지만, 실제로는 여러 정책의 책임을 떠안고 있는 "거대한 조건문 객체"가 된 셈입니다.

다형성을 이용한 리펙토링

다형성을 도입하여 구조를 변경해봅시다.

interface 할인정책 {
  calculateDiscount(total: number): number;
}

class 퍼센트할인 implements 할인정책 {
  constructor(private rate: number) {}

  calculateDiscount(total: number): number {
    return total * this.rate;
  }
}

class 천원할인 implements 할인정책 {
  calculateDiscount(total: number): number {
    return 1000;
  }
}

class 할인없음 implements 할인정책 {
  calculateDiscount(total: number): number {
    return 0;
  }
}

그리고 이렇게 할인정책쪽 코드가 수정했다고 해서 장바구니 클래스에는 변경된 점이 없습니다. 

class 장바구니 {
  constructor(
    private items: { name: string; price: number }[],
    private discount: 할인정책,
  ) {}

  getTotal(): number {
    const total = this.items.reduce((sum, item) => sum + item.price, 0);
    return total - this.discount.calculateDiscount(total); // 그대로임!!
  }
}

그리고 이젠 코드를 사용하는 곳에서 정책을 결정하여 넘겨줘야합니다.

// 할인 정책 결정 로직
function getDiscountPolicy(userType: 'basic' | 'vip' | null): 할인정책 {
  if (userType === 'vip') {
    return new 퍼센트할인(0.2);
  }

  if (userType === 'basic') {
    return new 천원할인();
  }

  return new 할인없음();
}

// 사용하는 쪽
const userType: 'basic' | 'vip' | null = 'vip'; // 서버 응답이나 로그인 정보로부터
const discountPolicy = getDiscountPolicy(userType);

const 장바구니인스턴스 = new 장바구니(
  [
    { name: '에어팟', price: 30000 },
    { name: '맥북 스티커', price: 5000 }
  ],
  discountPolicy // 👈 다형성 객체 주입
);

console.log(장바구니인스턴스.getTotal()); // 할인된 총합 계산

이 구조가 좋은 이유는 책임 소개가 각각 명확해졌다는 것입니다.

  • 할인 방식의 계싼은 각 할인정책 클래스에서
  • 정책 선택 분기는 getDiscountPolicy에서
  • 총합 계산은 장바구니.getTotal()에서

만약에 여기 정책이 추가된다고 하면, 할인정책 클래스가 하나생기고, getDiscountPolicy에 조건식이 하나 추가될 뿐입니다.

생각의 흐름대로라도 할인정책이 변경되었다고해서 장바구니의 로직이 변경되면 안됩니다.  이게 바로 수정에는 닫혀있고, 확장에서는 열려있는 구조라고 말할 수 있습니다.

다형성은 무조건 좋은가?

실은 꼭 그렇지는 않습니다. 방금까지 작성한 코드를 보면 알 수 있듯이 코드량이 늘어났습니다. 객체가 분리되었고, 설계가 나름 복잡해졌습니다.

혹자는 장바구니가 슈퍼장바구니라 할인정책도 알아서 계산하고 최종가격을 주는 기존 코드도 나쁘지 않다고 할수 있습니다. 그것도 정답일수 있겠죠.

또한 이전에는 할인정책클래스 내부에서 조건식에 따라 할인정책을 확인할수 있었는데, 지금은 장바구니를 만들때부터 getDiscountPolicy를 통해서 할인정책을 만들고 장바구니인스턴스에 넣어야합니다. 즉 할인이 계산되는 시기와 코드로 집어넣을때가 다릅니다. 이 점은 코드를 읽기 어렵게 만들 수 있습니다.

따라서 무조건 좋은가라고 하면 아닙니다. 하지만 그 기준은 책임의 크기와 무게에 있다고 개인적으로 말하고 싶네요. 할인 정책은 실제로 계속 변경되고 조건과 로직이 늘어나는 영역입니다. 따라서 단일 클래스에 몰아넣는 방식보다는 개별로 나누고 다형성을 통해 구현하는게 더. 좋다고 판단했습니다.

여기서 중요한점은 제 나름의 기준과 지식 철학을 바탕으로 "판단"했다는 것입니다. 그리고 이 "코드를 작성한 책임"도 저한테 있죠

마무리

ealry return으로 시작한 얘기를 클린코드, OOP, 다형성 그리고 책임의 역할까지 이어보았습니다.

올해 하반기의 목표는 OOP에 대한 책 5권을 읽고 제 나름의 OOP 철학을 프론트엔드와 연결해보는 것이었고, 이 글은 그 여정의 첫걸음입니다. 끝까지 할 수 있기를 

여담이지만 개발자들은 코드를 작성한 책임이 한 사람에게 몰리는 상황을 방지하기 위해 "코드 리뷰"라는 행위를 하곤 합니다. 여러분의 코드리뷰는 책임을 분산하기 위함인가요? 아니면 책임을 몰아주기 위함인가요?

반응형

댓글