들어가며
3주차 과제를 제출한 후 꽤 시간이 지났지만 회고록을 작성해본다.
이번 3주차 과제는 “로또”였다. 이전 기수에서도 동일한 문제로 3주차 과제가 진행되었고, 이번 7기에서도 같은 미션이 주어졌다.
1주차 미션에서는 OCP(Open-Closed Principle)와 SRP(Single Responsibility Principle)를 준수한 도메인 설계에 중점을 두었다. 2주차에서는 1주차 내용에 더해 원시값 포장과 일급 컬렉션을 적용하는 데 집중했다. 이번 3주차에서는 앞선 두 주차의 내용을 바탕으로 싱글톤 패턴을 적용하는 데 중점을 두고 미션을 수행하였다.
3주차에서는 잘못된 입력 시 해당 부분부터 재입력하는 로직이 추가되었는데, InputView에서 모든 유효성 검증 로직을 처리하게 되면서 레이어의 책임에 맞게 예외 처리를 하지 못한 점이 아쉬웠다. 레이어별로 역할을 분리하지 못하고 InputView에 많은 로직이 몰린 것은 분명 개선이 필요한 부분이었다.
하지만 이러한 아쉬움이 있었기에 더 나은 코드를 작성하기 위한 고민을 할 수 있었다고 생각한다.
앞으로도 부족한 점을 인정하고, 꾸준히 개선하며 나아가는 개발자가 되기 위해 노력해야 겠다.
본격적으로 회고를 시작해보자!
미션 링크 및 클린코드 스터디 링크
https://github.com/woowacourse-precourse/java-lotto-7/pull/1217
[로또] 이동훈 미션 제출합니다. by hoonyworld · Pull Request #1217 · woowacourse-precourse/java-lotto-7
감사합니다.
github.com
https://github.com/joconaco-study/read-joconaco
GitHub - joconaco-study/read-joconaco: 좋은 코드 나쁜 코드 탐구 스터디
좋은 코드 나쁜 코드 탐구 스터디. Contribute to joconaco-study/read-joconaco development by creating an account on GitHub.
github.com
배운 점
1️⃣ Enum(열거형)은 == 나 equals() 나 내부에서 == 로 비교한다.
조코나코 스터디에서 좋은 내용을 많이 공유해주시는 길준님께서 남겨주신 코멘트이다. 길준님께서는 Enum(열거형)에서 가독성을 위해 ==를 선호하신다고 하셨다.
문자열을 비교할 때, '=='는 객체 참조의 동일성을, equals()는 객체 내용의 동등성을 비교하는 것을 해당 글을 포스팅하면서 해당 개념을 정리를 했었기 때문에, equals()를 사용하는게 안전하게 비교를 할 수 있다고 설명을 드리려고 했다. 첨부하신 아티클을 읽기 전에는 말이다...
첨부해주신 아티클을 읽고 나니, Enum의 경우 equals() 메서드 내부적으로 ==로 비교를 하고 있다는 사실을 알게 되었다.
이는 Enum이 본질적으로 단일 인스턴스(Singleton)를 보장하기 때문에 가능한 것이였다. 따라서 Enum을 비교할 때는 equals()를 사용하는 대신, ==를 사용하는 것이 더 직관적이고 가독성을 높일 수 있다는 길준님의 의견에 공감할 수 있었다.
추가적으로 ==를 사용했을 때, 두 가지 장점도 존재했는데 이는 다음과 같다.
== 비교는 NullPointerException을 발생시키지 않는다.
== 연산자는 단순히 두 객체의 메모리 참조를 비교하므로, null과 비교할 때도 예외를 발생시키지 않는다. 반면 equals() 메서드는 호출된 객체가 null일 경우 NullPointerException을 던진다.
== 비교는 컴파일 타임에 타입 호환성 검사를 지원한다.
== 연산자는 컴파일 시점에 양쪽 피연산자의 타입을 확인하여 타입 호환성을 검사해서 서로 호환되지 않으면 컴파일 에러를 발생시킨다. 반면, equals() 메서드는 런타임에 타입이 다르면 false를 반환하지만, 컴파일 시점에서는 타입 오류를 감지하지 못한다.
길준님의 답변을 통해 새로운 관점을 배우게 되었고, 이번 경험은 문자열 및 Enum 비교에 대한 이해를 더욱 깊게 만들어 주는 계기가 되었다.
2️⃣ double 대신 BigDecimal을 사용하면 정밀도를 높여줄 수 있다.
길준님께서 또 한 번 유익한 인사이트를 주셨다.
공유해주신 아티클에서는 double과 BigDecimal의 차이를 설명하고 있었는데, 핵심은 다음과 같았다.
double은 부동소수점 연산으로 인해 정밀도 손실이 발생할 수 있는 반면, BigDecimal은 고정소수점 방식을 사용하여 높은 정밀도를 제공한다는 점이다.
이유는 double이 64비트 부동소수점 표현(IEEE 754 표준)을 사용하여 실수를 나타내기 때문이다. 부동소수점은 실수를 이진수로 표현하므로, 일부 10진수를 정확히 표현하지 못하는 경우가 발생한다.
반면, BigDecimal은 실수값을 10진법 그대로 표현하기 때문에, 부동소수점 연산에서 흔히 발생하는 정밀도 손실을 방지할 수 있다.
이번 기회를 통해 double과 BigDecimal의 차이를 더 명확히 이해할 수 있었다.
3️⃣ 서비스 레이어에 domain 모델을 compositon 하면 강결합이 발생한다.
해당 부분은, 길준님께서 주신 의견 중 정말 인상 깊었던 부분이다.
당시 나는 Service 계층에서 WinningResult 객체를 Composition하는 것이 Spring Boot에서 Repository를 통해 도메인 객체를 가져와 도메인 메서드를 호출하는 과정과 유사하다고 생각했었다. 그래서 서비스 계층에서 WinningResult를 Composition하는 방식을 선택했는데, 이로 인해 발생하는 “서비스 계층과 도메인 모델 간의 강결합” 문제를 간과했었다.
돌이켜보니, Spring Boot에서도 Service 레이어는 Repository만 Composition하며, Repository를 통해 특정 도메인을 가져와 도메인의 비즈니스 로직을 수행한다. 이는 서비스 계층이 도메인 자체에 직접 의존하는 것이 아니라, Repository를 통해 도메인을 가져와 협력하게 만드는 방식이라는 것을 깨달았다.
길준님 말씀처럼, 서비스 계층의 역할은 도메인 간의 협력을 중재하는 것이기 때문에 특정 도메인과 강결합을 이루는 것은 지양해야 한다고 생각되었다. 따라서, WinningResult 객체를 서비스 내부 상태로 유지하기보다는 지역 변수로 관리하거나 매개변수로 주입받는 방식이 더 적합하다는 결론에 도달했다. 이 과정을 통해 서비스와 도메인의 결합도를 낮추고, 서비스 계층의 역할을 더욱 명확히 할 수 있었다.
길준님의 통찰력 덕분에 다시 한번 내 코드를 돌아보고 개선점을 찾을 수 있었다. 앞으로도 이런 관점을 지속적으로 고민하며 발전해나가고 싶다. 😊
아쉬운 점
유효성 검증을 각 책임에 맞게 분배하지 못했다.
같이 "우테코 백엔드 스터디"에 참여 중인 건형님과 장순님이 주신 의견이다.
미션 구현 당시에는 재입력을 받아야 하기 때문에 검증 로직을 외부 Validator에서 대부분 처리하고, 이를 통해 안전하게 DTO나 도메인 객체를 생성하면 충분하다고 생각했었다. 그러나 작업을 진행하면서 이 방식이 불러오는 문제점들을 깨닫게 되었다.
입력값을 검증하는 Validator와 도메인 객체 내부에서 수행해야 할 무결성 검증 사이의 경계가 모호해지면서, 중복된 검증 로직이 발생한다.
문제 발생 시 재입력을 받을 수 있도록 하기 위해, InputView에서 Validator의 static 메서드를 호출하여 사용하였다.
이 방식은 로직이 간단하고, InputView에서 이미 검증된 데이터를 도메인 객체로 넘겨주기 때문에 외부에서 잘못된 데이터가 도메인 객체로 전달될 가능성을 줄일 수 있다는 점에서 좋은 설계라고 생각했다.
그러나 실제로 구현하면서 몇 가지 문제가 발생했다.
먼저, VO(Value Object)에서 값의 유효성을 보장하기 위해 VO 클래스 내부에서도 동일한 검증 로직을 중복해서 처리해야 했다.
또한, 입력값을 Integer로 변환하기 위해 Parser를 사용하는 과정에서도 비효율이 발생했다. NumberFormatException을 처리하기 위해 InputView의 유효성 검증 메서드에서 이미 Integer.parseInt를 수행했음에도 불구하고, 이후 로직에서 다시 Parser를 호출하여 동일한 형변환을 반복해야 했다. 이로 인해 동일한 데이터에 대해 두 번의 형변환 작업이 발생하게 되어 성능적으로도 불필요한 비용이 추가되게 되었다.
도메인 객체에 적절한 검증이 없으면 외부에서 잘못된 데이터를 삽입할 가능성이 존재한다.
Validator를 InputView에서 호출하여 유효성 검증을 처리했기 때문에, 검증이 완료된 데이터를 도메인 객체로 안전하게 넘길 수 있다고 생각했다. 하지만, 이 방식은 단독으로 개발할 때는 문제가 없을 수 있지만, 협업 환경에서는 큰 문제를 야기할 가능성이 있다는 사실을 알게되었다.
예를 들어, 도메인 객체의 유효성 검증을 InputView의 Validator에서 이미 처리했으니 도메인 내부에는 검증 로직이 필요 없다고 판단했다고 가정하자. 이런 경우, 다른 개발자가 해당 도메인 객체를 사용할 때 유효성 검증이 없는 상태로 데이터를 생성하거나 수정할 수 있다. 이로 인해, 잘못된 값이 도메인 객체에 삽입되더라도 이를 막을 방법이 없게 된다.
도메인 객체는 시스템의 무결성을 보장해야 하는 중요한 역할을 수행한다. 따라서 외부에서 유효성 검증을 거쳤다고 하더라도, 도메인 객체 자체적으로 데이터를 검증하여 항상 유효한 상태를 유지해야 한다. 협업 환경에서는 특히 도메인 객체 내부에서의 검증이 필수적이며, 이를 통해 데이터의 신뢰성을 보장할 수 있다.
이번 경험을 통해 입력값 검증과 도메인 객체의 무결성 보장이라는 두 가지 중요한 요소를 어떻게 분리하고 관리해야 하는지 깊이 고민해볼 수 있었다.
처음에는 InputView에서 Validator를 통해 유효성을 검증하고, 검증된 데이터를 도메인 객체로 넘기는 방식이 간단하면서도 효과적이라고 생각했다. 실제로 단독으로 개발하는 환경에서는 이 방식이 문제없이 작동했지만, 협업 환경이나 확장성을 고려했을 때는 여러 단점이 드러났다.
또한, 이번 경험을 통해 검증 로직의 책임을 명확히 분리하고, 중복을 최소화하며, 도메인 객체가 스스로 무결성을 보장할 수 있는 구조를 설계하는 것이 얼마나 중요한지 깨달을 수 있었다. 특히, 다음 미션에서는 재입력 로직을 Supplier를 활용해 에러를 잡도록 설계하고, 문제가 발생한 부분부터 다시 시작할 수 있도록 구현해야겠다는 생각이 들었다.
이 방식은 “우테코 백엔드 스터디”에서 버핑님이 사용하신 방법으로, 유효성 검증을 InputView 내에서만 처리하지 않아도 각 레이어의 책임에 맞게 설계하면서 재입력을 처리할 수 있는 점이 인상 깊었다. 이를 통해 에러 처리를 더 유연하고 효율적으로 관리할 수 있을 뿐만 아니라, 재입력 로직 또한 간결하게 유지할 수 있을 것으로 기대된다.
고민한 점
싱글톤 패턴의 이해와 구현 방식
‘자바의 정석’에서 싱글톤 패턴을 학습하며 기본적인 개념을 익혔지만, 책에서 제공하는 내용만으로는 싱글톤 패턴에 대한 충분한 이해를 얻기가 어려웠다. 이를 보완하기 위해 추가적으로 학습을 진행하며, 대부분의 자료에서 소개하는 lazy initialization(지연 초기화) 싱글톤 방식이 멀티 쓰레드 환경에서 if 조건 평가와 인스턴스 생성이 동시에 진행될 경우, 두 개의 인스턴스가 생성되는 동기화 문제를 초래한다는 것을 알게 되었다.
확장 가능한 설계를 위해, 멀티 쓰레드 환경에서도 안전한 싱글톤 방식을 사용하는 것이 필요하다고 판단했고 이에 대해 깊이 공부했다. 그 결과, Enum 기반 싱글톤과 Bill Pugh 싱글톤 같은 대안적인 구현 방식을 학습할 수 있었다.
💡 Enum 기반 싱글톤
자바의 Enum 클래스가 클래스 로딩 시점에 인스턴스를 생성하고, JVM이 이를 보장하므로 멀티 쓰레드 환경에서도 안전하게 사용할 수 있다. 이 방식은 간결하고, 추가적인 동기화가 필요 없다는 점에서 큰 장점을 가진다.
💡 Bill Pugh 싱글톤
static inner class를 활용한 방식으로, 클래스가 처음 호출될 때 한 번만 인스턴스를 생성하는 지연 초기화 방식을 안전하게 구현할 수 있다. 이 방식은 클래스 로딩과 초기화 과정을 분리하여 메모리 낭비를 줄이고 초기화 비용을 최적화할 수 있다는 점에서 매우 효율적이다.
이번 “로또” 미션에서는 외부 의존성을 관리하는 AppConfig 클래스에 Bill Pugh 싱글톤 방식을 도입하였다. 이 방식을 선택한 이유는AppConfig는 설정을 담당하는 클래스이기 때문에, 싱글톤으로 관리하여 애플리케이션의 모든 컴포넌트가 동일한 설정 정보를 공유하도록 만들어야 한다고 생각하였기 때문이다. 해당 싱글톤 방식을 적용함으로서 애플리케이션 전역에서 동일한 설정 정보를 참조함으로써 일관성을 유지할 수 있었고, 멀티 쓰레드 환경에서도 항상 동일한 인스턴스를 제공하여 안정적인 동작을 보장할 수 있게 되었다.
이번 학습을 통해 싱글톤 패턴의 다양한 구현 방식과 각 방식의 장단점을 비교하며, 상황에 맞는 설계를 선택하는 것이 얼마나 중요한지 깨달았다. 앞으로도 멀티 쓰레드 환경을 고려한 안전한 설계와 확장 가능한 구조를 목표로 계속 발전해 나가고자 한다. 😊
느낀점
늘어나는 요구사항과 제약조건 속에서의 설계
이번 미션을 진행하며, 점점 늘어나는 요구사항과 제약조건 속에서 코드를 작성하는 것이 쉽지 않다는 점을 실감했다. 특히, 추가되는 조건들이 기존 코드와 충돌하거나 예상치 못한 영향을 미칠 수 있다는 것을 깨닫고, 확장 가능하고 유지보수하기 좋은 코드를 작성하는 것이 얼마나 중요한지 다시 한 번 느꼈다.
이러한 경험은 실제 회사 환경에서도 빈번히 마주칠 수 있는 상황일 것이기에 더욱 의미 있게 다가왔고, 최대한 모든 요구사항을 반영하면서도, 코드의 확장성과 유연성을 유지하는 것이 얼마나 중요한지 깨닫게 되었다. 😊
'woowacourse-pre' 카테고리의 다른 글
[우테코 7기 프리코스] 2주차 회고 (0) | 2024.11.03 |
---|---|
[우테코 7기 프리코스] 1주차 회고 (0) | 2024.10.26 |