👓 추상 클래스를 왜?

블랙잭 미션 피드백 강의의 핵심은 “객체지향의 다형성을 이용한 조건문 줄이기” 였다.

  • 게임 내 규칙을 자바 객체로 추상화한다.
    • 힛(Hit): 처음 2장의 상태에서 카드를 더 뽑는 것
    • 스테이(Stay): 카드를 더 뽑지 않고 차례를 마치는 것
    • 블랙잭(Blackjack): 처음 두 장의 카드 합이 21인 경우, 베팅 금액의 1.5배
    • 버스트(Bust): 카드 총합이 21을 넘는 경우. 배당금을 잃는다.
  • 현재 상태에서 다음 상태의 객체를 생성하는 역할을 현재 상태가 담당하도록 한다.

제이슨이 4가지 상태를 유기적으로 전환시키는 유한상태머신(Finite-State-Machine)에 대해 설명하면서 자바에서 유한상태머신을 상태패턴으로 구현하는 방법에 대해 라이브 코딩을 진행했고, 결과적으로 아래와 같은 구조가 만들어졌다.

image

(아름다워)

짱짱하고 아름다운 구조의 다이어그램을 보며 감탄했지만, 인터페이스와 상속에 대해 막연했던 나는 ‘상속을 지양하라고 하면서 왜 추상 클래스를 사용해서 연결한거지?’, ‘인터페이스와 상속이 혼용된 기준이 뭐야?’ 라는 고민이 계속해서 피어났다.

‘Top-Down 방향으로 개발을 진행한다고 가정해보자. State 인터페이스를 통해서 먼저 구현해야할 메서드들을 강제하고, 카드를 뽑는중인 Running이랑 다 뽑은 Finished로 나눠. 그러고 게임 내 규칙 4가지를 분배하면… 그럼 저 Started는 뭐야?’

‘Bottom-Up 방향으로 개발을 진행한다고 가정하면, 게임 내 규칙 4가지 중에 Finished 3가지로 묶고 나머지 Hit을 Running으로 묶어. 그리고 Started로 묶고… 그럼 State가 존재하는 이유는 뭐야? 무슨 메서드들이 있는지 보여주는 명세서 같은 역할인가?’

혼자서 이리저리 짱구를 굴려봤지만, 결국 인터페이스와 추상 클래스가 혼용된 이유를 알아내지 못했다. 결국 킹갓제네럴마이티엠페러 제이슨한테 DM으로 질문을 드렸고, 답변과 어려가지 힌트를 얻을 수 있었다.


👓 아는 것부터 모르는 것 순서대로 구현

우선 Top-Down 이고 Bottom-Up 이고… 모두 잘못된 생각이다. 그렇게해서 추상클래스가 섞여들어간 것이 아니다.

라이브코딩에서 제이슨은 게임 내 4가지 규칙부터 시작해서 Running, Finished, State 단계로 묶어 올라가거나, State부터 시작해 4가지 규칙으로 분리시키며 내려가는 등을 한 것이 아니었다. “규칙들을 State 객체 하나로 묶어서 관리한다” 는 포괄적이고 간단한 설계를 머리속으로 진행한 상태에서 시작했다.

우선 State 인터페이스와 Blackjack, Stay, Bust, Hit 4가지 구현 클래스를 만들었다. 그리고 Blackjack, Stay, Bust 3가지 구현 클래스에서 중복으로 사용되는 메서드를 묶기 위해 Finished 추상 클래스를 만든다. HitRunning으로 한차례 추상화 시켰고, FinishedRunning에서 중복사용 되는 메서드를 한 번 더 추상화하기 위해 Started 라는 추상 클래스를 사용했다.

즉 자신이 아는 것(4가지 규칙, State)부터 모두 구현해두고 모르는 것들을 점차적으로 구현해나가는 방식이었다.


👓 TDD는 개발 방법론이지 설계론이 아니다

제이슨이 라이브 코딩을 진행하면서 TDD를 준수한 것 때문에 “TDD를 지키다보면 자연스럽게 저런 추상화가 가능한건가?” 라는 오해가 생길수 있었다. 그에 대해서 제이슨이 “TDD는 개발 방법론이지 설계론이 아니다.” 라는 힌트를 주셨다.

제이슨은 이미 “규칙들을 State 객체 하나로 묶어서 관리한다” 라는 설계를 마친 상태로 구현을 시작했다. 절대로 TDD로 “State 객체로 묶는다” 라는 설계가 가능했던 것이 아니다. 오해하지 말자.


👓 그래서 왜 혼용되는데?

Running, Finished 와 같은 추상 단계가 생겨났는지, 왜 저런 다이어 그램이 그려지는지도 모두 이해했다. 결국 추상 클래스가 혼용되는 이유는 무엇이었을까? 왜 RunningFinished를 인터페이스로 만들지 않았을까?

이유는 허무하게 간단했다. 인터페이스끼리는 연결이 안된다고 한다. 그게 가장 큰 이유였다.

게임 내 4가지 규칙을 State로 추상화하는 과정에서 추상화 단계를 Running,Finished 등으로 조금 더 분기시켰는데 이것들을 서로 연결하기 위해서 추상 클래스를 사용한 것 뿐이었다. (중복 사용되는 메서드들을 제거하기 위함도 있다.)

게다가 4가지 규칙과 추상 클래스들은 is-a 관계가 명확하게 이어지지 않는가? 만일 is-a 관계가 명확하게 이어지지 않을 경우 State 인터페이스 단계부터 잘못 설계된것이라고 생각하면 된다. 상속보다 컴포지션, 상속보다 인터페이스 등의 개념이 Running, Finished 같은 중간 단계에 위치한 추상 객체를 말하는 것이 아니라, State 와 같은 최상단 추상 객체를 대상으로 적용되는 것 같다.

“상속 별거 없네!”


👓 첫 개발자가 고생해야 다음 개발자들이 편하다

제이슨의 답변과 힌트를 받고 다이어그램 구조를 천천히 살펴보던 중, Hit을 한 단계 추상화시킨 Running 이 필요한 이유에 대해서 고민이 생겼다. Finished의 경우 3가지 규칙을 추상화시켜서 중복 사용되는 메서드들을 제거하는 등의 역할을 톡톡히 하고 있었지만, Running은 별 쓸모가 없어 보였다.

욘, 포츈과 함께 토론을 진행하면서 2가지 결론을 얻을 수 있었다.


  • Finished와 추상 단계를 맞춰주기 위해서 Running이 등장한 것이다.
  • 추후에 Hit 말고 또다른 규칙이 생성되면, Running에 연결하기 편하게 미리 만든 것이다.

서로 어느정도는 정답일거라는 걸 인지했지만, 오피셜을 확인하고 싶어서 또 제이슨에게 질문했다.

결론은 모두 맞는 말이었다. 다만 관점이 조금 달랐는데 우리는 단순히 “나”라는 관점에서만 생각했고, 제이슨은 “나중에 다른 개발자가 프로젝트를 이어 받았을 때” 라는 관점을 제시해주셨다.

다른 개발자가 다이어그램을 확인할 때 구조가 한 눈에 잘 들어오도록 추상 단계를 맞춰주는 것도 맞고, 다른 개발자가 추가 규칙을 구현할 때 Running 추상 단계를 추가할 일 없이 곧장 이어붙여서 사용 할 수 있도록 미리 만드는 의도도 있다고 한다.

“그래서 첫 개발자가 고생해야 다음 개발자들이 편하다는 말이 있는거에요.”

과연 한 방에 와닿는 이야기다.


킹갓제네럴마이티엠페러 제이슨의 고생으로 막연했던 상속과 인터페이스가 조금은 친숙해진 기분이다. 앞으로도 제이슨 자주 괴롭혀야지.

끗!

댓글남기기