ENUM, Clean Code with Java

2022. 5. 11. 23:59Spring/Java

Java Enum을 통해 If/else 분기문을 제거하여 클린코드를 지향하는 코드를 작성하는 것이 본 포스팅의 목표입니다.

 

 

문제점

몇몇의 코드를 관리할 때, if-else 문이 많아지면서 가독성이 떨어지는 경우가 발생하는 것을 경험해보셨을 텐데요.

필자도 이 문제에 대해 조금 더 깔끔한 관리를 할 수 없을지 고민하다가,

조금 더 깔끔하게 관리하기 위한 방법을 고려한 후, 공유하고자 기록하게 되었습니다.

 

IF-ELSE

은행에서 거래 시 예금, 출금, 이체라는 세 가지의 타입의 업무를 수행해야한다고 가정하겠습니다.

그리고 각각 "DEPOSIT", "WITHDRAWAL", "TRANSFER" 이라는 코드로 이들을 구분하고자 합니다.

이 때, 출금(WITHDRAWAL), 이체(TRANSFER)를 수행할 때는 각각의 세금이 발생합니다.

 

이를 코드로 작성하면 아래와 같습니다.

 

 

Transaction Class

Transaction 클래스는 operation에 따라 분리처리를 하는 doTransaction 메소드를 가지고 있습니다.

아래와 같이 해당 operation 코드에 따라 맞는 동작을 합니다.

 

public class Transaction {

    public void doTransaction(Account account, long cash, String operation) {

        if ("DEPOSIT".equalsIgnoreCase(operation)) {
            account.deposit(cash);
        } else if ("WITHDRAWAL".equalsIgnoreCase(operation)) {
            long tax = Math.round(cash * 0.20 / 100);
            cash = cash + tax;
            account.withdraw(cash);
        } else if ("TRANSFER".equalsIgnoreCase(operation)) {
            long tax = Math.round(cash * 0.10 / 100);
            cash = cash + tax;
            account.deposit(cash);
        } else {
            // not exist operation code exception
        }
    }
}

 

대략 어떤 코드인지, 이해를 위한 코드이기 때문에 자세한 설명은 생략하도록 하겠습니다.

위에서 나타난 Account 클래스를 참고차 확인하고 가겠습니다.

 

 

Account Class

Account 클래스는 잔고 Balance만을 가지고 있습니다.

 

@Getter
@Setter
@AllArgsConstructor
public class Account {
    private long balance;

    public void withdraw(long cash) {
        this.balance = this.balance - cash;
    }

    public void deposit(long cash) {
        this.balance = this.balance + cash;
    }
}

 

간단하게 구현했습니다.

실행을 위한 코드는 아래와 같습니다.

 

 

Client

실행 코드는 아래와 같습니다.

 

public void executeWithIfElse() {
    Account account = new Account(10_000);
    Bank bank = new Bank();
        
    System.out.println("Initial Balance : " + account.getBalance());

    bank.doTransaction(account, 1_000, "DEPOSIT");
    System.out.println("Balance after DEPOSIT : " + account.getBalance());

    bank.doTransaction(account, 1_500, "WITHDRAWAL");
    System.out.println("Balance after WITHDRAWAL : " + account.getBalance());
}

 

실행 시키면 아래와 같은 출력이 나옵니다.

 

 

Output

Initial Balance : 10000
Balance after DEPOSIT : 11000
Balance after WITHDRAWAL : 9497

 

 

 

Refactoring - ENUM

위의 코드는 은행 업무가 많아질 때, if/else 문이 길어지면서 가독성이 좋지 못한 코드가 될 수 있습니다.

이를 Enum으로 변경해 해당 코드를 관리해보겠습니다.

 

먼저, TransactionType이라는 enum을 제작합니다.

 

 

Enum

TransactionType Enum 은 업무 코드를 담고, 직접 그 실행 알고리즘을 가집니다.

 

public enum TransactionType {

    DEPOSIT {
        @Override
        public void doTransaction(Account account, long cash) {
            account.deposit(cash);
        }
    },
    WITHDRAWAL {
        @Override
        public void doTransaction(Account account, long cash) {
            long tax = Math.round(cash * 0.20 / 100);
            cash = cash + tax;
            account.withdraw(cash);
        }
    },
    TRANSFER {
        @Override
        public void doTransaction(Account account, long cash) {
            long tax = Math.round(cash * 0.10 / 100);
            cash = cash + tax;
            account.deposit(cash);
        }
    };

    public abstract void doTransaction(Account account, long cash);
}

 

위와 같이 if/else 구문에 해당하는 코드를 각각 관리하는 코드로 제작합니다.

이렇게 구성해주면 아래와 같이 실행할 수 있습니다.

 

 

Execute

실행 코드는 아래와 같습니다.

 

public void executeWithEnum() {
    Account account = new Account(10_000);
    System.out.println("Initial Balance : " + account.getBalance());

    TransactionType.DEPOSIT.doTransaction(account, 1_000);
    System.out.println("Balance after DEPOSIT : " + account.getBalance());

    TransactionType.WITHDRAWAL.doTransaction(account, 1_500);
    System.out.println("Balance after WITHDRAWAL : " + account.getBalance());
}

 

Output

결과는 위와 동일합니다.

Initial Balance : 10000
Balance after DEPOSIT : 11000
Balance after WITHDRAWAL : 9497

 

 

 

Strategy Pattern

위의 리팩터링 과정을 잘 생각해보면, 디자인 패턴의 Strategy Pattern으로 활용 가능할 수 있습니다.

Strategy Pattern 에 대한 이해는 해당 링크를 참고하시길 바랍니다.

 

 

 

 

 

 

먼저, 기존의 Strategy 코드를 확인 후 Enum으로 변경해보겠습니다.

 

Basic Strategy Pattern

먼저, Strategy에 해당하는 코드를 아래와 같이 같단히 작성합니다.

 

Strategy

public interface Strategy {
    public void execute();
}

class StrategyA implements Strategy {
    @Override
    public void execute() {
        System.out.println("Executing strategy A");
    }
}

class StrategyB implements Strategy {
    @Override
    public void execute() {
        System.out.println("Executing strategy B");
    }
}

 

Context

@Setter
public class Context {

    private Strategy strategy;

    public void setStrategy(Strategy strategy){
        this.strategy = strategy;
    }

    public void executeStrategy(){
        this.strategy.execute();
    }
}

 

 

Client

Context context = new Context();

context.setStrategy(new StrategyA());
context.executeStrategy();

context.setStrategy(new StrategyB());
context.executeStrategy();

 

Output

Executing strategy A
Executing strategy B

 

 

Using Enum

정의만 제외하면 위의 코드와 거의 동일합니다.

 

 

enum Strategy {
    STRATEGY_A {
        @Override
        void execute() {
            System.out.println("Executing strategy A");
        }

    },
    STRATEGY_B {
        @Override
        void execute() {
            System.out.println("Executing strategy B");
        }
    };

    abstract void execute();
}

 

 

Client

Context context = new Context();

context.setStrategy(Strategy.STRATEGY_A);
context.executeStrategy();

context.setStrategy(Strategy.STRATEGY_B);
context.executeStrategy();

 

 

 

 

Functional Interface

 

이번엔 Abstract 메소드가 아닌 함수형 인터페이스를 사용하는 예시를 만들어봤는데요.

Design Pattern, Strategy" 포스팅에서 다뤘던 Weapon을 구현해보겠습니다.

 

 

Strategy - Weapon Enum

먼저, Enum은 아래 코드와 같습니다.

'칼 공격', '총 발포'라는 공격 타입을 지정하고, time 인자를 받아 얼마동안 공격할지를 출력합니다.

이는 Consumer를 사용한 정의를 보여주고자 time을 추가 했어요.

 

목적에 맞는 함수형 인터페이스를 잘 구분해서 사용하실 수 있습니다.  

 

@AllArgsConstructor
public enum Weapon {

	KNIFE(time -> System.out.println(String.format("knife attack ... during %ss", time))),
	GUN(time -> System.out.println(String.format("gun fire ... during %ss", time)));

	public Consumer<Integer> attack;
}

 

 

Context - Fighter

Consumer의 accept를 이용해서 time을 인자로 넘겨줍니다.

추가로, 만약 weapon이 없다면 맨손 공격을 하게끔 null 체크를 추가합니다.

 

@Setter
public class Fighter {

	private Weapon weapon;

	public void attack(Integer time) {
		if (this.weapon == null) {
			System.out.println("bare-handed attack");
		} else {
			this.weapon.attack.accept(time);
		}
	}
}

 

 

Main

아래는 실행시킬 Client 코드입니다.

칼 공격을 5초, 총 공격을 3초로 설정했습니다.

Fighter fighter = new Fighter();

fighter.setWeapon(Weapon.KNIFE);
fighter.attack(5);

fighter.setWeapon(Weapon.GUN);
fighter.attack(3);

 

 

Output

위의 Client 코드에 대한 출력은 아래와 같습니다.

 

knife attack ... during 5s
gun fire ... during 3s

 

 

그럼 지금까지 Enum을 활용해서 코드를 깔끔하게 관리하는 방법에 대해 알아보았습니다.

오타나 잘못된 내용이 있다면 댓글로 남겨주세요!

감사합니다 ☺️ 

 

 

 

 

|  참고  |

medium: clean-code-with-java-replace-the-logical-condition-using-enum-if-else-statements

geeksforgeeks: how-to-implement-a-strategy-pattern-using-enum-in-java