들어가며
1주차 과제가 드디어 끝이났다!
이번 1주차 과제는 문자열 덧셈 계산기였다. 1주차 과제라서 지난 기수에 출제되었던 문제가 다시 나올 줄 알았는데, 예상과 달리 새로운 문제였다. 처음엔 조금 당황했지만, 한편으로는 새로운 문제를 풀어볼 수 있어 설레는 마음이 들었다. 😊
이번 미션을 구현하면서는 OCP(Open-Closed Principle)와 SRP(Single Responsibility Principle)를 준수한 도메인 설계에 초점을 맞추고자 했다. 프리코스에 참여하면서 확장 가능한 설계를 시도해봐야 겠다고 생각했는데, 이번 과제가 “덧셈 계산기”를 만드는 것이었다. 이를 접하자마자 자연스럽게 이런 고민이 떠올랐다.
“나눗셈 계산기”, “뺄셈 계산기”, “곱셈 계산기”로 변경해야 한다면 어떻게 설계해야 할까?
이 질문을 바탕으로, 기능 확장이 용이하도록 설계를 진행했다. 연산의 종류에 따라 다양한 정책이 추가되거나 변경될 수 있는 상황을 고려하여, 계산기의 핵심 역할을 담당할 Calculator 인터페이스를 설계했고, 각 연산(덧셈, 뺄셈, 곱셈, 나눗셈 등)을 별도의 구현체로 분리하여, 필요한 계산기를 주입받아 사용할 수 있도록 구성했다.
이런 설계를 통해 새로운 연산 방식이 추가되더라도 기존 코드를 수정할 필요 없이 확장이 가능하도록 구조를 완성할 수 있었다. 결과적으로, 변화에 유연하게 대응할 수 있는 계산기 시스템이 탄생하게 되었다! 🚀
하지만, 잘했다고 생각되는 부분이 있으면 아쉬운 점도 있는 법!
SRP(Single Responsibility Principle)를 준수하기 위해 “한 클래스는 하나의 변경 이유만 가지도록” 설계하려고 노력했다. 이를 위해 각 클래스가 한 가지의 public 메서드만 가지도록 코드를 작성했는데, 결과적으로 정보가 지나치게 파편화된 느낌을 주게 된 것 같다. 실제로 코드 리뷰에서도 “코드가 분산되어 있어 전체 흐름을 파악하기 어렵다”는 피드백을 받았다. SRP를 지키려던 의도가 오히려 코드의 가독성과 이해도를 떨어뜨리는 결과를 초래한 셈이다 😭
지금와서 생각해보면 "변경 이유"의 범위를 지나치게 세분화 했던 것 같다.
구분자 관리와 정규식 패턴 생성을 각각 다른 변경의 이유(책임)으로 보아서 Delimiter와 DelimiterManager를 분리하여 각각의 책임을 나눴었다.
하지만, "구분자 관리 및 정규식 생성"을 하나의 변경 이유로 봐도 되지 않을까?
그러면 DelimiterManager 클래스 만으로도 충분히 도메인 로직을 구현할 수 있었을 것 같다.
이번 경험을 통해 느낀 점은, 책임을 지나치게 세분화하지 않고, 논리적으로 연결된 작업을 하나의 클래스에 묶는 것이 오히려 SRP를 더 잘 준수하는 방법이 될 수 있다는 것이다.
앞으로는 각 클래스가 너무 작은 책임만 가지지 않도록, 책임의 범위를 적절히 판단하며 설계해야겠다고 다짐했다. 😊
말이 길어졌는데, 이제 진짜 본격적으로 회고를 시작해보자 🚀
미션 링크
https://github.com/woowacourse-precourse/java-calculator-7/pull/1466
[문자열 덧셈 계산기] 이동훈 미션 제출합니다. by hoonyworld · Pull Request #1466 · woowacourse-precourse/java
감사합니다.
github.com
잘한 점
1️⃣ 확장 가능한 설계
package calculator.domain;
public interface Calculator {
double calculate();
}
package calculator.domain;
import java.util.List;
public class SumCalculator implements Calculator {
private final List<Double> positiveNumbers;
public SumCalculator(List<Double> positiveNumbers) {
this.positiveNumbers = positiveNumbers;
}
@Override
public double calculate() {
return positiveNumbers.stream()
.mapToDouble(Double::doubleValue)
.sum();
}
}
위에서도 잠깐 언급했듯이, 이번 과제를 설계하면서 calculate 추상 메서드를 인터페이스에 선언하고, 각 연산 방식의 구현체에서 해당 메서드를 구현하도록 설계했다.
이 방식은 단순히 과제 요구사항을 충족시키는 데 그치지 않고, 설계의 확장성과 유지보수성을 극대화할 수 있는 구조라는 점에서 특히 만족스러웠다.
첫 번째로, 새로운 연산 방식이 추가되더라도 기존 코드를 수정할 필요가 없다는 점에서 큰 장점이 있었다. 인터페이스를 통해 “계산한다”는 공통된 역할을 정의하고, 각 구현체가 이를 기반으로 자신의 역할을 수행하도록 만들었다. 덕분에, 덧셈 외에 뺄셈, 곱셈, 나눗셈 등 다양한 연산을 유연하게 추가할 수 있는 구조를 만들 수 있었다. 이를 통해 “변경에는 닫혀 있고, 확장에는 열려 있다”는 OCP(Open-Closed Principle)를 실천할 수 있었다고 생각한다.
두 번째로, 인터페이스 기반의 설계를 통해 테스트가 용이해졌다. 각 구현체를 독립적으로 테스트할 수 있어, 예를 들어 SumCalculator와 SubtractionCalculator의 로직을 분리하여 검증할 수 있었다. 이러한 테스트의 용이성 덕분에, 설계 초기에 발견하지 못했던 일부 엣지 케이스도 빠르게 수정할 수 있었다.
세 번째로, 실제 코드 작성 과정에서 느낀 점은, 의존성 주입을 통해 구현체를 쉽게 교체할 수 있다는 점이다. 덕분에 실행 시점에 필요한 연산 방식에 따라 동적으로 구현체를 변경할 수 있는 유연한 구조를 만들 수 있었다.
이번 설계를 진행하면서 확장 가능성과 유연성을 고려한 설계가 얼마나 중요한지 다시 한번 깨달을 수 있었다. 특히, 인터페이스와 구현체를 나누는 방식이 단순히 과제를 넘어서, 실제 프로젝트에서도 얼마나 강력한 도구가 될 수 있는지를 느낄 수 있는 좋은 기회였다.
2️⃣ 사용자 편의성을 고려한 설계
이번 과제를 진행하면서 가장 신경 쓴 부분 중 하나는 사용자 편의성이었다.
기능 요구사항과 테스트 코드에서는 커스텀 구분자가 문자열의 맨 앞에 위치한 상태로 작성되어 있었다. (ex. //;\n1;2;3)
그래서 이러한 요구사항을 충족시키기 위해, 커스텀 구분자를 반드시 문자열의 맨 앞에서만 선언하도록 구현하면 훨씬 쉽게 테스트 코드를 통과할 수 있었다.
그러나 나는 다음과 같은 생각이 들었다.
사용자가 커스텀 구분자를 중간이나 끝에 위치시켰다고 해서 계산기가 정상적으로 작동하지 않는다면, 사용자 입장에서 불편하지 않을까?
내가 추구한 방향은 사용자 친화적인 계산기였다. 사용자 경험을 향상시키기 위해, 구분자가 문자열의 중간이나 마지막에 위치하더라도 이를 정확히 인식하고 계산이 정상적으로 이루어질 수 있는 구조를 설계하고 싶었다!
그래서 커스텀 구분자가 문자열의 어느 위치에 있더라도 정상적으로 인식하고 처리할 수 있는 설계를 고민했다. 이를 위해 구분자와 연산식을 명확히 분리하는 로직을 추가하고, 입력 문자열에서 커스텀 구분자를 찾아 제거한 뒤, 남은 부분을 연산식으로 추출하는 방식으로 기능을 확장했다.
결과적으로 새로운 로직을 적용한 결과, 사용자는 구분자를 반드시 앞에 작성하지 않아도 되었고, 중간이나 끝에 작성해도 계산이 정상적으로 이루어졌다. 또한 기능 요구사항을 넘어 사용자 친화적인 설계를 구현하며, 개발자로서의 책임감을 다시 한번 느낄 수 있었다.
이 과정을 통해 깨달은 점은, "단순히 시스템이 요구사항을 충족시키는 것에 그치지 않고, 사용자가 이를 얼마나 쉽게 활용할 수 있는지도 고려하는 것이 좋은 설계다" 라는 점이다.
이번 경험을 통해, 앞으로도 사용자 경험을 염두에 두며 설계를 진행해야겠다는 교훈을 얻었다. 😊
아쉬운 점
1️⃣ JDK 21의 새로운 기능을 충분히 활용하지 못함
이번 프리코스 7기에서 가장 달라진 점은 JDK 21 사용이었다.
작년에는 JDK 17을 사용하도록 요구했던 것과 달리, 이번 기수에서는 JDK 21을 사용하도록 변경되었다. 평소 스프링 부트 프로젝트에서도 JDK 17을 주로 사용해왔기에, 새로운 프로젝트를 시작할 때도 자연스럽게 JDK 17로 세팅하곤 했다. 사실 JDK 21에 어떤 기능이 추가되었는지도 모르고, 단순히 주변에서 많이 쓰는 버전이 JDK 17이었기 때문에 이를 고집했던 것 같다.
이번 기회를 통해 JDK 21의 새로운 기능을 학습하고 실제 코드에 적용해보기로 결심했다. 그래서 Virtual Threads, Sequences Collections 등 JDK 21에서 새롭게 추가된 기능들을 학습했다. 특히 Sequences Collections은 컬렉션을 다룰 때 가독성 좋은 코드를 작성할 수 있어 매우 유용하다고 느꼈고, 이를 활용해보고 싶었다.
하지만 이번 과제는 “덧셈 연산”처럼 순서가 중요하지 않은 연산을 중심으로 진행되었고, Sequences Collections을 적용할 만한 적절한 기회가 없었다. 새로운 기능을 학습하고도 이를 실제 코드에 적용하지 못했다는 점이 다소 아쉬웠다.
앞으로는 더 적합한 상황에서 이러한 기능들을 활용할 기회를 놓치지 않고 적극적으로 실험해봐야겠다는 생각이 들었다. 이번 경험을 계기로, 새로운 기술을 배우는 데서 그치지 않고 실질적으로 활용하는 데까지 이어지도록 노력해야겠다고 느꼈다!
2️⃣ 일급 컬렉션 활용의 미숙함
클린 코드를 작성하기 위해 일급 컬렉션을 최대한 활용하려고 노력했지만, 아직 익숙하지 않은 점 때문에 이를 완벽히 적용하지 못한 부분도 있었다.
일급 컬렉션은 컬렉션을 단순히 데이터의 집합으로만 보지 않고, 캡슐화를 통해 객체를 wrapping하고 관련 로직을 응집시켜 관리하는 데 유용한 설계 방식이다. 이를 통해 컬렉션 데이터를 안전하게 다루고, 객체의 책임을 명확히 할 수 있다는 점을 알고 있었지만, 실제 코드에서는 이를 잘 살리지 못한 사례들이 있었다.
다만, 구분자 관리 부분에서는 일급 컬렉션을 적용해볼 수 있었다. 예를 들어, DelimiterManager를 통해 구분자 리스트를 관리하고, 구분자 리스트로부터 정규식을 생성하는 로직을 응집시켰다. 이는 구분자 관리라는 책임을 하나의 클래스에 집중시키고, 변경이 필요할 때 해당 클래스만 수정하면 되도록 설계한 점에서 일급 컬렉션의 장점을 살린 사례라고 할 수 있다.
하지만 숫자 리스트나 다른 데이터 컬렉션에서도 일급 컬렉션을 적용할 여지가 있었음에도, 익숙한 방식에 의존하면서 이런 기회를 놓친 부분이 아쉬웠다. 앞으로는 일급 컬렉션을 더 적극적으로 활용해 설계와 코드의 명확성을 높일 수 있도록 학습과 연습을 지속해야겠다고 느꼈다. 특히, 일급 컬렉션이 가진 강점을 다양한 상황에 적용하며 실전에서 더 익숙하게 다루는 것이 목표다.
고민한 점
1️⃣ 책임의 분리 수준
위에서도 잠깐 언급했듯이, SRP(Single Responsibility Principle)를 준수하기 위해 “한 클래스는 하나의 변경 이유만 가지도록” 설계하려고 노력했다. 이를 위해 각 클래스가 한 가지의 public 메서드만 가지도록 코드를 작성했는데, 결과적으로 정보가 지나치게 파편화된 느낌을 주게 되었다.
지금 와서 생각해보면, “변경 이유”의 범위를 지나치게 세분화했던 것이 원인이었다. 예를 들어, 구분자 관리와 정규식 패턴 생성을 각각 다른 변경의 이유(책임)으로 보고 Delimiter와 DelimiterManager 클래스를 분리했지만, 이 두 작업은 사실 논리적으로 연결된 작업이었다.
“구분자 관리 및 정규식 생성”을 하나의 변경 이유로 묶는 것이 더 적절했을 것 같다. 이렇게 설계했다면, DelimiterManager 클래스 하나만으로 충분히 도메인 로직을 구현할 수 있었을 것이다.
이번 경험을 통해, 책임을 지나치게 세분화하지 않고, 논리적으로 연결된 작업들을 하나의 클래스에 묶는 것이 가독성과 유지보수성을 높이는 데 더 효과적일 수 있다는 점을 배웠다. 앞으로는 클래스의 책임을 정의할 때, 가독성과 설계의 단순성을 함께 고려하며 설계해야겠다고 다짐했다. 😊
2️⃣ 유효성 검증의 위치
유효성 검증 로직을 어디에서 처리해야 할지에 대해서도 고민이 많았다. 입력 DTO에서 처리해야 할지, InputView에서 바로 처리할지,
Validator 클래스를 분리해서 처리할지에 대해 고민을 거듭했다.
결국 고민 끝에, 다음과 같은 결론을 내렸다.
입력 단계의 유효성 검증은 DTO에서 처리한다.
비즈니스 로직과 관련된 유효성 검증은 Validator 클래스를 분리하여 처리한다.
이 결정을 내린 이유는 다음과 같다.
DTO는 데이터를 전달하는 역할을 가지고 있으며, Spring Boot에서 @NotNull, @NotBlank 같은 어노테이션을 사용해 유효성을 검증하는 구조가 자연스럽게 떠올랐다. 그래서 DTO가 입력 검증을 담당하는 것이 더 일관된 설계라고 생각했다.
반면, 비즈니스 로직에서의 검증은 Validator 클래스를 통해 분리하여 처리하는 것이 더 적합하다고 판단했다. 이유는 비즈니스 로직이 단순히 검증을 포함하기엔 이미 많은 책임을 가지고 있다고 느꼈기 때문이다. Validator를 분리함으로써 검증 로직과 비즈니스 로직이 명확히 구분되어 SRP 원칙을 지키며 설계를 할 수 있었다.
지금까지의 결론은 위와 같지만, 아직 유효성 검증 방식에 대해 저 방법이 정답이라고 확신할 수는 없다.
앞으로 프리코스를 진행하면서 다양한 상황에서 유효성 검증을 어떻게 처리하는 것이 가장 적절한지 계속 고민하고, 더 나은 방식을 찾아가고 싶다.
느낀점
1️⃣ 설계의 균형 잡기
SRP와 OCP를 지키며 설계를 진행하는 과정에서, “변경의 이유”를 지나치게 세분화하면 오히려 코드가 지나치게 파편화될 수 있다는 점을 깨달았다. 이번 경험을 통해, 논리적으로 밀접하게 연결된 작업은 하나의 변경 이유로 묶어서 처리하는 것이 더 나은 설계일 수 있다는 생각을 하게 되었다.
앞으로는 변경의 이유를 너무 세분화하지 않으면서도, 명확성과 간결성을 동시에 만족시키는 설계를 목표로 삼아야겠다고 다짐했다.
2️⃣ 협업의 가치
코드 리뷰 과정에서 팀원들로부터 다양한 피드백을 받으며 설계의 명확성과 코드의 가독성을 높이는 방법에 대해 배웠다. 특히, 내가 간과했던 부분이나 개선할 점들을 다른 시각에서 제안받으며 협업의 중요성을 다시금 느낄 수 있었다.
혼자 작성한 코드보다, 협업을 통해 다듬어진 코드가 훨씬 높은 품질을 가진다는 점을 실감했다.
3️⃣ 프리코스의 의미
이번 프리코스는 단순히 코드를 작성하는 데 그치지 않았다. 클린 코드와 확장 가능한 설계를 고민하며 스스로의 부족한 점을 깨닫고 개선하는 경험을 했다.
학습 계획을 실천하며 스터디 팀원들과 함께 성장해가는 과정에서 큰 동기부여를 얻었고, 팀원들과 함께한 노력과 피드백은 개인적인 성장뿐만 아니라 협업 능력의 중요성도 일깨워주는 값진 시간이 되었다.
'woowacourse-pre' 카테고리의 다른 글
[우테코 7기 프리코스] 3주차 회고 (0) | 2024.11.16 |
---|---|
[우테코 7기 프리코스] 2주차 회고 (0) | 2024.11.03 |