이어서..
오늘부터는 리팩토링하는 시간이다. 리팩토링 할 것은
- 네이밍
- 결합도 낮추기
- 다른 뭐든 필요한 리팩토링
이다. 좋은 네이밍은 역시 어렵다. 그래도 네이밍에 대한 감을 잡아가고 있다. 최대한 리팩토링 하고 있는데, 머리가 너무 복잡할 때는 역시 쓰면서 정리하는 게 최고인 것 같다. 머릿속으로 시뮬레이션 돌리는 거에는 역시 한계가 있는 것 같다.
에러..?
테스트 코드를 실행하면 다음과 같이 게임 종료 이후에도
"숫자 야구 게임을 시작합니다."
"숫자를 입력해주세요 : " 메시지가 출력되는 현상을 발견했다.
그런데 정말 신기하게도 내가 직접 Application
시작점에서 실행하면 저런 메시지가 나오지 않는다. 그리고 애초에 저 "숫자 야구 게임을 시작합니다."
는 Application
에서 객체를 생성할 때 딱 한번만 나오게 되어있다.
일단 테스트 코드 실행 시에만 발생하고, 테스트를 통과하지 못하는 것도 아니니 냅뒀다. 짐작하기를 스레드 쪽 관련된 문제가 아닐까 생각도 해봤는데, 디버거를 통해 봐도 스레드는 하나만 실행 중이다(...) 위급한 에러가 아니니 해야 할 것을 다 하고 살펴봐야겠다.
controller가 너무 많은 걸 하는데..?
이 애플리케이션에서 작성했던 유일한 controller 계층, BaseballGameController
클래스를 보니 이놈이 controller 본연의 역할을 넘어서서 다른 역할까지 하는 걸 보았다.
| 어제 작성하던 러프한 controller 코드
view 와 model 간의 연결다리 역할을 하는 것 뿐만 아니라, _*변수의 상태를 변경하고, 값을 받아오는 등 여러 역할을 해 객체 본연의 책임을 벗어난다고 생각했다. *_따라서 게임의 전반적인 상태와 흐름을 관리하는 역할을 가질 클래스가 하나 더 필요하다고 생각했다.
흐름 관리 클래스 작성
| 만들고 이후에 수정을 거쳐서 첫 작성 코드가 없다.
결국 우여곡절 끝에 BaseballGame
이라는 클래스를 하나 만들었다. 그런데 또 고민이 생겼다.
이놈을 단순한 model 이라고 볼 수 있을까?
이 친구는 다른 model 계층의 클래스들(인풋값 검증, 랜덤 숫자 생성, 스트라이크 볼 카운터..등등)에게서 값을 가져온다.(사진과는 다르게, 이 때는 아직 dto 클래스들에 대한 생각을 못 했다.) model의 값들이 이곳으로 모이고, 이 클래스만이 controller 와 소통을 한다. 다른 model 과 비교해서 좀 특별하지 않은가?
model과 service의 차이점이 뭔데?
service라는 계층이 존재한다는건 알고 있었다. 근데 정확히 뭔지는 몰랐다. model과 service의 차이점을 중요한 것만 빠르게 학습해 본 결과,
- service 도 model 에 속한다.
- service 는 model 내의 로직 중에서도 특히 비즈니스 로직을 담당하는 부분을 분리한 것이다.
- model 과 service 두 계층의 경계가 모호할 수도 있다.
이 BaseballGame
은 핵심 비즈니스 로직을 처리하므로 service 에 가깝다. 하지만 전통적인 mvc 패턴의 관점에서는 model의 일부로 볼 수도 있다.
나는 이 BaseballGame
이 가장 중요한 비즈니스 로직을 담고 있다고 생각했기 때문에, service 계층으로 분리했다. 클래스명도 BaseballGameService
로 바꿨다. service 계층의 클래스를 네이밍할 때 _맨 마지막에 Service를 붙이는 것이 권장되는 네이밍 방식_이라고 한다.
DTO 클래스 추가
게임의 전반적인 흐름을 관리하는 BaseballGameService
를 작성하던 도중, 가독성이 너무 떨어지는걸 느꼈다. 모든 model 객체들에서 값을 받아 꺼내오다 보니 변수명도 많아지고 구분도 힘들었다. 머릿속에서 시뮬레이션 하기도 어려워졌다. 그래서 문득 이런 생각을 하게 됐다.
'이 데이터를 가져오는 부분 좀 어디 짱박아놓을 수 없나...? 아! 데이터를 가져오고 꺼내는 클래스를 따로 만들자! 근데 이게 DTO인가 뭔가 하는 그거냐?'
DTO도 들어만 봤지, 어떤건지는 정확히 몰랐다. mvc 패턴 이름이 mvcd 패턴이 아니니까...(도중에 mvc 패턴이 여러 종류로 세분화 된다는 것을 알게 되었다.)
DTO에 대해서 알아보니, 여러 계층 간 데이터 전송을 담당하는 객체라고 한다. 주 목적이 바로 '데이터 전송'! 게다가 DTO는 단순히 데이터를 전달하는 목적만 가지기 때문에 Getter, Setter 를 자유롭게 사용해도 된다고 한다! 만세!
| 처음 작성한 GameResultDto
클래스의 일부. 클래스 필드를 보면 이 애플리케이션에서 사용하는 모든 변수가 있다. getter, setter는 변수 갯수만큼 있다....(추후에 수정했다...)
이렇게 DTO 클래스를 만들어 Service에서 직접 다른 Model 계층에 값을 요청하지 않고, Model 계층에서 DTO로 값을 보내고 Service 계층에서 DTO에게 값을 요청하게 했다. 이전 코드와 비교 사진은 없지만, 가독성이 훨씬 괜찮아졌다. 적어도 값을 어디서 꺼내오는지는 알게 됐으니까!
Controller -> Service 의존성 주입
그런데 DTO를 사용하고 나서 에러가 하나 발생했었다. 코드를 실행하면 모든 입력, 로직 출력값이 null
이 된 것이다. 또 삽질하며 이유를 파악하니, 각각의 Controller 클래스, model 계층의 클래스들, Service 클래스가 각각 새로운 GameResultDto 객체를 생성해서 값을 전달하거나, 가져와서 그랬다... 좀 깊게 생각해봤으면 될 문제인데, DTO를 사용하면 가독성 문제를 해결할 수 있겠다는 생각에 흥분되서 생각을 못했다. 그래도 원인을 파악해서 다행이었다.
GameResultDto
를 사용하는 가장 최상단의 클래스인 BaseballController
클래스에서 GameResultDto
객체를 생성하고, 이 객체를 BaseballGameService
객체에 생성자로 주입하여 최상위에서 만든 객체를 Service, Model 계층 전부가 공유하게 했다. 때와 목적에 따라서 공유하지 말아야 될 때도 있겠지만, 나는 꼭 모두가 같은 객체를 공유해야만 했다. 이렇게 이 문제는 해결했다!
아 근데... DTO에 손님이 너무 많지 않나?
시간이 또 지나고 여러 리팩토링을 하던 도중, 한 DTO 객체를 너무 많은 곳에서 호출한다고 느꼈다. Model 계층의 여러 객체들, Service 객체, Controller 객체.. 사실상 View 계층을 제외하고 대부분의 객체들이 하나의 DTO에 접근한다고 생각했다. 근데, 그러면 안되나? 문제점이 뭐가 있을지 고민해봤다.
- 강한 결합도를 가진다. 이것은 곧 모두가 접근하는
GameResultDto
객체 하나에서 문제가 발생하면 이곳을 참조하는 다른 모든 객체에 문제가 생긴다는 것을 의미한다. 또한 유지보수적으로도 좋지 않다. - Model 계층의 객체들, 숫자를 검증하는
InputNumberValidator
이던, 랜덤 숫자를 생성하는RandomNumberGenerator
이던... SRP 원칙을 위반하게 한다고 생각됬다. 예를 들어, 숫자를 검증하는InputNumberValidator
의validateAllInput()
메서드는 숫자를 검증해서 그대로 반환해주면 되는데, '숫자를 검증한다' '데이터를 저장한다' 라는 두 가지 역할을 하게 된다고 생각했다. |InputNumberValidator
클래스.
2번은 누군가는 '엥? 결국 숫자를 검증 해서 DTO에 반환(넘겨주는) 것 아냐?' 라고 말할수도 있겠지만, 나는 이 InputNumberValidator
객체뿐만 아니라 DTO에 값을 저장하는 다른 모든 Model 계층의 객체들도 두 가지 책임을 부여받았다고 판단했다. 그리고 1번 문제점으로도 리팩토링을 하기엔 충분한 이유가 되었다.
어떻게 두가지 값을 넘겨주지?
돌고 돌아 또 예전의 문제로 돌아왔다.
DTO에 직접 값을 넘겨주지 않으려면 BaseballGameService
에 반환값을 넘겨주고 거기서 DTO에 처리하게 하면 된다. 근데, 한 번에 두가지 값을 넘겨줘야 하는 메서드는? strike
와 ball
값을 동시에 넘겨주는 메서드는 어떻게 값을 넘겨줘야 할까?
1. 새 DTO 객체 반환
처음 생각한 방법은 DTO 클래스를 분리하고, 새 DTO 객체를 만들어서 반환하는 방법이었다.
GameResultDto
에서 숫자를 저장(검증받은 input, 랜덤 숫자) 하는 NumberDto
와 strike
, ball
갯수를 저장하는 StrikeBallResultDto
를 분리했다. 결과적으로 GameResultDto
에도 정답 여부, 게임 결과 메시지, 재시작 여부만을 저장하는 진정한 '게임 결과 DTO' 가 되었다. 이 부분은 매우 만족한다!
그리고 StrikeBallCounter
객체의 createStrikeBall()
메서드를 통해 생성자에 계산된 strike
, ball
값을 담아 StrikeBallResultDto
객체를 생성해서 반환하게 했다.
어라? 근데 이러면 역할이 2개인데?
공을 들여 리팩토링을 했는데, 어라라? 다시 또 생각하니 DTO를 생성해서 반환하는 모든 객체가 결국에는 'DTO에 값을 저장한다" 라는 책임을 부여받게 되었다. 아뿔싸...
DTO에 손님이 너무 많아! 결합도를 낮추자!
-> 근데 값을 두개 전달하는 메서드는 어떡하지?
-> DTO 객체를 만들어 반환시키자!
-> 어라? DTO에 손님이 너무 많아!
이런 무한 루프의 굴레를 벗어나야 했다. 값을 두 개 전달할 다른 방법을 찾아야 했다.
내부 클래스 사용
사실 3일차에 비슷한 고민을 했다. 그 때는 값을 두 개 넘겨줄 때 내부 클래스를 사용해서 넘겨줬는데, DTO를 사용하면서 내부 클래스를 사용하지 않게 됐다.
그렇다면 이전 방식대로 돌아가는 것이 맞는가? 코드가 너무 길어지진 않을까?
여기서 고민을 많이 했다. 어떤 게 최선의 방법일까? Pair
라는 것도 찾아보고 Map
을 사용할까 생각도 해 봤는데, Pair
는 오히려 가독성이 더 안 좋아질것 같았고, Map
은 이상한 말일수도 있지만, 코드의 맥락에 맞지 않는다고 느꼈다. 이건 그냥 직감이었다. 자료구조를 쓸 정도로 복잡한 로직도 아니고, 이 애플리케이션 내에선 딱 한군데, 우테코 라이브러리로 제공되는 Randoms
클래스를 사용할 때만 List
가 사용됐다. Map
, Set
, 심지어 배열조차 사용하지 않았다.
게다가 Map
으로 넘겨주면 받는 쪽에서도 또 Map
을 받고 또 다시 꺼내야 하는데, 그럴 바엔 내부 클래스를 사용하자고 생각했다.
| GameResult
클래스 안에 게임 결과 메시지와 정답 여부를 필드로 가지는 GameResultData
내부 클래스를 만들었다. 이는 값을 두 개 전달하는 다른 클래스에도 동일하게 적용됐다.
record 사용?
번뇌의 무한 루프 굴레를 벗어나 안도하며 리팩토링 할 다른 것들을 찾아보던 도중, 내부 클래스를 사용한 곳에서 내부 클래스를 record
로 대체할 수 있다는 IDE의 추천이 있었다.
솔직히 record
라는 것을 예전에 배우고 까먹고 있었는데, 써먹으면 좋을 것 같았다. 어차피 setter getter 메서드도 다 구현이 되고,
가독성이 좋아지니까.
마무리...
오늘은 포스팅 내용이 좀 많았다.
가독성을 위해서 1편 2편 나눠야 하나 생각했는데, 시간이 남으면 하기로 했다. 어차피 1주차 과제가 끝나야 올리니까... 회고글은 후순위이다.
처음에 mvc 패턴을 사용할 줄 모르는 상태에서, Model, View, Controller 를 넘어 DTO, Service 계층까지 이용할 줄은 상상도 못했다. 최대한 객체지향적으로 생각하고 개선하려다보니 자연스레 mvc 패턴을 구현하게 되는 것 같았다. 코드를 개선하기 위해 너무 많이 들여다봐서 안 보고 새로 작성하라면 막힘 없이 똑같이 작성할 수 있을 것 같다...
그리고 프로그래밍 실력과 안구의 습도는 반비례하는 것 같다. 눈 좀 계속 깜빡여야 하는데 너무 몰입하다보니 눈 깜빡이 알림이 보여도 깜빡이지 않고 코드만 보고 있는 나 자신을 발견하게 된다.
| 눈을 깜빡이라고 알려주는 Blinks 앱. 단돈 4,400원에 App Store 에서 만나보세요..
새로 알게된 점
1. '결합도' 에 대한 체득
강한 결합도, 약한 결합도 개념은 이론적으로 알고 있었는데, 이를 내가 작성한 코드에서 직접 경험하고 개선하려 하니 정말 체득이 되었다. 결합도를 낮추는 모든 방법을 체득한 건 아니지만, 이렇게 계층과 객체의 역할을 명확히 하는 것 만으로도 결합도가 낮춰진다는 것을 진심으로 체득했다.
2. Controller 도 Controller 만의 역할이 있다
뭔가 Controller 를 '객체' 라기 보다는 좀 특별한 무언가로 생각해서 역할, 책임, 협력의 관점에서 바라보지 못한 것 같다. Controller 도 View 와 Model 을 중재하는 역할이 있다는 것! 애도 객체라는 것!
3. DTO
DTO를 잘 몰랐고, 여기서 쓸 줄은 생각도 못 했는데 사용하게 되면서 자연스레 습득했다. DTO의 모든 것을 아는건 아니겠지만, 이놈이 데이터를 담고 꺼내오기 참 편한 놈이구나- 라는 것을 또 체득했다.
4. 항상 객체의 역할에 주의를 기울이자
코드를 작성해가며 머리가 복잡해지면 눈앞에 놓인 문제를 해결하기 위해 SRP를 위반하게 되는 것을 발견했다. 특히 Model 계층의 객체들이 '데이터를 저장한다' 를 가져 다시 리팩토링을 하며 잘 느꼈다. '이 객체가, 메서드가 어떤 역할을 할 것인가?' 를 곰곰히 생각해 보는 습관을 들여야겠다.
좋았던 점
1. Service, DTO 계층을 사용한 것
위에서도 말했지만, 여기까지는 바라지도 않았는데 사용하게 되니 되게 뿌듯했다. 정말로.
2. 매일 개선되어가는 코드
어제보다 오늘 더 리팩토링 되고 객체지향적으로 만들어지는 코드를 보면서 솔직히 좀 뿌듯했다. 생각하고 고려할 게 너무너무 많지만, 그렇게 힘을 들여 코드를 개선해 나갈 때 진짜 '내 작품' 같은 존재가 되가는 걸 느꼈다.
아쉬웠던 점
1. 침착하지 못한 것, 설계하지 않은 것
DTO를 사용해본다는 생각에 흥분해서 무작정 코드 작성부터 했다. 그런데 아무래도 Service 계층에서 Model의 값을 가져오는 로직들을 다 변경해야 하니, 작성 중에 에러도 몇 번 생겼고 '어, 어디 하고 있었더라' 하는 상황도 많이 발생했다. 미리 생각하고 설계해서 만들지 않아서 일어난 일이라고 생각한다.
포스팅에서야 몇 줄 만에 '이러이러해서 이러이러하게 만들었습니다~ 짜잔~' 이지만, 정말 눈알이 빠지는 고통과 교환한 리팩토링 과정이었다.
2. 눈을 깜빡이지 않은 것
농담이 아니고, 눈을 깜빡여야 하는데 계속 잊는다. 돈을 주고 눈 깜빡이 알림 앱을 샀는데도 안 깜빡인다. 내 안구건조증이 몰입의 부작용, 영광의 상처라고 생각하지만, 그래도 개발자는 눈이 생명인데... 어떻게 해야 잘 깜빡일 수 있을지 생각해봐야 겠다.
도움이 된 자료들
| 자바의 final
| [Spring] Controller, Service, Repository
| [Java] void Method 종료 하기] -> while 중첩문을 사용할 때 void 메서드를 어떻게 종료시켜야 할까 라는 고민에 대한 답. 포스팅에는 없지만, while 중첩문을 사용하지 않기 위해 진짜진짜 많이 고민했다. 결론은 while 중첩문을 사용해야만 하고, 대신 한 루프를 끝나면 return;
으로 종료해 주는 것.
'우아한테크코스' 카테고리의 다른 글
[우테코 6기 프리코스] 1주차 - 7일차 + 1주 전체 회고 (1) | 2023.12.20 |
---|---|
[우테코 6기 프리코스] 1주차 - 6일차 회고 (0) | 2023.12.20 |
[우테코 6기 프리코스] 1주차 - 4일차 회고 (1) | 2023.12.20 |
[우테코 6기 프리코스] 1주차 - 3일차 회고 (0) | 2023.12.20 |
[우테코 6기 프리코스] 1주차 - 2일차 회고 (1) | 2023.12.20 |