if문을 리팩토링하는 방법

e1psycongr00·2022년 11월 24일
0

if 문 혹은 switch 문 문제점

1, 2개의 분기문은 크게 문제가 없지만 분기가 너무 많아지면 메서드 내 코드 길이가 길어지고 좋지 않은 코드가 된다.

// Robot.class
public class Robot {

    private int y;
    private int x;

    public Robot(int y, int x) {
        this.y = y;
        this.x = x;
    }

    public void move(Command command) {
        if (command.equals(Command.UP)) {
            System.out.println("move up");
            this.y -= 1;
        }
        if (command.equals(Command.DOWN)) {
            System.out.println("move down");
            this.y += 1;
        }
        if (command.equals(Command.LEFT)) {
            System.out.println("move left");
            this.x -= 1;
        }
        if (command.equals(Command.RIGHT)) {
            System.out.println("move right");
            this.x += 1;
        }
        throw new IllegalArgumentException("입력 문제 발생,");
    }
}
public enum Command {
    UP,
    LEFT,
    RIGHT,
    DOWN;
}

move 메서드를 살펴보자. if분기가 많은데 만약 수정해야 할 일이 발생하면 내부를 수정해주어야 한다. 그렇기 때문에 OCP를 적용할 수 없다. 그리고 if 문이 너무 많이 존재하는 데 책임 측면에서 살펴보면 분기 조건에 따라 움직인다. 라는 생각으로 보면 하나의 책임으로 볼 수 있을 수도 있지만 매 분기별 구현 코드를 move에 모두 작성했기 때문에 단일 책임 원칙에도 좋지 못한 코드이다. 그래서 이 코드를 7가지 방법으로 리팩토링 해보고 맘에 드는 방식을 활용해보자.

1. if -> Enum 커맨드

커맨드 enum이란 enum을 활용해 커맨드 패턴을 수행한 것이다. 커맨드 패턴이란 간단히 설명하면 내부 구현된 알고리즘 코드를 외부 클래스로 분리해서 주입해서 사용하는 패턴이다. enum을 활용한 커맨드 패턴은 추상 메서드의 익명성을 활용한다.

// 추상 메서드를 활용해 전략화
public enum Command {
    UP {
        @Override
        public Point execute(Point p)){
            return Point.add(p, new Point(-1, 0));
        }
    },
    LEFT {
        @Override
        public Point execute(Point p) {
            return Point.add(p, new Point(1, 0));
        }
    },
    RIGHT {
        @Override
        public Point execute(Point p) {
            return Point.add(p, new Point(0, 1));
        }
    },
    DOWN {
        @Override
        public Point execute(Point p) {
            return Point.add(p, new Point(0, -1));
        }
    };

    public abstract Point execute(Point p);
}
// 전략 enum 패턴이 적용된 robot
public class Robot {

    private Point point;

    public Robot(int y, int x) {
        this.point = new Point(y, x);
    }

    public void move(String input) {
        Command command = Command.valueOf(input);
        point = command.execute(point);
        throw new IllegalArgumentException("입력 문제 발생,");
    }
}
// interface를 활용한 예제
public enum Command {
    UP(p -> Point.add(p, new Point(-1, 0))),
    DOWN(p -> Point.add(p, new Point(1, 0))),
    LEFT(p -> Point.add(p, new Point(0, -1))),
    RIGHT(p -> Point.add(p, new Point(0, 1)));

    private final Movement movement;

    Command(Movement movement) {
        this.movement = movement;
    }

    public Point execute(Point p) {
        return movement.execute(p);
    }

    @FunctionalInterface
    private interface Movement {

        Point execute(Point p);
    }
}

enum 커맨드 패턴의 장점은 관련된 command를 한 곳에 묶어서 enum에 보관하기 때문에 가독성이 굉장히 좋다. 그러나 abstract를 활용한 위의 코드는 OCP가 적용되지는 않는다. 그렇기 때문에 큰 규모가 아닌 분기문의 갯수가 4, 5 개 있지만 로직상 너무 명확해서 더 이상 변경되거나 추가 될 일이 많지 않고 구현 코드가 간단할 때 사용한다. 위에 코드도 대각선 움직임 4개를 추가할 수도 있지만 역시 구현 코드가 간단하기 때문에 그 정도는 괜찮다고 생각한다.

if -> Command 패턴

우선 다형성을 활용하기 위해 interface를 선언하고 행동을 기중으로 다양한 구현 클래스를 만든다. 그리고 분기문 사용을 없애기 위해 HashMap를 활용한다.

// Command 인터페이스
public interface Command {

    Point run(Point point);
}

// down concrete command
public class DownCommand implements Command {

    @Override
    public Point run(Point point) {
        return Point.add(point, new Point(1, 0));
    }
}

// left concrete command
public class LeftCommand implements Command {

    @Override
    public Point run(Point point) {
        return Point.add(point, new Point(0, -1));
    }
}

// right concrete command
public class RightCommand implements Command {

    @Override
    public Point run(Point point) {
        return Point.add(point, new Point(0, 1));
    }
}

// up concrete command
public class UpCommand implements Command {

    @Override
    public Point run(Point point) {
        return Point.add(point, new Point(-1, 0));
    }
}
// 로봇 클래스
public class Robot {

    private Point point;

    private static final Map<String, Command> commands;

    static {
        commands = new HashMap<>();
        commands.put("up", new UpCommand());
        commands.put("down", new DownCommand());
        commands.put("left", new LeftCommand());
        commands.put("right", new RightCommand());
    }

    public Robot(int y, int x) {
        this.point = new Point(y, x);
    }

    public void move(String input) {
        Command command = commands.get(input);
        this.point = command.run(point);
    }
}

command라는 행위를 interface로 정하고 각각의 행동에 대해 구현을 하기 때문에 객체지향적인 코드라 할 수 있다. 확장에도 열려있고 static에 기능이 추가된다면 key, value를 추가해주면 된다. 그러나 문제점은 각 구현체의 생성을 hashmap으로 관리했기 때문에 해당 입력이 없다면 null을 반환한다. 이는 잠재적으로 문제가 있을 수 있다. 그리고 구현체의 생성 관리를 로봇이 담당한다. 좀 더 리팩토링을 한다면 움직임에 대한 구현체 생성에 대한 기능을 팩토리 클래스에 맡기는 방법이 있다.

팩토리가 추가된 커맨드 패턴

// 커맨드 생성을 담당하는 팩토리 클래스
public class CommandFactory {
    
    private final Map<String, Command> commands = new HashMap<>();
    
    private void init() {
        commands.put("up", new UpCommand());
        commands.put("down", new DownCommand());
        commands.put("left", new LeftCommand());
        commands.put("right", new RightCommand());
    }

    public CommandFactory() {
        init();
    }
    
    public Command createCommand(String input) {
        if (commands.containsKey(input)) {
            return commands.get(input);
        }
        throw new IllegalArgumentException("요청에 없는 입력");
    }
}
// 최종 로봇 클래스
public class Robot {

    private final CommandFactory commandFactory = new CommandFactory();
    private Point point;


    public Robot(int y, int x) {
        this.point = new Point(y, x);
    }

    public void move(String input) {
        Command command = commandFactory.createCommand(input);
        this.point = command.run(point);
    }
}

이렇게 분리함으로써 기존 command 패턴과 달리 Command 구현부터 생성에 대한 로직을 로봇은 담당할 필요가 없어졌다. 코드 수정 발생시 factory와 구현부만 추가 또는 수정해주면 된다. 로봇은 움직이면 좌표를 업데이트한다. 그리고 내부 구현 로직은 어떻게 동작하는지 모른다. 딱 객체지향에 부합한다고 할 수 있다. 다만 코드가 길다.

if 문 -> state 패턴

state 패턴은 여러 행위에 따라 하나의 state가 변경되는 예제보다는 여러 state가 여러 조건에 따라 상태가 변하는 예제에 적합하다.

상태 패턴은 의존성 주입을 하지만 주입한 형태가 다형성을 통해 바꿈으로써 if문을 줄인다. 행위의 내부 구현을 해당 객체에 맡기지 않고 상태 클래스에게 위임한다. 이렇게 동작을 상태에 위임한 객체를 context라 한다.

엘리베이터가 지하 3층부터 20층 까지 있음
20층인 경우 더 이상 올라 갈 수 없음.
1층에서 아래를 호출하는 경우 지하 1층
지하 3층보다 아래로 내려갈 수 없음.

이런 예제가 있는 경우 다음과 같이 풀 수 있다.

public class KoreanElevator implements Elevator {

    private static final int MAX_FLOOR = 20;
    private static final int MIN_FLOOR = -3;

    private int floor = 1;


    @Override
    public void up() {
        if (floor == -1) {
            floor = 1;
        }
        if (floor == MAX_FLOOR) {
            return;
        }
        floor += 1;
    }

    @Override
    public void down() {
        if (floor == 1) {
            floor = -1;
            return;
        }
        if (floor == MIN_FLOOR) {
            return;
        }
        floor -= 1;
    }

    @Override
    public void showFloor() {
        if (floor < 0) {
            System.out.println("B" + floor);
            return;
        }
        if (floor > 0) {
            System.out.println(floor);
        }
    }

    @Override
    public int getFloor() {
        return floor;
    }
}

이 정도 샘플 코드는 응집도도 좋고 조건문도 엄청 복잡하진 않아서 리팩토링하지 않아도 괜찮지만, if문을 줄이고 state패턴을 해보는 것이 목적이므로 한번 해보자.

public class Elevator {

    public static final int MAX_VALUE = 20;
    public static final int MIN_VALUE = -3;
    public static final int ONE_VALUE = 1;
    public static final int UNDER_ONE_VALUE = -1;

    public final FloorState ONE_FLOOR = new OneFloor(this);
    public final FloorState MAX_FLOOR = new MaxFloor(this);
    public final FloorState POSITIVE_FLOOR = new PositiveFloor(this);
    public final FloorState UNDER_ONE_FLOOR = new UnderOneFloor(this);
    public final FloorState NEGATIVE_FLOOR = new NegatieFloor(this);
    public final FloorState MIN_FLOOR = new MinFloor(this);

    private int floor = 1;
    private FloorState state = ONE_FLOOR;



    public void up() {
        state.up();
    }


    public void down() {
        state.down();
    }


    public void showFloor() {
        state.print();
    }


    public int getFloor() {
        return floor;
    }

    public void setFloor(int floor) {
        this.floor = floor;
    }

    public void setState(FloorState floorState) {
        this.state = floorState;
    }
}
public class PositiveFloor implements FloorState {

    private final Elevator elevator;

    public PositiveFloor(Elevator elevator) {
        this.elevator = elevator;
    }

    @Override
    public void up() {
        int nextFloor = elevator.getFloor() + 1;
        elevator.setFloor(nextFloor);
        if (nextFloor == Elevator.MAX_VALUE) {
            elevator.setState(elevator.MAX_FLOOR);
        }
    }

    @Override
    public void down() {
        int nextFloor = elevator.getFloor() - 1;
        elevator.setFloor(nextFloor);
        if (nextFloor == Elevator.ONE_VALUE) {
            elevator.setState(elevator.MAX_FLOOR);
        }
    }

    @Override
    public void print() {
        System.out.printf("%d 층입니다%n", elevator.getFloor());
    }
}

public class MaxFloor implements FloorState {

    private final Elevator elevator;

    public MaxFloor(Elevator elevator) {
        this.elevator = elevator;
    }

    @Override
    public void up() {
    }

    @Override
    public void down() {
        int floor = elevator.getFloor();
        elevator.setFloor(floor - 1);
        elevator.setState(elevator.POSITIVE_FLOOR);
    }

    @Override
    public void print() {
        System.out.println("꼭대기 층입니다");
    }
}

이런 식으로 객체 자체 state가 분기의 한 조건이기 때문에 if문을 줄이고 코드를 짤 수 있다. 그러나 지금 state 의 concrete 객체를 매번 생성하지 않기 위해서 미리 final로 클래스 내부에 생성했는데 오히려 지저분해 보이긴 하다. 이 부분은 구현 클래스 생성 부분을 따로 빼주어서 구현하면 좀 더 깔끔해 보일 것이다.

단점은 state와 context간의 결합도가 높다.

profile
엘 프사이 콩그루

0개의 댓글