3, 4주차 기간동안 진행된 로또 미션에 대한 회고를 작성한다.
https://github.com/woowacourse/java-lotto/pull/275
https://github.com/woowacourse/java-lotto/pull/309

💡 컨트롤러는 비즈니스 로직을 알 필요가 없다

레이싱 게임 미션을 진행하면서까지 생각했던 MVC 구조는 아래와 같았다.

슬라이드1

컨트롤러가 모든 도메인들을 문어발식으로 컨트롤하는 형태가 당연하다고 생각했는데, 포츈과 페어를 진행하는 동안 포츈이 작성한 레이싱 게임 컨트롤러 코드를 보고 ‘아…! Controller가 정말 Model-View 와 주고 받기 활동만 하네! MVC관계를 명확하게 보여주는 것 같다!’ 라는 인사이트를 얻게 됐고, 아래와 같은 방식의 MVC 구조로 구현했다.

슬라이드2

LottoGame 을 통해 비즈니스 로직에 대해 명령을 내리고, 컨트롤러는 뷰에게 전달해주는 동작만 시키자 ‘핵심 비즈니스 로직’과 ‘데이터 전달’의 역할이 명확하게 구분지어지게 됐다. 잊지말자. 컨트롤러는 도메인과 뷰 사이를 잇는 통로일뿐, 비즈니스 로직을 알 필요가 없다.


💡 일급 컬렉션은 더 많은 일을 해야해

일급 컬렉션에 대한 이해가 부족했던 나머지, “일급 컬렉션에 서비스 로직이 포함되어도 괜찮나?” 라는 고민이 생겨났다. 정답은 “그렇다” 이다. 일급 컬렉션을 컬렉션으로만 사용하면 감쌀 이유가 없다. 일급 컬렉션의 가장 큰 장점은 상태와 행동을 한 곳에서 표현 할 수 있다는 점이다. 더 자세한 설명은 일급 컬렉션의 소개와 써야할 이유 를 작성하신 이동욱님의 블로그에서 확인하자.


💡 뷰도 검증 로직을 포함할 수 있다.

image image

도메인은 전체 서비스에 필요한 복잡한 로직 중 한가지를 담당하는 객체다. 데이터 타입까지 신경써주기엔 자신의 책임을 다하기에도 벅차다. 간단한 데이터 변환 정도는 뷰에서도 해줄 수 있다. 반대로 말하면, 사용자가 입력하는 쉼표 정도까지 도메인이 알 필요는 없다. 쉼표가 따로 의미를 갖는가? 쉼표는 그저 도메인에 필요한 데이터들을 구분하는 구분자 역할일 뿐이다. 이 구분자는 프론트의 성격에 따라 쉼표가 아닌 다른 특수문자로 바뀔 수도 있다. 그렇게 생각하면 뷰에서 split, 검증을 해주는 것이 오히려 더 자연스럽다.

중요한 것은 뷰에선 정말 원시적인 데이터 변환(문자열 -> 정수), 검증만 진행하고 핵심 검증은 도메인 단에서 진행해야 한다는 것이다. 뷰는 절대로 도메인의 서비스 로직을 알면 안된다. 그 순간 뷰가 아니다. 또한 뷰에서 모든 검증을 철저하게 진행한다고 해도 컨트롤러를 타고 도메인까지 전달되는 과정에서 데이터가 변하지 말라는 법은 없다. 단순한 입력 검증 외에 서비스 로직과 관련된 검증은 도메인에서 진행해주어야 한다. 사실 이런 이슈들 때문에 현업에서는 프론트 단에서 1번, 백엔드 단에서 1번, 총 2번의 검증을 진행한다고 한다.

프리코스 때 얻었던 인사이트로 “데이터가 컨트롤러를 타고 이동하는 과정에서 변질될 가능성이 존재하니 무조건 도메인 쪽에서!” 에 갖혀있었는데, 코니가 철저히 깨부서주셨다. 뷰도 간단한 변환, 검증 로직을 가질 수 있다.


💡 Boolean 타입을 위해서라도 영어 공부를

image

isCanBuyAmount 는 도대체?️ 영어 공부 좀 하자. 🤦‍♂
Bool 변수 이름 제대로 짓기 위한 최소한의 영어 문법


💡 static final은 무조건 상수

image

단순히 String, int 와 같은 값들만을 상수라 부르고 이것들만 대문자 컨벤션을 지키면 되는 줄 알았다. 그러나 일반적인 객체 클래스도 static final 키워드가 붙으면 컨벤션을 지켜주어야 한다. 지식이 늘었다!


💡 Enum도 도메인이다

image

MVC 패턴에서 도메인은 절대 뷰와 관련된 로직을 가져선 안된다고 한다. Enum도 예외는 없다. Enum은 도메인으로 분류된다. 그렇기 때문에 일반적으로 뷰와 관련된 로직을 가져선 안된다. (약간의 예외는 있다. 블랙잭 미션에서 등장한다.)


💡 테스트는 구체적으로, 필요한 곳에만

image image image

isEqualTo 같은 경우 특정 값이 완전히 일치하는지 확인하는 메서드로, 값이 다를 시 바로 테스트 실패를 결정하기 때문에 별도로 경계값 검사를 할 필요가 없다. 경계값 검사는 범위를 포함하는 값을 검증할 때 사용하는 것이다.

assertion 에서는 수 많은 테스트용 메서드를 제공한다. assertion에 대해 더 많이 공부하고 정리해서 포스팅하자.

테스트는 단순히 내가 궁금한 것을 테스트하는 것뿐 아니라, 프로젝트를 이어받는 개발자가 설명서처럼 참고하는 영역이다. 테스트 이름을 구체적으로 작성할수록 이해를 도울 수 있다.

항상 생각하고 테스트를 작성하자… 😅


💡 VO 캐싱

단순한 원시값 하나까지도 객체로 포장하다보면 필연적으로 인스턴스가 많아질 수 밖에 없다. 아무리 GC를 믿어야 하는 Java개발자라도, 인스턴스가 동시다발적으로 많이 생성되고 죽는 환경에 대해서는 성능 이슈를 고민해야한다.

로또 미션에서 가장 많이 생성되고 사라지는 인스턴스는 로또 번호다. 중복되어 사용되는 1~45 사이의 로또 번호 인스턴스를 캐싱하는 방법에 대해 배울 수 있었다. 자세한 내용은 역시 Javable 블로그를 참고 하자.

추가로 기억할 점은 “캐싱을 어디에 하는가” 이다. 초기엔 캐싱된 인스턴스를 제공하는 LottoNumberGenerator 같은 유틸 클래스를 떠올렸는데, 로또 번호를 표현하는 객체에 캐싱하는게 자연스러운지에 대한 고민이 커져갔다. 이 부분에 대해 인비가 “사실 로또 번호 인스턴스를 사용하려는 측에서는 로또 번호를 표현하는 객체부터 접근할 것이기 때문에 로또 번호 객체에 캐싱하는 것이 좋을 것 같다.” 는 의견을 공유해줬다. (킹갓인비)


💡 Stream API를 공부하자

서로 다른 2가지 컬렉션을 어떻게 깔끔하게 연결할까에 대해 고민이 많았는데, Stream API 중 두 컬렉션을 연결하는 concat 메서드가 존재했다.

public LottoTickets joinLottoTickets(final LottoTickets lottoTickets) {
    return new LottoTickets(Stream.concat(
            this.lottoTickets.stream(),
            lottoTickets.lottoTickets.stream()
        ).collect(Collectors.toList()));
}

이외에도 강력한 메서드들이 즐비한다. Stream API를 더 많이 공부해보자.


💡 객체에 메세지 좀 보내라

image image

getter가 연쇄적으로 일어난다는 것은 디미터 법칙을 위배하고 있다는 것이다. (디미터 법칙에 대한 자세한 내용은 javable 블로그를 정독하자.) 디미터 법칙을 지키는 방법은 간단하다. 객체에 메세지를 보내면 된다. 자꾸 꺼내쓰지 말고 메세지를 보내자.

역시나 근본적인 문제는 객체에 메세지를 보내지 못하고 있다는 점이다. 사실 이때까지도 “나도 객체에 메세지를 보내고는 싶은데… 잘 안되는걸 어떡해…” 라는 심정이었다. 블랙잭 미션이 되어서야 메세지가 안보내진 이유가 객체간 협력 구조가 잘못되어 메세지의 방향이 일직선이 되지 않았기 때문이라는걸 깨닫게 됐다. 지금이라도 알면 됐지. 객체에 메세지 좀 보내자.


💡 책임과 협력, 행동

image

모든 메서드들이 하나의 객체를 매개변수로 필요로 하고 있었다. 이렇게 된 이유는 간단하다. 필요한 객체가 너무 단순한 행동들만 수행하고 있고, 이는 제대로 된 협력을 하고 있지 못함을 뜻한다. 너무 많은 메서드에서 하나의 객체를 필요로 한다면, 그 객체의 책임이 너무 가벼운건 아닐지, 제대로 분리된 것인지 의심할 필요가 있다.


💡 근본있는 자신감을 가지자

코니의 리뷰를 받으면서 가장 중요하게 배운 부분이라 생각한다. 로또 미션을 진행하면서 절반의 시간은 “이게 맞을까?”, “이렇게 해도 괜찮을까?” 라는 생각으로 보냈다. 숨이 턱턱 막히는 기분이었고 아슬아슬 줄타기 하는 느낌이 썩 싫었다. 그 때 코니가 피드백을 남겨주셨다.

“…하는게 맞지 않나?” 이런 생각은 하지 마세요! 위에서 답변을 드린 것과 같은 이야기인데, 이건 맞고 틀리고의 문제가 아니에요. 그리고 확신을 가질 수 있는 기준을 말씀하셨는데, 개발하면서 확신을 가지고 뭔가를 하는 날이 올 수 있을까 저는 잘 모르겠네요. 그 아슬아슬한 줄타기 하는 느낌을 계속 안고 가야 하는게 이 직업 아닐까요? ㅎㅎㅎ 물론 제가 아직 경험이 일천한 초초초초주니어 개발자라 이렇게 생각하는 것일 수도 있겠지만요.

그렇다. 개발자는 어차피 평생 줄타는 느낌으로 살아가야하는 직종이다. 이쪽 방면에서 좋으면 저쪽 방면에선 나쁜, 계속되는 트레이드-오프를 경험하면서 매 순간 최적의 선택을 해야하는 직업이다. 자신감을 가져야 한다. 단 그 자신감은 내가 왜 이런 선택을 했는지 명료하게 설명할 수 있는 밑받침이 있어야한다. 근본있는 자신감을 가지자.


페어 회고

로또 미션을 함께 진행한 포츈 은 호탕한 동네형 같은 크루다. 프로그래밍 미션에 관련된 이야기 외에도 이런저런 이야기 나누기를 정말 좋아하고, 요즘도 시간 여유가 있으면 같이 맥주 한잔 하면서 이야기를 나누곤 한다.

포츈과 함께하면서 가장 흡수하고 싶었던 장점은 자기 의견을 짧고 명확하게, 힘있게 전달하는 능력이었다. 포츈과 처음 만났을 때 포츈 본인이 작성해온 TIL들을 보여줬는데, 의견의 짧고 명확함이 여기서 오는 것 같았다. 자신이 공부하고 고민했던 두리뭉술하고 긴 부분들을 회고를 통해 짧고 명확하게 표현할 수 있게 된다. 나도 블로그를 통해 조금씩 정리해오곤 있었지만, 사실 블로그 포스팅은 매일매일 하기가 어려웠다. 그래서 노션에 따로 그날그날 새로 배운 개념들에 대해 정리하기 시작했다. 힘있게 전달하는 능력은 회고를 통해 얻은 “내가 이건 확실히 알아” 메타인지를 바탕으로 자신감을 더해서 말하는 것이라 생각된다. 꾸준히 회고를 지속해봐야지.

잘했다고 생각이 드는 점은 저 강력한 힘을 가진 포츈의 의견에 맞서서 내 의견을 내세웠다는 점, 포츈의 의견을 무작정 수용한게 아니라 끝까지 이해하려 시도하고 결국 이해했다는 것(LottoGame). 반대로 아쉬운 점은 역시나 포츈에게 내 의견을 어필할 때 길고 장황하게 전달한 것. 그래서 의견이 삼천포로 빠지거나 이해시키는데 시간이 걸렸다.

포츈이 “막구 페어 ㄱㄱ?” 라고 묻는다면 “ㅋㅋㅋ ㄱㄱ” 라고 응할거 같다. 아직도 포츈한텐 따라하고 싶은 장점이 많다. 나는 포츈에게 흡수하고 싶은 장점을 보여주고 있는 사람인가? 내가 내세울 수 있는 내 장점은 뭐지? 잘 모르겠다. 우선 샐리, 포츈 장점부터 흡수해야겠다.

끗!

댓글남기기