repository
https://github.com/howudong/java-lotto-6/tree/howudong
3주 차 소감
요구사항이 복잡해지기 시작한 것 같다. 확실히 1~2주 차는 맛보기였다..
이번 주차에는 입력과 출력에 필요한 도메인 정보들이 다르다. 그래서 처음으로 DTO를 적용시켜 봤다.
DTO라는 개념 자체는 간단하지만 이를 직접 사용한다는 것은 간단하지 않았다.
'DTO로 어디서 바꿔줘야 하는가?', '이를 효율적으로 사용하기 위해선 어떻게 해야 하는가?'
이런 것들을 고려하는 데에 개인의 주관이 많이 들어갔다.
DTO로 변환시켜 주기 위한 service 계층을 사용하다 보니,
자연스럽게 프로그램 규모가 커지고 복잡해지기 시작했다.
이럴 때일수록 중요한 것이 객체지향적인 설계라고 생각하여,
이 큰 규모의 프로그램 안에서도 최대한 객체들 간의 모듈화와
코드의 가독성을 지키기 위해 노력해 가며 구현해 나갔다.
하지만 그럼에도, 객체가 많아지고 하다 보니, 역할 분리가 모호해지고 여러 의존관계가 생겼다.
어떻게든 객체지향적 설계를 유지해보려고 해도,
점점 감당하기가 힘들어져 실력에 대한 무기력함이 들더라..
이번 주차는 기능 구현을 위해 어쩔 수 없이 객체 간의 통일성을 깨뜨린 부분이 있다.
그래서 주먹구구식으로 프로그램을 구현했다는 느낌을 지울 수가 없다.
분명 더 좋은 방식이 있을 것 같은데, 실력이 부족해서 이를 생각해내지 못했다.
그리고 Dto와 Service의 관한 개념이 부족하다는 것이 계속 느껴졌다.
Service 계층의 역할, DTO의 역할, 개념을 확립해두지 않고 사용하는 느낌이라 불쾌감이 많이 들었던 것 같다.
여러모로 얼마나 내가 모자란지에 대해 느낄 수 있었던 주차였다.
돌려 말하면 더 많이 배워야 한다는 것을 뼈저리게 느꼈으니, 성장할 수 있는 계기가 됐다고 생각한다.
집중적으로 고려했던 부분
Java 컨벤션 지키기
2주 차 피드백을 보니, 내가 자바 컨벤션에 대해 한 가지 잘못 알고 있었던 것이 있었다.
생성자가 인스턴스 필드보다 뒤에 와야 했었다.
나는 여태까지 생성자가 인스턴스 필드보다 앞에 와야 한다고 잘못 생각하고 있었다.
이런 기본적인 컨벤션을 착각하고 있었으니, 이번에는 훨씬 더 컨벤션을 꼼꼼히 지키기 위해 노력했다.
살아있는 개발 문서(README)
항상 리드미를 체크해 가면서 작업을 이어나가려고 노력했다.
생각하는 건 항상 쉬웠는데, 실천하는 것이 어려웠다. 왜냐하면 여간 귀찮은 게 아니었기 때문이다.
만약 클래스 이름이 바뀌면 리드미도 바꾸고, 추가되는 것이 있으면 적었다.
코드 짜면서 이것까지 하는 게 선뜻 쉬운 일은 아니었다.
기능 개발을 할 때마다 함께 개발 문서를 갱신해 주는 것을 버릇 들이기가 생각보다 힘들었다.
리팩터링 할 때 추가되는 클래스들까지 다시 적어야 하니 힘들었다.
개발 문서를 적으면서 든 생각이 README는 어느 정도까지 자세히 적어 아하는 걸까?
2주 차 피드백 문서를 보면 그렇게 자세히는 안 써도 된다고는 하는데..
이게 어느 정도까지 이은 지가 헷갈리는 부분이다.
테스트 주도 개발(TDD)
저번주차부터 TDD 방식의 이점을 느끼게 되어, 이번에도 TDD로 설계하고 개발했다.
이번에 또다시 느낀 것이, 프로그램이 커지다 보니 객체지향을 지키기가 정말 어렵더라.
그래서 더욱 TDD가 중요하지 않나 생각이 들었다.
테스트를 위한 설계를 하면 어느 정도 객체지향적인 클래스가 작성되기 때문에,
다른 방식들 보다는 설계하기가 편하다고 느껴졌다.
항상 테스트코드를 짜는 것을 염두에 두면서 모델을 설계하였다.
쉬운 테스트를 위해선 어떻게 도메인을 구성해야 할까?
이번에 크게 한 가지 깨달은 점은,
테스트코드를 위해 접근 제한자를 public으로 열지 않아도 된다는 것이다.
만약 원래에 같은 패키지에 있었다면, 테스트코드도 그 규칙을 따라간다.
그러니까 같은 패키지 내에 있다면, default까지만 풀어도 된다는 것이다.
이 사실을 아니까 테스트코드 짜는 것이 조금은 편해졌다.
예외 발생 시 입력 다시 받기를 수행하는 클래스 구현
예외가 발생했을 때, 예외 메시지를 출력하고 함수를 재실행하는 클래스를 따로 빼는 것에 대해 고민을 했다.
하지만 이걸 사용하지 않으니 많은 클래스에서 try-catch를 사용해야 했는데,
이게 가독성이 매우 떨어지고, 또한 반복적인 코드 또한 많아졌다.
그래서 가독성을 우선순위로 생각하고, 반복적인 코드를 줄여주기 위해 클래스를 구현했다.
public final class ErrorHandler {
private ErrorHandler() {
}
public static void tryUntilNoError(Runnable method) {
try {
method.run();
} catch (IllegalArgumentException e) {
ErrorHandlingView.viewErrorText(e.getMessage());
tryUntilNoError(method);
}
}
}
함수형 인터페이스 Runnable을 사용하여 실행해야 하는 함수 자체를 매개변수로 넘길 수 있도록 했다.
해당 부분에서 에러 메시지를 출력하고 다시 실행하도록 했다.
예외를 따로 처리해 주는 클래스를 작성하니 가독성이 좋아졌을까?
// 예외 처리 클래스 사용 전
public void process(Map<String, ? super Dto.Input> inputs,
Map<String, ? super Dto.Output> outputs) {
try {
Long price = getBuyLottoPrice(inputs, outputs);
viewBuyLottoNumbers(outputs, price);
} catch (IllegalArgumentException e) {
System.out.print(e.getMessage());
process(inputs, outputs);
}
}
// 예외처리 클래스 사용 후
public void process(Map<String, ? super Dto.Input> inputs,
Map<String, ? super Dto.Output> outputs) {
ErrorHandler.tryUntilNoError(() -> {
Long price = getBuyLottoPrice(inputs, outputs);
viewBuyLottoNumbers(outputs, price);
});
}
예외 처리 클래스를 사용하여 리팩토링 하기 전과 후를 비교했다.
try-catch문이 없어서 들여 쓰기가 적어졌고, 또한 중복 코드가 사라져 가독성이 훨씬 좋아졌다.
확실히 사용하길 잘한 것 같다.
View에 DTO만 전달할 수 있게 하기
내 생각에 데이터베이스가 없는 현재 미션에서 DTO를 사용하는 이유는,
View에 Domain 정보를 최대한 은닉하기 위해서라고 생각한다.
즉 view들은 dto에 있는 정보들만을 사용할 수 있도록 해야 한다는 것이 이번에 주된 생각이었다.
public abstract class InputDto {
InputDto() {
}
}
public abstract class OutputDto {
OutputDto() {
}
}
public interface Controller {
void process(Map<String, InputDto> inputs, Map<String, OutputDto> outputs);
}
그래서 Dto를 inputDto, outputDto 두 개로 분리하였고,
각각 컨트롤러에서 파라미터를 전달받을 때
InputView은 InputDto만, OutputView는 outputDto만 넣을 수 있도록 하였다.
고민했던 부분(의문점)
DTO는 어디서 생성해야 하는가?(Service vs Controller vs View)
이런 생각을 하게 된 것이 Dto를 입력/출력 별로 나눈 후부터 들었다.
inputDto를 사용하는 것은 입력 뷰인데, 그렇다면 입력을 받아서 생성하는 것도 inputView어야 하지 않나?
검색을 해보면 여러 가지 답변들이 많지만, 결론적으론 보는 관점에 따라 다르다는 것이었다.
결국에 내 코딩 가치관(?), 설계를 할 때 중요시하는 것에 따라 이를 일관성 있게 적응시켜야 한다고 생각했다.
내가 중요시 생각하는 것은 코드 가독성이다.
따라서 해당 dto 의미상 주체에 붙어있는 것이 더 낫다고 판단했다.
view는 dto를 이용해서 로직을 수행한다기보다는, 그저 dto를 통해 화면에 출력해 주는 역할을 한다.
따라서 생성이라는 로직은 View를 컨트롤하는 Controller가 가지는 게 맞다고 판단했다.
그리고 setter를 통해 InputView에서 값을 지정하는 방식이 명료하다고 생각했다.
private void inputLottoNumbers(WinningLottoInputDto dto) {
if (dto.getLotto() != null) {
return;
}
String input = Console.readLine();
for (String split : input.split(DELIMITER)) {
InputValidator.validateIsNumber(split.trim());
InputValidator.validateIsEmptyValue(split.trim());
}
// InputView에서는 setter를 통해서 값 설정만 해줬다.
dto.setLotto(convertToLotto(input));
}
Service의 역할은 어디까지일까?
DTO를 구현하면서 필연적으로 만들게 된 것이 Service 계층이었다.
왜냐하면 DTO를 생성하는 역할을 Service에게 맡길 생각이었기 때문이다.
그런데 DTO를 생성하는 역할을 가진 것이 Controller인가 Service인가에 대해 궁금증이 들었다.
혹자는 레이어드 아키텍처 입장에서는 Controller가 가지는 게 맞다고 하고,
혹자는 Service에는 순수 자바 로직만을 가져야 하기 때문에
Service에서 처리하여 Controller의 부담을 덜어주는 것이 맞다는 말도 있다.
나는 후자의 손을 들었다. 전자의 입장을 동의하지 않아서라기보다는,
전자의 논리를 제대로 이해하지도 못하고 이 논리를 사용한다는 것 자체가
좋은 프로그래밍이 아니라고 생각하기 때문이다.
난 항상 내가 짠 코드에 대한 근거가 있길 바란다.
그래서 아직 전자의 입장으로서는 내 코드에 적절한 근거가 생길 수 없다고 판단했다.
그리고 Service단이라는 것이 나에게는 정말 애매했던 것이,
Service는 결국 순수한 비즈니스 로직만으로 이루어진 것이라고 했다.
그렇다면 Domain에서도 이를 구현해도 되지 않을까?
Domain에도 비즈니스 로직이 들어가는 경우도 있고 하니
Service단과 Domain을 어떻게 구별해서 생각해야 할지가 여전히 의문이다.
난 Service 단을 dto의 생성과 여러 도메인이 협응 해야 하는 비즈니스 로직 정도에만 사용했다.
이 방법이 맞는 것인지, 결합도를 낮추는 것 말고는 어떤 또 다른 장점이 있는지를 알 수가 없다.
검색을 해봐도 모호한 얘기만 나올 뿐 확실한 정의는 존재하지 않는 것 같았다.
내가 짠 코드는 정말 객체지향적인 설계였을까?
프로그램이 커지고 클래스가 많아지다 보니 자연스럽게 든 생각이었다.
내가 짠 코드가 객체지향적으로 잘 짜였는지 어떻게 확인할 수 있을까?
TDD 방식으로 코드를 설계하였지만, 정말 모듈화가 잘 이뤄졌을까?
다른 객체에게 최대한 영향이 적게 설계가 됐나?
나는 그렇다고 생각하지만, 다른 사람의 입장에서 보면 또 모를 이야기이다.
내가 짠 다이어그램을 보니, 한없이 작아지는 듯한 느낌이 들었다.
어떤 것이 객체지향적인 것일까? 이를 효과적으로 알 수 있는 방법은 무엇이 있을까?
아쉬운 점
DTO를 제대로 활용하지 못한 점
컨트롤러를 통해 DTO만을 이용하는 구조를 만들었는데, 이게 효율적으로 쓰였다고 느껴지지 않았다.
어떤 부분에서는 안 쓰니만 못한 거같이 처리가 된 것 같아서 많이 아쉽게 느껴졌다.
Dto에 대한 확실한 개념이 잡히지 않은 상태에서 사용한 것이 독이 된 것 같다.
Dto에 대해 확실히 알아보고 난 뒤에 사용해야 할 것 같다.
이렇게 사용하다 보면 오히려 프로그램이 복잡해져만 갈 것 같다.
static Method 의존 문제
종종 static 메서드를 사용했었다. 그런데 의존성 주입 방식을 사용한 것이 아닌,
내부에서 클래스를 정의해 줘서 강제로 의존되게 만들어준 것이 몇몇 존재한다.
이런 의존이 객체지향적이지 않아 많이 아쉽게 느껴졌다.
어떻게든 고쳐보고 싶었는데, 실력부족인지 정보의 부족인지 아무래도 고치기가 힘들었다.
public final class ErrorHandler {
private ErrorHandler() {
}
// static Method 사용
public static void tryUntilNoError(Runnable method) {
try {
method.run();
} catch (IllegalArgumentException e) {
// 이런 식으로 강제로 연관맺어주는게 맞을까?
ErrorHandlingView.viewErrorText(e.getMessage());
tryUntilNoError(method);
}
}
}