[JAVA] if문 제거하기

이재훈·2023년 8월 15일
0

JAVA8

목록 보기
21/23

자바와 객체지향

if문이 많은 코드의 문제점

if문은 자바에 핵심 문법입니다. 하지만 너무 많은 if문은 코드를 읽기 힘들게 만들고, 코드를 수정하기도 힘들게 만들고, 문제가 발생했을 때 디버깅도 어렵습니다. 따라서 같은 로직이라면 if문이 없이 작성된 코드가 가독성이 높은 코드가 될 가능성이 높습니다.

public class Client {
    public int someMethod(CalculateCommand calculateCommand) {
        CalculateType calculateType = calculateCommand.getCalculateType();
        int num1 = calculateCommand.getNum1();
        int num2 = calculateCommand.getNum2();

        int result = 0;

        if (calculateType.equals(CalculateType.ADD)) {
            result = num1 + num2;
        } else if (calculateType.equals(CalculateType.MINUS)) {
            result = num1 - num2;
        } else if (calculateType.equals(CalculateType.MULTIPLY)){
            result = num1 * num2;
        } else if (calculateType.equals(CalculateType.DIVIDE)) {
            result = num1 / num2;
        }

        return result;
    }
}

이 코드는 많은 if문을 가지고 있습니다. 하지만 그렇다고 해서 읽기 어려운 코드는 아닙니다.
하지만 이 코드는 동작적으로도 문제를 가지고 있습니다.

1. calculateType에 대한 null 체크 없음
2. 나누기 시 num2가 0이 아닌지 체크 없음

해당 로직을 추가해보도록 하겠습니다.

public class Client {
    public int someMethod(CalculateCommand calculateCommand) {
        CalculateType calculateType = calculateCommand.getCalculateType();
        int num1 = calculateCommand.getNum1();
        int num2 = calculateCommand.getNum2();

        int result = 0;

        if (calculateType != null && calculateType.equals(CalculateType.ADD)) {
            result = num1 + num2;
        } else if (calculateType != null && calculateType.equals(CalculateType.MINUS)) {
            result = num1 - num2;
        } else if (calculateType != null && calculateType.equals(CalculateType.MULTIPLY)){
            result = num1 * num2;
        } else if (calculateType != null && calculateType.equals(CalculateType.DIVIDE)) {
            if (num2 == 0) {
                throw new RuntimeException("0으로 나눌 수 없습니다.");
            } else {
                result = num1 / num2;    
            }
        }

        return result;
    }
}

코드가 굉장히 복잡해진 것을 확인할 수 있습니다. 간단한 체크 로직이 추가되었지만 코드의 가독성이 굉장히 떨어진 것을 확인할 수 있습니다. 이 코드를 개선해보도록 하겠습니다.

빠르게 반환하기 - Early return

public class Clienttwo {
    public int someMethod(CalculateCommand calculateCommand) {
        CalculateType calculateType = calculateCommand.getCalculateType();
        int num1 = calculateCommand.getNum1();
        int num2 = calculateCommand.getNum2();

        int result = 0;
        
        if (calculateType == null) {
            return result;
        }
        
        if (calculateType.equals(CalculateType.DIVIDE) && num2 == 0) {
            throw new RuntimeException("0으로 나눌 수 없습니다");
        }

        if (calculateType.equals(CalculateType.ADD)) {
            result = num1 + num2;
        } else if (calculateType.equals(CalculateType.MINUS)) {
            result = num1 - num2;
        } else if (calculateType.equals(CalculateType.MULTIPLY)){
            result = num1 * num2;
        } else if (calculateType.equals(CalculateType.DIVIDE)) {
            result = num1 / num2;
        }

        return result;
    }
}

계산하는 로직 전에 체크하는 로직을 먼저 진행하여 Early return 또는 exception을 발생하도록 하는 코드입니다. 직전의 코드보다 가독성이 나아졌습니다.

Enum 안으로 넣기

public enum CalculateType {
    ADD((num1, num2) -> num1 + num2),
    MINUS((num1, num2) -> num1 - num2),
    MULTIPLY((num1, num2) -> num1 * num2),
    DIVIDE((num1, num2) -> num1 / num2);

    private BiFunction<Integer, Integer, Integer> expression;

    CalculateType(BiFunction<Integer, Integer, Integer> expression) {
        this.expression = expression;
    }

    public int calculate(int num1, int num2) {
        return this.expression.apply(num1, num2);
    }
}

enum 클래스에 연산을 넣었습니다. 해당 코드의 설명은 전에 게시글에 있으니 참고하시면 되겠습니다.

public class Client {
    public int someMethod(CalculateCommand calculateCommand) {
        CalculateType calculateType = calculateCommand.getCalculateType();
        int num1 = calculateCommand.getNum1();
        int num2 = calculateCommand.getNum2();

        int result = 0;

        if (calculateType == null) {
            return result;
        }

        if (calculateType.equals(CalculateType.DIVIDE) && num2 == 0) {
            throw new RuntimeException("0으로 나눌 수 없습니다");
        }

        result = calculateType.calculate(num1, num2);

        return result;
    }
}

Client 클래스의 코드에 if문을 지워지게 됩니다. 지금 코드도 나쁘지 않지만 더 개선해보도록 하겠습니다.

if (calculateType == null) {
    return result;
}

if (calculateType.equals(CalculateType.DIVIDE) && num2 == 0) {
    throw new RuntimeException("0으로 나눌 수 없습니다");
}

이 코드는 말하자면 유효성 검사를 하는 코드입니다. 이 코드는 언제 실행되어야 할까요? 현재는 두변수를 사용하는 시점에 유효성 검사를 하고 있습니다. 이 체크 로직은 변수를 사용하는 곳에는 항상 들어가야하는 코드가 되었습니다.

사용하는 시점이 아닌 CalculateCommand를 생성하는 시점에 유효성 검사를 하는 것이 맞습니다.

생성 시점에 유효성 검사하기

public class CalculateCommand {
    private CalculateType calculateType;
    private int num1;
    private int num2;

    public CalculateCommand(CalculateType calculateType, int num1, int num2) {
        if (calculateType == null) {
            throw new RuntimeException("calculateType는 필수 값입니다.");
        }
        
        if (calculateType.equals(CalculateType.DIVIDE) && num2 == 0) {
            throw new RuntimeException("0으로 나눌 수 없습니다.");
        }
        
        this.calculateType = calculateType;
        this.num1 = num1;
        this.num2 = num2;
    }

    public CalculateType getCalculateType() {
        return calculateType;
    }

    public int getNum1() {
        return num1;
    }

    public int getNum2() {
        return num2;
    }
}

CalculateCommand 생성자에서 유효성 검사 코드를 추가하였습니다. 이제 Client 코드를 확인해보도록 하겠습니다.

public class Client {
    public int someMethod(CalculateCommand calculateCommand) {
        CalculateType calculateType = calculateCommand.getCalculateType();
        int num1 = calculateCommand.getNum1();
        int num2 = calculateCommand.getNum2();

        int result = calculateType.calculate(num1, num2);

        return result;
    }
}

if문이 완전히 없어진 코드가 되었습니다. 코드의 가독성은 굉장히 개선되었고, 이제 코드를 사용할 때마다 유효성 검사를 하지 않아도 CalculateCommand를 생성하는 시점에 알아서 유효성 검사를 할 수 있게 되었습니다.

리팩토링의 정의

결과의 변경없이 코드의 구조를 재조정 하는 것, 가독성을 높이거나 유지보수를 편하게 하는 것 입니다. 위에 코드를 리팩토링 했다고 할 수 있는 것 입니다.

리팩토링과 테스트 코드의 관계

결과의 변경없이 코드가 바뀌었다는 것을 어떻게 알 수 있을까요? 바로 테스트코드가 이를 증명해줍니다. 리팩토링 전의 테스트코드의 결과와 바뀐 코드의 테스트코드의 결과가 같다면 그것은 결과가 같다는 것을 보장해주는 것과 같습니다.

결론

개발자는 단순히 기능을 개발하는 것 뿐만 아니라 추후 유지보수에 대해서도 생각을 해야하고 확장성과 가독성을 생각해서 코드를 작성해야 합니다. 그것을 가능하게 하기 위해서는 객체 지향적으로 코드를 작성할 줄 알아야 하고, 테스트 코드도 작성할 줄 알아야 좋은 개발자가 된다고 생각합니다.


해당 게시글은 프로그래머스 스쿨 강의
"실무 자바 개발을 위한 OOP와 핵심 디자인 패턴(푸)"
를 정리한 내용입니다. 쉽게 잘 설명해주시니 여러분도 강의를 듣는 것을 추천드립니다.

profile
부족함을 인정하고 노력하자

0개의 댓글