예외 처리 리팩토링 - ye-oe cheoli lipaegtoling

Cooper's devlog


1. 강의 링크

https://www.youtube.com/watch?v=g-nsT3NRK2o&list=PLqaSEyuwXkSppQAjwjXZgKkjWbFoUdNXC&index=45

예외 처리 리팩토링 - ye-oe cheoli lipaegtoling

2. 학습 목표

QuestionController 중복코드 제거 및 리팩토링

  • try/catch를 이용한 Exception
  • validation(Result) class를 이용한 예외 처리

3. 과정

httpSessionUtils.isLoginUser(session) 및 isSameWriter 중복 코드 리팩토링 (update, updateForm, delete)

1. try/catch를 이용한 Exception

(1)QuestionController

1_ hasPermission

예외 처리 리팩토링 - ye-oe cheoli lipaegtoling
update method

반복되는 코드를 최소화하기 위해 hasPermission 메소드

  • if(!HttpSessionUtils.isLoginUser(session) : 로그인 여부에 따른 예외처리
  • if(!question.isSameWriter(loginUser) : 작성자id - loginId를 비교  
  • 모든 예외 통과 시, true 리턴

2_ hasPermission을 try/catch문에 추가하여 예외처리 구현

-updateForm

예외 처리 리팩토링 - ye-oe cheoli lipaegtoling
updateForm method

-update

예외 처리 리팩토링 - ye-oe cheoli lipaegtoling
update method

-delete

예외 처리 리팩토링 - ye-oe cheoli lipaegtoling
delete method

2. result 객체를 생성해서 작성하기

(1) Result

예외 처리 리팩토링 - ye-oe cheoli lipaegtoling
Result
  • ok : 유효성 검상 통과 method(static)
  • fail : 유효성 검사 실패 method & errorMessage 전달
  • Result(boolean valid, String errorMessage) : 실패 시, 결과 주입을 위한 생성자

(2)QuestionController

1_valid

예외 처리 리팩토링 - ye-oe cheoli lipaegtoling
valid method
  • 예외처리에 따른 메세지를 Result 객체에 주입
  •  if(!HttpSessionUtils.isLoginUser(session) : 로그인 여부 확인 예외처리
  • if(!question.isSameWriter(loginUser) : 작성자 일치여부 확인 예외처리

2_updateForm, update, delete

  • 유효성 검사 결과를 result 객체에 주입하여 로직 진행
  • if문 예외 처리한 후, 로직 진행 방식(else문을 최대한 사용하지 않는 방식으로 진행)

-updateForm

예외 처리 리팩토링 - ye-oe cheoli lipaegtoling
updateForm method

-update

예외 처리 리팩토링 - ye-oe cheoli lipaegtoling
update method

-delete

예외 처리 리팩토링 - ye-oe cheoli lipaegtoling
delete method

* 예외 처리 

1. 예외란

- 예외 : 잘못된 코드, 부정확한 데이터, 예외적인 상황에 의하여 발생하는 오류

2. 예외 처리기 (try-catch, throws)

try{
	// 예외가 발생할 수 있는 코드
} catch{
	// 예외 처리
} finally {
    // try 블록이 끝나면 무조건 실행된다
}
public void writeList() throws IOException
{ 
	...
}

3. 예외의 종류

1) Error 

- 자바 가상 기계 안에서 치명적인 오류 발생

- H/W 문제 등으로 파일을 읽을 수 없는 경우

- 컴파일러가 체크하지 않음!

2) RuntimeException

- 프로그래밍 버그나 논리 오류에 기인한다

- 컴파일러가 체크하지 않음! 

3) 기타 예외 

- 회복할 수 있는 예외로 반드시 처리해야 함

- 컴파일러가 체크함!

1. 리팩토링

1) 에러 코드를 예외로 치환 : 에러 발생 사실을 에러 코드로 표현

2) 리팩토링 카탈로그

이름 에러 코드를 예외로 치환
상황 에러 발생 사실을 에러 코드로 표현함
문제 정상 처리와 에러 처리가 혼재함
에러 코드 전파 처리가 넓은 범위에 있음
해법 에러 코드 대신에 예외를 사용함
결과 o 정창 처리와 에러 처리를 명확하게 분리 가능
o 에러 코드를 반환해서 전파하지 않아도 됨
o 에러 관련 정보를 예외 객체 저장 가능
x 에러 발생 부분과 에러 처리 부분이 분리되기 때문에 알기 어려워지는 경우도 있음

2. 예제 프로그램

클래스명 역할
Robot 명령어를 실행하는 로봇을 나타내는 클래스
Command 로봇 제어 명령어를 나타내는 클래스
Main 로봇 움직임을 확인하는 클래스
Position 로봇 위치를 나타내는 클래스
Directon 로봇 방향을 나타내는 클래스 

1) 리팩토링 전

package before;
import java.util.*;

public class Robot {
    private final String _name;
    private final Position _position = new Position(0, 0);
    private final Direction _direction = new Direction(0, 1);
    public Robot(String name) {
        _name = name;
    }
    public void execute(String commandSequence) {
        StringTokenizer tokenizer = new StringTokenizer(commandSequence);
        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            if (!executeCommand(token)) {
                System.out.println("Invalid command: " + token);
                break;
            }
        }
    }
    public boolean executeCommand(String commandString) {
        Command command = Command.parseCommand(commandString);
        if (command == null) {
            return false;
        }
        return executeCommand(command);
    }
    public boolean executeCommand(Command command) {
        if (command == Command.FORWARD) {
            _position.relativeMove(_direction._x, _direction._y);
        } else if (command == Command.BACKWARD) {
            _position.relativeMove(-_direction._x, -_direction._y);
        } else if (command == Command.TURN_RIGHT) {
            _direction.setDirection(_direction._y, -_direction._x);
        } else if (command == Command.TURN_LEFT) {
            _direction.setDirection(-_direction._y, _direction._x);
        } else {
            return false;
        }
        return true;
    }
    public String toString() {
        return "[ Robot: " + _name + " "
            + "position(" + _position._x + ", " + _position._y + "), "
            + "direction(" + _direction._x + ", " + _direction._y + ") ]";
    }
}
package before;
import java.util.*;

public class Command {
    public static final Command FORWARD = new Command("forward");
    public static final Command BACKWARD = new Command("backward");
    public static final Command TURN_RIGHT = new Command("right");
    public static final Command TURN_LEFT = new Command("left");
    private static final Map<String,Command> _commandNameMap = new HashMap<String,Command>();
    static {
        _commandNameMap.put(FORWARD._name, FORWARD);
        _commandNameMap.put(BACKWARD._name, BACKWARD);
        _commandNameMap.put(TURN_RIGHT._name, TURN_RIGHT);
        _commandNameMap.put(TURN_LEFT._name, TURN_LEFT);
    }
    private final String _name;
    private Command(String name) {
        _name = name;
    }
    public String getName() {
        return _name;
    }
    public static Command parseCommand(String name) {
        if (!_commandNameMap.containsKey(name)) {
            return null;
        }
        return _commandNameMap.get(name);
    }
}
package before;
public class Position {
    public int _x;
    public int _y;
    public Position(int x, int y) {
        _x = x;
        _y = y;
    }
    public void relativeMove(int dx, int dy) {
        _x += dx;
        _y += dy;
    }
}
package before;
public class Direction {
    public int _x;
    public int _y;
    public Direction(int x, int y) {
        _x = x;
        _y = y;
    }
    public void setDirection(int x, int y) {
        _x = x;
        _y = y;
    }
}
package before;
public class Main {
    public static void main(String[] args) {
        Robot robot = new Robot("Andrew");
        System.out.println(robot.toString());

        robot.execute("forward right forward");
        System.out.println(robot.toString());

        robot.execute("left backward left forward");
        System.out.println(robot.toString());

        robot.execute("right forward forward farvard");
        System.out.println(robot.toString());
    }
}

2) 리팩토링 실행

① 에러 종류에 맞는 적절한 예외 작성

⑴ 예외 상태가 아니라면 예외를 사용하지 않음

⑵ 복구 가능한 에러라면 검사 예외 선택

⑶ 복구 불가능한 에러 또는 프로그래머 실수로 인한 에러라면 비검사 예외 선택

⑷ 컴파일

② 메서드를 호출하는 쪽 변경(검사 예외)

⑴ 호출하는 쪽에서 에러를 처리한다면 try~catch 추가

호출하는 쪽에서 에러를 처리하지 않는다면 throws 절 추가

컴파일해서 테스트

③ 메서드를 호출하는 쪽 변경(비검사 예외)

* 검사 예외와 비검사 예외 

예외 처리 리팩토링 - ye-oe cheoli lipaegtoling

3) 리팩토링 후

import java.util.*;

public class Robot {
    private final String _name;
    private final Position _position = new Position(0, 0);
    private final Direction _direction = new Direction(0, 1);
    public Robot(String name) {
        _name = name;
    }
    public void execute(String commandSequence) {
        StringTokenizer tokenizer = new StringTokenizer(commandSequence);
        try {
            while (tokenizer.hasMoreTokens()) {
                String token = tokenizer.nextToken();
                executeCommand(token);
            }
        } catch (InvalidCommandException e) {
            System.out.println("Invalid command: " + e.getMessage());
        }
    }
    public void executeCommand(String commandString) throws InvalidCommandException {
        Command command = Command.parseCommand(commandString);
        executeCommand(command);
    }
    public void executeCommand(Command command) throws InvalidCommandException {
        if (command == Command.FORWARD) {
            _position.relativeMove(_direction._x, _direction._y);
        } else if (command == Command.BACKWARD) {
            _position.relativeMove(-_direction._x, -_direction._y);
        } else if (command == Command.TURN_RIGHT) {
            _direction.setDirection(_direction._y, -_direction._x);
        } else if (command == Command.TURN_LEFT) {
            _direction.setDirection(-_direction._y, _direction._x);
        } else {
            throw new InvalidCommandException();
        }
    }
    public String toString() {
        return "[ Robot: " + _name + " "
            + "position(" + _position._x + ", " + _position._y + "), "
            + "direction(" + _direction._x + ", " + _direction._y + ") ]";
    }
}
import java.util.*;

public class Command {
    public static final Command FORWARD = new Command("forward");
    public static final Command BACKWARD = new Command("backward");
    public static final Command TURN_RIGHT = new Command("right");
    public static final Command TURN_LEFT = new Command("left");
    private static final Map<String,Command> _commandNameMap = new HashMap<String,Command>();
    static {
        _commandNameMap.put(FORWARD._name, FORWARD);
        _commandNameMap.put(BACKWARD._name, BACKWARD);
        _commandNameMap.put(TURN_RIGHT._name, TURN_RIGHT);
        _commandNameMap.put(TURN_LEFT._name, TURN_LEFT);
    }
    private final String _name;
    private Command(String name) {
        _name = name;
    }
    public String getName() {
        return _name;
    }
    public static Command parseCommand(String name) throws InvalidCommandException {
        if (!_commandNameMap.containsKey(name)) {
            throw new InvalidCommandException(name);
        }
        return _commandNameMap.get(name);
    }
}

3. 코드 추가 수정

1) 분류 코드를 상태/전략 패턴으로 치환

- if문의 연쇄는 switch문과 마찬가지로 악취를 풍긴다

- 따라서 forward, backward, turn_right, turn_left는 상태/전략 패턴으로 리팩토링한다.

import java.util.*;

public abstract class Command {
    public static final Command FORWARD = new Forward();
    public static final Command BACKWARD = new Backward();
    public static final Command TURN_RIGHT = new Right();
    public static final Command TURN_LEFT = new Left();
    private static final Map<String,Command> _commandNameMap = new HashMap<String,Command>();
    static {
        _commandNameMap.put(FORWARD._name, FORWARD);
        _commandNameMap.put(BACKWARD._name, BACKWARD);
        _commandNameMap.put(TURN_RIGHT._name, TURN_RIGHT);
        _commandNameMap.put(TURN_LEFT._name, TURN_LEFT);
    }
    private final String _name;
    protected Command(String name) {
        _name = name;
    }
    public String getName() {
        return _name;
    }
    public static Command parseCommand(String name) throws InvalidCommandException {
        if (!_commandNameMap.containsKey(name)) {
            throw new InvalidCommandException(name);
        }
        return _commandNameMap.get(name);
    }
    public abstract void execute(Robot robot);

    // 중첩 클래스

    private static class Forward extends Command {
        public Forward() {
            super("forward");
        }
        @Override public void execute(Robot robot) {
            robot.forward();
        }
    }

    private static class Backward extends Command {
        public Backward() {
            super("backward");
        }
        @Override public void execute(Robot robot) {
            robot.backward();
        }
    }

    private static class Right extends Command {
        public Right() {
            super("right");
        }
        @Override public void execute(Robot robot) {
            robot.right();
        }
    }

    private static class Left extends Command {
        public Left() {
            super("left");
        }
        @Override public void execute(Robot robot) {
            robot.left();
        }
    }
}
import java.util.*;

public class Robot {
    private final String _name;
    private final Position _position = new Position(0, 0);
    private final Direction _direction = new Direction(0, 1);
    public Robot(String name) {
        _name = name;
    }
    public void execute(String commandSequence) {
        StringTokenizer tokenizer = new StringTokenizer(commandSequence);
        try {
            while (tokenizer.hasMoreTokens()) {
                String token = tokenizer.nextToken();
                executeCommand(token);
            }
        } catch (InvalidCommandException e) {
            System.out.println("Invalid command: " + e.getMessage());
        }
    }
    public void executeCommand(String commandString) throws InvalidCommandException {
        Command command = Command.parseCommand(commandString);
        command.execute(this);
    }
    public void forward() {
        _position.relativeMove(_direction._x, _direction._y);
    }
    public void backward() {
        _position.relativeMove(-_direction._x, -_direction._y);
    }
    public void right() {
        _direction.setDirection(_direction._y, -_direction._x);
    }
    public void left() {
        _direction.setDirection(-_direction._y, _direction._x);
    }
    public String toString() {
        return "[ Robot: " + _name + " "
            + "position(" + _position._x + ", " + _position._y + "), "
            + "direction(" + _direction._x + ", " + _direction._y + ") ]";
    }
}

4. 한 걸음 더 나아가기

1) 검사 예외와 비검사 예외

- 검사 예외는 보통 java.lang.Exception의 하위 클래스로 선언 

  해당 구문을 try-catch문으로 감싸거나 throws 절을 붙인다.

- 비검사 예외는 java.lang.RuntimeException의 하위 클래스로 선언한다.

2) 예외 계층

- 세세한 에러 처리를 하고 싶으면 계층 아래쪽에 있는 클래스를 예외로 잡는다.

- 여러 에러를 한꺼번에 처리하고 싶다면 계층 위쪽에 있는 클래스로 예외를 잡는다.

3) java.io.EOFException에 대해

- 데이터를 읽으려고 했지만 더는 데이터가 없을 때 발생하는 예외!

4) 비검사 예외와 사전 확인용 메서드

- 비검사 예외는 사전 확인으로 예외 발생을 회피 가능한 상황에 사용하는게 좋다.

5) 실패 원자성

- 예외를 던질 때 객체가 이도저도 아닌 상태에 빠지지 않게 하라