외부활동/우아한테크코스 [프리코스]

[우아한테크코스 7기] 프리코스 3주차 회고

softmoca__ 2024. 11. 5. 13:51
목차

https://github.com/woowacourse-precourse/java-lotto-7/pull/56

 

[로또] 최영철 미션 제출합니다. by softmoca · Pull Request #56 · woowacourse-precourse/java-lotto-7

제 코드를 보려와주셔서 감사합니다. ☺️ 제 코드를 읽으시면서 가독성이 떨어지시거나 코드 및 PR 커멘트의 의도를 파악하기 힘드셨던 부분은 강하게 피드백 해주시면 감사하겠습니다 ! 또 한

github.com

 

 

 

시작하며 

 

프리코스에 몰입한지 벌써 3주가 지났다. 정말 체감상 3일간 하나의 학교 과제 혹은 프로젝트의 하나의 기능을 구현하는것과 같이 짧게 느껴졌다..! 이제 앞으로 1번의 미션 밖에 남지 않았다니 뭔가 아쉽다..! 프리코스가 4주가 아니라 우아한테크캠프와 같은 7주였으면 좋겠다고 생각하며 3주차 회고를 시작해본다.

 

이번 3주차 미션을 진행하며 가장 폭발적으로 성장을 했다고 생각한다.  지난 2개의 미션을 진행하며 고민한 점과 학습한 내용을 온전히 3주차 미션에 적용해 보았다. 그 과정 중 가장 큰 변화와 성장은 단연코 '테스트코드'의 위력이었다.

부끄럽게도 프리코스를 접하기 이전의 나는 테스트코드와 정말 안친한 개발자였다. 테스트코드 보다는 빠른 손과 타이핑으로 포스트맨과 친한.. 늘 학교 수업과 여러 강의를 통해 테스트 코드의 이론적 중요함에 대해 인지를 하고 있음에도 늘 테스트코드 작성을 피해왔다.

하지만 프리코스 1주차에 매번 프로그램을 실행시켜 콘솔에 입력값을 넣고 출력하는게 귀찮아서 처음 작성해본 테스트코드 부터, 2주차 미션을 진행하며 처음 작성해본 객체지향적인 프로그래밍을 통해 만든 객체들의 행동들이 잘 작동하는지 확인하기 위해 작성해본 테스트코드를 작성하며 점차 친해지기 시작했다.

 

그리고 결국 이번 3주차 미션을 진행하며 온몸을 다해 테스트코드의 위력을 느끼게되었다.

3주차에는 구현해야하는 기능이 많았고, 고려해야하는 프로그래밍 요구 사항이 더욱 많았다. 그래서 새로운 기능을 추가하고, 2주간 코드리뷰와 학습을 통해 배운 내용을 적용하며 리팩토링을 많이 하게 되었다.

그 과정에서 새로운 기능과 리팩토링을 하며 이전에 작동했던 로직들에 버그가 발생한게 아닌가 라는 불안감을 테스트코드를 작성하며 해소할수 있었다. 그와 더불어 당연하게 작업 시간을 줄여 효율적으로 새로운 내용들을 적용할 수 있으며 테스트 코드를 작성하며 놓친 기능명세를 발견하며 새로운 도메인 지식을 습득할 수 있었다. 

또한 객체지향적인 사고와 설계를 통해  프로그래밍을 하는 과정에서 객체지향의 특성을 활용한 기술들이 뭉쳐 더욱 강력한 효과를 만들어 내며 더욱 객체지향 특성을 잘 이해하고 더욱 잘 지킬수 있음을 느꼈다 !

그로인해 현재 진행하고 있는 프로젝트와 이후에 진행할 프로젝트에 하루 빨리 테스트 코드를 작성하며 진행해보고 싶다는 욕심이 생겼다.

프리코스를 통해 '테스트코드'의 중요성을 몸소 느꼈다는것 하나 만으로 나는 정말 더 바랄것이 없을 정도이다 !(물론 1차에 합격해서 최종테스트코드를 친다면 더더더더욱 좋을꺼같다..ㅎㅎ)

 

3주차 미션을 진행하며 너무 즐겁고 폭발적인 성장을 경험하여 서론이 길었다 !
본격적으로 회고를 시작해보자 !

 

고민했던 것 

1️⃣   상수 관리

1-1 상수 관리 Enum을 꼭 사용해야 할까 ?

그저 상수라고 enum을 사용하는 것 보다는 등급이나 계급과 같은 서로 연관성이 있는 상수들과 비교 등의 작업을 수행할 가능성이 높은 상수에 대해서면 enum을 사용하는것이 좋다고 생각하여 3주차 미션에서는 메세지 관리에 enum을 사용하지 않았다.

그 주 이유로 Enum의 장점으로 타입안정성, 리팩토링 용이성, 기능확장 등이 있지만 그저 getMesage()와 같은 추가 메서드 호출로 인해 사용 구문이 길어진다는 점이 메시지를 다루는 로직에서는 장점 보다는 가독성 저하의 단점이 더 크게 느껴졌다.
반면에 당첨 금액의 등급을 나타내는 로직에는 Enum이 너무 효과적이라고 생각했다.
당첨등급과 일치하는 개수, 부연설명과 같이 서로 연관된 데이터와 당첨등급을 결정하는 비즈니스 로직을 하나로 묶어 캡슐화을 통해 관리한다는 점이 너무 효과적이라고 생각했다.
또한 EnumMap을 통해 HashMap보다 빠른 성능과 적은 메모리를 사용하며 출력 형태 또한 Enum에 정의한 순서를 보장해줘서 출력 로직에 신경을 쓰지 않아도 된다는 점과 함께 큰 시너지를 느꼈다.

 

1-2 상수 관리는 global하게 전역적으로 ?! 그냥 해당 도메인에 ?!

 

저는 이전 2주차 미션을 진행하며 상수는 모두 global패키지에서 전역적으로 관리하게 구현하였다.
상수의 네이밍을 통해 무슨 역할을 하는지 알수 있으므로 수정과 가독성, 유지보수에 문제가 없다고 생각했다.

하지만 무조건 global하게 전역적으로 관리하는것 보다는 3주차 로또 미션을 구현하며 각자의 쓰임새 문맥에 맞게 관리하는 것이 좋다는 생각이 들어 global패키지와 도메인 내부 두곳에서 관리하였다.
에러메세지, 입출력 안내 메세지 같은 경우는 전역적으로 메세지를 중앙 집중화로 관리해 일관된 메세지 형식을 유지하는것이 더 중요하다고 생각했다. 도메인 내에서만 주로 사용되는 상수는 관련 도메인 로직과 함께 관리해서 응집도를 향상시키며 도메인의 변경사항 추적에 용이함을 챙길수 있는것이 좋다고 생각했다.

 

3주차 미션을 진행하며 위와같은 고민을 하였고 나만의 근거를 통해 구현을 하였다.

단일책임 원칙을 다소 위반하는 나의 의견에 대해 리뷰어 분들은 상수와 Enum에 대해 어떻게 생각하는지 궁금하여 PR 커멘트를 통해 코드 리뷰를 요청하였다.

 

📝 리뷰어분들의 의견 1

1-1 상수 관리 Enum을 꼭 사용해야 할까 ?

저도 불필요하다고 생각해요. 단순 getter 이외에 추가적인 기능을 가지고 있지 않기 때문이에요.

1-2 상수 관리는 global하게 전역적으로 ?! 그냥 해당 도메인에 ?!

(에러 메시지를 제외하고)1차 적으로 도메인엔서 관리하는게 깔끔하다고 생각해요.
그리고 단일책임 원칙을 위반하기 보다는 보는 시야의 범위가 다르다고 생각해요.

 

의견 1을 주신 리뷰어분은 1번에 대해는 나와 같은 의견이었지만 2번에 대해 '시야'라는 새로운 논점을 가지게 되었고, 대면 코드 리뷰를 통해 논의한 내용을 아래정리해 보았다.

 

책임의 관점

단일 책임 원칙(SRP)은 하나의 클래스나 모듈이 단 하나의 변경 이유만 가져야 한다는 원칙이다.

상수를 글로벌하게 관리하면 중앙 집중화로 인해 상수 변경이 여러 도메인에 영향을 미치며, 이를 관리하는 클래스가 너무 많은 책임을 가지게 될 위험이 있다. 반면, 도메인 내부에서 상수를 관리하면 해당 도메인의 변경사항에만 국한되므로, 클래스의 책임 범위가 명확하고 응집도가 높아진다.

 

 

시야의 관점

글로벌 상수 관리는 모든 도메인에서 사용할 수 있는 공용 자원의 역할을 하며 이는 재사용성과 일관성을 높이는 장점이 있지만, 모든 도메인과 관련된 정보를 포함하면서 지나치게 포괄적이고 비대해질 위험이 있다.

도메인 내부에서 상수를 관리하면 로컬 문맥에 맞는 정보만을 포함하게 되어, 해당 도메인의 변경이나 로직 추적이 더 용이하고, 클래스 간의 결합도가 낮아진다.

 

상수 관리에서의 책임과 시야

글로벌 상수는 에러 메시지, 공통 포맷, 시스템 전반에서 통용되는 값처럼 여러 도메인에 걸쳐 일관성을 유지해야 하는 경우에 적합하며 이는 프로젝트 전반의 시야에서 바라보는 것이다.

도메인 내 상수는 해당 도메인에만 국한된 비즈니스 로직 관련 값이나 상수를 다루는 데 적합하며, 이는 특정 도메인의 책임과 문맥을 유지하기 위함이다.

따라서 "책임" 관점에서는 도메인 내 상수 관리가 SRP를 지키고 응집도를 높이는 방향이며, "시야" 관점에서는 글로벌 상수 관리가 프로젝트 전반의 일관성을 유지하는 데 유리하다.

 

논의의 결론

책임에 맞는 시야 설정: 상수를 사용하는 문맥과 변경 이유를 기준으로 책임과 시야를 조율해야 한다.

전역(global): 에러 메시지, 공통 텍스트 포맷 등 시스템 전반의 일관성이 중요한 경우.

도메인 내부: 특정 도메인에 한정된 비즈니스 로직에서만 사용되는 상수는 도메인 내부에 배치하여 응집도와 변경 용이성을 높임.

 

이를 통해 단일 책임 원칙을 위반하지 않으면서도, 시야와 범위를 명확히 나눠 유지보수성과 가독성을 동시에 확보할 수 있다.

 

 

 

📝 리뷰어분들의 의견 2

1-1 상수 관리 Enum을 꼭 사용해야 할까 ?

단순히 하나의 값을 나타내고자 할 때는 final class를 사용하는 것이 적합하고, 
여러 개의 다른 값을 하나의 의미로 묶어서 표현하고자 할 때는 enum을 적용하는 것이 적합한 것 같다고 생각합니다.
이유 : 코드 리뷰를 받고, 1주차 문자열 계산기 코드를 구분자를 enum으로 리팩토링 했었습니다. 
COMMA(",")와 같은 형식으로 상수를 선언했는데, 단순히 value만 관리하는 final class에 비해 장점을 느끼지 못했습니다. 
오히려 정의해야 하는 생성자와 getter만 추가되었고, 기존 final class가 코드의 간결성과 명확성이 더 높다고 느껴졌습니다. 

타입체크의 안정성은 좋은 기능이라고 생각되는데, 굳이 왜 enum을 써야할까? 라는 고민이 들었었습니다. 
그런데 한 블로그에서 차를 관리하는 열거형에 아우디라는 상수를 선언하고 내부 value를 
A4, A6, A8으로 정의하는 하는 것을 보며 "아! 모델명이 달라도 아우디라는 차량임은 같으니깐 
저렇게 연관된 값을 관리하면 가독성이 좋겠다"는 생각이 들었었습니다.

 

1-2 상수 관리는 global하게 전역적으로 ?! 그냥 해당 도메인에 ?!

이 부분은 취향차이 인 것 같아요! 저도 해당 도메인에 상수를 선언하는 것을 선호합니다.
global하게 전역적으로 사용이 되어도, 1번의 depth를 타고 상수 클래스로 넘어가는게 불편하다고 느꼈습니다. 
그래서 다른 클래스에서 사용이 되어도 중복으로 도메인에 상수를 선언하는 편입니다 ㅎㅎ

근데 "전역적으로 일괄 적용을 하는 상수를 이용을 하면 좋을 것 같다" 라는 생각이 든 부분이 한가지 있습니다! 
그 점은 밑에서 예를 들어 볼게요!

예를 들어, 지금은 우테코 7기 이지만 내년이면 우테코 8기가 시작이 됩니다. 
도메인마다 7기 상수를 관리했으면 도메인마다 찾아가서 수정을 해야 하기에 불편함이 있지만,
전역적으로 관리를 했으면 한 번만 전역 상수의 숫자를 바꾸면 모든 클래스에 전파가 되어서 편리할 것 같아요!

의견 2을 주신 리뷰어분은   더욱 자세한 근거와 예시를 통해 대체로 나의 의견과 비슷하게 생각하였다.

확실히 실제 예시를 통해 의견과 근거를 알게되니 설득력이 상당한것 같다 !

 

2️⃣  메서드 추출을 통한 가독성 향상

하나의 메서드가 하나의 일을 할 수 있게 함과 동시에 if문의 조건자체도 메서드로 추출을 하면 로직의 이해 필요없이 그저 메서드명을 통해 무슨 작업을 하는지 추상화 시킬수 있다고 생각해서 if문의 대부분의 조건들을 모두 메서드로 추출하였다.
이를 통해 가독성은 정말 많이 올라감을 느꼈지만 메서드 호출 오버헤드가 누적되어 성능에 악영향을 끼치거나 과도한 추상화로인해 디버깅이 어려워지고 오히려 코드 복작도가 올라갈수 있겠다고 생각했다.
하지만 정말 간단한 조건 이외에 가독성에 도움이 되며 추상화가 과하지 않는 선에서 메서드로 추출을 하기 위해 노력했다.

public void calculateResult(Lottos lottos, WinningNumbers winningNumbers) {
    for (Lotto lotto : lottos.getLottos()) {
        int matchCount = winningNumbers.countMatchNumbers(lotto);
        boolean matchBonus = winningNumbers.matchBonus(lotto);
        WinningPrize winningPrize = WinningPrize.valueOf(matchCount, matchBonus);

        if (winningPrize != WinningPrize.NONE_PRIZE) {
            results.put(winningPrize, results.get(winningPrize) + 1);
        }
    }
}

위와 같은 메서드를 아래와 같이 추출해보았다.

분명 처음 위와같은 메서드를 추출하는 과정에서

하나하나의 기능을 모두 메서드로 추출하며, if문과 같은 조건을 코드를 읽고 이해하지 않고 메서드명을 통해 무슨 조건을 검사하는지 알 수 있게 하며 '코드를 읽는 것'이 아닌, 그저 '소설'과같은 줄글을 읽는 느낌을 가질 수 있었다.

 

하지만  미션 제출 이전 최종 점검을 하는 과정에서 너무 과도하게 추상화를 해서 오히려 가독성을 떨어뜨린게 아닐까 ?라는 의문이 생겨 제 3자들인 스터디원들에게 해당 고민에 대해 코드 리뷰를 요청하였다.

public void calculateResult(Lottos lottos, WinningNumbers winningNumbers) {
        for (Lotto lotto : lottos.getLottos()) {
            WinningPrize winningPrize = calculateWinningPrize(lotto, winningNumbers);
            addValidResult(winningPrize);
        }
    }
    
private void addValidResult(WinningPrize winningPrize) {
    if (isWinningPrize(winningPrize)) {
        updateWinningResult(winningPrize);
    }
}

private WinningPrize calculateWinningPrize(Lotto lotto, WinningNumbers winningNumbers) {
    int matchCount = winningNumbers.countMatchNumbers(lotto);
    boolean matchBonus = winningNumbers.matchBonus(lotto);
    return WinningPrize.valueOf(matchCount, matchBonus);
}

private boolean isWinningPrize(WinningPrize winningPrize) {
    return winningPrize != WinningPrize.NONE_PRIZE;
}

private void updateWinningResult(WinningPrize winningPrize) {
    results.put(winningPrize, results.get(winningPrize) + 1);
}

📝 리뷰어분들의 의견 1

1 depth가 베스트라고 생각은하는데, 최대 2 depth 까지도 괜찮다고 생각합니다! 
오히려 수직 움직임이 많으면 가독성을 헤치는 것 같아요.

📝 리뷰어분들의 의견 2

메서드 추출 != 가독성 향상 이라고 생각을 합니다!
너무 세분화해서 분리를 하면 오히려 비즈니스 로직이 한눈에 들어오지 않는 상황을 겪어 봤기 때문입니다 ㅎㅎ
그래서 적절히 메서드를 분리하는 것도 개발자의 역량이라고 생각이 드네요!

if문을 private 메서드로 추출하는 것은 가독성 면에서 좋다고 생각합니다. 
그런데 말씀해주신 오버헤드 문제도 발생을 할 수 있지만, 하드웨어 성능이 점점 좋아지기에 
해당 이슈가 발생할 확률은 낮다고 생각이 듭니다 . 
그래서 이 부분은 "가독성을 챙기고 오버헤드는 나중에 고려를 해야한다"고 생각이 듭니다. 
처음부터 오버헤드를 걱정하여 메서드를 분리하는 것을 망설인다면, 추후에 성능에 문제가 없어 분리를 하려고 해도,
까먹거나 이미 작업이 많이 진행되어 코드에 손을 대기 망설이는 순간이 올 수도 있을 것 같다고 생각합니다.

리뷰어분들의 의견은 대체로 너무 과도한 메서드 추출로 만들어진 추상화는 가독성을 헤친다는 의견이다.

하지만 역시 정해진 정답이 있다기 보다는 문맥에 맞게 '적당하게' 메서드를 추출하며 추상화를 하는게 베스트라고 생각을 하셨다.

'적당히'라는 단어는 정말 어려우면서 중요한 단어같다. 
이러한 '적당히'를 이루기 위해서는 역시 많은 경험과 고민을 통해 나만의 근거를 찾는 과정이 필요하다고 생각된다.

3️⃣  원시값 포장을 넘어 VO ?!

‘원시값 포장’이라는 개념에 대해 알게 되어 적용해 보는 과정에서 ‘원시값 포장’은 VO의 부분집합 개념이니 VO로 만드는것이 좋지않을까 ? 라는 고민을 해보았고 현재 NO라는 답변을 가지게 되었다.

 

그 이유로, 내가 구현한 Money, BonusNumber의 경우 비교 로직이 없어 equals/hashCode를 재정의 할 필요성이 없었기 때문에 오히려 핵심 기능(유효성 검증, 생성 시그니처 책임 등등 )에 집중해서 명확한 책임과 의도를 보여주는 것이 더 큰 장점이라고 생각했기 때문이다. 무조건 더 많은 기능을 제공하며 확장가능성을 염두해 보는것 보다는 현재의 설계에 맞는 역할을 가질 수 있는 것이 더 알맞다고 생각 했다. 분명 VO가 더욱 많은 기능을 제공하며 유지보수성과 확장성을 챙길 수 있지만 내가 구상하고 설계한 프로그램의 설계에는 오히려 독이라고 생각하였다.  이에 대해 나의 생각과 근거에 대한 다른 분들의 의견이 궁금하여 PR 커멘트를 통해 리뷰어분들의 의견과 해당 의견에 대한 근거들을 요청하였다.

 

4️⃣  출력과 검증의 책임의 기준

출력의 책임

출력 로직을 domain에서 ToString을 통해 구현할지 View에서 출력 로직을 책임질지에 대한 고민을 하였다.
그리고 우선 저는 MVC 계층에 맞게 출력에 관한 로직은 우선 View에 구현을 했다.
로또의 번호가 정렬되어 출력되는 로직을 View에서 구현할 경우 도메인 객체인 Lotto가 출력 형식에 대해 몰라도 되어 관심사 불리가 잘 이루어져 출력 형식이 바뀔 경우 View에서 유연하게 처리가 가능할꺼라 생각 했다. 또한 출력 요구 사항이 변경될 경우 domain을 수정하는것이 View를 수정하는것 보다 영향이 더 클꺼라고 생각했다.

하지만 무조건 View에서 출력을 책임지면 장점만 있는것이 아니라 매번 정렬을 위한 새로운 리스트를 생성해야하며, 메모리가 소비된다는 단점이 있었다. 하지만 두 장단점을 비교해본 결과 아무래도 약간의 성능을 위해 이후의 큰 유지보수 빚을 지게 되는 것과 domain 보다는 View에서 요구사항 변경 가능성이 크기 때문에 View가 출력 책임을 가지게 했다.

 

검증의 책임

검증 또한 특정 하나의 계층에서 책임을 담당 하는것이 아닌 나만의 기준을 통해 ‘입력값 검증’과 ‘도메인 검증’으로 나누어 책임을 배정했다.

‘입력값 검증’의 경우 사용자가 정말 실수로 잘못 입력했을 경우 빈값, 횟수(양의정수)를 입력해야 하지만 다른 문자를 입력한 경우, 음수를 입력한 경우 혹은 입력 형식을 지키지 않은 경우에 대한 검증을 진행하게 했다.
‘도메인 검증’의 경우 기능 명세서의 문맥을 통해 파악한 로또의 숫자가 1-45사이인지, 로또 하나의 숫자는 6개인지, 하나의 로또에 중복된 숫자가 있는지, 보너스 숫자가 당첨 로또의 숫자와 중복이 되는지, 로또 하나에 중복된 숫자가 있는지 등등 도메인의 조건을 검증 파악하도록 하여 이후 요구사항이 변경될 경우 하나의 domain내부에서 수정을 할수 있다는 장점이 있다고 생각다.

 

위와 같이 특정 큰 기능들의 책임 기준에 대해 3주차 미션을 진행하며 많은 고민을 하였다.

SRP에서 말하는 "객체는 '하나의 책임'만 가져야한다." 에서의 책임을 어디서 부터 어디까지로 잡아야하는지가 혼란스럽다.

정적팩토리 메서드, VO를 통해 객체 내부에서 생성자로 생성하기 이전의 검증 또한 검증 클래스를 거쳐야 하는가 ?

MVC 패턴에서 View 계층에서는 그저 전달받은 데이터를 출력만 해야하는가 ? 그렇다면 객체에서 ToString으로 재정의 하게된다면 출력에 대한 책임은 그저 객체가 가지고 있어도 되는것인가 ?

와 같은 의문이 머리속에 매번 멤돌았고 이에 대한 다른 리뷰어분들의 의견이 너무 궁금해 PR의 커멘트를 통해 코드 리뷰를 요청하였다.

 

📝 리뷰어분들의 의견 1
출력의 책임

view에는 정말 정말 최소한의 로직만 들어가야 한다고 생각해요. (예를들어 for문, if문)
왜냐면 비즈니스 로직이 view layer에 유출된다면 비즈니스 로직 수정 시 view도 건드려야 하기 때문이에요.
정말 최악의 상황에 해당 view를 더 이상 쓸 수 없다면 어떻게 될까요? 
이 경우 모든 로직을 새로운 view에 이식해줘야 하는 사태가 벌어진다고 생각해요. 
그러면 실수가 생길수도 있기 때문에 안전하지 못한 방법이라고 생각해요.

그리고 toString()을 가능하면 재정의 하는게 좋다고 생각해요! 
해당 도메인의 메타데이터를 문자열의 형태로 표현하는 것도 도메인이 가져야할 몫이라고 생각하기 때문이에요. 
어떻게 생각하면 이것도 가벼운 도메인 로직이기 때문에 view에 위치하는 건 적합하지 않다고 생각합니다!

Veiw를 더이상 쓸 수 없게 되다는 상황예시를 들으니 확 이해가 되며 바로 설득이 되었다 !


검증의 책임

사실 이건 어디서 하든, 해당 위치의 역할에 맞게 제대로만 하면 된다. 고 생각합니다.
스프링 부트도 security, aop, dto, domain, service 이렇게 뭐 다양한 곳에서 검증을 할 수 있는데요.

security는 보안 검증
aop는 데이터 복호화 검증
dto는 null 검증, 이메일 형식 검증 등등
domain은 dto에서 하는 검증 + 비즈니스 검증
service는 도메인 협력 과정에서 필요한 검증
여기서 domain에서 왜 DTO가 했던 검증을 다시하지? 라고 생각하실 수 있습니다.
저는 이 부분을 TC랑 연결해서 이야기 하고 싶은데요.

만약 domain에 null 검증이 없다면 Exception이 터지게 되죠. 
혹은 뒤에 연결된 도메인 로직이 정상적으로 수행되지 않을 것이라고 봅니다. 
즉, Null Safe하지 않은 도메인 모델이 된다고 생각합니다.

Lotto lotto = lotto.of(null);
lotto.has(...)    //🔥Exception
물론 검증할 소스가 많아지면 별도의 패키지로 분리를 고려해봐야 겠지만,
그래도 null 체크는 domain이 가져가는게 맞다고 생각합니다.

결과적으로, 저는 프리코스기간 동안엔 도메인에서 대부분의 검증을 처리하고 있습니다.

Spring과 연관지어 설명해주시니 정말 머리가 뻥 뚫린것 같이 이해가 확 되었다 !!!
안그래도 DTO에서 한 검증을 왜 다시 domain에서 하지? 라는 생각을 했는데 TC와 나는 고려하지 못한  Null Safe에 대한 검증을 고려하면 좋은 방법이라고 생각한다.

📝 리뷰어분들의 의견 2

출력의 책임

이 부분은 toString을 재정의 했는가?에 따라서 출력 변동의 책임이 결정될 것 같습니다!
toString을 재정의 했다면 도메인 내에서 출력 메시지를 변동해야하고, 
toString을 재정의 하지 않았다면 뷰에서 출력 메시지를 변동해야하기 때문입니다.
(둘 다 변경을 해야할 경우도 생길 것 같네요.)

 

검증의 책임

이번에는 검증의 책임을 view에서 저도 동일하게 가져갔는데요! 
이유는 재입력 로직이 발생하기 때문에 view 단에서 검증을 해주는 게 맞는 것 같다고 생각이 들었습니다.

그런데, 미션이 끝나고 생각해보니 검증의 책임은 나눠서 가져야 한다고 생각이 듭니다. 
왜냐하면 다른 개발자가 객체를 가져다 쓸 때 or 테스트 코드를 작성할 때, 
도메인에 대한 유효성 검증 코드는 도메인에 없기 때문에 예외가 발생하지 않기 때문입니다.

그래서 컨트롤러에서 예외를 캐치해주고, 각 도메인, view에서 본인의 책임만 에러처리를 해주는게 좋다고 생각이 들게 되었습니다!

의견 2를 주신 리뷰어의 의견은 의견 1을 주신 리뷰어의 내용와 같았으며 모두 너무 좋은 예시 상황을 통해 두분의 의견과 근거에 설득되어 이해와 공감을 할수 있었다 !

 

배운것 및 잘한점

1️⃣   테스트를 작성하는 나만의 이유

3주차 로또 미션을 구현한 가장 최근의 나의 커밋 내역이다.

'시작하며' 목차에서 이미 회고한것과 같이 테스트와 리팩토링을 병행하며 나만의 테스트코드를 작성해야하는 이유를 가지게 되었다.

 

2️⃣ 원시값 포장, 일급 객체, 일급 컬렉션

  • Money, BonusNumber → 원시값 포장
  • Lotto, WinnginNumbers → 일급 객체
  • Lottos → 일급 컬렉션

2주차 미션 코드 리뷰의 피드백을 통해 알게된 원시값 포장, 일급 객체, 일급 컬렉션에 대해 3주차 로또 미션을 진행하며 적용해 보았다.

 

Money, BonusNumber → 원시값 포장

원시값 포장을 적용하며

데이터 유효성 검증을 캡슐화하여 생성 시점에 유효성 검증을 보장하며, 검증 로직이 한곳에 모여 관리가 용이하게 하였다. 그로인해 잘못된 값이 객체 생성 시점 자체에서 들어올 수 없는 구조를 구축할 수 있었다.

또한 관련 상수와 검증 규칙이 함께 관리되어 요구사항(규칙) 변경 시 한곳 만 수정을 하면되어 도메인 규칙을 코드로 가독성 좋게 명확히 표현할 수 있었다. 그와 동시에 타입 안정성을 컴파일 시점에 보장 받을수 있어 버그 발생의 가능성을 줄일수 있었다. 

 

Lotto, WinnginNumbers → 일급 객체

로또 번호와 관련된 모든 책임 하나의 클래스에 있으며 데이터 조작은 오직 정의된 메서드를 통해서만 가능해 불변성을 보장하였다. 객체 생성 시점에서 모든 유효성을 검증하며 비즈니스 규칙 위반 자체를 불가능한 구조를 구축할수 있었으며, 도메인 용어를 메서드 이름으로 사용함으로써 비즈니스 로직을 명호가하게 표현하며 사용하는 쪽에서 코드에 대한 이해의 편의성을  올릴 수 있었다.

그로인해 코드의 응집도를 높이고 캡슐화를 강화하며 유지보수성 증가, SRP 준수, 재사용성과 테스트 용이성 향상을 모두 챙길 수 있었다.

 

Lottos → 일급 컬렉션

내부 컬렉션의 불변성을 유지하여 데이터 무결성을 보장하고, 컬렉션과 관련된 로직(autoGenerate, getSize)을 한 곳에서 관리함으로써 응집도를 높이며, "로또 목록"이라는 비즈니스 의미를 명확히 표현할 수 있었다.

또한, 컬렉션 조작을 제한하여 데이터 접근 방식을 일관되게 유지하고, 새로운 요구사항이 생길 때도 Lottos 내부에 로직을 추가하면 되므로 유지보수와 확장이 용이하며, 더불어 컬렉션과 관련된 로직이 캡슐화되어 있어 독립적인 테스트가 쉬워지고, 결과적으로 전체적인 코드 품질의 향상을 얻을 수 있었다.

 

위와 같은 객체지향의 특성을 잘 활용하며 특성들이 하나씩 모여 엄청난 시너지와 정말 강력한 무기로 작용하게 됨을 몸소 느낄수 있어 위 내용들을 적용하며 너무 짜릿했다. 그리고 앞으로 더욱 객체지향프로그래밍을 학습하고 체화하며 얼마나 더욱 이런 시너지가 커질지 기대하게 되며 더욱 OOP에 흥미를 가지게 되었다.

 

3️⃣ 정적팩토리메서드

Money.wons(), Lotto.from()/auto(), Lottos.autoGenerate()/from(), BonusNumber.from(), WinningNumbers.of() 등의 명확한 이름을 가진 메서드로 객체 생성 의도를 드러낼수 있었으며, from(값 기반), wons(반환 기반), of(조합 기반) 등의 네이밍 컨벤션을 통해 생성 방식을 구분하고, 자동 생성 로또(auto)와 당첨 번호 생성(from) 등 동일 클래스 내에서도 다른 생성 목적을 메서드 이름으로 명확히 구분할수 있어 가독성을 상당히 올릴수 있었다.

또한 생성자를 private로 감추며 생성 시점에 유효성 검증을 강제하고, Money처럼 다른 통화 단위 지원 시 확장이 용이하도록 설계하며, 동일한 시그니처로 다양한 생성 방식을 제공하여 생성 로직의 분리와 재사용성을 높이고, 각 정적 팩토리 메서드의 역할이 명확해 테스트 작성이 용이하며, 모든 생성 로직이 캡슐화되어 객체 생성을 더 엄격하게 제어할 수 있었다.

그로 인해 이후 수동 생성 로또 기능 추가와 같은 확장성을 고려할 수 있으며, 비즈니스 도메인의 용어를 메서드 이름에 반영하여 코드를 읽는 사람이 도메인 맥락을 더 쉽게 이해할 수 있도록 하여, 결과적으로 코드의 가독성, 유지보수성, 재사용성, 테스트 용이성 모두 한번에 챙길수 있었다.

 

보완해야할 점 아쉬운점

1️⃣   원시값 포장 개선 VO, Record

나와 같이 BonusNumber라는 VO를 적용한 리뷰어분이  나에게 위와 같은 의견에 대한 나의 생각을 물어보았다.

 

위 의견을 정독하고 보니 왜 나는 해당 프로그램에서 사용하는 Number자체를 VO를 적용하지 않고 딱 BonusNumber에만 VO를 적용을 했지?! 라는 생각이 머리속에 쾅 ! 들었다.

그로인해 일관성이 부족해지고 오히려 복잡성이 증가되었음을 알게 되었다.

 

그래서 이후에는 위와같이 하나의 개념에 대해 VO를 적용하는 것이 아니라 Number와같은 더욱 포괄적인 개념 자체를 VO로 만들도록 해야겠다는 생각을 하게 되었고, record를 사용해보면 해당 이슈를 해결할수 있을꺼같다는 생각이 들어 이후 3주차에는  record에 대해 공부한 뒤 적용해 보고자 한다 !

 

 

2️⃣ double 정확도

 

나는 3주차 미션을 구현하며 실수에 대한 처리를 해야하는 로직에서 '정확도측면'을 고려하지 않고 큰 생각 없이 그저 기본 자료형인 double을 사용했다. 하지만 코드리뷰를 통해 '정확도'을 더욱 챙길수 있는 BigDecimal에 대해 알게 되었다 !
이후에는 실수 처리 --> double이 아니라 '정확도'를 고려해보아야 겠다고 생각하였다.

 

3️⃣ view에서의 정렬

위 피드백은 나의 '고민한것'의 목차에 이미 정리한 것과 같이 도메인 내부에서 정렬을 한채로 객체를 생성한다면 나의 고민이 해결 된다는 것을 느꼈고, 이후에는 객체 생성 과정에서 출력과 이외의 비즈니스 규칙을 챙길수 있는지 더욱 생각해보는 것이 좋다 생각했다.