우아한테크코스 레벨2 웹 체스 미션 정리

디우·2022년 6월 23일
0

1 단계 - 체스(Spring 적용하기)

웹 체스 1단계 저장소 링크

PR 링크

리뷰어: 앨런

고민한 내용

PRG 패턴

"Post-Redirect-Get" 패턴을 지킬 수 있도록 구현하려고 노력하였다.
웹 브라우저에서 HTTP Post 요청은 새로고침 시에 이전 요청과 동일한 요청을 중복하게 되어 의도치 않은 결과를 낳을 수 있다는 것을 알게 되었고, 체스 말 이동(/move) 후에 redirect를 함으로써 새로운 요청으로 옮겨줄 수 있도록 개선해보았다.

  • PRG 패턴
    HTTP POST 요청에 대한 응답이 또 다른 URL로의 GET 요청을 위한 리다이렉트여야 한다는 것을 의미한다.
    즉 쉽게 말해서 POST 방식으로 온 요청에 대해서 리다이렉트하여 GET 요청으로 보내는 것을 말한다.
  • PRG 패턴을 사용하지 않으면 발생하는 문제점
    - 첫번째로 새로고침으로 인한 동일한 요청이 연속적으로 보내지는 이슈가 있다. (POST는 멱등성을 보장하지 않는다.)
    • 두번째로는 POST 요청은 URL을 복사하더라도 다른 사람과 공유할 수 없다.

하지만 이렇게 수정을 하다 보니 Redirect시에 한글 파라미터가 깨지는 문제가 발생하였고, 다음의 블로그 글을 참고하여 파라미터에 대해서 인코딩한 값을 주도록 수정해보았다.
Redirect 시 한글 파라미터가 깨지는 문제

favicon.ico

다음으로는 "favicon.ico"를 읽어들이는 문제를 겪었습니다.
request에 대한 매핑을 단순히 "/{gameNaame}"과 같이 루트 바로 밑으로 설정해두니 "favicon.ico"를 읽어 제대로 된 요청에 대한 처리를 못하는 이슈를 마주하게 되었습니다. 이는 간단하게 루트 바로 밑에 두는 것이 아니라 uri를 "/game/"을 모든 매핑에서 기본으로 가지도록 함으로써 해결해볼 수 있었습니다.

RedirectAttributes

"/game/{gameName}/move" 에서 에러 발생시(ex. 잘못된 이동, 다른 팀의 말 이동) 에러 메시지를 출력해줘야하지만 "redirect:/game/{gameName}"을 수행하기 때문에 에러 메시지를 전달하지 못하는 문제가 있었고, RedirectAttributes와 함께 @RequestParam(value = "error", required = false) 를 사용하여 에러 메시지를 출력해줄 수 있도록 하였습니다.

질문

    public ChessGame findByName(String gameName) {
        String sql = "select CHESSGAME.turn, CHESSGAME.game_name, PIECE.type, PIECE.team, PIECE.`rank`, PIECE.file from CHESSGAME, PIECE\n"
                + "where CHESSGAME.game_name = PIECE.game_name AND CHESSGAME.game_name = ?;";

        List<ChessGame> result = jdbcTemplate.query(connection -> {
            PreparedStatement preparedStatement = connection.prepareStatement(sql, ResultSet.TYPE_SCROLL_SENSITIVE,
                    ResultSet.CONCUR_UPDATABLE);
            preparedStatement.setString(1, gameName);
            return preparedStatement;
        }, chessGameRowMapper);

        if(result.isEmpty()) {
            return null;
        }

        return result.get(0);
    }

    private final RowMapper<ChessGame> chessGameRowMapper = (resultSet, rowNum) -> new ChessGame(
        getTurn(resultSet),
        resultSet.getString("game_name"),
        makeCells(resultSet)
    );

    private String getTurn(ResultSet resultSet) throws SQLException {
        resultSet.beforeFirst();
        resultSet.next();
        return resultSet.getString("turn");
    }

    private Map<Position, Piece> makeCells(ResultSet resultSet) throws SQLException {
        resultSet.beforeFirst();

        Map<Position, Piece> cells = new HashMap<>();

        while (resultSet.next()) {
            Position position = makePosition(resultSet);
            Piece piece = makePiece(resultSet);
            cells.put(position, piece);
        }

        return cells;
    }

ChessGameDao 에서 위와 같이 두 테이블을 함께 조회하고 있습니다. 그러다 보니 "game_name"과 "turn"은 하나의 데이터만 필요한데, 피스들의 데이터는 여러개이므로 ResultSet의 커서를 움직여서 각각 필요한 데이터를 조회해오고 있습니다.
그리고 ResultSet의 커서를 움직이기 위해서 PreparedStatement를 사용해야했고, 따라서 우리가 필요한 데이터는 ChessGame 하나인데, "jdbcTemplate.query()"의 결과로 List를 가져와야하는 불필요한 작업이 필요해졌습니다.

그래서 페어와 고민한 내용은 지금 현재 코드를 유지하는게 좋을지 혹은 CHESSGAME에 대한 조회, PIECE들에 대한 조회 총 2번의 쿼리를 이용하여 ChessGame을 만드는 것이 나은지를 고민하였습니다.

저는 하나의 데이터(ChessGame)을 위해서 분리된 2개의 쿼리를 작성해야한다 라는 점이 어색하게 느껴졌습니다.
turn, game_name 그리고 piece들의 정보 모두 ChessGame이라는 하나의 도메인을 위한 데이터들인데 이를 서로 독립적인 각각의 쿼리를 통해서 가져오고 이를 합친다라는 과정에서 여러 가지 문제가 발생할 수 있지 않을까하는 생각도 들었습니다.
(아직 쿼리를 보내고 결과를 받는 등의 과정에서 여러가지 문제들을 만나보지 못해서..구체적으로 어떤 문제다라고 이야기는 못하지만..두 개의 쿼리 중 하나의 쿼리만 성공하는 경우에 발생하는 문제도 있을 수 있을 것 같습니다.)

혹시 이 내용에 대한 앨런의 의견을 들어볼 수 있을까요???😅

피드백 내용

질문에 대한 답변

질문 주신 내용을 'chessgame과 피스들을 한 번에 조회하기, 나눠서 조회하기 중에 고민중이다.' 로 정리할 수 있을 것 같다.

우선 전 비즈니스적으로 chessgame과 피스들이 한 번에 조회되어도 어색하지 않는지를 확인하는 편입니다.
피스들과 체스게임은 라이프사이클이 같다고 생각하는데요. 한 번에 조회되도 좋겠네요.

또 각각의 장/단점을 비교해보면 좋을 것 같은데요. 그러면 선택할 수 있는 폭이 좁혀질 것 같습니다.

Q. 프레임워크에 만들어져 있는 상수(StandardCharsets.UTF_8)가 있는데 이를 활용해보면 어떨까요?

    public String move(@PathVariable String gameName,
                       @RequestParam("from") String from, @RequestParam("to") String to,
                       Model model, RedirectAttributes redirectAttributes) throws UnsupportedEncodingException {
        String encodedParam = URLEncoder.encode(gameName, "UTF-8");

A. java에서 StandardCharsets 클래스를 통해 "표준 문자 집합"에 대한 상수를 정의해주고 있는지 몰랐습니다. 개선해보겠습니다.

Q. 코멘트 남겨주신 것처럼, 여기 상태가 있네요..! 이 부분으로 여러명이 접속 했을 때 문제가 발생 할 수 있어요. (상태를 공유해서)다음 단계에서 이 부분을 제거하고 비즈니스를 풀어보면 좋겠습니다~

    private final ChessGameDao chessGameDao;
    private final PieceDao pieceDao;
    private ChessGame chessGame;

A. ChessGame이라는 인스턴스 변수를 제거하고, 그 때 그때마다 DB에 쿼리를 날려서 데이터를 가져오고 처리를 하게끔 수정해보았습니다!

Q. private으로 작성하면 동작 안할 것 같은데요. 한번 확인해보시겠어요?~

    private void rollback() {
        chessGameDao.remove("test");
    }

A. AfterEach (JUnit 5.0.1 API) 문서를 참고해보니 "AfterEach의 경우 must have a void return type, must not be private, and must not be static. 즉, 반환타입이 void이고, private이 반드시 아니어야하며 static 또한 불가능 하다는 내용이 있네요!! 제대로된 사용법을 모르고 사용했었네요...🥲

Q. 런타임에 jdbctemplate이 변경되지 않도록 final을 붙이면 좋을 것 같습니다~

@Repository
public class ChessGameDao {

    private JdbcTemplate jdbcTemplate;
    
    ...
}

A. 의존성 주입에 대해서 저랑 페어 둘 다 잘 모르는 상태였어서..필드 주입을 시도하다보니 final이 누락되었던 것 같아요..!!

필드 주입 대신 생성자 주입을 사용한 이유는 다음 블로그 글을 참고하였습니다!!
생성자 주입을 @Autowired를 사용하는 필드 주입보다 권장하는 하는 이유


2 단계 - 체스(동시에 여러 게임 하기)

웹 체스 2단계 저장소 링크

PR 링크

리뷰어: 앨런

고민한 내용

슬렉 대화 내용중 일부

이전 1단계에서 피드백 받았던 내용과 같이 상태를 제거하여 여러 명이 접속했을 때 상태를 공유해서 발생할 수 있는 문제를 제거해보았습니다.
또한 DM 드렸던 것과 같은 이유로 game_name 대신 별도의 AutoIncrement를 사용하는 컬럼을 추가하여 해당 컬럼을 PK로 사용하도록 변경하였습니다.
(여기에 게임이름을 PK로 사용하지 않아야 하는 이유는 테이블간에 연관관계를 맺을 때 PK를 사용하는데, game_name을 PK로 하는 경우 game_name의 변경 여파가 다른 테이블들 곳곳에 전파될 수 있기 때문이다. 또한 게임이름이 유일하지 않다라는 요구사항 변경에 따라 테이블 설계에 변경을 유발하게 될 수도 있다.)
마지막으로 @ExceptionHandler를 이용하여 예외 처리 로직을 별도로 분리해보았습니다!

피드백 내용

Q. 체스게임을 시작하는 요청같은데요. GetMapping으로 하신 이유가 있을까요?
클라이언트입장에서는 Get method는 조회하는걸로 인지할 수 있는데, 자원이 생성되네요. PostMapping이 자연스러울 것 같은데 어떻게 생각하시나요?

    @GetMapping("/start")
    public String start(@RequestParam("game_name") String gameName, @RequestParam("password") String password) {
        Long id = chessService.save(gameName, password);
        
        return "redirect:/game/" + id;
    }

A. HTTP Method 사용에 대한 강의를 들으면서 HTTP Method 사용법에 대한 이해없이 사용하고 있다는 것을 인지할 수 있었습니다. 새로운 리소스를 생성하는 경우에는 POST 메소드가 더 적절할 것 같습니다!
(POST 메소드는 주로 새로운 리소스를 생성할 때 사용하는 HTTP Method이다.)
수정된 코드는 다음과 같다.

    @PostMapping("/start")
    public String start(@RequestParam("game_name") String gameName, @RequestParam("password") String password) {
        Long id = chessService.save(gameName, password);

        return "redirect:/game/" + id;
    }

Q. @PostMapping("/delete")로 지정하신 이유가 있을까요? @DeleteMapping을 고려해볼 수 있지않을까요? 추가로 여기는 game의 식별자를 받지 않는 이유가 있을까요?

    @PostMapping("/delete")
    public String delete(@RequestParam(value = "game_name", required = false) String gameName,
                         @RequestParam(value = "password", required = false) String password) {
        State state = chessService.findStateByGameNameAndPassword(gameName, password);
        chessService.deleteByGameNameAndPassword(state, gameName, password);

        return "redirect:/";
    }

A. handlebars를 사용하는 상태에서 DeleteMapping 을 적절히 사용할 수 있는 방법을 찾지 못하였고, POST를 유지하기로 앨런과 결정하였으므로 "/delete"를 유지하여 "삭제"의 의미를 나타낼 수 있도록 하겠습니다...🥲
(현재 코드에서 삭제 이후에 redirect를 통해서 "/"(루트)로 이동해야 하는데, 현재 웹 기반 컨트롤러에서 delete method로 요청받고, GET으로 요청되지 않는 이슈가 존재하여 POST 매핑을 유지하였다.
form 태그에서는 GET과 POST만 지원한다. 또한 DELETE 후 redirect하는 링크에 delete 메소드로 요청한다. 이는 브라우저에서 GET, POST만 호환하는 규칙 때문..)
html5 form에서 GET과 POST만 지원하는 이유
(구현한 코드와는 별개로, @DeleteMapping("/game")과 같이 DeleteMapping을 통해 '삭제'라는 의미를 나타내고, URL은 자원을 나타낼 수 있도록 하는 것이 적절하다고 생각합니다!)

앨런: url을 보고 어떤 리소스를 삭제하려는지 파악은 어려운 것 같아요. 만약 식별자로 삭제한다면 /game/:id/delete 이런 네이밍을 고려해볼 수 있을 것 같아요.

: API를 설계할 때 "행위"는 http 메소드로 지정(행위는 URL이 아닌 Method로 표현하기)하고, 자원을 명사로써 path로 지정하라 라는 내용의 체스 미션 피드백 수업을 들엇습니다!!
그런 의미에서 /game/:id/delete 보다 /game/:id 를 URL로써 사용하고, @DeleteMapping 혹은 메소드명을 통해서 삭제의 의미를 나타내는 건 어떤가요??

앨런: 네 좋습니다! 웹 기반 컨트롤러에서 delete method로 요청받고, redirect 시킬 때 GET으로 요청되지 않는 이슈가 있으니 참고해주세요. (delete -> redirect -> delete)

Q. 또 컨트롤러 내에서 비즈니스의 플로우를 작성하지마시고, 서비스에 위임하는게 좋아보입니다.다른 컨트롤러도 적용해주세요~

    public String delete(@RequestParam(value = "game_name", required = false) String gameName,
                         @RequestParam(value = "password", required = false) String password) {
        State state = chessService.findStateByGameNameAndPassword(gameName, password);
        chessService.deleteByGameNameAndPassword(state, gameName, password);

        return "redirect:/";
    }

A. 컨트롤러는 "gameName과 password를 통해서 삭제를 한다" 라는 것에만 관심이 있지, "어떻게(how)" 삭제를 하는지는 알 필요가 없다고 생각합니다. 즉, 말씀해주신대로 비즈니스 흐름(어떤 메소드를 호출하는지 그리고 어떤 순서로 호출하는지)에 대해서는 알 필요가 없고, 이를 서비스에 위임하는게 적절해 보입니다! 수정해보도록 하겠습니다!

(이전까지는 deleteByGameNameAndPassword()와 같이 메소드명을 작성하였는데, 이렇게 메소드명이 길어지는 경우 deleteBy(gameName, password)와 같은 네이밍으로 메소드명도 짧으면서 의미를 확실히 전달할 수 있다는 생각이 드네요!! 이 부분도 한 번 반영해보도록 하겠습니다!😃)

    @PostMapping("/delete")
    public String delete(@RequestParam(value = "game_name", required = false) String gameName,
                         @RequestParam(value = "password", required = false) String password) {
        chessService.deleteBy(gameName, password);

        return "redirect:/";
    }

Q. DAO: 하나의 쿼리를 보내는 책임
Service: 트랜잭션단위의 비즈니스 흐름을 책임
중복이름을 제어가 필요한건 비즈니스 영역이기 때문에 Service로 이동하는게 좋아보이네요.

@Repository
public class ChessGameDao {
	private final JdbcTemplate jdbcTemplate;
    
    public ChessGameDao(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    
    public Long save(ChessGameDto chessGameDto, String password) {
        validateDuplicateGameName(chessGameDto.getGameName());
        
        String sql = "insert into chessgame (game_name, turn, password) values (?, ?, ?)";
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
            ps.setString(1, chessGameDto.getGameName());
            ps.setString(2, chessGameDto.getTurn());
            ps.setString(3, password);
            return ps;
        }, keyHolder);

        return Objects.requireNonNull(keyHolder.getKey()).longValue();
    }

A. 동의합니다. DAO는 Data Access Object가 의미하는 것과 같이 DB에 접근하고 쿼리를 날려 결과를 받아오는데에만 집중하면 된다고 생각합니다. 따라서 말씀해주신 것과 같이 트랜잭션 단위의 비즈니스 흐름을 책임지는 Service 쪽으로 옮기는 것이 적절한 것 같습니다!
"체스 게임을 생성할 때 이미 동일한 게임 이름이 존재하는지 유무를 검사"하는 부분을 Service쪽으로 이동해보았습니다.

개선된 코드는 다음과 같다.

    public Long save(ChessGameDto chessGameDto, String password) {
        String sql = "insert into chessgame (game_name, turn, password) values (?, ?, ?)";
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
            ps.setString(1, chessGameDto.getGameName());
            ps.setString(2, chessGameDto.getTurn());
            ps.setString(3, password);
            return ps;
        }, keyHolder);

        return Objects.requireNonNull(keyHolder.getKey()).longValue();
    }

Q. limit 1 을 사용하면 모든 테이블을 탐색하지 않을 수도 있겠네요

    private void validateDuplicateGameName(String gameName) {
        String sql = "select count(*) from CHESSGAME where game_name = ?";
    private void validateDuplicateGameName(String gameName) {
        String sql = "select * from CHESSGAME where game_name = ? LIMIT 1";

A. LIMIT을 사용하여 하나의 row가 검색되면 더 이상 탐색하지 않음으로써 모든 테이블의 row를 탐색하지 않아도 되어 성능 개선이 있을 수 있을 것 같습니다! (특히 row가 10,000개 이상 100,000개 이상과 같이 정말 많아지는 경우 그 이점을 더욱 명확히 느낄 수 있을 것 같습니다!)

jdbcTemplate.queryForObject()는 조회 결과가 없는 경우 EmptyResultDataAccessException을 발생시킵니다.
따라서 위의 코멘트 변경과 함께 해당 쿼리문을 수정하여 다음과 같이 코드를 작성할 수 있었습니다.

private void validateDuplicateGameName(String gameName) {
    try {
        chessGameDao.findOneByGameName(gameName);
        throw new IllegalGameProgressException("동일한 이름의 게임이 이미 존재합니다.");
    } catch (EmptyResultDataAccessException ignored) {
    }
}

즉, 우리가 원하는 실행흐름(정상흐름)이 EmptyResultDataAccessException 이기 때문에 위와 같이 코드를 작성하게 되었습니다. 그런데 try-catch 구무에서 오히려 예외가 발생할 때 정상흐름이 된다는 게 어색하게 느껴졌습니다.
1. 실무에서도 위와 같은 코드가 생길 때가 종종 있는지 궁금합니다!
2. 위와 같은 코드에서 발생할 수 있는 문제점이 있을까요? 만약 있다면 어떤 것이 있는지 궁금합니다! 저는 우선 가독성 측면에서 단점이 있다고 생각합니다. (이 코드를 처음보는 사람이 이해하기 위해서는 많은 시간이 들어가지 않을까?)

위와 관련한 슬렉 대화 내용

Q. 커스텀예외를 등록하신 이유가 있을까요?

public class ExistGameException extends IllegalArgumentException {
    public ExistGameException() {
        super("동일한 이름의 게임이 이미 존재합니다");
    }
}

A. 모든 IllegalArgumentException이 논리적으로 동일한 예외라는 생각이 들지 않았습니다.
예를 들어, 크게는 "동일한 게임 이름 존재"와 "체스게임에서 움직임 예외"는 동일하지 않다는 생각을 하였습니다.
그래서 이 예외들을 구분해주기 위해서 커스텀예외를 등록하였습니다..!
(또 예외 메시지도 가능하면 해당 커스텀 예외 안으로 위임해주고 싶었습니다..😅)

앨런 : 답변 감사합니다. 또 커스텀예외를 사용했을 때 단점도 있을까요?

: 자바에서 표준예외를 잘 제공하고 있기 때문에, 커스텀 에외를 남용했을 때 문제가 발생할 수 있을 것 같습니다.
인자값이 부적절한 경우에 던지는 예외인 IllegalArgumentException 이나 작업을 수행하기에 부적합한 상태인 경우 던지는 예외인 IllegalStateExceptionUnsupportedOperationException 등 자바에서 제공하는 우리에게 익숙한 표준 예외를 사용함으로써 충분히 예외를 표현할 수 있습니다.
따라서 이런 표준예외를 적절하게 사용하지 않고, 커스텀 예외를 계속해서 만들게 되면 우리가 "직접" 관리 해줘야 할 커스텀 예외 클래스가 지나치게 많아질 수 있다고 생각합니다.
다음 글을 참고해보았습니다! custom exception을 언제 써야 할까?

Q. 이쪽 코드는 이전 페이지로 이동한다고 볼 수 있을까요?

@ControllerAdvice
public class ExceptionController {

    @ExceptionHandler(IllegalArgumentException.class)
    public String handleMove(RedirectAttributes redirectAttributes, IllegalArgumentException e, HttpServletRequest request) {
        redirectAttributes.addAttribute("error", e.getMessage());

        String referer = request.getHeader("Referer");
        String[] split = referer.split("\\?");

        return "redirect:" + split[0];
    }
    ...
]

A. 네 맞습니다. 이전 페이지로 이동하지만 뒤에 붙는 쿼리스트링을 제거해주지 않으면 계속해서 쿼리 스트링이 중첩되는 문제가 있어 쿼리스트링을 제거해주기 위해 referer.split("\?"); 가 필요했습니다.

앨런 : 기존 자바패키지에 제공하는 기능을 활용해보는건 어떨까요?

: URI.create()를 통해서 인자로 전달한 문자열을 URI 객체를 생성하고, getPath()를 이용해서 해당 URI의 디코딩된 경로만을 반환함으로써 쿼리스트링의 중첩을 막을 수 있겠군요! 또한 보다 명시적인 것 같습니다.

@ControllerAdvice
public class ExceptionController {

    @ExceptionHandler(IllegalMoveException.class)
    public String handleMove(RedirectAttributes redirectAttributes, HttpServletRequest request) {
        redirectAttributes.addAttribute("error", "잘못된 이동입니다.");

        String referer = request.getHeader("Referer");
        String previousPath = URI.create(referer).getPath();

        return "redirect:" + previousPath;
    }
}

Q. 크게 다른 행위를 하지않는다면 하나로 묶을 수 있지않을까요?

    @ExceptionHandler(IllegalPasswordException.class)
    public String handleEmptyResult(RedirectAttributes redirectAttributes, IllegalPasswordException e) {
        redirectAttributes.addAttribute("error", e.getMessage());

        return "redirect:/";
    }

    @ExceptionHandler(IllegalDeleteException.class)
    public String handleDelete(RedirectAttributes redirectAttributes, IllegalDeleteException e) {
        redirectAttributes.addAttribute("error", e.getMessage());

        return "redirect:/";
    }

    @ExceptionHandler(ExistGameException.class)
    public String handleExistGame(RedirectAttributes redirectAttributes, ExistGameException e) {
        redirectAttributes.addAttribute("error", e.getMessage());

        return "redirect:/";
    }

A. 아하! 이런 관점에서 앨런이 커스텀 예외를 등록한 이유를 여쭤본 것 같습니다!
이렇게 보면 "게임 움직임에 관련된 예외"와 그렇지 않은 예외, 즉 예외에 따른 처리로 분리를 할 수 있을 것 같습니다.
그리고 이렇게 되면 굳이 IllegalPasswordException, IllegalDeleteException, ExistGameException 이라는 커스텀 예외를 만들 필요가 없게 되네요😅
개선해보겠습니다!
IllegalPasswordException, IllegalDeleteException, ExistGameException이 세가지 예외를 묶은 하나의 또 다른 예외 (IllegalGameProgessException)를 만들었습니다.
단순히 IllegalArgumentException을 사용하게 되면 IllegalMoveExceptionIllegalArgumentException을 상속받으므로 컴파일 에러가 발생하기 때문에..

@ControllerAdvice
public class ExceptionController {

	...
    
    @ExceptionHandler(IllegalGameProgressException.class)
    public String handle(RedirectAttributes redirectAttributes, IllegalGameProgressException e) {
        redirectAttributes.addAttribute("error", e.getMessage());

        return "redirect:/";
    }
}

Q. 서버에서 발생한 예외메시지를 클라이언트에 그대로 전달하고 있는데요. 이 부분은 좀 더 고민해보면 좋을 것 같습니다.

    @ExceptionHandler(IllegalPasswordException.class)
    public String handleEmptyResult(RedirectAttributes redirectAttributes, IllegalPasswordException e) {
        redirectAttributes.addAttribute("error", e.getMessage());

        return "redirect:/";
    }

A. 지금의 경우 클라이언트에 보여줄 예외 메시지가 어떠한 이유로 변경된다면 서버의 코드(Exception의 예외메시지 부분)를 변경해야하는 문제가 있을 수 있을 것 같습니다. 따라서 서버에서 ResponseEntity를 사용하여 예외에 따른 적절한 상태코드를 front에 넘겨주고 front에서 적절하게 요청에 따른 예외 메시지를 보여줄 수 있을 것 같습니다..!!
그런데..현재와 같이 Handlerbars 사용시에는 어떻게 예외와 그에 대한 메시지를 분리해줄 수 있을지 모르겠습니다...ㅠㅠ

앨런 : Handlerbars를 사용하는경우는 상태코드를 활용하기 어렵지만,
도메인에서 발생한 예외메시지와 사용자에게 전달할 예외메시지는 달라야할 것 같습니다! 중요한 정보가 노출될 가능성이 있다고 생각해요

Q. 현재 SpringController로 되어있는데요, 이쪽에 책임이 너무 많은 것 같습니다. 크게 HomeController와 GameController로 나눠볼 수 있을 것 같아요.

A. 게임진행(ex. 기물 움직임)과 관련된 사용자 요청을 받아서 처리하는 쪽(GameController)과 전체적인 게임 관리(ex. 게임 삭제, 게임 시작 등)(HomeController)로 분리하여볼 수 있을 것 같습니다! 개선해보도록 하겠습니다~_~

Q. 디우가 생각하는 서비스 레이어는 어떤 역할이라고 생각하시나요~

A. 비즈니스 플로우에 대한 책임을 가진다고 생각합니다. 예를 들어, ChessGame을 생성하고, progress() 메시지를 보내는 행위 혹은 이전에 코멘트 주셨던 것과 같이 DAO를 사용해서 State(상태)를 조회해오고 이를 사용해 삭제를 하는 등의 비즈니스 흐름에 대한 책임을 가지고 있다고 생각합니다. 즉, DAO 조회 결과등을 가공 그리고 도메인을 사용하여 비즈니스 로직의 '흐름'을 제어하는 책임을 가지고 있다고 이야기할 수 있을 것 같습니다.
또한 저의 코드에서 validateDuplicateGameName() 과 같은 비즈니스 로직도 Service의 역할이라고 생각합니다!

정리하면 다음과 같을 것 같이 정리할 수 있을 것 같습니다!
Controller: 사용자 요청을 받고, 그에 따른 적절한 Service 호출 및 응답
Service: 비즈니스 로직 처리(도메인의 비즈니스 로직 처리와는 차이가 있다고 생각합니다. 예를 들어, "게임진행(progress)"이라는 것은 도메인의 책임이지만, "중복된 이름이 있는지 검증한 후 게임을 진행시키고 이를 DB에 저장한다."(도메인을 활용한 비즈니스 흐름 제어)라는 비즈니스 책임은 Service라고 이야기 할 수 있을 것 같습니다.
DAO: DB 접근에 대한 책임 (단순히 쿼리를 날리고, 그에 대한 결과를 반환)

현재 제가 생각하는 서비스 계층은 위와 같은데 지적해주시거나 보충해주시면 너무 감사할 것 같습니다!!🙏
(저번에 포비께서 다른 크루에게 같은 질문을 할 때, 생각해보았던 것인데, 아직 제 생각에 확신은 없는 것 같아 미션을 진행하며 사용한 Controller, Service, DAO를 기반으로 정리해보았습니다.😃)

로운 : 정리 감사합니다! 정리하면 서비스는 도메인로직의 묶음이라고 생각할 수 있겠군요. "중복된 이름이 있는지 검증한 후 게임을 진행시키고 이를 DB에 저장한다." 이런 내용이면 서비스 레이어에서 영속성 관련된 비즈니스 룰을 책임질 수 있겠고, 서비스에서 트랜잭션을 책임지기도 하겠네요. 적다보니 서비스 레이어에 트랜잭션을 추가하면 좋겠군요!

profile
꾸준함에서 의미를 찾자!

0개의 댓글