각종 후기/우아한테크코스

[우아한 테크코스 3기] LEVEL 2 회고 - 스프링 입문 체스 1, 2단계 미션 1차 피드백을 받아보다 (77일차)

제이온 (Jayon) 2021. 4. 19.

안녕하세요? 제이온입니다.

 

어제 1차 피드백이 왔고, 그것을 오늘 검프와 함께 적용해 보았습니다.

 

 

데일리 미팅

오늘 데일리 미팅 진행자는 아론이었고, 자신과 관련된 것을 퀴즈로 내고 그것을 맞히는 게임을 진행해 주었습니다. 예를 들어, '아론의 본명은?' 이라고 크루들에게 말하면, 크루들은 정답을 채팅으로 치는 것이죠. 그리고 정답자는 다음 질문자가 됩니다. 이렇게 하다가 마지막 남은 크루가 다음날 데일리 미팅 진행자가 되는 것입니다.

 

저는 살면서 딱 한 번 알바를 했는데, 그것이 무엇일지에 대해 퀴즈를 냈습니다. 처음에는 고깃집 서빙이니, 상하차니 여러 가지 나왔는데, 저는 온라인으로 진행했다고 힌트를 드렸습니다. 그랬더니, 금방 에드가 맞춰주셨습니다. 저는 작년에 고3을 대상으로 수시 컨설팅 알바를 한 적이 있습니다. 약 10개월간 진행하면서 생기부 활동도 정해주고, 보고서 써 주고, 전공 수업도 약간 해 주고 그런 역할을 했죠.

 

아직 어색한 크루들이 자신과 관련된 이야기를 서로 함으로써 조금 더 친해지는 계기가 되었다고 생각합니다.

 

 

1차 피드백

어제 오후 5시 쯤 pr을 보냈는데, 약 7시간 뒤에 리뷰가 왔습니다. 빠른 시간인데도 불구하고 12개의 리뷰를 보니, 상당히 퀄리티가 높았습니다. 특히, 제가 장문의 질문 5개에 대해서도 완벽히 답변을 해 주셔서 정말 감사했습니다.

 

아직 검프는 리뷰어가 오지 않았기도 하고, 서로 맞춰본 코드를 그대로 냈기 때문에 피드백도 페어 프로그래밍으로 반영해 보기로 했습니다. 여태까지 페어 중에서 피드백도 협업한 것은 검프가 처음이었습니다.

 

 

 

 

첫 번째 질문은 Build Tool의 Gradle 설정에서 Build and run에 대해 Gradle을 쓸지, IDEA를 쓸지입니다.

 

 

 

 

위 사진에 나와있는 IntelliJ IDEA와 Gradle 중 휴는 어떤 것을 선택하시는지 여쭈어 보았죠. 개인적으로는 속도 및 출력 화면때문에 IDEA를 사용하신다고 하면서 링크를 남겨 주셨습니다.

 

 

 

 

두 번째는 첫 번째 질문에 이어서 나오는 것으로, 기본 생성자와 setter를 언제 열어주어야 하는지에 대한 질문입니다.

 

제 페어는 Gradle을 사용하고 저는 IDEA를 사용할 때 저만 에러가 발생하는 경우가 있었습니다. 이것은 Gradle은 gson을, IDEA는 Jackson을 사용하기 때문입니다. 여기서 gson은 기본 생성자와 setter가 없어도 되지만, jackson은 필요한 것을 눈으로 확인하였습니다. 자세한 원리는 이곳에서 참고하실 수 있지만, 저는 아직 이해를 잘 안가서 추후 학습을 한 뒤에 보기로 하였습니다.

 

 

 

 

세 번째 질문은 변수의 접두사가 js로 넘어왔을 때 자동으로 사라지는 것에 관한 내용입니다. 

 

 

public class GameStatusDto {

    private List<PieceStatusDto> pieces;
    private ScoreDto scoreDto;
    private boolean isGameOver;
    private Color winner;

    public GameStatusDto() {
    }

    public GameStatusDto(final Pieces pieces, final ScoreDto scoreDto, final boolean isGameOver, final Color winner) {
        this.pieces = pieces.pieces()
            .stream()
            .map(PieceStatusDto::new)
            .collect(Collectors.toList());
        this.scoreDto = scoreDto;
        this.isGameOver = isGameOver;
        this.winner = winner;
    }

    public List<PieceStatusDto> getPieces() {
        return pieces;
    }

    public ScoreDto getScoreDto() {
        return scoreDto;
    }

    public boolean isGameOver() {
        return isGameOver;
    }

    public Color getWinner() {
        return winner;
    }
}

 

 

해당 코드를 보시면, boolean 타입의 isGameOver가 있고, getter도 isGameOver()인 것을 아실 수 있습니다.

 

이 링크에 의하면, getWinner()에서 get이 제거된 winner가 반환되듯이, isGameOver()도 접두사인 is가 제거된 gameOver를 반환한다고 합니다. 앞으로 boolean 타입 변수에 대해서는 주의가 필요하겠습니다.

 

 

 

 

네 번째 질문은 DB에 데이터가 엉키는 오류입니다. 해당 콘솔의 출력문이 잘 보이지 않는 것 같아서 아래 새롭게 사진을 첨부했습니다.

 

 

 

 

처음에 화이트 턴부터 시작해서 "e2-e4"는 화이트턴, "f7-f6"은 블랙턴까지 잘 왔으나, created_date가 같은 이유에선지는 모르겠는데 "f6-f5"인 블랙턴의 이동 데이터가 먼저 들어온 것을 확인하실 수 있습니다. 이것에 대해 질문해 보니, 휴는 created_date를 nanoseconds까지 표현할 것을 조언해 주셨습니다. 어떻게 nanoseconds까지 표현할지 구글링을 열심히 하다보니 해답을 찾을 수 있었습니다.

 

 

CREATE TABLE IF NOT EXISTS chess (
    chess_id VARCHAR(36) NOT NULL,
    name VARCHAR(64) NOT NULL,
    winner_color VARCHAR(64) NOT NULL,
    is_running BOOLEAN NOT NULL DEFAULT FALSE,
    created_date TIMESTAMP(6),
    PRIMARY KEY (chess_id)
);

 

 

위와 같이 TIMESTAMP의 괄호 안에 3이나 6 값을 주시면 됩니다. 정밀도를 표현한다고 이해하시면 되겠습니다. 이 부분은 수정하고나니까 nanoseconds까지 체크하므로 동시성 이슈가 발생하지 않게 되었습니다.

 

 

이 다음부터는 저의 개인적인 질문이 아닌, 리뷰어님의 피드백입니다.

 

 

 

 

첫 번째 피드백은 ID를 UUID보다는 Auto_Increment로 사용하라는 것입니다. 사실 이 부분은 검프가 UUID로 쓰길래 처음 학습하자는 취지에서 사용해 보았습니다. UUID는 16비트 문자열로, 약 40억개의 데이터를 표현할 수 있습니다. 그래서 Auto_Increment에 비해 성능이 떨어지고 가독성도 좋지 않다는 단점이 있습니다. 다만, 게임 서버와 같이 여러 개의 서버를 구축하는 분산 시스템에 경우에는 Auto_Increment일 때 A서버와 B서버의 ID가 겹칠 수 있습니다. 하지만, UUID는 두 서버의 ID가 겹칠 확률이 낮습니다.

 

물론, 현재 체스 게임은 단순한 프로그램이므로 Auto_Increment가 적합해 보입니다만, 이번 기회에 UUID와 랜덤 함수를 익힐 수 있었습니다. 리뷰어님이 다음 피드백에서 바꾸라고 하시면 수정할 계획입니다.

 

 

두 번째 피드백은 테이블의 기본키는 id, 외래키는 '테이블명_id'를 사용하라는 것입니다. 저도 처음에는 리뷰어님의 의견을 지지하였으나, 다음과 같은 코드를 보고 나서는 생각이 조금 바뀌었습니다.

 

 

select * from movement as mv 
left outer join chess as ch 
using(chess_id)

 

 

이 코드는 풀어서 쓰면 아래와 같습니다.

 

 

select * from movement as mv 
left outer join chess as ch 
on mv.chess_id = ch.chess_id

 

 

두 테이블간의 필드가 같으면 이렇게 using 이라는 키워드를 사용하여 코드의 길이를 줄일 수 있게 되는 것이죠. 리뷰어님은 이 부분에 어떻게 생각하실지는 모르겠으나, 검프의 의견을 따르면서 새로운 지식을 얻을 수 있었습니다.

 

 

 

 

세 번째 피드백은 ResponseEntity를 사용하여 반환하라는 것입니다. 저는 그전까지 개인적으로 CommonResponseDto를 정의하여 통신할 데이터를 만들어주었습니다.

 

 

public class CommonResponseDto<T> {

    private T body;
    private int statusCode;
    private String message;

    public CommonResponseDto(final int statusCode) {
        this(null, statusCode, "");
    }

    public CommonResponseDto(final int statusCode, final String message) {
        this(null, statusCode, message);
    }

    public CommonResponseDto(final T body, final int statusCode) {
        this(body, statusCode, "");
    }

    public CommonResponseDto(final T body, final int statusCode, final String message) {
        this.body = body;
        this.statusCode = statusCode;
        this.message = message;
    }

    public T getBody() {
        return body;
    }

    public int getStatusCode() {
        return statusCode;
    }

    public String getMessage() {
        return message;
    }
}

 

 

물론, 스파크에서는 ResponseEntity를 따로 지원해 주지 않으므로 만들어야겠지만, 스프링에서는 그렇지 않습니다. HTTP 상태 코드나 에러 메시지도 지원하며, 여러 가지 유용한 메소드를 제공합니다. 따라서, 이에 맞게 리팩토링해 보았습니다.

 

 

    @PostMapping
    public ResponseEntity<CommonResponseDto<Object>> saveChess(@RequestBody final ChessSaveRequestDto requestDto) {
        chessService.saveChess(requestDto);
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(new CommonResponseDto<>(HttpStatus.CREATED.value()));
    }

 

 

위와 같이 ResponseEntity를 반환하되, body에 CommonResponseDto를 보내주었습니다. 그리고 ResponseEntity의 상태 코드와 동일한 코드를 넘겨주었죠. 다만, 리뷰어님이 의도하신 것이 CommonResponseDto를 아예 제거하고 다른 방식으로 하라는 것인지 잘 모르겠어서 추가로 질문을 남겨 두었습니다.

 

 

 

 

네 번째 피드백은 커스텀 익셉션을 사용하라는 것입니다. 저는 현재 모든 에러를 대충 RuntimeExceptime으로 박고 400번 에러코드로 통일했습니다. 하지만, 사용자의 잘못이 아닌 서버의 잘못이라면 어떨까요? 500번대 에러 코드가 나와야 정상이겠지만, 제 코드는 항상 400번 에러 코드를 반환할 것입니다.

 

이러한 문제를 해결하기 위하여, 사용자의 요청과 관련한 에러를 커스텀으로 만들고, 그 외에 익셉션에 대해서만 500번 에러 코드를 보내도록 수정하였습니다.

 

 

@RestControllerAdvice
public class GlobalExceptionHandler {

    Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler({ChessException.class, MovementException.class,
        BlankException.class, StateException.class})
    public ResponseEntity<CommonResponseDto<Object>> handleCustomException(RuntimeException exception) {
        logger.info(exception.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(new CommonResponseDto<>(HttpStatus.BAD_REQUEST.value(), exception.getMessage()));
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<CommonResponseDto<Object>> handleException(RuntimeException exception) {
        logger.info(exception.getMessage());
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new CommonResponseDto<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버 내부 에러입니다. "));
    }

}

 

 

위와 같이 두 메소드로 분리하였습니다.

 

 

 

 

또한, 검프의 의견에 따라 커스텀 익셉션도 하나 하나 나열하는 것이 아니라 위와 같이 추상화하였습니다. 아마 검프가 아니었다면 저 많은 익셉션들이 @ExceptionHandler 안에 들어갔을 겁니다....

 

리뷰어님이 남겨주신 커스텀 익셉션 관련 링크는 이곳입니다.

 

 

 

 

다섯 번째 피드백은 API URI를 수정하라는 것입니다. 리팩토링 이전, MoveRequestDto에는 방의 이름, source 위치, target 위치를 필드로 갖고 있었습니다. 하지만, 이것보다는 URI에 방의 이름을 추가하여 '/hue/pieces'와 같이 URI를 수정하라고 조언해 주셨습니다. 나중에 시간될 때, 이 문서를 보면서 REST API 디자인의 감을 익혀야겠습니다.

 

그 외에 final 키워드를 붙이는 이유에 대해서 물어보였는데, 이 부분은 2차 피드백때 한꺼번에 공유하려고 합니다.

 

 

정리

검프와 같이 피드백을 반영하면서 정말 많은 인사이트를 얻을 수 있었습니다. 이 모든 것은 스프링을 잘하고 추상화 센스가 있는 검프와 꼼꼼하게 다양한 방면에서 리뷰해 주신 휴 덕분이라고 생각합니다.

 

오늘도 더욱 성장했음을 느끼는 하루였습니다.

댓글

추천 글