우아한테크코스 7기 프리코스 4주차 회고

송선권·2024년 11월 12일
13
post-thumbnail

시작하기 앞서

변명인 걸 알지만.. 4주차 과제가 많이 어려웠다. (정말로... 진짜로...)

분명 구현에만 수십 시간을 사용한 것 같은데 버그가 끊이지를 않았다. 버그를 잡으면 이것 때문에 다른 곳에서 버그가 나고.. 또 고치면 또 나고.. 이런 상황의 연속이었다. 어디서 이렇게 끊이지도 않고 계속 나오는지... 🤦‍♂️

결국 제출 마감일까지도 버그를 잡고 있었다.

누적 커밋 통계를 보면 이번 과제에 상당히 많은 시간을 투자한 것을 알 수 있다. 물론 다른 분들은 더 열심히 하셨겠지만.. 나도 정말 열심히 했다.. 그냥 그렇다고.....

아무튼 그래서 하고싶은 말은..

이번주에는 리팩토링과 테스트 코드에 많은 시간을 투자하지 못했다. 코드리뷰는 열심히 했지만(그때는 과제가 이렇게까지 어려울 줄은 몰랐지..) 과제 구현에 급급해 코드리뷰에서 얻은 인사이트들을 정리하거나 적용하는 시간을 가지지 못했다.

결국 피드백들을 이번 과제에 적용하지는 못했지만 코드리뷰 과정에서 유익한 내용을 많이 얻을 수 있었으니 늦게라도 이를 정리하고자 한다.

🐣 지난주 피드백

중첩 enum


등수를 정할 때 보너스 번호의 일치 여부는 3가지로 나눌 수 있다. (일치, 불일치, 상관없음)
상관없음의 경우도 표현하기 위해 보너스 번호 일치 여부를 Boolean 자료형으로 표현하고 null 값을 활용했다.

하지만 여기에 Boolean 대신 별도 enum으로 래핑하여 사용하는 것이 어떤지 피드백이 들어왔다. 미처 생각해보지 못했는데 NPE를 예방하고 코드의 직관성을 높이는 좋은 방법인 것 같다.

의미있는 주석

public enum Rank {
    // ...
    public int getRank() {
        return this.ordinal() + 1;
    }
}

위 코드를 보고 getRank() 메서드가 어떤 의미인지 직관적으로 이해가 가는가? 나는 잘 이해가 가지 않았다. Rank가 등수라는 의미를 가지기는 하지만 Rank.getRank()는 뭘 반환하겠다는 의미인지 모호하다고 생각한다. 심지어 그 몸체를 열어서 코드를 봐도 잘 이해가 안간다. 그래서 주석을 추가했는데, 여기에 바로 피드백이 들어왔다. ㅠㅠ


내 소중한 주석아... 귀 막아...

메서드명과 반환타입을 통해 어느 정도 유추가 가능하기 때문에 주석이 불필요하다는 의견이었다. 충분히 공감되는 내용이었기에 다음부터는 주석을 달기 전에 더 고민해봐야겠다는생각이 들었다.

ordinal() 함수 사용 지양하기

같은 코드에 또다른 피드백이 달렸다. ordinal()은 enum 요소 순서에 의존적인 메서드이기 때문에 여기에 의존하기보다 별도 필드를 추가하는 것은 어떤가에 대한 의견이었다. 나도 이 부분을 작성할 때 같은 부분이 우려되었지만 이런 방법은 생각해보지 못했는데 정말 좋은 것 같다.

TreeSet

로또 번호를 오름차순으로 정렬해야 한다는 요구사항이 있었다. 나는 누락해버렸지만.. TreeSet을 활용하면 자동으로 정렬되니 도입해보는 것은 어떨까 하는 피드백이 들어왔다. 자료구조를 잘 몰라서 이런 게 있는지 몰랐는데 더 공부해봐야 할 것 같다.

메서드나 조건에 이름 지어주기

검증 메서드를 만들 때 특별한 이름없이 validate()로 작성했는데 이 부분에서 피드백이 들어왔다. 확실히 이 검증 로직이 어떤 부분을 검증하는 것인지 직관적으로 이해하기 힘들어 보인다. 조건문이나 이 로직을 감싸는 메서드에 유의미한 이름을 지어줘서 더 직관적인 코드를 만들어봐야겠다.

다음은 리뷰어분께서 참고자료로 제공해주신 우아한테크세미나 자료이다. 관심이 간다면 한번 읽어보자!
[우아한테크세미나] 리팩토링 정리 (By 자바지기 박재성님)

Setter 지양하기

지난주에 나는 스터디원분께 setter를 사용할 때는 경각심을 가졌으면 좋겠다는 피드백을 드렸는데, 그 피드백이 다시 나한테 돌아왔다 😭

당첨번호와 보너스번호를 따로 입력받아야 하는 요구사항 특성 상 객체 생성과 동시에 모든 필드를 초기화하는 것이 힘들어보였다. 그래서 어쩔 수 없지~ 이건 어쩔 수 없는거야! 다른 방법이 없잖아?라는 생각으로 setter를 개방했던 것 같다.

하지만 다시 생각해보면 전혀 옳은 방향이 아닌 것 같다. 생성자를 적극 활용해서 개방되는 setter의 수를 줄일 수도 있었을 것이고(이 부분은 인지했었지만 일관성을 위해 포기했었다), 이번에야말로 불변 객체를 효율적으로 활용할 수 있었을 것 같다(setter이긴 하지만 강건성을 조금이나마 높여주니..).

setter를 사용할 때는 충분히 고민해보고 사용하는 습관을 들여야겠다.

불변 객체에 record 활용해보기

Wallet 객체는 불변 객체로 유지하여 강건성을 챙기고자 했는데, 왜 불변 객체를 만들면서 record를 적용하지 않았는지 질문이 들어왔다. 사실 큰 뜻은 없었고 record는 간단하게 불변 객체를 만들고자 할 때(주로 DTO)만 주로 사용해왔기에 자주 사용되는 도메인 클래스에 record를 적용할 생각은 해보지 못했다. 하지만 record로 클래스를 선언한다는 것은 불변 객체임을 강하게 명시할 수 있기에 정말 좋은 방향이라고 생각한다. 다음부터는 불변 객체 사용 시 record 도입을 진지하게 고민해봐야겠다.

DTO의 역할은 어디까지인가

로또게임에서는 DTO에서 데이터 검증 및 가공 절차를 수행하도록 작성했다. 이 절차는 View나 Controller, Model 어디의 관심사도 아닌 애매한 부분에 속해있다고 생각한다. 데이터를 주고받는 과정에서 자연스럽게 이루어져야 하는 절차이기에 DTO의 생성자 역할로 위임하는 것이 매우 적절해보였다. 하지만 여기에 대해 피드백이 들어왔다...!

리뷰어분께서는 추상적인 layer 개념을 구체화하여 다양한 고민을 하신 것이 느껴졌다. 내부 계층의 변경이 외부에 영향을 미치는 것은 당연하지만 그 반대는 적절하지 않은 설계라고 말씀해주셨다. 완전 공감가는 내용이었다. 입력 요구사항의 변경이 내부 로직까지 영향을 미치는 것은 좋지 않은 설계임이 분명하다.

DTO가 입력 레이어보다 내부에 위치해 있기 때문에 내부 레이어에 외부 레이어가 의존하는 형태(InputView가 DTO에 의존하는 형태)는 바람직하지 않을 수 있다고 해주셨다. 정말 하나같이 깊게 고민한 흔적이 보이는 적절한 근거들을 제시하며 의견을 주셔서 너무 감사했다.

하지만 나는 그래도 지금 형태가 더 메리트있게 느껴진다. 영향력이 계층을 역전하는 일이 있어서는 안되겠지만 View와 DTO는 그 간격이 모호하게 느껴진다. 그리고 DTO에서 데이터 검증과 가공 절차를 수행하는 것이 너무 부드럽고 자연스럽다. 관심사마저 DTO의 것으로 느껴진다. 리뷰어분의 의견도 정말 공감하고 동의하는 부분이 많고 적절한 방향성이라고 생각하지만, 내 방법도 틀리지는 않았다고 생각하고 개인적으로 조금 더 메리트있어 보인다. 자세한 내용이 궁금하다면 위 이미지에 첨부된 장문의 편지를 읽어보자.

메서드 분리

위 코드는 service 계층인데, 잘 보면 playerlottos를 가져와 순회하며 순위를 직접 매기고 있는데, 여기에 대해 피드백이 들어왔다.

player의 직접적인 상태 변경을 service에서 직접 수행하는 것보다는 player에게 위임하는 것이 어떤가 하는 의견이었다. 이 부분은 정말 한 치의 고민도 없이 리뷰어분의 의견이 적절한 방향이라고 생각했다. service에서 도메인 객체 내부 로직을 과하게 건드리고 있었다.

앞으로는 (도메인).(get...).(...)의 형태를 조심해야겠다.

@ParameterizedTest에 이름 지어주기

@ParameterizedTest 어노테이션의 name 속성을 활용하는 방향에 대한 피드백이 들어왔다. 처음보는 구문이라 찾아보니 각 파라미터에 이름을 지정해주는 것이었고, 정말 유익해보여서 다음부터는 적용해보는 것도 좋겠다.

관심이 있다면 아래 글의 9. Customizing Display Names를 읽어보자.

Guide to JUnit 5 Parameterized Tests - Baeldung

이번주에 주로 고민한 내용

이번주는 버그잡느라 정신없이 지나가서 남길 게 있나 싶다... 그래도 최대한 적어보겠다!

안내 메시지 구현 방법

이번 과제에서는 사용자의 주문 내용 하나하나에 대해 프로모션 증정 안내프로모션 미적용 안내와 같은 메시지를 보내고 상호작용해야 한다. 지금까지의 과제가 Model단에서 로직을 전부 수행하고 View로 넘기는 방향이었다면 이번에는 Model에서 View와 유기적으로 상호작용하며 추가 응답을 가져와야 했다. 이 구조를 대체 어떻게 작성할 수 있을까??

가장 먼저 떠오른 방법은 Model에서 View를 호출하는 것이었다. 하지만 이렇게 되면 Controller의 존재 가치가 사라져버린다.. 이 상황만은 절대 막고 싶었다. 다른 방법은 없을까?

Model에서 어떤 안내 메시지를 띄웠는지 상태로 저장하면 되지 않을까? 하지만 주문은 한 번에 여러 상품이 들어오기 때문에 안내 메시지가 단건이라는 확신이 없었다. 그리고 안내 메시지의 응답에 대해 유기적으로 주문상황이 바뀌어야 했다.

흠... 🤔

그래도 이 방법은 가능할 것 같다. 제공되는 안내 메시지를 컬렉션을 활용해 관리한다면 여러 안내 메시지를 띄우더라도 일괄적으로 관리할 수 있을 것 같다. 관리만 잘 할 수 있다면 유기적인 주문내용 변경은 어떻게든 구현할 수 있어 보인다. 하지만 service는 싱글톤 성격을 추구하기에 상태를 유지하고싶지 않았다. 그래서 Repository에서 안내 메시지를 컬렉션으로 관리하도록 구현했다.

여기서 한가지 문제가 발생한다. 안내 메시지가 한 번에 여러 개 날아갈 수 있다면, 각 안내 메시지끼리 서로 구분할 수 있어야 한다. controller에서는 service로 안내 메시지에 대한 응답을 넘겨줄 때 어떤 메시지에 대한 응답인지를 알려줄 수 있어야 한다.

어떻게 구현할 수 있을지 고민한 결과, Repository 내부의 컬렉션을 실제 DB처럼 구성해보기로 했다. Map<Integer, Notice> 컬렉션을 만들고 key를 PK로써 활용했다. 그리고 findById()save(), remove() 등의 메서드를 실제와 유사하게 동작하도록 직접 구현했다. 그리고 controller에서는 service로 안내 메시지 결과를 전달할 때 key 값을 함께 전달하여 어떤 안내 메시지에 대한 응답인지 구분할 수 있도록 했다.

롤백? 검증!

구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])
[사이다-1],[감자칩-1000000]

상식적으로 저만큼 감자칩 재고를 쌓아두고 있지는 않을 것이다. 그럼 감자칩 재고가 부족하다는 예외가 발생하겠지?

예외가 잘 출력된다. 그럼 여기서 아무 상품이나 다시 구매해보자.

오.. 엥? 분명 비타민워터 하나만 구매하려고 했는데 사이다를 끼워팔기 당해버렸다..

이 상황은 각 상품에 대한 주문 로직이 순차적으로 동작하기 때문에 발생한다. 위의 예시를 프로그램 입장에서 보면 다음과 같다.

  1. 사이다를 1개 구매했다.
  2. 이제 감자칩을 구매하려는데 수량이 이상해서 재입력을 요청했다.
  3. 비타민워터를 1개 구매했다.

각 주문에 대한 로직이 독립적이기 때문에 감자칩 구매에서 예외가 발생해도 이전에 주문한 건에 대해서는 구매처리가 이미 끝난 것이다. 트랜잭션이 없어서 일괄 롤백도 힘들다. 그럼 이전 구매사항을 어떻게 롤백할 수 있을까?

사실 롤백까지 갈 필요도 없다. 실제 구매를 수행하기 전에, 일괄적으로 검증을 먼저 거치면 된다. 재고 수량을 뛰어넘는 구매 요청이 들어와도 실제 구매 전에 미리 검증하고 예외처리해버리면 그만이다.

public void order(List<OrderRequest> orders) {  
    Store store = storeRepository.get();  
    Customer customer = customerRepository.get();  
    orders.forEach(request -> store.validateStocks(request.name(), request.quantity())); // 사전 검증
    orders.forEach(request -> store.buy(customer, request.name(), request.quantity())); // 실제 구매
    noticeRepository.saveAllByCustomer(customer);  
}

service 메서드에 검증로직 한 줄만 추가해줬다.

이제 잘 돌아간다~~ 🥳

롤백..? 트랜잭션...!?

위에서 슥 지나간 말이 하나 있다.

트랜잭션이 없어서 일괄 롤백도 힘들다.

트랜잭션이 없으면 만들면 되는 것 아닌가? 과제를 진행할 때는 시간이 없어서 사전 검증으로 문제를 해결했지만, 근본적으로는 트랜잭션을 사용하면 훨씬 강건한 프로그램이 될 것 같다.

그래서 간단하게 java로 트랜잭션을 직접 구현해봤다!!

구현 과정을 정리해뒀으니 궁금하다면 아래 게시글을 읽어보자!

JAVA로 트랜잭션 구현하기

💬 개발 외에도

코드리뷰

이번주에는 오픈된 과제를 보고 많이 어렵다는 걸 어렴풋이 짐작하고 있었다(아니 그래도 이정도일줄은 몰랐죠..😭😭😭). 그래도 최종 테스트를 대비해서 요구사항도 안읽은 상태로 시간재고 제대로 시작하려고 했다. 대신 코드리뷰에 할애하는 시간을 3일에서 2일로 줄여 짧은 시간동안 코드리뷰를 최대한 많이 주고받고 싶었다.

그 결과 첫날에만 14개의 PR을 리뷰하는 기염을 토하며 일주일동안 총 23개의 PR을 리뷰했다. PR 하나마다 8~20개의 코멘트를 달았으니 어림잡아 3~400개의 코멘트를 달은 것 같다.

그 결과 내 PR에서는 16명의 참여자분들과 총 216개의 코멘트를 주고받을 수 있었다..!!

코멘트 개수는 아쉽게도 간발의 차로 2등으로 마무리했다.

경험 공유

지난주에는 주로 고민한 내용을 정리한 함수형 인터페이스와 표준 API회고를 작성해 공유했다. 코드리뷰를 할 때 내가 고민한 것과 같은 문제로 고민하는 사람들이 정말 많았다. 마침 정리해둔 글이 있었기에 열심히 홍보하고 다녔는데 다들 좋아해주셔서 감사하다. 🥰

회고도 내 눈에는 쓰고 싶은 거 다 쓰고 혼자 떠드는 것 같은데 다들 유익하게 봐주시는 것 같아서 뿌듯하다! 😆

스터디

이번 모임에서는 지난주처럼 경험공유, 코드리뷰, 몹프로그래밍을 진행했다. 지난 시간에 구현을 거의 끝냈는데 스터디룸 시간이 끝나버려서 중간에 멈췄는데, 일주일만에 다시 잡으니 하나도 기억이 안났다.. 모두들... ㅋㅋㅋㅋㅋ

일단 기억을 주섬주섬 꺼내와서 코드를 완성했는데, 리팩토링하려고 보니 TDD에서 사용하던 코드와 비즈니스 로직에서 사용하는 코드가 다르다는 걸 뒤늦게 깨달아버렸다... 정신차리고 고치니 남은 시간이 얼마 없었다. 이번주에는 리팩토링할 생각에 설렜는데 이게 이렇게 될 줄이야..

그래도 마지막에 30분정도 리팩토링을 진행했을 때 쓰레기같던 코드가 갑자기 예뻐지는 게 눈에 보여서 재미있었다. 다음 모임에서는 진짜 리팩토링 끝내봐야겠다..!!

회고

이번주 과제를 하면서 벽도 느끼고 정말 아직 많이 부족하구나 느끼는 계기가 되었다. 이대로라면 1차를 합격해도 최종 과제테스트에서 떨어질 것 같다. 버그덩어리 최소한으로 동작하는 쓰레기를 만드는 데만 10시간이 걸렸다... 1차 합격 발표가 나오기 전까지 열심히 연습해봐야겠다. 좋은 메타인지 기회가 되었던 것 같다.

우아한테크코스 자소서를 쓰기 시작할 때가 얼마 전인 것 같은데 벌써 프리코스 4주가 끝나버렸다. 짧다면 짧고 길다면 긴 시간이었는데, 정말 많은 것들을 배우고 느낄 수 있는 소중한 시간이었다. 작년 프리코스와 달리 이번에는 커뮤니티를 적극적으로 활용하고자 했다.

매주 학습 내용을 올리고 회고도 올리고 코드리뷰도 많이하고 다른 사람들 회고도 구경하며 댓글을 쓰고 다녔다. 나를 알아보는 사람도 많아진 것 같아서 뿌듯하기도 하다. 커뮤니티를 통해 상호작용의 중요성을 체감할 수 있었다. 어디가서 체험해보기 힘든 값진 시간이었다.

프리코스를 진행하면서 일어난 재밌는 일이나 느낀 점들이 정말 많은데, 여기에 전부 담기엔 너무 좁은 것 같다. 가능한 빨리 프리코스 회고를 정리해서 올려보겠다!

프리코스 전체 회고입니다! 😆
우아한테크코스 7기 프리코스 전체 회고

다들 프리코스 기간동안 고생 많으셨습니다!! 🥳🥳🥳
프리코스는 끝나지만 앞으로도 각자의 자리에서 화이팅해봐요!!

참고자료

[우아한테크세미나] 리팩토링 정리 (By 자바지기 박재성님)
Guide to JUnit 5 Parameterized Tests - Baeldung

8개의 댓글

comment-user-thumbnail
2024년 11월 12일

멋있네요 :)
저도 선권님이랑 같이 7기에 들어가고 싶어요~

1개의 답글
comment-user-thumbnail
2024년 11월 12일

마지막 짤이 여운이 남아요ㅠ 고생하셨어요!😄🎉

1개의 답글
comment-user-thumbnail
2024년 11월 12일

내용이 재밌어서 술술 읽혀요!! ㅋㅋㅋㅋ 중간 중간 사진도 넘 귀엽네요.. 코드 리뷰, 스터디, 블로그 활동하시는 거 보면 정말 누구보다 열심히 하시는 것 같아서 대단하세요.. 👍👍 이번주차 미션 저도 너무 어려웠는데 수고 너무 많으셨습니다!! 제 댓글이 본문에 보이는데 저거 아직 고민 중이라.. 1차 발표 나기 전에 꼭 답글 남길게요 😁

1개의 답글
comment-user-thumbnail
2024년 11월 12일

함수형 인터페이스 잘 참고해서 적용해봤습니다~!

1개의 답글