오늘 한 것은,
1. 완료한 것들 커밋 컨벤션 지켜서 커밋하기
2. TDD 마무리하고 메인 코드로 옮기기
3. 과제 테스트 통과하기
4. 리팩토링(진행중)
였다. 어제 회고글에서 오늘 하기로 한 것들을 기대한 만큼 해냈다. 역시 뿌듯하다. 특히 TDD 이후 과제 테스트를 원큐에 성공했을 때의 기쁨이란... 이번에도 혼자서 박수쳤다.
그리고 오늘 고민했던 내용들이 되게 많은데, 계속 의식의 흐름이 하나의 고민 -> 파생되는 다른 고민 -> 또 파생되는 다른 고민 -> 이렇게 넘어간다.
고민이 생각 날 때마다 빠릿하게 기록해놔야겠다...
TDD를 진행하며 컨트롤러 외에 모든 테스트를 완료했고, 완료된 테스트들을 test 브랜치에 전부 커밋하고 푸쉬했다. 이번에 커밋하며 신경 썻던건, 우테코측에서 제공한 커밋 컨벤션대로 명령형 어조로 작성하고, '...작성', '...구현', '...완료' 등 하나의 단어를 일관되게 사용하는 것이었다. 나는 테스트 과정에는 '작성' 으로, 이후 메인 코드에서의 기능 구현은 '구현' 을 사용했다.
그리고 저번보다는 깔끔한 커밋 기록을 남기고자 했다. 저번에는 수정하고 리팩토링한 커밋 기록이 너무 많아서, 필요한 건 커밋하되 가독성 좋은 커밋 기록을 남기려고 노력했다.
TDD를 완벽히 하지 못한 것들(Car
클래스, 기타 서비스 로직 등)이 남아있었는데, 오늘 과제에 사용한 시간 중 초반 5-6시간은 TDD 마무리에 사용한 것 같다. 코드 작성 도중에 리팩토링도 되게 많이 했는데, 예를 들면 승자를 체크하는 checkWinner()
메서드는 원래 LinkedHashMap
의 타입으로 <Car, Integer>
를 받았었는데, 생각해보니 자동차 이름으로만 구분지어도 될 것 같아서 <String, Integer>
타입으로 수정하는 리팩토링이 있었다. | ⬆️ checkWinner()
메서드
아무튼, 드디어 길고 긴 TDD 과정을 끝내고 메인 코드로 모든 작업을 옮겼다. 이후 정상 작동할 때의 성취감은 말로 다할수 없다.
'어라? 그냥 옮긴 코드 그대로 실행하면 통과 하는거 아닌가요?'
라고 물을 수도 있겠지만, 가능한 독립적인 테스트를 하기 위해서 클래스들끼리 의존하지 않게 했던게 많았다. 그래서 서로 필요한 클래스들을 만나게 해주고, 또 하는 김에 리팩토링도 계속 해주고, 컨트롤러를 작성하고 하다보니 필요성을 느껴서 게임 흐름을 관리하는 서비스 클래스도 하나 만들고...
그렇게 몇시간을 또 보내고 결국 테스트를 첫 시도에 성공했다!
그만큼 TDD를 꼼꼼히 했다는 반증인것 같아서 (아직 TDD가 능률이 잘 나오지는 않지만) 너무 좋았다. TDD를 진행하며 느꼈던 '이런 것까지 테스트를 해야 하나...' 하고 자괴감 아닌 자괴감까지 들었던 것이 모두 보상받는 기분이었다.
우선 많은 시간을 쏟은 것 중 하나가 컨트롤러를 작성하고 리팩토링 하는 것이었다.
적어도 게임의 시작점이라고 볼 수 있는 run()
메서드는 1주차 과제처럼 아주 깔끔하게 작성하고 싶었다. | ⬆️ 1주차 과제의 run()
메서드
그래서 컨트롤러에 일단은 메서드 분리를 생각하지 않고 애플리케이션을 정상적으로 수행할 수 있게만 작성하고, 이후 하나씩 메서드 분리와 게임의 흐름을 관리하는 서비스 클래스로의 메서드 분리를 진행했다.
이 정도면 정말 깔끔하다고 생각한다. setRacing()
에서는 레이싱 준비를 위한 정보들(자동차 이름, 전진 시도 횟수)를 준비시키고, createCars()
에서는 차량들을 준비시키고, addObserverToCar()
로 컨트롤러가 옵저버 역할을 하게 하고, startRace()
로 경기 진행과 결과를 출력시킨다.
이렇게 보니 정말 현실 세계의 자동차 경주를 구현한 것 같다.
사실 이게 오늘의 핵심이다. 오늘 가장 많이 고민했던 것이기도 하고, 쉽사리 결론을 내릴 수 없었다.
고민은 이것이었다.
학습한 바, 정적 메서드는 클래스의 인스턴스 상태에 의존하지 않고, 동일 입력에 대해 항상 동일 출력을 제공하는 유틸리티에 적합하다.
그런데, 컨트롤러와 서비스, 도메인 클래스를 제외한 내가 구현한 모든 클래스의 메서드들이 여기에 해당되는데, 이것들을 모두 정적 메서드로 허용해도 되는가?
만약 허용하게 된다면, 객체지향이 아니라 절차지향을 따라가는 것이 아닌가?
이 질문을 시원하게 대답해주는 글을 찾지 못했다. 그래서 선배 개발자(고인물)분들이 많이 계시는 모 커뮤니티에 위와 같은 내용으로 질문을 남겼다.
감사하게도 많은 선배 개발자분들이 답변을 친절히 해 주셨는데, 답변 내용을 정리하자면 다음과 같다. (혹시라도 해당 질문이 궁금하시면 이곳으로 가시라)
-> [Java] 메서드를 정적으로 선언하는 기준이 궁금합니다!
static 으로 사용하는건 날짜, 문자열, 파일 처리 등등의 범용성 있는 기능들입니다.
해당 작업이 다른 클래스에서도 자주 등장한다면 별도의 클래스(TransformUtil)을 만들어서 이곳으로 빼낸 후 컨트롤러에서 호출하는 식이 좋습니다. 이러한 변환 작업을 하는 메서드가 보통 static 이겠죠. 하지만 코드를 작성 중일 때 이것과 동일한 작업이 나중에 또 나올지는 아직 모를겁니다. (...) 나중에 다른 클래스에서 똑같은 코드를 작성 중일 때 중복을 걷어내면 됩니다.
객체지향적으로 개발을 하지만 실행 준비가 된 이후 절차지향적으로 실행됩니다. 정적 메서드는 절차지향적인 처리 흐름을 이해하기 쉽게 하기 위해 사용됩니다. 쉽게 이야기해서 입력에 대한 출력이 예측되잖아요.
(...) 입력과 출력의 눈에 보이는 기능에 정적 메서드를 사용합니다. 객체지향적이지 않고 절차 지향적이지만 중복기능 사용 코드를 쉽게 하는 장점이 있거든요. 중복되는 코드 정보, 은닉해야 하는 멤버데이터를 사용하지 않는 메서드에 정적을 사용합니다.
추가로 로그, 커넥션 등 AOP 횡단관점의 공통기능도 정적 메서드에 사용할 수 있으니 쉬운 코드를 만들기 위해 입력과 출력이 예측되는 공통 기능, 유틸성 기능 등 정
적메서드를 사용하면 좋습니다.
우선 static method 는 상태 관리가 없는 함수를 정의할 때 사용할 수 있습니다. 이때 static method 를 가지는 Class 는 인스턴스화 되면 안되고, 내부 상태 변수(static 변수도 지양)를 가지면 안되는 패턴입니다.
java 는 기본적으로 객체지향성을 기본 개념으로 만들어진 언어로, static method 를 그다지 좋아하지 않지만, 이후 functional 기능의 추가로 활용성이 조금 늘었습니다.
따라서 static 메서드는 객체로 정의할 필요가 없는 보다 더 작은 범위에서 재활용성을 위한 함수를 작성하는데 쓰면 돼고, 일반 메서드는 Class 범위에서 객체의 상태변수와 함께 쓰이는데 사용하면 됩니다.
메서드 대신 함수라는 용어를 사용하였는데, 함수라 하면, 입력 파라미터로만 응답을 만들어야 하는 것이고, 메서드는 클래스 내부의 상태변수로 응답을 만들거나, 내부 상태를 수정하거나 하는 것입니다.
여기에서 답변을 드리자면,
객체지향은 객체의 상태변수를 캡슐화하여 복잡성을 줄일수 있는 곳에서 써야 합니다. static 메서드만 쓴다고 절차지향이 되는 것은 아닙니다. 오히려 요즘 유행하는 함수형 프로그래밍에 더 가깝게 할 수 있습니다.
(대부분의 메서드가 static 이어도 괜찮나는 질문에 대해)
억지로 (인스턴스로) 만들 필요는 없습니다. 순수 함수로 가능하다면 그것도 좋은 방법입니다.
java에서는 모든 함수가 class안에 정의되어 '함수'와 '메소드'로 나눠서 생각하는게 좋지는 않습니다만, 함수 정의 안에 this (암묵적 사용 포함)를 사용하는 경우만 메소드로 생각하시게 좋을 것 같습니다.
엄청 도움이 되는 조언들을 많이 해주셨다. 맘 같아선 고인물 개발자분들 몇명 납치해서 옆에 앉혀놓고 주 60시간 인간 GPT로 만들고 싶다...
그래서 내가 내린 정적 메서드 사용에 관한 현재의 결론은, "간단한 입출력 기능과 공통적으로 사용되는 함수들(상태를 관리하지 않는, 값을 받아 처리해서 값을 넘겨주는)에만 사용한다." 이다. 학습하는 것이 더 많아지고 고려할 것이 많아질수록 이 기준점은 변경될 수 있겠지만... 지금으로썬 나름의 기준을 정한 것으로 충분한 것 같다.
테스트를 통과했기 때문에 마음에 여유가 좀 생겼다. 내일부터는 많은 리팩토링과 조금의 리드미 작성을 하려 한다.
목표로 하고 있는 추가 리팩토링 항목은,
1. 생성자 주입 사용
2. 싱글톤 또는 정적 팩토리 메서드 사용(필요하다면)
3. DTO 사용으로 Car 객체 캡슐화 지키기
4. 정규 표현식 캐싱하기(matches() 생성 비용 높음)
5. 재사용되어야 할 객체 파악해서 재사용하기
이고, 더 이상 리팩토링 할만한게 눈에 보이지 않는다 싶으면 요구사항에 따라 구현한 기능에 대한 테스트를 진행할 예정이다.
1. 정적 메서드는 어떨 때 사용해야 하는가?
사실 이런 것처럼 딱 정해놓은 기준이 없는 것들이 좀 어려운 것 같다. 또 반면에 그만큼 판단을 잘 하는 개발자가 잘하는 개발자라는 생각도 든다. 옵저버 패턴 학습 이후로 또 든든한 지식을 배워가서 좋았다.
1. 정적 메서드 사용에 대한 결론을 나름대로 내릴 수 있었던 것
(선배 개발자님들 감사합니다...)
2. 테스트 완료!
과제 테스트를 통과해 놓고 추가적인 작업들을 하는 것과 통과하지 못한 상태에서 추가적인 요소들을 고려하는 것은 정말 다르다. 아무래도 후자가 압박감 등의 이유로 온전한 개발, 즐거운 개발을 할 수 없지 않을까? 일단 완성한 코드를 천천히 그리고 꼼꼼히 개선할 만한 점을 찾는게 훨씬 즐겁다. (물론 처음부터 완벽하게 작성할 수 있다면 그게 제일 즐거울 것 같다)
3. 깔끔한 커밋 로그
1주차 과제때는 조금 전에 수정한 걸 다시 수정해서 커밋하는 경우가 많았는데, 이번에는 겁먹지 않고 revert를 적극적으로 활용하며 커밋 로그를 깔끔하게 만들었다!
1. 깔끔한 TDD 과정을 하지 못했던 것
TDD 책에서 보여준 것처럼 매끄럽고 깔끔한 TDD 진행을 하고 싶은데, 아직 시행착오를 많이 겪는다. 저자분도 TDD가 익숙하지 않으면 꽤 어렵고 시간이 많이 걸리는 개발 방법이라고 하셨는데, 정말 맞는 것 같았다.
TDD를 하지 않고 그냥 바로 메인 코드에 작성했으면 시간은 좀 덜 걸릴 것 같긴 했다. 그래도 경험치를 쌓아가는 중 아닐까? 이후 사람들과 협업도 하고 하려면 당연히 배워야 하는 과정이라고 생각한다.
2. 1주차 회고에 비해 새로 배운 점이 줄어든 것
사실 어쩔 수 없는거라고 생각한다. 1주차에서 새로 배운 것들로 2주차에 활용을 하고 있으니... 만나는 대부분의 것들이 새로웠던 1주차가 많이 재밌었던 것 같다.
지금도 충분히 재밌고 즐겁지만, 좀 더 많은 것들을 배우고 학습해서 적용할 수 있다면 더 즐거울 텐데!
즐기면서 하는 몰입이 무엇인지를 몸소 느끼시는 게 글에서 뿜어나오네요!! ㅎㅎㅎ
😮👍🙌