이번 과제에서는 1주차의 코드에 대해서 피드백을 받은 내용을 최대한 반영하고자 노력하였다.
새로 추가된 요구 사항에 나와있는 것처럼 indent depth가 3이 넘지 않도록 구현하기 위해 함수를 최대한 작게 만드는 일에 집중하였다. 1주차 때 느꼈지만 함수를 세분화한다는 것은 생각보다 쉽지 않다. 함수화할 수 있다고 생각치 못한 부분에서 관련된 피드백을 많이 받았던지라, 이번 과제에서는 그러한 부분을 많이 신경쓰며 개발하고자 하였다.
2주차 프리코스 저장소는 아래 링크에 있다.
https://github.com/woowacourse-precourse/java-racingcar-6
⚡ 구현 과정 요약
⭐ 프로젝트 개요
- 게임에 참여하는 자동차 이름을 입력한다.
- 자동차가 전진을 시도하는 총 횟수를 입력한다.
- 총 횟수만큼 자동차가 전진을 시도한다.
- 자동차의 무작위 숫자가 4보다 크다면 전진한다.
- 게임이 끝나면 우승자를 출력한다.
🛠 시스템 흐름도
📝구현 기능 목록
자동차 이름 요청 문구 출력 기능
- 자동차의 이름을 입력할 것을 콘솔에 출력한다.
자동차 이름 입력 기능
- 뷰에서 자동차의 이름을 입력받는다.
- 쉼표(,)를 기준으로 구분한다.
- 입력의 유효성을 검사한다.
- 입력 단에서의 검증
- 빈 문자열이 입력되는 경우를 검증한다.
- 쉼표가 비정상적으로 입력되는 경우를 검증한다.
- Car 도메인 단에서 검증
- 자동차의 이름이 5자 이하인지 검증한다.
- 자동차의 이름이 알파벳으로 이루어져 있는지 검증한다.
- Cars 도메인 단에서 검증
- 2대 이상의 자동차가 입력되었는지 검증한다.
- 중복되는 자동차의 이름이 없는지 검증한다.
- 입력 단에서의 검증
자동차의 이름 저장 기능
- 자동차의 객체를 생성한다.
- 이름, 전진한 횟수를 저장하는 필드를 추가한다.
- 객체 생성 시 이름을 저장하는 생성자를 만든다.
- 입력된 자동차의 이름을 객체에 저장한다.
- 이름을 통해 자동차 객체를 생성한다.
- 자동차 객체를 리스트로 변환하여 반환한다.
총 횟수 요청 문구 출력 기능
- 전진을 시도하는 총 횟수의 입력을 요청하는 문구를 출력한다.
총 시도 횟수 입력 기능
- 뷰에서 총 시도 횟수를 입력 받는다.
- 입력의 유효성을 검증한다.
- 숫자로 구성된 입력인지 검증한다.
- 숫자의 값이 1보다 크거나 같은 입력인지 검증한다.
자동차 전진 시도 기능
- 총 시도 횟수만큼 자동차의 전진을 시도한다.
- 각 자동차마다 무작위로 숫자를 생성한다.
- 생성된 숫자가 4 이상이면 자동차 객체를 업데이트한다.
- 총 시도 횟수만큼 자동차들을 이동하는 함수를 호출한다.
- 자동차의 총 갯수만큼 전진을 시도하는 함수를 호출한다.
자동차 전진 후 상태 출력 기능
- 모든 자동차의 전진을 시도한 후의 상태를 출력한다.
- toString()을 오버라이딩하여 자동차의 상태를 출력하는 함수를 생성한다.
- 모든 자동차의 전진을 시도한 후 출력 함수를 호출한다.
게임이 끝나면 우승한 자동차를 출력한다.
- 모든 자동차 객체에서 최댓값을 구한다.
- 최댓값에 해당하는 모든 객체를 선택하여 리스트로 만든다.
- 뷰에서 리스트를 형식에 맞추어 출력한다.
🧐 미션 구현 과정에서 고민한 것들 & 지난 주차 피드백 적용
1. 아키텍처 패턴
이전 과제에서는 MVC 패턴을 적용하였다. 그러나 피드백에서 컨트롤러 간의 참조가 이루어져 있어 MVC 아키텍처 패턴의 원칙에 어긋난다라는 피드백을 받았고, 서비스 레이어를 추가할 것을 권고받았다.
나는 MVC 패턴이라면 무조건 Model, View, Controller 패키지만 존재해야한다고 판단하였고, 기타 레이어가 들어가게 되면 아키텍처 패턴의 원칙에 어긋난다고 판단하여 꾸역꾸역 (?) ComputerController 객체를 만들어서 참조하도록 하였다.
로직이 간단해지긴 하였지만, 피드백을 받은 것처럼 컨트롤러 간의 의존성이 발생하게 되어 외부의 요청을 받고 응답을 반환하는 역할을 수행하는 컨트롤러의 책임이 너무 커지게 되었다.
그래서 이번 주차 과제에서는 다중 레이어드 패턴, 특히 4-tier 아키텍처 패턴 적용을 시도하였다.
아키텍처와 관련된 내용 학습은 테코톡 중 <누누, 다즐의 클린 아키텍처>를 참고하였다. 테코톡 내용을 간단히 요약하면 다음과 같다.
💡 아키텍처란 ?
어떤 대상의 구성과 동작 원리, 구성 요소 간의 관계를 설명하는 설명서이다.
소프트웨어는 크게 '기능', '구조' 이 두가지의 가치를 제공하는데, 아키텍처는 구조적인 가치의 튼튼한 기반이 되는 설계도이다.
💡 레이어드 아키텍처란 ?
데이터베이스 주도 설계이며, 상위 레이어는 하위 레이어를 알 수 있고, 하위 레이어의 변경은 상위 레이어에 영향을 준다.
💡 클린 아키텍처란 ?
- 레이어드 아키텍처에서 상위 레이어가 하위 레이어에 의존하고 있으므로, 독립적이지 않다는 문제가 있다.
- 의존성 역전을 통해서 DB가 아닌 도메인에 의존하는 방법으로 해결한다.
- 핵심 규칙을 담고 있는 도메인이 중심 도메인이 세부 사항에 의존하지 않는다
- 레이어의 종류
- Entity 레이어 : 도메인 패키지 안에 위치해있고, 외부 요소에 대해 모른다.
- Use case 레이어 : 비즈니스 로직을 포함한다. 객체를 받아와 업데이트를 처리한다.
- Adapter 레이어 : 데이터를 Use case에서 사용하는 형태로 변경한다.
- Infrastructure : DB와 레이어의 통신 작업을 수행한다.
💡 헥사고날 아키텍처란 ?
외부 요소와 핵심 비즈니스 로직이 소통할 때 포트를 통해서 간접적으로 소통한다. 컨트롤러에서는 포트를 통해 Use case에 접근할 수 있다. Use case에서는 도메인에 구현되어있는 Respotiory를 통해 결과를 보장할 수 있다.
나는 여러 아키텍처 중에서 레이어드 아키텍처를 선택하였고, 컨트롤러 클래스의 간결화와 비즈니스 로직을 서비스 레이어로 모두 이동하여 가독성이 좋게 구현하는 것을 목표로 하였다
그러나 해당 아키텍처에 적용하는 것에 실패했고, 결국에는 MVC 패턴으로 구현하게 되었다.
프로그램 요구사항이 간단해서 다중 레이어드 아키텍처를 고집하려다 보니, 싱크홀 안티 패턴이 생기게 되었다. 레이어 간의 비즈니스 로직을 거치지 않고 무의미하게 레이어를 통과하게 되는 현상을 말한다. 실제로 아래와 같이 한 줄의 코드로 서비스 레이어의 함수가 발생하고, 전체적으로 서비스 레이어가 제공하는 비즈니스 로직이 불분명해지는 현상이 있었다.
public class RaceService {
public Cars generateCars(String rawNames) {
return Cars.of(parseStringToList(rawNames));
}
public void playRace(Cars cars) {
cars.tryMoveCars();
}
public List<String> getWinners(Cars cars) {
return cars.findMostMovedCarName();
}
private String[] splitNames(String names) {
return names.split(DELIMITER.getSymbol());
}
private void validateRange(Integer count) {
if (isLessThanMinCount(count)) {
throw RaceException.of(ErrorMessage.INVALID_COUNT_RANGE);
}
}
private boolean isLessThanMinCount(Integer count) {
return count < MIN_RACE_COUNT.getValue();
}
}
그래서 과감히 MVC 패턴으로 돌아가서 서비스 레이어를 제거하고, 컨트롤러를 주축으로 뷰와 도메인을 참조하며 개발하도록 수정하였다.
2. 상수 (Constants) 관리 방안
프로그램 내에 존재하는 모든 매직 넘버와 문자열을 상수로 정의하였다. 피드백 중 하나가 이를 안티 패턴이라고 언급하였는데, 내 예상으로는 간단한 요구사항에 상수 클래스를 분리하는 것이 더 소모적이고, 읽는 사람도 일일이 값을 찾아야해서 가독성이 떨어지게 되는 이유라고 판단하였다.
그러나 고민해본 결과 확장성을 고려해서 Enum 클래스를 사용하는 것이 최선이라고 생각했고, 더 자세하고 명확한 변수명을 사용하고 적절한 접두사를 사용하는 것으로 해소할 수 있을 것이라고 판단하였다.
결과적으로 모든 상수의 특성에 따라 메시지, 기호, 숫자로 나누어 저장하였고 필요에 따라 참조하여 사용하는 방식으로 결정하였다.
3. 일급 컬렉션
이번에는 여러 대의 자동차를 저장하는 객체를 일급 컬렉션으로 관리하기 위해 일급 컬렉션과 관련된 예습을 하였다.
일급 컬렉션을 사용하는 이유 (techcourse.co.kr)
참고한 테코블 문서를 간단히 요약하면,
💡 일급 컬렉션이란 ?
리스트와 같은 컬렉션 객체를 멤버 변수로 유일하게 가지고 있는 클래스를 일급 컬렉션이라고 한다. 즉 컬렉션을 Wrapping하고 있고, 도메인에 대한 검증을 서비스 로직이 아닌 일급 컬렉션의 생성자에서 수행할 수 있다는 장점이 있다.
💡 일급 컬렉션에서 보장하는 불변성에 대하여
final 구문은 리스트의 재할당을 막는다. 즉 변경은 가능하다는 말이다. 일급 컬렉션을 통해 이러한 불변성을 해소하는 방안이라고 알려져있는데, 일급 컬렉션으로 감싸도 리스트는 변할 수 있다.
이 문제를 해결하기 위해서는 unmodifiableList를 사용할 수 있다. 그러나, 되도록 getter를 사용해 리스트를 그대로 반환하지 않도록 하는 것이 좋은 설계이다.
일급 컬렉션을 사용하게 됨으로써 Cars - Car - (Number, Position) 이렇게 자동차의 정보에 접근하는 흐름을 만들어냈다. 클래스를 분리하니 자연스럽게 도메인을 검증하는 로직을 몰아넣었던 Validator 클래스의 역할을 여러 도메인 레이어에 분산하게 되었다.
4. 유효성 검증
1주차에서는 유효성을 검증하는 로직을 하나의 클래스에서 모두 구현하였고, 필요할 때마다 정적 메서드를 참조하여 이를 사용하는 방식으로 구현하였다.
처음에는 이 피드백이 잘 이해가 안되었는데, 일급 컬렉션과 관련된 문서를 읽다 보니 도메인 레이어에서 검증을 수행하는 것의 장점에 대해 이해하게 되었다. 입력에 대한 유효성 검증의 역할을 서비스 레이어가 맡게 된다면, 책임이 분리되지 않고 입력이 있을 때마다 서비스 레이어가 일일이 검증을 해야한다.
사실 서비스 레이어는 비즈니스 로직을 처리해야하는데, 사소한 검증 로직까지 모두 맡아 버리게 되면 책임이 커지고 코드도 지저분해지게 된다.
이에 따라 입력값에 대한 검증 로직을 뷰 레이어, 혹은 도메인 레이어에 넣게 되었고, 유효성 검사의 특성에 따라 도메인 내에서도 책임을 한번 더 분리하였다.
- 입력 단에서의 검증
- 빈 문자열이 입력되는 경우를 검증한다.
- 쉼표가 비정상적으로 입력되는 경우를 검증한다.
- Car 도메인 단에서 검증
- 자동차의 이름이 5자 이하인지 검증한다.
- 자동차의 이름이 알파벳으로 이루어져 있는지 검증한다.
- Cars 도메인 단에서 검증
- 2대 이상의 자동차가 입력되었는지 검증한다.
- 중복되는 자동차의 이름이 없는지 검증한다.
이렇게 검증 로직의 위치를 설계하였고, 유효성 검증을 수행하는 책임이 어디에있는가?를 생각해보며 검증하는 메서드를 여러 레이어로 분산하였다.
추가로, 여기에서는 정적 내부 클래스를 사용하였다. 사실 도메인 레이어에 검증 로직과 같은 복잡한 함수가 들어가는 것을 탐탁치 않아 했다. 도메인 클래스는 간단히 가져가야한다는 관념 때문인지, 자잘한 메서드를 넣는 것이 불쾌하게 느껴졌다. 그래서 찾게된 대안은 내부 클래스를 사용하는 것이다.
💡 내부 클래스(Nested Class) 의 종류
(1) 스태틱 내부 클래스
내부 클래스가 다른 클래스와 유사하지만 다른 카테고리 (본 클래스)를 가지고 있을 때 사용한다.
(2) 로컬 내부 클래스
클래스에 자주 쓰이는 작업을, 외부에 공개할 필요가 없는 클래스에서 구현하고 싶은 경우 사용한다. 익명 내부 클래스
(3) 익명 내부 클래스
public class Cars {
private List<Car> cars;
private Cars(final String userInput) {
List<Car> cars = Parser.parseStringToCarList(userInput);
Validator.validateCars(cars);
this.cars = cars;
}
...
private static class Validator {
private static void validateCars(final List<Car> cars) {
validateListSize(cars);
validateDuplicateNames(cars);
}
...
}
}
이렇게 정적 내부 클래스로 Validator를 정의하고 내부 참조로 검증을 수행하는 것으로 수정하였다.
5. toString() 오버라이딩
너무 진귀한 리뷰들이다 ... 시간 내어 리뷰해주시는 분들이 너무 감사할 따름이다. 😭
toString()은 클래스의 정보를 위와 같은 형식으로 반환하는 Object 클래스의 메소드이다. 객체의 고유 정보를 출력하고 싶을 때 사용하는 메소드이지만, 내가 만든 객체를 원하는 형식에 맞추어 출력하기 위해 이 메소드를 오버라이딩하여 구현할 수 있다.
@Override
public String toString() {
return "새로운 문자열";
}
위와 같이 @Override라는 메서드 시그니처를 사용해 원하는 형식의 문자열을 렌더링하여 반환하면 된다.
이번 주차의 미션의 경우, 자동차의 정보를 출력하고 최종 우승자의 정보를 출력하는 것에 이 함수를 재정의하여 사용하였다.
👏 느낀 점
자잘한 부분까지 생각하면 더 적을 것이 많지만, 우선 크게 다섯 가지의 부분에 대해서 고민하며 프로그램을 설계하였다. 함수를 최대한 작게 만들기 위해 고민하는 과정에서 클래스 분리, 메서드 분리, 내부 클래스 생성 등을 적용해보았다. 또한, 더 읽기 쉽고 깔끔하고 확장성이 좋은 코드를 만들기 위해 상수 클래스 적용, 일급 컬렉션 적용 등의 내용을 공부해보고 설계에 적용해볼 수 있었다.
다중 레이어드 아키텍처 패턴을 적용해보고자 하였으나, 패턴에 맞추어 요구사항을 구현하려다 보니 코드가 오히려 더 지저분하게 되는 경험을 하였다. 디자인 패턴이든, 아키텍처 패턴이든 역시 수단이지 목적이 되서는 안된다는 말을 한번 더 실감하였고 요구사항에 맞게 적절한 패턴을 선택하고 클래스 간의 유기적인 흐름을 파악하는 것을 우선해야겠다는 생각을 하였다.
'우아한테크코스 > 레벨0' 카테고리의 다른 글
[회고] 우아한 테크코스 최종 코딩테스트 대비 - 다리 건너기 (0) | 2023.12.13 |
---|---|
[회고] 우아한테크코스 1차 심사 합격 + 앞으로의 계획 (0) | 2023.12.13 |
[회고] 우아한 테크코스 프리코스 1주차 회고 - 숫자 야구 게임 (5) | 2023.11.27 |
[회고] 우아한테크코스 프리코스 4주차 회고 - 크리스마스 프로모션 (0) | 2023.11.17 |
[회고] 우아한테크코스 프리코스 3주차 회고 - 로또 게임 (0) | 2023.11.11 |