Clean Code

Java 리팩토링 - 조건문 분해하기

1. 알고자 하는 것

이전 Java 리팩토링 강의 내용 모두 유익하다고 생각했지만, 유독 이 내용은 간단해보이면서도 실무에서 정말 많이 적용 가능한 리팩토링 기법이라고 생각한다. 실제로 코드리뷰 때에도 해당 부분에 대해서 얘기를 많이 했고, 대체로 누군가 짠 코드를 처음 보는 입장이라고 생각해보았을 때, 조건문을 분해(or 추출)했을 때의 가독성이 정말 높아진다고 생각한다.

 

내가 짠 코드를 처음 보는 사람이 쉽게 이해할 수 있는 코드를 짜는 데에 좋은 첫 걸음이 될만한 부분이라고 생각한다.

항상 좋은 코드, 읽기 쉬운 코드를 짜도록 노력하는 습관을 들이자.

  • 조건문 분해하기

 

 

2. 알게된 것

  • 프로그램에는 언제나 조건이 있다.
  • if-else로 분기되는 조건문은 단순한 조건인 경우에는 이해하기 쉽다.
  • 또한, 조건문 내 액션이 단순한 경우에도 역시 가독성이 떨어지지 않는다.
// "조건"과 "액션" 모두 간결하고 짧다.
if (user.isVip()) {
    vipCount++;
} else {
    normalCount++;
}

 

  • 하지만 다음과 같이 "조건"과 "액션"이 모두 복잡한 로직으로 구현되어 있다면?
  • "조건"과 "액션" 모두 "의도"를 명확하게 표현하지 못한다.
  • 즉, 가독성이 떨어지고 한 눈의 해당 메서드의 의도와 동작을 예상할 수 없다.
// if-else 문의 true, false가 어떤 조건에 의해 결정되는지 ?
// true일 때와 false일 때 액션이 무엇을 수행하는지 ?
private Participant findParticipant(String username, List<Participant> participants) {
        Participant participant;
        if (participants.stream().noneMatch(p -> p.username().equals(username))) {
            participant = new Participant(username);
            participants.add(participant);
        } else {
            participant = participants.stream().filter(p -> p.username().equals(username)).findFirst().orElseThrow();
        }

        return participant;
}

 

  • 이전에 살펴보았던 함수 추출(Extract Method)을 "조건"과 "액션"에 적용해 조건문 내에서 각각이 구현을 나타내는 것이 아닌 의미를 나타낼 수 있도록 한다.
  • 이 때, 구현을 나타내던 부분을 하나의 의미를 갖는 메서드로 추출하므로 메서드명을 의미/의도를 잘 나타낼 수 있도록 결정하는 것이 중요하다.
private Participant findParticipant(String username, List<Participant> participants) {
        Participant participant;
        // 1. "조건" 메서드 추출 (새로운 participant인가?)
        if (isNewParticipant(username, participants)) {
            // 2. "액션" 메서드 추출 (새로운 participant라면 participant 객체 create)
            participant = createNewParticipant(username, participants);
        } else {
            // 2. "액션" 메서드 추출 (기존 participant라면 participant 객체 find)
            participant = findExistingParticipant(username, participants);
        }
        return participant;
}


// 메서드 추출
private Participant findExistingParticipant(String username, List<Participant> participants) {
    return participants.stream().filter(p -> p.username().equals(username)).findFirst().orElseThrow();
}

private Participant createNewParticipant(String username, List<Participant> participants) {
    Participant participant;
    participant = new Participant(username);
    participants.add(participant);
    return participant;
}

private boolean isNewParticipant(String username, List<Participant> participants) {
    return participants.stream().noneMatch(p -> p.username().equals(username));
}
  • 조건은 username에 해당하는 사용자가 participants 목록 내 존재하지 않는 새로운 사용자인지 여부에 따라 분기됨을 알 수 있다.
  • 액션1(true)은 새로운 사용자이므로 participant 객체를 추가한다.
  • 액션2(false)는 기존 사용자이므로 기존 participant 객체를 찾는다.
  • 여러 줄로 복잡하게 구현되어진 조건과 액션이 3가지 의미를 띄도록 리팩토링 됨을 알 수 있다.
  • 또한, 이렇게 메서드 추출로 조건문을 분해했을 때, 조건 ? 액션1 : 액션2와 같이 삼항연산자로 추가적인 리팩토링이 가능하도록 쉽게 유도할 수 있다.
private Participant findParticipant(String username, List<Participant> participants) {
        // 새로운 사용자라면 새로운 participant 객체 생성 / 기존 사용자라면 기존 participant 객체 반환
        return isNewParticipant(username, participants) ?
                createNewParticipant(username, participants) :
                findExistingParticipant(username, participants);
}

 

 

 

 

3. 정리

  • 복잡한 조건문 분기는 그 depth가 깊어질수록 가독성이 떨어진다.
  • "조건"과 분기에 따른 "액션" 로직이 복잡해 가독성이 떨어질 때, 함수 추출(Extract Method)을 활용해 적절한 메서드명으로 나타냄으로써 복잡한 구현이 아닌 "의미"를 표현하도록 조건문을 분해한다.
  • 이렇게 복잡한 로직이 하나의 의미를 띄도록 조건문을 분해했다면, 조건 ? 액션1 : 액션2와 같은 삼항연산자 등의 추가적인 리팩토링의 가능성을 쉽게 파악할 수 있다.

 


Reference

강의 - 코딩으로 학습하는 리팩토링 (백기선 강사님)