이어서...
오늘 한 것은
1. 옵저버 패턴 학습
2. 설계(mvc 패턴 + 옵저버 패턴)
3. 구현(TDD)
였다.
사실 처음에는 '오늘은 설계만 끝내도 좋다-' 라고 생각했다. 하나의 패턴을 새로 학습하는 데에 적지 않은 시간이 걸릴 것이라 예상했기 때문이다. 그런데 예상보다는 패턴 이해에 시간이 적게 걸려서 설계와 80% 정도까지 구현을 했다.(그래도 한 6시간은 패턴 학습에 쓴 것 같다)(그리고 새벽까지 구현을 했다...)
시간 사용 비율은 [옵저버 패턴 공부 : 설계 : 구현(TDD)] 을 [6 : 2 : 2] 정도의 비율로 시간을 쓴 것 같다. 오늘 밤에 처음 코드 작성을 시작했는데, 몇 시간 만에 80% 정도의 구현을 했다. 그 만큼 *설계에 공을 들이고 머릿속으로 끊임없이 시뮬레이션을 해서 좋은 효율을 낸 것 같다.
1주차 과제에서 감을 잡아서 나중에 리팩토링 할 것을 최대한 줄이기 위해서 코드 하나하나에 '이렇게 작성하는 이유'
와 '장단점'
을 계속 생각했던 것 같다. 덕분에 생각치도 않았던 메소드 벤치마킹
까지 해보게 되었다.
옵저버 패턴 학습
옵저버 패턴에 대해서 알아보는데, mvc 패턴과 적절히 결합하기도 좋고, 특히 이 '자동차 경주 게임' 의 요구사항(자동차 이동 결과 출력)에도 찰떡이라고 생각이 들어서 옵저버 패턴을 사용하기로 결정했다.
옵저버 패턴을 공부하는 데에 우테코 시작 전에 미리 사뒀던 [개발자가 반드시 정복해야 할 객체 제향과 디자인 패턴]
책이 정말 많은 도움이 되었다.
옵저버 패턴을 잘 설명해주신 좋은 블로그 포스팅도 발견해서 두개에서 학습한걸 적절히 사용해서 패턴을 사용할 수 있었다.
| ⬆️ 책에서 설명하는 옵저버 패턴의 일부 발췌. 고려해야 할 주의사항까지 설명해줘서 너무 좋았다.
옵저버 패턴의 원리를 간단히 설명하자면,
옵저버 패턴에는 주제(Subject)가 있고 옵저버(Observer 또는 구독자 Subscriber) 가 있다.
주제는 옵저버 목록을 관리하고, 옵저버를 등록하고 제거할 수 있어야 한다.
주제는 상태가 변경되면 정해지지 않은 임의의 객체, 즉 옵저버에게 통지한다.
이렇게 한 주제 객체의 상태 변화를 정해지지 않은 임의의 객체에게 변경 사실을 알려주는 패턴이 옵저버 패턴이다. (1:N 관계)
옵저버 패턴을 사용할 수 있는 예시를 말하자면, 날씨 정보와(1) 날씨 정보 구독자들(N), 신문과(1) 신문 구독자들(N)의 관계가 있을 수 있겠다.
왜 옵저버 패턴을 사용하려 하나?
나는 옵저버 패턴이 mvc 패턴과 자동차 경주 게임의 요구사항에도 적절하게 쓰일 수 있는 패턴이라고 생각했는데, 그 이유는 다음과 같다.
1. mvc 패턴의 원칙
model 계층은 controller 에게 변경 사항을 통지할 수 있는 방법을 구현해야만 한다.
controller 계층은 모델이나 뷰의 변경을 모니터링 해야 한다.
새로 알게 된 위 mvc 패턴의 원칙을 지키고자 했는데, 여기서 '변경 사항' 은 결국 'Car' 의 상태라고 생각했다.
옵저버 패턴을 사용하면
'Car' -> 주제
controller -> 옵저버
로 만들 수 있을 것 같았다. 이렇게 하면 model 계층이 controller에게 변경 사항을 통지하고, controller 계층은 주요 상태 변경점인 Car의 변경을 모니터링 하게 되어 mvc 패턴의 원칙을 지킬 수 있게 된다.
2. 변경 사항 즉시 반영
자동차 경주 게임의 요구 사항을 보면 실행 결과가 순차적으로 나온다. 옵저버 패턴을 사용하면 이걸 쉽게 구현할 수 있을 것 같았다. 만약 게임 종료 이후에 한번에 결과를 출력하게 한다면, 자동차의 갯수가 많아질수록 비효율적이라고 생각했다.
- 자동차가 100개라면 자동차 객체 각각의 이동을 담을 배열 또는 List가 100개가 필요하고, 이것들에게 라운드 횟수만큼 접근해서 값을 저장해야 한다.
- 그리고 100 x 라운드 횟수만큼 문자열 결합을 하거나 100개의 자료구조에 접근해서 이동 출력을 100 x 라운드 횟수만큼 해야 한다.
반면에 옵저버 패턴을 사용하면, Car 객체에게 이동 명령을 내릴 때 각 Car 객체의 이동 정도를 출력하게 할 수 있을것 같았다. 물론 옵저버 패턴을 사용하지 않더라도 Car 객체가 이동하는 동시에 이동 정도를 출력하게 할 수도 있겠지만, 의존도나 코드의 구조가 훨씬 간단해질 것 같았다.
설계
)
| ⬆️ 머릿속에서 떠도는 생각들을 잡기 위해 급하게 적은 것들...
옵저버 패턴을 학습하는 동시에 설계를 했다. 진짜 머리 많이 썼다.
옵저버 패턴을 처음 배우는데, 이를 mvc 패턴과 결합해서 써먹는 동시에 model 계층이 controller (옵저버) 를 모르게 해야 하니 머리에 쥐 나는줄 알았다.
저렇게 적긴 했지만, 설계와 완전히 똑같이 코드를 작성하지는 않았다. 설계와 구현은 항상 조금씩 달라지는 법이니까...
의도한 설계는 다음과 같다.
Car
는 주제,controller
는 옵저버다.controller
나service
에서Car
객체들에게'이동'
을 요구한다.- 모든
Car
객체는 내부 메서드로 받은랜덤 숫자
를 기준으로 이동하거나, 이동하지 않는다(상태 변경). 동시에controller
에게 상태 변경을 통지한다. (이동하지 않아도 통지해야 한다. 차가 이동하지 못했더라도 결과를 출력해야 하기 때문.) - 상태 변경을 통지받은
controller
는view
에게Car
객체 각각의 이동 정도를 출력할 것을 요청한다.
구현(TDD)
역시 오늘도 구현 할 때 시행착오를 좀 겪었다.
오늘의 주요 쟁점은
1. 어떤 자료구조를 사용할 것인가?
2. split() vs StringTokenizer()
3. 어떻게 Car 객체가 controller 에게 변경 사항을 통지하는 동시에 controller를 알지 못하게 할 것인가?
가 주요 쟁점이었다. 특히 3번이 골때리는 문제였다.
어떤 자료구조를 사용할 것인가?
String 타입으로 입력을 받고 어떻게 조작하기만 하면 됐던 1주차 과제와는 다르게, 이번 2주차에서는 *자료구조가 필요해보였다. *
('배열 대신 Collection을 사용하라' 는 1주차 공통 피드백도 있었다.)
1. 입력을 ',' 단위로 잘라서 어떤 자료구조에 담을 것인가?
2. 자동차 객체는 어떤 자료구조에 담을 것인가?
3. 우승자를 비교할 때 어떤 자료구조를 사용할 것인가?
라는 질문이 생겼다.
1. 입력을 ',' 단위로 잘라서 어떤 자료구조에 담을 것인가?
처음에는 LinkedHashSet
을 사용하려 했다. 입력한 자동차 이름 순서대로 결과를 출력해야 하기에, 중복을 포함하지 않으면서도 입력 순서를 유지할 필요가 있다고 생각했기 때문이다.
그런데 TDD 도중, '중복된 자동차 입력' 을 검증할 수 없다는 사실을 깨달았다. ** 최초 입력된 문자열을 일단 ,
단위로 분리해야 검증을 할 수 있는데, 문제는 문자열을 분리해 LinkedHashSet
에 담는 과정에서 **중복이 이미 사라지기에, 중복을 검증할 수가 없는 것이다.
중복이 없으면 좋은거 아닌가? 라고 생각할 수 있지만, 이는 사용자의 잘못된 입력값을 검증하지 못했다는 얘기이므로 꼭 해결해야 하는 문제였다.
그래서 분리한 문자열을 중복을 허용하는 List에 담아 해결했다.
게임 내에서 중복된 이름이 존재해선 안되지만, 이를 검증하기 위해 오히려 중복을 허용해야 했다.
| ⬆️ 테스트 성공!
2. 자동차 객체는 어떤 자료구조에 담을 것인가?
자동차 객체는 이번에는 LinkedHashSet
에 담기로 했다. List나 Set이나 이번에는 같은 역할을 수행하지만, Set이 대부분의 경우 List보다는 더 빠르기 때문이다. 그리고 출력 순서를 유지해야 하니 LinkedHashSet
을 사용하기로 했다.
3. 우승자를 비교할 때 어떤 자료구조를 사용할 것인가?
일단 '어떻게' 우승자를 비교할 것인가를 생각해봤다. 출력 요구사항처럼 매 순간 우승자를 업데이트 해주기에는 마지막에 최종 출력을 하는것에 비해 너무나도 비효율적이라고 생각했다. 최종 출력은 딱 한번만 Car 객체들의 이동 정도를 비교하면 되니까.
이를 위해 LinkedHashMap
을 사용하기로 했다. 일단 검색에 매우 유리했고, 공동 우승자가 있을 수 있으니 중복 가능한 이동 정도(value)를 바탕으로 자동차 이름(key) 를 가져오는 방법이 좋을 것 같았다. 중복 우승자가 있을 경우 먼저 입력했던 순서대로 출력해야 하니, 순서 유지도 필요했다.
split() vs StringTokenizer()
문자열 분리를 위해 처음에는 StringTokenizer()
메서드를 사용했었다.
이 때는 분리된 입력을 LinkedHashSet
에 담았기 때문에 split()
메서드로 배열을 만들고 만들어진 배열을 또 변환하는 것이 좋지 않다고 생각했었다.
그런데 자료구조를 LinkedHashSet
에서 List
를 사용하기로 한 겸에 split() 사용을 다시 한번 고려해보았다. 일단은 객관적인 성능 지표가 필요했다.
| Java - StringTokenizer 클래스 (+ vs split() 메소드 )
내가 예전에 자바를 공부할 때 작성했던 포스트다. 결론을 요약하자면,
메모리 효율성 측면에서, 일반적인 상황에서
StringTokenizer()
가 더 좋은 것은 맞다. 하지만split()
은StringTokenizer()
보다 더 유연하고, 사용하기 쉽다.
성능 차이도 사실상 그렇게 많이 나지 않고,
무엇보다StringTokenizer()
는 레거시 코드(Legacy Code)이다.
그래도 한번 더 검증이 필요했다. split()
으로 만든 배열을 List
로 변환하는것까지의 경우를 한번 검증해보고 싶었다.
이를 위해 다른 프로젝트를 하나 더 생성해서 jmh
로 벤치마킹을 시도했다.
jmh
는 벤치마킹을 위한 외부 라이브러리인데, 오늘 처음 알고 사용해봤다.
벤치마킹 결과, 성능을 비교하면List+split()
> List+StringTokenizer()
> LinkedHashSet + StringTokenizer()
였다.
비록 편차가 크긴 하지만, split()
을 사용했을 때 성능이 제일 좋았다. 반면에 HashSet
과 StringTokenizer()
를 쓴 조합이 가장 성능이 좋지 않았다. 아무래도 HashSet
의 중복 검사 때문인 것 같았다.
useTokenizer()
에게 가장 유리하게 편차를 적용해서 비교하더라도, useSplit()
이 약 6157735
점, useTokenizer()
이 6521207
점으로 큰 차이가 나지 않았다.
옛날 포스팅에서 분석했던 글 그대로다. 성능도 split()
이 더 좋았고, 편차를 생각하더라도 정말 조금의 차이를 위해 가독성을 희생하는 메리트가 없다.
따라서 StringTokenizer()
대신 split()
을 사용하기로 결정했다.
어떻게 Car 객체가 controller 에게 변경 사항을 통지하는 동시에 controller를 알지 못하게 할 것인가?
이게 제일 어려웠다. 왜냐면, Car
객체가 스스로 상태를 관리하고 옵저버에게 상태 변경을 알려줘야 했는데, 그럴려면 Car
객체의 내부 메서드에서 controller
의 메서드를 호출해야 하기 때문이었다.
| ⬆️ Car 내부의 moveCar()
메서드. 테스트 코드라 아직 랜덤 숫자와 관련된 로직은 없다. 자동차 이동 정도를 표현하는 moved
변수를 변화시키고 옵저버에게 통지하게 한다.
| ⬆️ Car
의 내부 메서드 notifyObservers()
에서 controller
의 이 display()
메서드를 실행시켜서 변화를 통지한다. 이것도 테스트 중이라 아직 러프하다.
책을 몇번이나 다시 보고 포스팅도 몇번이나 보면서 깨달았다.
아! 인터페이스가 이럴 때 쓰는 거구나!
인터페이스를 사용해서 Car
의 notifyObservers()
가 Observer
의 display()
를 호출하게 하면 된다. 그리고 controller
는 Observer
를 상속하게 만든다.
먼저 Car
객체 내부에서 옵저버를 담는 subscribers
리스트를 만들게 하고,
controller
에서 모든 Car
객체에 자기 자신을 옵저버로 등록해준다.
그리고 Car
의 notifyObservers()
메서드에서 controller
가 아닌 Observer
인터페이스의 display()
에 자기 자신을 매개변수로 넘겨준다.
이로서 Car
객체는 controller
를 알지 못하지만, controller
에 자기 자신의 상태 변경을 통지할 수 있게 되었다!!!!!!
여기까지 했을 때, 정말 얻은게 많았다.
먼저, 간단한 옵저버 패턴은 이제 혼자서도 사용할 수 있겠다는 생각이 들었다. 머리로 아는 것과 실제로 사용해 보는건 정말 다르니까. 원리를 알고 쓰는것과 모르고 쓰는건 천지차이라고 생각한다.
원래 옵저버 패턴은 주제와 옵저버가 1:N의 관계를 맺지만, 이 애플리케이션에서는 N:1의 관계를 맺게 됐다. 상황에 맞게 잘 변형해서 적용했다고 생각한다.
그리고 옵저버를 List로 관리하는 건 좀 생각해 볼 필요가 있겠다. 여기서 옵저버는 하나만 있으면 돼서 List로 받을 필요가 없는데, 확장성을 생각하면 그대로 놔둬도 될 것 같고...
그리고 인터페이스와 추상화로 어떻게 결합도와 의존도를 낮추는지, 어떻게 객체지향을 달성하는지 실감하지 못했는데 '아, 이런게 인터페이스구나' 하고 직관적으로 알게 되었다.
물론 인터페이스의 역할은 이 예시에서 보여준 것만이 아니겠지만, 두루뭉술하게 이해하게 되던 인터페이스를 좀 더 실전적으로 체득하게 되었다.
여기까지만 하고 잘려 했는데, 내가 설계하는 이 애플리케이션 전체의 이해도가 갑자기 십분 이해되면서 우승자 결정 기능까지 작성하게 되었다. 사실상 남은건 테스트, 네이밍 수정, 메서드 분리, 랜덤 숫자 생성 코드 작성 정도만 남은 것 같다.
마무리..
오늘 정말 많은 걸 배웠다. 특히 새로운 패턴을 학습하고 사용할 수 있게 된게 정말 좋았다. 자료구조를 무턱대고 쓰지 않고 장단점을 비교하는 습관을 가지려는 것도 좋았고, 인터페이스를 조금이나마 직관적으로 깨닫게 된 것도 좋았다.
시간이 벌써 새벽 3시다. 늦게까지 작업했지만, 한번 영감이 떠오르고 번뜩일 때 해치워 버려서 시간상 효율은 더 좋지 않았을까? 내일 다시 하려면 까먹을까봐 두려웠다. 난 만족했다! 완성될 결과물이 벌써 기대가 된다.
새로 알게된 점
1. 옵저버 패턴
2. mvc 패턴에 옵저버 패턴 결합
3. jmh, 벤치마킹
4. 인터페이스의 사용법 중 하나 - 의존성 분리!
좋았던 점
1. 설계에 공을 많이 들인 것
설계에 힘을 많이 실으니 확실히 구현 시간이 대폭 절감됨을 체감했다!
2. 옵저버 패턴을 학습하고 사용할 수 있었던 것
솔직히 새로운 패턴을 배우다 이도저도 아니게 될까봐 조금 걱정했는데, 다행히 잘 사용할 수 있었다.
3. mvc 패턴에 옵저버 패턴을 결합한 것
결합에 성공했을 때 쾌감은 말로 다할 수 없다... 도파민 한껏 충전했다.
4. 벤치마킹으로 로직의 성능을 직접 비교해본 것
벤치마킹을 jmh로 간편히 할 수 있다는 것을 알게 되었다! 너무 좋은 도구인 것 같다.
5. 이전에 내가 썻던 포스팅이 지금 내게 도움이 되는 것
6. 인터페이스를 조금 체득한 것
아쉬웠던 점
1. 삘 받아 구현하다가 TDD를 잘 사용하지 못한것. 다시 한번 테스트를 차근차근 해봐야 겠다.
도움이 된 자료들
| 💠 옵저버(Observer) 패턴 - 완벽 마스터하기
| 옵저버 패턴
| Java - StringTokenizer 클래스 (+ vs split() 메소드 )
| [책]개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴
| [JUnit] JUnit5 예외 테스트코드 작성(assertThrows)
| [Java] gradle 환경에서 JMH를 사용하여 벤치마킹하기
'우아한테크코스' 카테고리의 다른 글
[우테코 6기 프리코스] 2주차 - 4일차 회고 (0) | 2023.12.20 |
---|---|
[우테코 6기 프리코스] 2주차 - 3일차 회고 (0) | 2023.12.20 |
[우테코 6기 프리코스] 2주차 - 1일차 회고 (1) | 2023.12.20 |
[우테코 6기 프리코스] 1주차 - 7일차 + 1주 전체 회고 (1) | 2023.12.20 |
[우테코 6기 프리코스] 1주차 - 6일차 회고 (0) | 2023.12.20 |