이번 체스미션은 Level 1의 화룡점정이나 다름없었다.
레벨 1에 있는 마지막 미션이자, 가장 요구사항이 복잡한 미션이기 때문이다.
1~3단계는 체스 게임을 콘솔로 구현하는 것이었고,
4~5단계는 웹 UI, db연결로 이어하기 기능까지 구현하는 것이었다.
다만, 여기서 웹을 Spark Java으로 만들도록 요구사항이 주어졌다.
아마 스파크 자바에 대한 이해를 높이기 위한 미션이라기보단, api를 설계하고 DAO와 그에 따른 테스트 작성을 경험시키게 해보려는 미션이라 생각된다 ㅎㅎ
내 체스 PR은 여기서 확인할 수 있다.
1~3단계 PR: https://github.com/woowacourse/java-chess/pull/287
4~5단계 PR: https://github.com/woowacourse/java-chess/pull/369
체스의 행(File)과 열(Rank)는 Enum으로?
결론부터 적자면, 나는 enum으로 만들지 않았다.
enum을 쓰면 확실히 validate라든지 특정 상수를 Rank, File에게 넘겨주어 책임을 일부 분리할 수 있다는 장점이 존재한다.
그럼에도 불구하고 내가 enum을 사용하지 않은 이유는 바로 증감 연산이 불가능하단 점이다.
private Position proceed(final Direction direction) {
return Position.of(Rank.from(Rank.getRank() + direction.getRank()), (char) (File.from(File.getFile() + direction.getFile())));
}
만약 여기서 Rank, File이 enum이었다면 위처럼 Rank나 File에서도 getter 메서드로 값을 꺼내와야된다.
아니라면, Rank, File 내에서 다음 값에 대해 따로 지정을 해주어야한다. 이렇게 하면 getter 메서드를 사용하지 않을 수 있긴 하지만, enum 열거형에서 특정 값의 다음값, 이전값을 따로 지정해줘야 하는 번거로움이 존재한다.
따라서 이번 rank, file은 enum으로 지정해주지 않고 1~8, 'a'~'h'의 값을 사용하기로 했다.
따라서 Position은 아래와 같이 캐싱해주었다.
static {
for (int rank = MIN_RANK_RANGE; rank <= MAX_RANK_RANGE; rank++) {
addCachePosition(rank);
}
}
private final int rank;
private final char file;
private Position(final int rank, final char file) {
this.rank = rank;
this.file = file;
}
private static void addCachePosition(int rank) {
for (char file = MIN_FILE_RANGE; file <= MAX_FILE_RANGE; file++) {
CACHE.add(new Position(rank, file));
}
}
기본 생성자는 private으로 만들어주고,
다른 곳에서 Position을 사용할 때는 체스판의 64개 좌표를 캐싱해둔 값들을 사용해주게 하기 위해 정적 팩토리 메서드를 만들어주었다.
알고리즘을 해두길 잘했다
이번 미션에서는 룩, 비숍, 나이트, 폰 등 여러 체스말들이 존재하며, 각각의 방향에 따른 이동가능여부를 파악하는 것이 중요하다.
특정 기물의 이동방향에 맞게 움직였는지 파악해야하는 로직을 작성할 때, 여러가지 다양한 방법이 존재하겠지만, 나는 Direction enum을 따로 만들어주어 여기서 방향 확인 역할을 담당하게 하였다.
이 때 나는 유클리드 호제법을 썼다.
그 방법은 아래와 같다.
- 현재 위치와 도착 위치의 좌표 차를 구한다.
- 그 좌표차의 최대공약수만큼 좌표차를 나눠준다. ex) (4, -4) => (1, -1)
- 이 좌표차로 filter로 특정 Direction과 일치하는지 확인한다.
사실 알고리즘을 몰라도 쉽게 사용가능한 방법이긴 하지만...
그냥 ps를 해둔 것이 백해무익하진 않았다고 합리화하고 싶었다 ㅜㅜ
턴을 번갈아가는 과정을 재귀로 구현했었다가...
1~3단계에서는 콘솔로 작성했었기 때문에 턴을 번갈아가는 과정을 컨트롤러에(!) 넣은데다가, 그 과정을 재귀로(!!!) 작성했었다.
private void progressGame(final ChessGame chessGame, final Player currentPlayer, final Player opponentPlayer) {
showMap(chessGame.createMap());
if (isTurnFinished(chessGame, currentPlayer, opponentPlayer)) {
return;
}
progressGame(chessGame, opponentPlayer, currentPlayer);
}
각 턴을 바꿔주는 것은 도메인로직이라기보단, 도메인 로직을 실행시키기 위한 준비단계라 판단했었기 때문이다.
물론 위와 같이 해도 콘솔에서 실행할 때는 딱히 큰 문제는 없다.
문제는 4~5단계에서 발생한다...
4~5단계에선 api 설계가 추가되는데,
post("/move", (req, res) -> {
final MoveDto moveDto = gson.fromJson(req.body(), MoveDto.class);
try {
final ChessMap chessMap = chessService.move(chessWebGame, moveDto);
return gson.toJson(chessMap);
} catch (final Exception e) {
return gson.toJson(new ErrorMessageDto(e.getMessage()));
}
});
'/move' api를 호출하는 작업을 재귀적으로 호출할 수 있겠는가?
만약 굳이 재귀적인 성질을 살리자면, main (또는 컨트롤러)에 flag 변수를 설정해두어 api를 실행할 때마다 매번 바꿔주는 방법이 있겠다(...)
브리에게 이 현상을 얘기해보았고, 브리는 로직을 조금 수정해보는게 어떻겠냐고 제안해주었다.
특히 재귀는 보통 api 설계 시 controller layer와 같이 요청에 따른 응답을 보내주는 단계에선 거의 쓰이지 않는다고 말해주었다.
따라서 턴 정보를 도메인에 추가해주고, 턴을 바꿔주는 작업 또한 도메인로직에 위치하도록 변경해주었다.
컨트롤러는 요청에 대한 응답만 넘겨주도록 하자.
docker로 환경세팅을 편하게 해주자
docker를 사용하면 로컬에 mysql을 설치하지 않고도 mysql을 이용할 수 있다.
포트 번호 세팅, mysql 권한 설정 등.
나는 로컬에서 8.0.19 버전의 mysql을 사용하고 있었는데, 이번 미션에서 도커를 사용하면서 8.0.2x 버전을 로컬에 설치하지 않고도 사용해보는 기회를 얻을 수 있었다. (뭐 큰 차이가 있진 않지만...)
이제 docker ps 명령어를 이용해 현재 프로그램 목록을 찾고 아래 명령어를 입력해서 도커를 작동시켜주자.
docker exec -it {프로그램} bash
여기서 mysql -u {유저} -p{패스워드}를 입력해주면 도커의 mysql도 이용가능하다!
로컬에 mysql이 없어도 잘 작동된다~~
대신 그만큼 용량을 많이 잡아먹는다... 재부팅하면 용량이 다시 늘어나있긴 하지만
처음으로 작성해본 DAO, 그리고 service의 역할
@Override
public List<PieceDto> findPiecesByTeam(final Team team) {
final String sql = "select * from piece where piece.team = ?";
final List<PieceDto> pieces = new ArrayList<>();
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, team.getName());
final ResultSet resultSet = statement.executeQuery();
addPiece(resultSet, pieces);
return pieces;
} catch (final SQLException e) {
e.printStackTrace();
}
return pieces;
}
DAO는 비즈니스 로직이 실행될 때 db table의 변화를 담당해주는 역할을 맡는다.
dao가 존재하지 않을 경우, 체스말이 잡아먹히는 때 게임 상에서는 말이 잡아먹혔지만 db table에서는 말이 삭제가 되지 않는 현상이 발생한다. dao에서 db와 접근하기 위해 SQL 쿼리를 사용하기 때문이다.
JAVA에선 흔히 객체를 사용하지만, db table은 사용자에게 값을 보여주기 위해 String, int와 같은 원시값 형태로 데이터들을 저장한다. 그렇기 때문에 자바 객체를 그대로 table에 저장할 수 없고, 그 객체의 값들을 getter로 꺼내어 table에 넣어주어야 한다. db table에 넣을 값들을 가진 객체 (DTO)를 만들어주면 책임 분리 및 디미터의 법칙을 지키면서 설계할 수 있게 된다.
비즈니스 로직을 처리하고 DAO로 db table 데이터값들을 변경해주는 역할을 Service에서 담당하게 된다.
private void loadTurn(final ChessWebGame chessWebGame) {
final TurnDto turnDto = turnDao.findTurn();
Team turn = Team.from(turnDto.getTurn());
chessWebGame.loadTurn(turn);
}
예시 코드 중 하나로,
Dao에서 TurnDto를 받아오고 TurnDto를 Turn(Team)으로 바꿔주고 있다.
나는 DTO <-> 도메인 변환 로직을 Service에서 처리해주고 있는데, 이 역할을 어디에 놓을지는 사람마다 다르기 때문에 별도로 여기에 언급하진 않으려 한다.
확실한건 db 연동을 하고나니 DTO, DAO에 대한 필요성을 느꼈고, 이러한 객체들이 생기다보니 service의 역할을 한 층 더 실감하게 되었다는 것이다.
DTO가 필요한 이유
Piece 객체는 아래 객체들을 상태로 가진다.
private final State state;
protected Position position;
State에는 각 Piece의 이름, 점수 정보가 담겨있다.
그런데 db table에 객체를 담는 것은 불가능하다.
아래처럼 String 또는 int 등의 원시값을 담아줘야 한다.
CREATE TABLE piece (
position varchar(3) not null primary key,
name varchar(2) not null,
team varchar(5) not null,
foreign key (team) references player (team)
);
insert into piece (position, name, team) values ('a1', 'r', 'WHITE');
그렇기 때문에 나는 dto는 클라이언트에게 보여줄 값들 (주로 String타입)을 가지게 하였다.
덕분에 DAO에서도 별도의 처리 없이 바로 preparedStatement에 값을 set해줄 수 있었다.
private void initializePiece(final Player player, final List<PieceDto> pieces,
final PreparedStatement statement) throws SQLException {
for (PieceDto piece : pieces) {
statement.setString(1, piece.getPosition());
statement.setString(2, player.getTeamName());
statement.setString(3, piece.getName());
statement.executeUpdate();
}
}
piece.getPosition()의 값이 Position 객체가 아닌 String이기 때문에 SQL 쿼리에 바로 값을 setString해줄 수 있었다.
도메인은 DTO를 의존하지 않는 것이 좋다
나는 ChessService에서 Dao에게 DTO를 받아, 그 객체를 바로 도메인에 넘겨주고 있었다.
그렇게 pr을 보내고 나니, 리뷰어 제이에게 아래와 같은 피드백을 받았다.
DTO를 바로 도메인에 넘겨줄 경우, 도메인에서 DTO -> domain 변환 작업이 필요하다.
도메인은 도메인 로직을 수행하는 데에만 집중해야되는데, 웹 UI 뷰로 인해 별도의 변환 로직이 추가된 것이다.
이는 도메인이 뷰에 의존하는 코드이므로 변경의 유연함이 확 떨어지게 된다.
DTO <-> domain 변환 로직은 Service 혹은 Controller에 위치하게 해주자.
도메인에는 domain들만 넘겨주자.
마찬가지로, 도메인에선 domain들만 반환해주고 dto 반환은 금물이다.
제이의 피드백을 받고 변경한 덕분에 domain 변경을 최소화하여 4~5단계 적용을 완료할 수 있었다!
+) 리뷰어님과 라이브코딩
리뷰어 제이랑 라이브코딩하면서 질의응답하기도 했다!
js 비동기처리에 익숙하지 못했고,
몇 가지 버그도 존재했었기 때문에 리뷰어 제이에게 이것저것 많이 질문드렸는데,
제이가 정말 감사하게도 라이브코딩으로 질의응답을 해주었다 ㅎㅎ
감사해요 제이!!
그 외에 이번 미션을 통해 gson도 다뤄볼 수 있었고, html, js, css 코드 작성 및 api 설계 및 db도 다뤄볼 수 있었다.
SELECT Orders.CustomerId, Customers.CustomerName, sum(Quantity*Price)
FROM OrderDetails
join Orders on OrderDetails.OrderId = Orders.OrderId
join Customers on Orders.CustomerId = Customers.CustomerId
join Products on OrderDetails.ProductId = Products.ProductId
group by Customers.CustomerName
order by sum(Price*Quantity) desc
이렇게 3중 join도 사용해볼 수 있었고 (...)
docker 덕분에 이렇게 용량 부족 현상을 겪어볼 수도 있었다 (......)
빨리 전역해서 맥북을 사든지 해야겠다.
'JAVA > 우아한테크코스 4기' 카테고리의 다른 글
[220419] 우아한테크코스 레벨2 개학 후기 (6) | 2022.04.20 |
---|---|
[220208 ~ 220408] 우아한테크코스 레벨1 후기 (0) | 2022.04.17 |
[220328] 우아한테크코스 4기 7주차 후기 (feat. 슬로와의 미션 회고) (5) | 2022.03.28 |
[220323] 블랙잭 미션 피드백을 통해 배운 점 (2) | 2022.03.23 |
[220320] 우아한테크코스 4기 6주차 후기 (6) | 2022.03.21 |