← ALL ENTRIES

조합 메소드로 run 메소드 리팩토링하기


게임 클래스의 복잡한 run 메소드를 단일 추상화 수준 원칙에 따라 조합 메소드로 리팩토링하는 실전 과정. 가독성·중복 제거·의존성 고립을 위한 메소드 추출 기법과, 추출한 메소드를 적절한 클래스로 옮기는 책임의 이동까지 다룬다.

1. 리팩토링 대상: run 메소드

  • run 메소드는 이해·수정·테스트가 모두 어려운 전형적인 나쁜 메소드다.
  • 단일 추상화 수준 원칙(SLAP)에 따라 **조합 메소드(Composed Method)**로 리팩토링하기 좋은 후보.

📝 NOTE 조합 메소드 동일한 추상화 수준의 코드들로만 구성되어, 각 단계가 문장처럼 읽히는 메소드.

2. 리팩토링 절차: 주석 → 메소드 추출

리팩토링의 첫 단계는 비슷한 목적의 코드를 묶고 그 위에 주석을 다는 것.

2-1. 코드를 목적별로 묶고 주석 달기

run 메소드는 세 덩어리로 구분된다.

위치하는 일주석
상단환영 문구·방 위치·명령어 목록 출력 (게임 소개)환영 문구 출력
중간루프 돌며 입력 받고 파싱하고 명령어 실행 (플레이 플로우)GamePlay
하단게임 종료 문구 출력 (한 줄이지만 의도 차이가 큼)작별 문구 출력

💡 TIP 주석만 떼어 읽으면 메소드가 하는 일이 한눈에 보인다: “run은 환영 문구를 출력하고, 게임 플레이를 실행하고, 작별 문구를 출력한다.” 코드 길이와 무관하게 실행 단계를 동일한 추상화 수준에서 설명한다.

2-2. 주석을 메소드 이름으로 변환

📌 IMPORTANT 좋은 메소드 이름 = 호출하는 클라이언트의 의도를 표현하는 이름. 이미 달아둔 주석이 곧 클라이언트(run)의 의도이므로, 주석을 그대로 메소드 이름으로 바꾼다.

  • 세 덩어리를 welcome, play, farewell로 추출.
  • 추출 후 주석은 불필요하므로 삭제.
  • 결과: run은 추상화 수준이 동일한 조합 메소드가 되어 이해·수정·확장이 쉬워짐.

3. 재귀적 적용: welcome도 조합 메소드로

추출한 메소드도 다시 조합 메소드로 만들 수 있다. welcome은 세 정보를 출력하지만 무엇을 출력하는지 드러나지 않는다.

  • 출력 정보에 따라 코드를 나누고 주석을 붙임 → 각 부분을 메소드로 추출:
    • showGreeting (인삿말)
    • showRoom (현재 방 정보)
    • showHelp (도움말)
  • 이제 welcome은 문장처럼 읽힌다: “인사말을 표시하고, 현재 방 정보를 표시하고, 도움말을 표시한다.”
  • 세부 내용이 궁금하면 해당 메소드로 이동해 읽으면 됨.

4. 언제까지 추출하는가? — 한 가지 작업 / 한 가지 이유

📌 IMPORTANT 메소드가 **오직 한 가지 작업(= 클라이언트가 달성하려는 한 가지 목적)**만 수행할 때까지 추출한다.

  • “한 가지 작업”은 **“한 가지 이유로만 변경됨”**으로 바꿔 표현할 수 있다.
  • 예) showRoom을 방 이름과 설명으로 더 쪼개는 건 무의미 — 클라이언트는 둘이 함께 출력되길 원하기 때문. 변경 관점도 클라이언트의 의도를 포함한다.

4-1. 변경 이유와 응집도

📝 NOTE 응집도

  • 여러 이유로 변경되는 메소드 → 응집도 낮음
  • 한 가지 이유로만 변경되는 메소드 → 응집도 높음
  • 리팩토링 전 welcome은 환영 문구·방 정보·명령어 목록 어느 것을 바꿔도 수정됨 → 응집도 낮음.
  • showRoom은 방 정보를 바꿀 때만 수정됨 → 응집도 높음.

5. 재사용의 이점: look / help 명령어 추가

응집도 높은 작은 메소드는 여러 곳에서 재사용 가능하다.

  • look 명령어 → 현재 방 정보 출력: 이미 추출한 showRoom 재사용.
  • help 명령어 → 도움말 출력: 이미 추출한 showHelp 재사용.
  • 파싱 switch문에 look/help 케이스만 추가하면 끝. 작게 나누지 않았다면 재사용이 어려웠을 것.

6. play 메소드 리팩토링

play는 여전히 길고 복잡하다. 지난 시간의 세 가지 목적으로 메소드를 추출한다.

6-1. 가독성 향상을 위한 추출

  • 이동 로직: switch문 안에 숨은 방향별 위치 변경 → moveEast, moveWest, moveSouth, moveNorth로 추출. → play를 보면 “4방향 이동”임이 바로 보임.
  • running 변수의 의도 추출: 인스턴스 변수 running의 세 쓰임을 의도가 드러나는 메소드로 추출.
    • true 대입 = 게임 시작 → start()
    • while 조건 체크 = 실행 중 여부 판단 → isRunning()
    • false 변경 = 실행 종료 → stop()

6-2. 중복 제거를 위한 추출

  • 잘못된 명령 입력 시 출력문 중복 → showOnNone으로 추출.
  • move 메소드들의 공통 로직 정리:
    • 이동 후 방 정보 출력 → 기존 showRoom 재사용.
    • X·Y 좌표로 방 찾기 중복 → roomAt으로 추출.
    • “이동 불가” 오류 메시지 중복 → showBlocked로 추출.

6-3. tryMove로 실행 플로우 통합

  • 추출 후 move 메소드들의 형태가 거의 동일해짐(x/y 증감값과 범위 체크 변수만 다름).
  • 공통 플로우를 tryMove로 추출하고 X·Y 증가값을 파라미터로 전달 → 중복 제거.
  • tryMove 안 복잡한 조건문(이동 가능 여부 판단) → blocked 메소드로 추출해 목적 명시.
  • blocked 조건문의 추상화 수준 정리:
    • 방 이동 불가 판단은 roomAt으로 하지만, 지도 밖 이탈은 X·Y를 직접 비교 → 추상화 수준 불일치.
    • X·Y 직접 비교 부분을 excluded로 추출해 추상화 수준을 동일하게 맞춤.

6-4. 의존성 고립을 위한 추출

📝 NOTE 의존성 고립 외부(콘솔·키보드 등)에 의존하는 저수준 코드를 별도 메소드로 분리해, 고수준 게임 로직에서 격리한다.

  • 입력 프롬프트 출력(콘솔 의존) → showPrompt로 추출.
  • Scanner로 키보드 입력 받기(저수준) → input으로 추출.
  • 남은 파싱 switch문의 추상화 수준이 상대적으로 낮아 보임 → parseCommand로 추출.

💡 TIP 여기서 더 추출하면 과도하게 세분화된다. 메소드 추출은 여기까지. 이후는 클래스 수준에서 개선.

7. 다음 단계: 책임의 이동 (클래스 수준 개선)

📝 NOTE 책임의 이동 추출한 메소드를 적절한 클래스로 옮기는 것. 한 클래스에서 다른 클래스로 메소드를 이동시키는 작업을 객체지향에서 책임의 이동이라 부른다.

  • 책임 이동에 널리 쓰이는 두 가지 방법:
    1. 값 객체(Value Object)
    2. 단일 책임 원칙(SRP)
  • 다음 시간에 값 객체부터 다룬다.

핵심 요약

  • 조합 메소드 리팩토링의 정석 절차: 목적별로 묶기 → 주석 달기 → 주석을 메소드 이름으로 → 주석 삭제.
  • 좋은 메소드 이름은 구현이 아니라 호출하는 클라이언트의 의도를 표현한다.
  • 추출의 종료 기준은 “한 가지 작업 = 한 가지 이유로만 변경”, 이는 곧 높은 응집도를 의미한다.
  • 메소드 추출의 세 목적: 가독성 향상 · 중복 제거 · 의존성 고립. 응집도 높은 작은 메소드는 재사용도 쉽다.
  • 메소드 추출 이후의 개선은 클래스 수준에서 책임의 이동(값 객체, SRP)으로 이어진다.