체스 미션 리뷰 정리

예외처리의 방향성

체스 미션 중, source에서 target까지 정해진 방향으로 계속 한 칸씩 움직이면서 장애물을 확인하는 로직이 있었다.
file값과 rank값을 더하고 빼서 정해진 방향으로 한 칸씩 이동하는데 이동 로직이 정해진 방향으로 board 내부의 값까지 이동하기 때문에 board밖으로 position값이 나가지 않을텐데, 뭔가 예외처리를 해줘야할 것 같아서 try catch문을 이용하여 예외를 잡고 null을 반환했다. 하지만 실제 로직에서 써먹지는 않는다. 대체 어디까지 예외처리를 해야할까?
코드는 아래와 같다.

//Position class -> 실질적으로 Position값을 direction으로 향할 때 쓰는 메서드
public Position move(final Direction direction) {
	return new Position(direction.moveFile(file), direction.moveRank(rank));
}

//File class -> 가로 방향 이동
public File move(final int distance) {
    try {
        return of((char) (value + (char) distance));
    } catch (IllegalArgumentException exception) {
        return null;
    }
}
//Rank class -> 세로 방향 이동
public Rank move(final int distance) {
    try {
        return of(value + distance);
    } catch (IllegalArgumentException exception) {
        return null;
    }
}
//MoveCheker -> Position class의 move메서드를 사용하는 메서드, 구현 로직 상으로는 Position이 Baord 밖으로 나갈 일이 없다
private boolean isBlocked(final Direction direction, final Position from, final Position to) {
    final Position next = from.move(direction);
    if (next.equals(to)) {
        return false;
    }
    if (!board.findPiece(next).equals(new EmptyPiece())) {
				return true;
    }
    return isBlocked(direction, next, to);
}

그렇게 구성한 이유는 아래와 같다.

enum이 정해진 범위 내의 상수들이 있을 때, enum을 사용하면 범위 외의 값이 왔을 때 조건문을 줄일 수 있고 해당 상수를 찾을 때 더 편하기 때문에 사용했다.
그렇기 때문에 특히 File이나 Rank enum은 위치 값이기 때문에 enum에서 만들어진 상수들 이외의 값이 들어오면 처리를 꼭 해야한다고 생각했다.
처음에는 exception을 던졌는데 이러면 move하는 과정에서 사용자의 의도와는 다르게 예외가 발생할 수 있다고 생각해서, null을 반환하도록 했다.

이 부분에 대해서 리뷰 요청을 드렸고, 리뷰어님의 답변을 듣다보니 예외처리에 대한 생각이 바뀌었다.

나의 생각을 말하자면 아래와 같다.

우테코 초기에 예외처리 항목을 기능 목록과 함께 적었었는데, 이 과정에서 예외처리를 상세하게 하는 것이 잘하는 것 같았다. 그래서 포켓몬 잡듯이 생기지 않을 예외까지도 모두 찾으려고 했던 것 같다.
예외처리는 하드웨어적이든, 소프트웨어적이든 많은 비용을 소모한다.
하나의 예외를 발생시키기 위해서 별도의 코드가 필요하고 이를 처리하기 위해서도 코드가 필요하다.
우리는 어쩌면 잠재적인 예외를 줄이도록 고용되는 사람일 수도 있다.
그렇게 하기위해 좋은 구조를 생각하고 좋은 설계를 해야한다.

따라서, 위의 코드는 일종의 오버프로그래밍이라고 볼 수 있다는 결론을 내리고 예외 처리 부분을 삭제했다.

상속 과정에서 사용하지 않는 메서드

나의 예시로 보면, 체스판에 놓일 Piece를 추상 클래스로 만들어놓고 King이나 Queen과 같은 기물들을 만들어 Piece를 상속 받도록 만들었다.
하지만 체스판에 기물이 없는 곳이 있을 텐데, 이 부분을 왠지 모르겠지만 null로 저장해놓기 싫었다.
그래서 빈 Piece를 EmptyPiece라는 클래스를 만들어 추상클래스 piece를 상속받았다.
이 과정에서 사용하지 않는 메서드가 있어서 리뷰어님께 리뷰 요청을 드렸는데, 이럴 때를 UnsupportedOperationException을 던지는 방식으로 구현할 수 있다는 것을 알았다.

테스트 코드의 일관성 유지하기

이번에 테스트 코드를 작성하면서, @DisplayName을 거의 다 작성했지만 내용이 없는 정말 짧은 테스트의 경우에는 @DisplayName을 적지 않았다.
이정도 코드면 안적어도 되겠지 하는 어리숙한 생각이였는데, 어떻게 리뷰어님이 딱 아시고 리뷰를 주셨다.

테스트 코드의 이해도와 일관성을 위해서 @DisplayName을 적어주든 제거하든 통일하는 편이 좋아요.
만일 팀에 새로운 개발자가 오셔서 협업할 때에 어디에는 적혀있고 어디에는 없다면 혼란스러울거에요!@DisplayName을 적는 시간보다 적어야하나 말아야하나 고민하는 시간이 더 들거구요 😬

어플리케이션을 이해하기 위해 테스트 코드를 먼저 확인하는 것이 더 이해하기 쉬울 때도 있다.
나는 협업 경험이 거의 없는 편이다.
그렇다보니 항상 다른 사람들이 보는 내 코드를 못 보는 것 같다.
리뷰어님의 리뷰를 참고하자면, 내가 새로 온 개발자라고 생각하고 내 코드를 보는 습관? 같은게 있어야 할 것 같다!

파일 끝에 개행


리뷰 받기 전까지는 몰랐는데 모든 파일 끝에 개행이 있어야 한다.
당장 어플리케이션이 돌아가지 않는 것은 아니지만, github에서 보면 오류가 뜰 것이다.

알아본 결과, C 컴파일러인 gcc는 POSIX에 근거해서 동작하였는데, 소스코드를 한 줄씩(line by line)으로 읽게되므로 파일 끝에 EOF가 없으면 문제가 발생한다.
그렇기 때문에 파일은 끝이 났지만 개행문자가 없어서 한 줄이 끝나지 않은 것으로 인식해서 정상적으로 동작하지 않는 이슈가 발생할 수 있다.

도메인에 대한 나의 오해

이번 미션에서 도메인에 대한 나의 생각을 살짝쿵 적어보자면, 도메인비지니스 로직이 있는 곳이고 비지니스 로직어플리케이션을 완성하기 위해 꼭 필요한 로직이라고 생각했다.
그래서 꼭 필요한 로직이 아니라면 그건 도메인이 아니라고 생각했었다.
예를 들어 요구사항을 지키기 위해 뷰가 꼭 필요한 로직은 아니다. 그러므로 뷰는 도메인이 아니다.
문제는 내가 잘 모르는 상태패턴을 써보겠다고 state를 사용하면서 생겼는데,,
이제 나의 오해에 대해 살짝쿵 적어보자면 state가 없어도 어플리케이션을 완성할 수 는 있기 때문에 state가 도메인은 아니라고 당시에 여겼던 것 같다.
저는 state가 없어도 어플리케이션을 완성할 수 있다 라는게 굳이 이걸 쓰지 않아도 다른 걸로 대체가 가능하다 라고 생각을 하고 있었는데 어차피 대체할 다른 부분이 필요하니까 생각해보면 게임을 만들기 위해 꼭 필요한 로직이였다!

자원을 직접 닫을 경우 try-finally 보다는 try-with-resources를 사용하기

Dao를 직접 구현해서 사용할 때 나는 아래와 같이 JDBP에 connection을 요청했다.

@Override
    public void save(final String name, final String position, final Long gameId) {
        final String sql = "insert into board (name, position, gameId) values (?,?,?)";
        try {
             final Connection connection = JdbcConnector.getConnection();
             final PreparedStatement statement = connection.prepareStatement(sql)
            statement.setString(1, name);
            statement.setString(2, position);
            statement.setLong(3, gameId);
            statement.executeUpdate();
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

이런 상황을 위해 선언된 객체들에 대해서 try가 종료될 때 자동으로 자원을 해제해주는 try-with-resources가 자바7부터 제공되었다.
Java는 try구문이 종료될 때 객체의 close() 메소드를 호출한다.
사용하는 방법은 try 선언하고 괄호를 만든 후 나머지는 그대로 쓰되, 괄호 안에 사용하고 close할 자원들을 쓴다!
리뷰를 반영한 코드는 아래와 같다.

@Override
    public void save(final String name, final String position, final Long gameId) {
        final String sql = "insert into board (name, position, gameId) values (?,?,?)";
        try (final Connection connection = JdbcConnector.getConnection();
             final PreparedStatement statement = connection.prepareStatement(sql)) {// 이 부분은 안넣어도 된다!
            statement.setString(1, name);
            statement.setString(2, position);
            statement.setLong(3, gameId);
            statement.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

자바9에서는 try-with-resources가 더 향상되어서 원래 try문 밖에서 선언된 자원은 try문 안에서 사용할 수 없었기 때문에 내부에서 한 번 더 초기화를 해야 했다.
하지만 자바9부터 밖에서 선언된 자원도 try문 안에서 사용할 수 있도록 향상 되었다.

//자바 9이전엔 이렇게 사용했어야 했다.
try (final Connection connection = JdbcConnector.getConnection();) 
//자바 9부터는 아래와 같이 사용해도 된다.
final Connection connection = JdbcConnector.getConnection();
try (connection)

이번에 너무 좋은 리뷰어님을 만나 배운게 위에 글 말고도 너무너무 많은데 아직 제대로 숙지를 못한 것도 많아서 이 정도만 쓰고 보충할 예정이다!
어제보다 성장한 내가 되길 :)

좋은 웹페이지 즐겨찾기