학습 목표
경험해야할 학습 목표
- Q&A 서비스를 활용해 레거시 코드를 리팩토링하는 경험
- 지금까지 학습한 내용을 기반으로 TDD, 클린코드, 객체지향 프로그래밍하는 경험
객체지향 생활 체조 원칙
- 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.
- 규칙 2: else 예약어를 쓰지 않는다.
- 규칙 3: 모든 원시값과 문자열을 포장한다.
- 규칙 4: 한 줄에 점을 하나만 찍는다.
- 규칙 5: 줄여쓰지 않는다(축약 금지).
- 규칙 6: 모든 엔티티를 작게 유지한다.
- 규칙 7: 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
- 규칙 8: 일급 콜렉션을 쓴다.
- 규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.
GRASP
GRASP - General Responsibility Assignment Software Patterns
- 책임 기반 객체지향 관점에서 객체에 책임을 할당하기 위한 패턴을 정리한 것을 의미
- 책임할당에 기반한 객체 설계 원칙 : applying uml and patterns 책의 GRASP 설명 정리한 내용
- GRASP : GRASP을 잘 설명하고 있는 영어로 된 문서
객체지향 5원칙(SOLID)
- SRP (단일책임의 원칙: Single Responsibility Principle)
- 작성된 클래스는 하나의 기능만 가지며 클래스가 제공하는 모든 서비스는 그 하나의 책임(변화의 축: axis of change)을 수행하는 데 집중되어 있어야 한다
- OCP (개방폐쇄의 원칙: Open Close Principle)
- 소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려있고, 변경에는 닫혀있어야 한다.
- LSP (리스코브 치환의 원칙: The Liskov Substitution Principle)
- 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다. 즉, 서브 타입은 언제나 기반 타입과 호환될 수 있어야 한다.
- ISP (인터페이스 분리의 원칙: Interface Segregation Principle)
- 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.
- DIP (의존성역전의 원칙: Dependency Inversion Principle)
- 구조적 디자인에서 발생하던 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의 역전 원칙이다.
미션4, 볼링 후기다.
자동차경주, 로또, 사다리타기 에 배웠던 내용을 모두 활용하는 마지막 미션이다.
🚀 step1 - 질문 삭제하기 기능 리팩토링
이 교육이 추구하는 레거시 코드를 리팩토링하는 단계다.
예제가 레거시 기반의 JPA 로 데이터베이스까지 연동되었다는 가정하에 진행하는 과제다.
이 과제는 포비님이 과제 진행 상관없이 꼭 경험하라고 권장하는 중요한 과제다.
처음에 접근하기 당황했다.
이전까지는 순수 자바 환경에서만 진행했었는데
갑자기 JPA 기반의 예제가 나오니 JPA 기준도 맞추면서 해야하나?!!! 생각도 했지만
신경쓰지말고 객체지향에 맞게 진행하면 된다는 리뷰어의 답변과
테스트 환경도 MOCK 이 있어서 테스트만 문제없이 모두 통과하면 되는 리팩토링 단계다.
레거시 코드를 리팩토링하면서 중간에 비어있는 로직을 설계하고 구현하고 제출했지만
피드백을 받았을 때, 이전 미션을 하면서 학습한 내용 중 잘못된 걸 알게 된다.
바로 불변객체를 가변으로 사용했다는거...
불변은 말 그대로 변함이 없어야하지만 인스턴스 필드에 final 이 있지만 요소를 변경시키는 List.add(값) 를 했었다... 🙈
그리고 하나의 메서드에서 처리해도 될 것을 불필요하게 두 개의 메서드로 나눈 것도 존재했고...
문맥 흐름이 이상하다는 부분도 있었다.
모두 수정하고 재요청한 뒤 통과하게 되었다.
🚀 step2 - 볼링 점수판(그리기)
지금까지 과제 중 가장 어려웠던 과제다.
투구 할 때 마다 투구 결과를 노출시키는 조건이 까다롭고
지켜야할 원칙들이 너무 많아서 (상단의 더보기) 더 힘들었다.
이 단계를 진행하는데 최소 3일이 걸렸고, 설계만 하루에 한번씩 갈아엎고 진행했다...
볼링에 대해 너무 생소해서 그런것도 있지만 이전과는 다르게 룰이 너무 많았다.
스트라이크(strike) : 프레임의 첫번째 투구에서 모든 핀(10개)을 쓰러트린 상태, "x"로 표시
스페어(spare) : 프레임의 두번재 투구에서 모든 핀(10개)을 쓰러트린 상태, "/"로 표시
미스(miss) : 프레임의 두번재 투구에서도 모든 핀이 쓰러지지 않은 상태
거터(gutter) : 핀을 하나도 쓰러트리지 못한 상태. 거터는 "-"로 표시
10 프레임은 스트라이크 혹은 스페어면 한 번을 더 투구할 수 있다.
접근하는 방법에 따라 많은 설계가 나오기도 하고, 갈아엎는 경우도 가장 많을 거다.
볼링은 10 프레임으로 진행되며 1~9프레임과 10프레임은 서로 다른 특징을 갖고 있어서
1~9프레임과 10프레임은 다루게 구성했다. 프레임마다 가지게 되는 내부요소 Pins 를 NormalPins, FinalPins 로 나눈것.
인터페이스 Pins 는 다음과 같이 구성했다.
구현체마다 공용되는 상수를 모아서 Pins 에게 넘겼다.
public interface Pins {
String MAX_OVER_PINS = "넘어뜨리는 볼링핀은 10개가 최대입니다.";
int NORMAL_PINS_MAX_SIZE = 2;
int MAX_PINS = 10;
int FIRST_INDEX = 0;
boolean IS_FIRST = true;
Pins first(int countOfDownPin);
Pins next(int countOf);
boolean isEnd();
ScoreRule scoreRule();
List<Pin> pins();
}
게임을 진행하면서 투구할 때 마다 List < frame > frames 에서
해당 프레임(가장 마지막이 해당 프레임)의 인덱스를 사용하는 중복되는 곳이 많아 lastIndex() 메소드로 빼서 사용했다.
(중복되는 코드가 없어야 된다고 생각했기 때문)
private int lastIndex() {
return frames.size() - MINUS_INDEX_ONE;
}
그리고 리뷰어에게 받은 피드백으로 위의 interface Pins를 수정하게 된다.
인터페이스에 상수를 넣은게 나쁜 프랙티스였단다.😂
구현체마다 상수를 옮기는 것으로 수정했다. (맥락에 맞게 따로 관리해야한다)
lastIndex() 도 무의미한 메소드 분리라 지적당했다.
이를 사용하는 장소마다 의미가 다르고 private 를 통해 가져오는 것이 더 어색하고 디버깅하기 불편한 구조라 한다.
덕분에 중복되는 것도 무조건 나눠서 관리하는게 정답이 아니라는 걸 알게되었다.
피드백 반영하고 다음 단계로 넘어가게 된다.
🚀 step3 - 볼링 점수판(점수 계산)
이전 볼링에서 요구사항으로 점수 계산이 추가된 단계다.
이 단계도 볼링 점수 규칙에 대한 룰 때문에 고민이 있었다.
파악한 볼링 점수의 규칙이다
- 스트라이크 경우, 다음과 다다음 투구 때 쓰러진 핀 개수만큼 보너스를 획득한다 (보너스 점수 2번)
- 스페어 경우, 다음 투구 때 쓰러진 핀 개수만큼 보너스를 획득한다 (보너스 점수 1번)
- 10라운드 경우, 첫 투구 때 스트라이크나 두번째 투구에서 스페어일 때 점수 계산을 위해 세번째 투구 기회가 있다.
- 이전 라운드 점수 계산이 끝이 안나면 이후 점수는 결과에 노출 안한다.
점수 계산도 난해했다. 이론상 0~300점까지 획득 가능한 점수다.
특히 마지막 규칙, 점수 결과에 노출 안한다는거에 많은 고민을 하게 된다.
그러다 결국 최대한 간단하게 접근을 해보게 된다.
프레임이 끝나면 해당 프레임의 Score 를 생성하고,
이후 투구를 할 때 마다 보너스 기회가 있으면 추가 점수를 획득하는 방법으로 진행했다.
그 결과 구조를 튼튼하게 잘 잡았다는 칭찬👍👍 과 함께
이름 짓기에 신경을 써달라는 요청만 받게된다.
이름 때문에 가독성이 떨어진다고... (영어 공부를 합시다) 🙈🙈🙈
🚀 step4 - 볼링 점수판(n명)
대망의 마지막 과제 차례다.
이전까지는 1명이 볼링을 했지만 이제는 입력된 유저 수 만큼 볼링이 진행되어야한다.
순서에 상관없이 한명이 투구할 때 마다 모든 상황을 노출해야한다.
여기서도 고통을 받게 된다
유저가 여러명이 되니 Users 를 추가하고
Users 의 List < User > 에서 차례대로 투구 할 때 마다 결과로 노출해야 되는게
UI 와 도메인을 결합해야 가능하지 않는가 생각하게 되고, 이를 피하기 위해 방법을 찾게 되는데...
아무리 생각해도 컨트롤러에서 User 마다 실행하고 결과를 노출하는게 가장 낫다 판단하고 진행하게 된다.
진행하기 앞서 정리한 구현 조건이다
- 모든 유저의 프레임이 끝날 때 까지 반복한다.
- 해당 유저의 프레임이 끝나기 전까지 다음 유저에게 투구 기회를 줄 수 없다
위의 조건으로 다중 반복문 방식으로 접근을 했다.
while (모든유저볼링종료) {
...
while (유저프레임종료) {
...
}
}
미리 말하자면... while 문도 방법이긴 한데 잘못되었다.
while 문을 사용하면 while문마다 종료되기 위한 로직이 추가적으로 필요하고, 잘못되어 복잡한 로직이 생길 수도 있기 때문
이는 남이 파악하기 힘든 구조가 될 수도 있다.
피드백해준 리뷰어도 이 부분을 파악하기 힘들어하셨으며,
이를 최대한 유지하며 코드를 개선하는 것도 오히려 더 큰 가독성을 헤칠 수 있을거 같아.
결국엔 while문 자체를 없애고 단순하게 for문 방식으로 1~10프레임 진행하고
프레임 번호를 조건에 보내는 방식으로 변경했다
// 이전 코드
while (users.isAllPlay()) {
...
}
// 이후 코드
for (int i = 0; i < LAST_FRAME_NUMBER; i++) {
...
}
for문을 사용하니 모든 유저의 프레임이 종료되었는지 확인하는 불필요한 로직도 제거되고
이전보다 파악하기 쉬운 코드로 개선되었다.
그리고 내부 반복문 구조를 처음엔
Users 의 List < Users > users 를 가져와서 사용했는데
// Controller 클래스
...
while (users.isAllPlay()) {
users.users().forEach(user -> bowlingPlay(users, user));
}
// Users 클래스의 users
public List<User> users() {
return users;
}
users() 를 없애고 함수형 인터페이스로 변경했다.
// Controller 클래스
...
for (int i = 0; i < LAST_FRAME_NUMBER; i++) {
int frameNumber = i;
users.forEach(user -> bowlingPlay(frameNumber, users, user));
}
// users() 를 대체
public void forEach(Consumer<? super User> users) {
this.users.forEach(users);
}
이를 한번 더 개선해서 다음과 같이 변경했다.
IntStream.range(0, LAST_FRAME_NUMBER)
.forEach(frameIndex -> users.forEach(user -> bowlingPlay(frameIndex, users, user)));
그리고 마지막 미션을 통과하게 되었다.👍👍
후기
이전과는 다르게 볼링은 난이도가 높았고, 난감한 상황을 많이 마주친 미션이였다.
지켜야 할 규칙이 많아지고 이를 의식하면서 진행하는게 힘들었지만 더욱 더 시야가 넓어진 기분이다.
현재 이전에 통과한 미션 코드를 다시 한번 훑어보고 있는데
개선이 필요한 부분이 많이 보인다... 🙈🙈🙈🙈
미션 수준에 맞는 규칙만 통과되면 넘어간 기분이랄까....?😂😂😂
모든 미션을 통과했지만 이는 튜토리얼이였고 이제서야 본 게임을 시작한다고 생각한다.
그리고 매번 미션을 넘어갈 때 마다 느끼는 거지만 미션마다 다양한 방식으로 접근할 수 있고, 새로운 인사이트를 많이 얻을 수 있으니
객체지향 생활 체조원칙, GRASP, 객체지향 5원칙(SOLID) 를 지키면서 자동차 미션부터 볼링까지 다양하게 반복해야겠다.
그리고 아직 교육이 끝난것도 아니고 3주간의 시간이 더 남았으니
최종 후기는 3주 뒤에 작성해야겠다.
'교육 및 인강 > TDD, Clean Code with Java' 카테고리의 다른 글
TDD, Clean Code with Java - 미션3, 사다리 후기 (0) | 2021.04.06 |
---|---|
TDD, Clean Code with Java - 미션2, 로또 후기 (0) | 2021.03.29 |
TDD, Clean Code with Java - 미션1, 자동차 경주 후기 (0) | 2021.03.18 |
TDD, Clean Code with Java 시작하며.. (2) | 2021.03.18 |