본문 바로가기
개발 도서

Five Lines of Code - 4장. 타입 코드 처리하기

by 성건희 2023. 9. 22.
반응형

Five Lines of Code - 4장. 타입 코드 처리하기

if else 를 사용하지 말자

public play(User user) {
  if (user.getStatus == Status.RUN) {
    user.setStamina(user.getStamina - 1);
  } else if (user.getStatus == Status.WAIT) {
    user.setStamina(user.getStamina + 1);
  }
}

위 코드에서 user status ‘ATTACK’ 이 추가되었다.

public play(User user) {
  if (user.getStatus == Status.RUN) {
    user.setStamina(user.getStamina - 1);
  } else if (user.getStatus == Status.WAIT) {
    user.setStamina(user.getStamina + 1);
  } else if (user.getStatus == Status.ATTACK) {
    user.setStamina(user.getStamina - 2);
  }
}

else if 문이 하나 더 추가되었다.
그런데 다른 메서드에서도 user 의 status 에 따라 값을 넣어주는 부분이 10군데나 있었다.
즉, 나는 status 1개를 추가 했는데 10군데를 수정해야 한다..
혹은 실수로 하나라도 추가를 안한다면.. 바로 이슈로 이어질 것이다.


또한 위에서부터 if 문을 읽어나가야하므로 코드 가독성도 떨어진다.
해결 방법이 없을까?

다형성을 사용해 if else 를 제거하자

위 예제에서 if 문을 제거하는 실습을 진행해보자.

1) 인터페이스를 만들어라

유저의 상태를 나타내는 UserStatus 인터페이스를 추가해보자.

interface UserStatus {
  void act(User user);
}

2) 인터페이스 구현체를 만들어라

그 후 UserStatus 를 상태에 맞게 구현해주자.

class Run implements UserStatus {

  @Override
  void act(User user) {
    user.setStamina(user.getStamina - 1);
  }
}
class Wait implements UserStatus {

  @Override
  void act(User user) {
    user.setStamina(user.getStamina + 1);
  }
}
class Attack implements UserStatus {

  @Override
  void act(User user) {
    user.setStamina(user.getStamina - 2);
  }
}

3) 기존 코드 수정

public play(User user, UserStatus userStatus) {
  userStatus.act(user);
}

다형성을 통해 if else 를 제거하니 코드가 훨씬 깔끔해졌다!

메서드 인라인화

메서드 추출이 기존 코드를 별도의 메서드로 분리하는 것이라면,
메서드 인라인화는 분리된 메서드가 더이상 가독성에 도움이 되지 않을때 메서드를 제거하고 기존 코드로 사용하는 방법이다.
보통 메서드가 한 줄만 있는 경우 메서드 인라인화 한다.
하지만 한 줄이더라도 코드가 복잡해서 가독성을 해치는경우, 메서드를 분리하는게 더 나을수도 있어 상황에 맞게 인라인화하자.

긴 if 문의 리팩터링

if 문은 함수의 시작에만 배치해야 한다.

함수안에 if else 문이 중간에 추가되어있는 코드가 있다면, if 문이 함수의 시작 지점에 오도록 메서드로 분리해야 한다.

if 문을 메서드로 분리했다면, if - else 문을 사용하지 말 것의 규칙을 지키도록 코드를 개선해보자.
위에서 설명한 다형성을 통한 if else 제거 기법을 활용하면 제거할 수 있을 것이다.

일반성 제거

일반성이란 여러 개념들을 하나의 개념으로 결합한 것을 말하는데, 일반성은 유연성이 떨어지고 코드를 변경하기 어렵게 만든다.
예를 들어 자동차 경주 메서드를 구현한다고 해보자.

public enum Car {
  AVANTE, K5, BMW530I, BENZ_ECLASS
}

public void run(Car car) {
  ...
}

입사한지 얼마안된 개발자가 위 코드를 봤을 때는 ‘모든 자동차의 종류가 car 매개변수에 들어오겠구나’ 라고 생각할 수 있다.
하지만 실제로는 ‘AVANTE’ 와 ‘K5’ 만 사용한다면..?
위 처럼 짜여진 코드는 혼동을 가져올 수 있다.
따라서 아래처럼 메서드 전문화 기법을 통해 해결할 수 있다.

메서드 전문화

메서드 전문화란 일반화를 줄이고 좀 더 특정화된 버전의 함수를 도입하는 과정을 말한다.
위 코드를 메서드 전문화 해보자!

public void runAvante() {
}

public void runK5() {
}

비슷한 로직을 2개의 메서드로 분리하다보니 코드의 중복처럼 보일 수 있어 많은 프로그래머의 본능에 반하는 방법이라고는 한다.
하지만 위처럼 좀 더 전문화된 메서드는 더 적은 위치에서 호출되어 필요성이 없어지면 더 빨리 제거할 수 있다.
또한 가독성도 더 좋아진다.

Switch 문을 사용하지 마라

switch 문은 다음의 문제가 있다.

  1. default 문으로 인해서 우리가 실수로 case 문을 추가하지 않았는데도 에러가 발생하지 않는다. (잘못된 동작을 할 수 있음)
  2. break 키워드를 만나기 전까지 계속 실행되어 (폴루스 로직) break 사용을 실수로 누락할 수 있다.

위 해결책은 default 문을 사용하지 않고, 모든 케이스에 return 을 지정해서 폴루스 문제를 해결할 수 있다.
더 나은 방법은 switch 문도 다형성을 통해 제거하는 방법이다.

코드 중복 처리

남아있는 if-else 문을 클래스의 코드 이관 으로 리팩토링 할 수 있다.
위에서 설명한 다형성을 이용해서 if-else 문을 제거하면 뭔가 클래스 메서드마다 불필요하게 정의하여 중복처럼 보인다. 그냥 추상 클래스를 정의해서 공통 코드를 넣는건 안되나?

인터페이스 VS 추상 클래스

할 수는 있다. 그렇게 하면 중복은 제거된다.
하지만 인터페이스는 구현체에서 재정의를 강제하지만, 추상 클래스는 이미 만들어진 메서드에 대해서는 구현을 강제하지 않아서 개발자의 실수가 생길 수 있다. (하지만 자바에서도 default 메서드가 나와서 추상 클래스처럼 정의가 가능해지긴함..)

인터페이스로만 상속받기

추상 클래스를 사용하는 이유는 일부 메서드에서는 기본 구현을 제공하고 다른 메서드는 추상화하기 위해서다.


장점

  • 중복 제거가 가능

단점

  • 코드 공유는 커플링(결합)을 유발한다.
    • 필요하지 않은 메서드도 빈값을 넣는 등으로 재정의를 해야함

따라서 인터페이스를 사용하고, 상속 방식보다는 컴포지션 방식을 사용하자.

반응형

댓글