7, 8, 9주차 기간동안 진행된 체스 미션에 대한 회고를 작성한다.
https://github.com/woowacourse/java-chess/pull/207
https://github.com/woowacourse/java-chess/pull/240

HTML, CSS, JS 가 뒤엉켜서 괴로웠던 체스미션… 백엔드를 선택하기 정말 잘했다고 느낀 미션이었다. 😢


♟️ 함수형 인터페이스를 익히자

image image

처음 streamAPI의 orElseThrow 문을 사용할 땐, 예외 메세지를 함께 던져줄 방법을 찾지 못해서 try-catch문을 사용했었다. try-catch문의 사용으로 점점 indent 제한 압박이 느껴지기 시작하면서는 안일한 생각으로 자위하며 예외 메세지 던지기를 포기했다.

‘IllegalArgumentException 표기도 되는데 어림잡겠지…’

해결방법은 의외로 간단했다. 메서드 참조로 줄인 orElseThrow 문의 인자를 함수형 인터페이스로 바꾸면 되는 것이다. 메서드 참조가 들어가는 곳엔 함수형 인터페이스가 들어갈 수 있다. 그리고, 안일한 생각으로 자위하지 말고 해결방법을 어떻게든 찾아보자. 나는 아직 적당히 타협 할 수 있는 수준이 아니다.


♟️ 부정 연산자를 지양하자

image

복잡한 코드 속에 숨어있는 ! 는 자잘한 휴먼에러를 일으킨다. 그리고 대부분의 휴먼에러는 컴파일러가 도와주지 못하고, 많은 시간을 잡아먹힌다. 휴먼에러를 하나라도 더 줄이기 위해 많은 개발자들이 컨벤션을 만든다. 나도 동참하자!


♟️ 또, 또 일급 컬렉션

image image

일급 컬렉션이 담당 컬렉션을 반환하는 것도, streamAPI의 flatMap 문을 사용하게 된 것도 모두 일급 컬렉션을 제대로 활용하지 못해서 그렇다. 굳이 컬렉션 하나만 가진 객체를 만들고 일급 컬렉션이라 이름을 붙이는 이유가 뭘까? 이런 코드를 만들지 않게 하기 위함이다.

자동차 경주 미션 회고, 로또 미션 회고, 블랙잭 미션 회고 에서도 일급 컬렉션에 대한 후회가 계속 나왔다. 매 미션마다 피드백 받는 일급 컬렉션이지만, 체스 미션까지 오니 직접 언급해주시지 않아도 일급 컬렉션 문제임을 먼저 알아보는 수준은 되었다. 일급 컬렉션에 대해 따봉👍을 받는 것을 목표로 더 달리자.


♟️ 데이터베이스 연동에서 느껴지는 괴리감

image

이전 미션까진 전체 로직을 담당하는 객체의 필드값으로 진행 상태를 기억시키는게 당연했다. 그런데 체스 미션에선 진행 상태를 데이터베이스에 저장하고 불러와야 했다. 객체의 정보를 데이터베이스에 연동하는 작업을 반복하다보니 어느 순간 괴리감이 느껴졌다.

“데이터베이스에 게임 정보를 하나씩 추가 할수록 체스게임 객체 인스턴스들이 하나씩 필요없어지는거 아닌가?”
“이러다 모든 객체가 static하게 변하는거 아냐?”

괴리감이 느껴지는 이유는 단순했다. 데이터베이스를 사용하지 않는 미션들에 너무 익숙해져있었다. 나는 백엔드 개발을 공부중이다. 데이터베이스에 정보들을 저장하는게 당연하다. 원래 데이터베이스에 저장해야할 정보들을 객체 필드에 저장하고 있었다. 그렇게 생각하면 별거 아니다. 처음부터 다시 공부하고 다시 익숙해지면 된다.


♟️ 이벤트 소싱과 마이그레이션

image

이번에도 3대450 (쳤던) 인비의 아이디어로 큰 도움을 받았다.

처음 구현 단계에서 체스 보드와 말, 게임 상태에 대해 데이터베이스에 직접 저장하겠다고 온갖 고집을 부렸다. 그러나 문자열 Piece들을 깔끔하게 객체 변환하는 방법을 찾아내지 못해 곤욕을 치뤘다.

image

곤욕.png

결국 고집을 꺾고 인비의 아이디어를 따라 게임에 입력된 명령어들을 모두 기록하는 형태로 갈아엎었다. 이벤트 소싱 으로 불리는 저장 기법이었다. 이벤트 소싱은 적용하기도 용이했고, 도메인 수정도 최소화되어 코드의 간결함이 유지되었다.

그러나 리뷰어 닉께선 이벤트 소싱의 부정적인 측면을 보완하기 위해 데이터베이스 구조 변경을 요구하셨다. 이벤트 소싱의 대표 단점인 ‘히스토리가 깊어질수록 부하가 커진다’의 경우 체스게임에서 쉽게 만날 수 있는 규칙인 50수 제한을 통해 해결 할 수 있다고 생각했지만, ‘특정 시점 보드판에 대한 검색이 불가능하다’ 와 ‘마이그레이션이 어렵다’ 2가지 단점을 해결할 방법이 도저히 떠오르지 않아 수긍 할 수 밖에 없었다.

(특히 마이그레이션의 경우, 체스 프로그램이 동작하는 환경 뿐만 아니라, 모든 체스 프로그램이 명령어를 해석하는 방법이 달라질 수 있으므로 사실상 마이그레이션이 불가능했다.)

데이터베이스가 추가됨으로 인해서 신경써야할게 점점 늘어간다. 정신 바짝 차리자…


♟️ try-with-resources

image

이펙티브 자바 스터디를 시작한지 얼마 되지 않았을 때 봤던 try-with-resources가 이제서야 등장했다.

try-with-resources는 AutoCloseable을 구현한 구현체에 한해서, try문 괄호에 선언하면 예외 발생시 자동으로 자원을 회수해준다. try문 괄호에 선언을 해야하기 때문에 코드가 점점 지저분해지는데, indent 2~3을 넘기는건 흔한 일이 되었다.

JAVA9 부터는 회수할 자원을 미리 선언하고, try문 괄호에 선언한 변수를 집어넣는 것으로 깔끔한 코드 작성이 가능하지만, 이번 미션에선 JAVA8을 사용중이었다. JAVA 버전별 차이를 의식할 기회가 거의 없었는데, 이번에 크게 느꼈다.

또, AutoCloseable 구현체에 대해 오해한 부분이 있었다.

public interface ResultSet extends Wrapper, AutoCloseable { ... }

ResultSet 객체는 AutoCloseable을 구현하고 있었지만, PreparedStatement는 그렇지 않았다.

때문에 어떻게 자동으로 자원이 회수되는지 계속 고민이었는데, getConnection() 메서드로 호출되는 Connection 인터페이스가 AutoCloseable을 구현하고 있기 때문에 자원이 회수된다고 생각을 했다.

그러나 틀린 생각이었다!

public interface PreparedStatement extends Statement { ... }
public interface Statement extends Wrapper, AutoCloseable { ... }

PreparedStatement는 부모클래스 Statement가 구현한 AutoCloseable 덕분에 자원이 회수되는 것이었다.

그렇다면 과연 getConnection()로 넘어온 Connection 자원은 회수될까? 의문은 오라클 공식 문서 를 통해 해결 할 수 있었다.

In this example, the resource declared in the try-with-resources statement is a BufferedReader. The declaration statement appears within parentheses immediately after the try keyword. The class BufferedReader, in Java SE 7 and later, implements the interface java.lang.AutoCloseable. Because the BufferedReader instance is declared in a try-with-resource statement, it will be closed regardless of whether the try statement completes normally or abruptly (as a result of the method BufferedReader.readLine throwing an IOException).

앞서 이야기한 것과 같이, try문 괄호에서 선언된 자원에 대해서만 자동 회수를 해주기 때문에, try문 괄호 밖에서 선언된 자원인 Connection은 자동으로 회수되지 못한다. 다음 미션에서는 모든 외부 자원을 깔끔하게 해제할 수 있도록 노력해보자.


♟️ Custom Exception

image

JDBC 연결/해제 예외처리 동작에 수업에서 사용했던 printStackTrace() 메서드를 별 생각없이 그대로 사용했다. 그리고 피드백을 받았다.

printStackTrace()는 시스템 정보를 노출시키는 보안 문제와, 반환 값이 없어서 해당 정보를 별도로 이용할 수 없다는 문제점을 갖고 있었다. 이 때문에 에러사유에 대한 로그는 별도의 프레임워크를 사용한다고 하는데, Java 기본을 익히는 단계에서 프레임워크 사용은 꺼려졌다. 그래서 DataAccessException 이라는 이름의 Custom Exception을 만들어 예외처리를 진행했다.

이전에 customexception을-꼭-써야하는가 글에 “최대한 있는 걸(Legacy Exception) 사용하고, 도저히 찾아봐도 없으면 그 때 (Custom Exception을) 만들자.” 라고 정리를 해두었는데 적절한 예시에 적절하게 시도해본 것 같다. 잘했다! 😆


♟️ 서비스 레이어의 등장

image image

이미지 출처 - [용어] MVC, Spring Framework MVC, Controller, Service, DAO, DTO, VO - 개념

이전 미션부터 서비스레이어를 사용하는 크루들이 많이 보였다. MVC 패턴 사이 어디에 속해있는지 구분도 명확하게 느껴지지 않았고, 사용하는 방법이나 예시도 도통 모르겠어서 사용을 꺼렸다. 그런데 이번 체스 미션에서 나도 모르게 DAO에 서비스레이어의 역할을 수행 시키고 있었다. 아마 리뷰어님이 알려주시지 않았다면 레벨2까지도 모르지 않았을까…

DAO, DTO도 아직 제대로 정리하지 못한채 감만 잡고 사용하고 있는데, 서비스레이어까지 등장했다. 더 이상 정리를 미루다간 제대로 남는게 하나도 없을거같다. 한시라도 빨리 개념들을 정리하자. 레벨2에 곧장 써먹을 것들이다.


페어 회고

체스 미션을 함께 진행한 에드 는 치타! 딱 치타가 생각나는 페어다. 집중하는 순간에는 폭발적인 생산성과 아이디어를 보여주지만, 체력이 조금 빨리 방전된다. 😅

에드의 탐나는 장점은 코드의 안정감, 탄탄함. 찰리에게 느꼈던 신뢰와는 또 다르다. 에드는 ‘테스트 코드 작성할게 너무 많네요.. 귀찮네요..’ 라고 이야기하지만, 막상 코드 작성을 시작하면 복잡한 로직도 순식간에 작성하면서 테스트 코드까지 완성한다. (엄살…🙄)

체스말 별 이동전략과 점수 계산 로직을 구현할 때, 로직들이 다소 복잡했음에도 에드는 순식간에 구현을 끝냈다. 어떻게 저리 빨리 구현할 수 있나 뒷조사(깃허브 훔쳐보기👀)를 해본 결과, 오랜기간 지속적으로 풀어온 알고리즘/구현 문제들이 에드의 속도를 만들어준 것 같았다. 곧장 에드의 속도를 따라가는건 욕심이다. 나도 알고리즘 문제를 꾸준히 풀면서 차근차근 굳은 살을 늘려야지… 요즘 잘 지키지 않았던 1일 1알고리즘을 다시 실천하자.

에드와 페어를 하면서 잘했다고 생각이 드는 점은, 에드의 체력에 맞춰보려 노력한 것(에드도 나에게 맞추기 위해 힘들었겠지만 😂). 건대 근처 카페에 오전 10시까지 도착해서 오후 4시까지만 바짝 하기도 하고, 오후에 모여서 더 짧게 하기도 했다. 사실 ‘기한 내에 완성 할 수 있을까’ 라는 압박감을 느꼈지만, 페어의 템포에 맞추는 것도 페어 프로그래밍 중 일부라고 생각해서 최대한 여유를 가지려고 노력했다.

아쉬운 점으론, 여유를 가지려고 노력했음에도 제출 당일엔 압박감을 이기지 못한 것. 제출 당일엔 테스트코드 없이 프로덕션 코드만 작성하기도 했다. 우선 동작하는 프로그램을 생각하고 추후에 추가기능을 넣는 것으로 진행했다면 시간이 그렇게까지 부족하지 않았을텐데, 너무 많은 기능을 욕심내다가 시간 압박에 쫒겼다. 더 섬세하게 시간을 분배할 수 있는 능력을 키워야겠다고 뼈저리게 느꼈다.

또, 페어 기간이 일주일이나 되었음에도 에드와 프로그래밍 외적으로 대화한 시간이 초밥 먹을 때 밖에 없었다는게 아쉬웠다. 체스 미션에 난이도에 짓눌려서 에드와 함께 소프트 스킬을 키울 엄두를 못냈다. 미션 완성이 페어 프로그래밍의 전부가 아닌데… 레벨2부턴 정말 포비에게 도전한다는 마인드로 임해야겠다. 내쫒으려면 내쫒으라지.

에드랑은 꼭 다시 한 번 페어를 해보고 싶다. 더 세심하게 시간을 분배하고, 시간내로 ‘할 수 있는 것’과 ‘할 수 없는 것’을 구분해서 짧은 시간 내에 극한의 효율을 뽑아내는 페어 프로그래밍… 그러려면 시간을 분배하는 스킬부터 키워둬야겠다.

끗!

댓글남기기