강의

멘토링

커뮤니티

인프런 커뮤니티 질문&답변

mit.inhrdev님의 프로필 이미지
mit.inhrdev

작성한 질문수

오브젝트 - 설계 원칙편

3-2. 조합 메서드로 리팩터링하기

3-2 메서드를 얼마나 작게 나누는게 적절한가요?

해결된 질문

작성

·

34

·

수정됨

1

안녕하세요. 좋은 강의 감사합니다. 덕분에 객체지향에 대한 이해가 많이 정리되었습니다.


3-2 강의를 보다 메서드 크기 관련해서 궁금한 점이 있어 질문드립니다.

콘솔 입력 의존성(12:17) 부분에서 입력을 분리하는 예시로 아래와 같이 nextLine() 호출을 별도 메서드로 추출하신 것으로 이해했습니다.

private String input(){
    return Scanner.nextLine();
}

그런데 이 관점이라면, 입력 “수집(I/O)” 뿐 아니라 “입력 파싱(문법)”도 별도의 책임으로 분리해야 하는 것 아닌지 고민이 됩니다.
예를 들어:

private String[] inputParsing(String input){
    return input().toLowerCase().trim().split("\\s+");
}

강사님의 강의 및 "객체지향의 사실과 오해" 책을 읽고 난 뒤, SRP의 책임을 ‘하나의 변경 이유’로 보았습니다.

위의 콘솔 입력(nextLine)을 분리한 건 입력 장치 변경이라는 액터에 대한 책임 분리로 이해했습니다.
그렇다면 입력 파싱(toLowerCase/trim/split)은 명령 문법 변경이라는 또 다른 액터에 대한 책임이므로, 이 역시 별도로 분리해야 SRP를 완전히 지키는 게 아닐까요?

 

물론 이렇게까지 분리하면 가독성이 떨어질 수 있고, 실제로는 변경 가능성도 낮아 “가치가 작은 분리”로 느껴지기도 합니다. 강사님의 의견이 궁금합니다!

+ 외에도 추상화 수준을 맞추기 위한 메서드 추출(9:25) 강의 자료에

image.png

isRunning 함수의 반환이 void로 되어있으나 실제 내부에선 running == true를 반환합니다. github 코드 상에는 해당 함수가 없어보입니다.

답변 3

0

조영호님의 프로필 이미지
조영호
지식공유자

mint.inhrdev님 안녕하세요.

좋은 질문 남겨주셔서 감사합니다.

 

먼저 SRP라는 용어는 메서드가 아니라 클래스 또는 모듈 단위에서 사용하는 용어입니다.

메서드 수준에서는 단순히 응집도라는 개념을 사용하고 SRP라는 용어를 사용하지 않습니다.

기본적으로 SRP에서 말하는 개념과도 상관이 없고 강의에서도 조합 메서드 패턴이라고 표현하고 SRP라는 용어는 사용하지 않고 있습니다.

 

조합 메서드로 리팩터링할 때 변경을 고민하기도 하지만 변경만이 메서드를 나누는 절대적인 기준은 아닙니다.

강의에서도 설명드렸던 것처럼 조합 메서드의 목적은 추상화를 동일한 수준으로 맞춰서 읽고 이해하기 쉽도록 만드는 것이기 때문에 주된 목적은 가독성을 향상시키는 것입나다.

따라서 질문을 SRP vs 가독성이라는 표현 대신 가독성을 향상시키기 위해 메서드를 얼마나 작게 나누는게 적절한가로 바꿔서 표현하는게 좋겠습니다.

 

먼저 질문에서 말씀하신 아래 코드는 “입력 파싱”이 아니라 공백을 기준으로 “토큰을 분리”하는 작업이라는 점을 짚고 넘어가야 할 것 같아요.

input().toLowerCase().trim().split("\\\\s+");

실제로 입력을 파싱하는 로직은 parseCommand() 메서드에서 처리하고 있습니다.

private void parseCommand(String input) {
  String[] commands = input.toLowerCase().trim().split("\\\\s+");
  switch (commands[0]) {
    case "go" -> {
      switch (commands[1]) {
        case "north" -> moveNorth();
        case "south" -> moveSouth();
        case "east" -> moveEast();
        case "west" -> moveWest();
        default -> showUnknownCommand();
      }
    }
    case "look" -> showRoom();
    case "help" -> showHelp();
    case "quit" -> stop();
    default -> showUnknownCommand();
  }
}

이제 질문은 위 코드를 아래처럼 변경하는게 parseCommand() 메서드 내부의 추상화 수준을 일관성 있게 만드는데 도움이 되느냐가 될겁니다.

private void parseCommand(String input) {
  String[] commands = tokenize(input);
  switch (commands[0]) {
    case "go" -> {
      switch (commands[1]) {
        case "north" -> moveNorth();
        case "south" -> moveSouth();
        case "east" -> moveEast();
        case "west" -> moveWest();
        default -> showUnknownCommand();
      }
    }
    case "look" -> showRoom();
    case "help" -> showHelp();
    case "quit" -> stop();
    default -> showUnknownCommand();
  }
}

private String[] tokenize(String input) {
  return input.toLowerCase().trim().split("\\\\s+");
}

아래 메서드가 전체적인 추상화 수준에서 일관성이 있어서 코드를 이해하기 쉽게 만든다고 판단된다면 두번째 코드처럼 분리하고, 해당 로직이 분리할 정도로 복잡하지 않거나 분리할 정도로 중요한 부분이 아니라면 그대로 두면 됩니다.

제 개인적으로는 배열을 공백으로 분리하는 작업이 별도의 메서드로 분리할 정도로 중요하지 않고 parseCommand 자체가 switch 문을 이용해서 배열을 파싱하는 전체적으로 추상화 수준이 낮은 코드이기 때문에 메서드 전체의 추상화 수준에서는 이 상태로 둬도 괜찮다고 판단했습니다.

만약 이 메서드를 동일한 추상화 수준으로 맞춘다면 아래 코드처럼 변경하는게 좋을겁니다.

private void parseCommand(String input) {
  parseTokens(tokenize(input));
}

private String[] tokenize(String input) {
  return input.toLowerCase().trim().split("\\\\s+");
}

private void parseTokens(String[] command) {
  switch (commands[0]) {
    case "go" -> {
      switch (commands[1]) {
        case "north" -> moveNorth();
        case "south" -> moveSouth();
        case "east" -> moveEast();
        case "west" -> moveWest();
        default -> showUnknownCommand();
      }
    }
    case "look" -> showRoom();
    case "help" -> showHelp();
    case "quit" -> stop();
    default -> showUnknownCommand();
  }
}

결과적으로 paerseCommand() 메서드를 두 개의 작은 메서드를 호출하는 메서드로 나눌 것인가, 아니면 현재의 코드를 그대로 둘 것인가의 결정인데, 저 같은 경우에는 위 코드가 이전 코드보다 그렇게 개선된 것으로 보이지 않고 오히려 너무 세분화돼 보여서 파싱 흐름을 이해하기 어렵게 만들기 때문에 그대로 뒀다고 보시면 됩니다.

만약 원칙에 따라 두 개의 작은 메서드로 나누는게 코드를 이해하기에 더 좋다고 판단된다면 나누셔도 좋습니다.

 

두번째로 언급하신 isRunning 메서드는 장표에 오류가 있고 코드에서는 누락되었네요.

아래와 같이 isRunning() 메서드로 추출하는게 맞습니다.

 

private boolean isRunning() {
  return running == true;
}

 

 

너무 많은 장표를 만들다보니 확인하지 못한 부분이 많은데 확인해 주셔서 감사합니다.

이 부분은 수정해서 커밋해 놓을게요. 🙂

0

mit.inhrdev님의 프로필 이미지
mit.inhrdev
질문자

첫번째 질문에 대한 대략적인 대답은 5-4 강의에서 확인하였습니다. 그러나 여전히 SRP vs 가독성에 대해서는 확립이 되어있지 않습니다. 이에 대해 의견 공유해주시면 감사하겠습니다!

외에도 isRunning 함수가 이후 강의에서도 사용되는데 코드와 비교해 한번 확인 부탁드립니다.

0

안녕하세요, 인프런 AI 인턴입니다. mit.inhrdev님.

SRP(단일 책임 원칙)에 대한 질문을 주셨네요. SRP는 객체가 하나의 책임, 즉 변경의 이유를 가진다고 설명합니다. 강의를 통해 콘솔 입력의 nextLine()을 별도 메서드로 추출한 이유는 입력 장치의 변경이라는 변경 이유 때문이었죠.

질문하신 입력 파싱( toLowerCase, trim, split)에 대한 부분도 명령 문법 변경이라는 변경 이유가 있으므로 이 역시 별도로 분리하여 관리하는 것이 SRP를 엄밀히 따르는 방법일 것입니다. 그러나 이러한 분리는 실제로 비즈니스 규칙이나 동작이 자주 변할 가능성이 있거나 복잡한 경우에 더욱 강조될만한 것이고, 일반적으로 간단한 경우엔 과도한 세분화가 오히려 가독성을 떨어뜨릴 수 있습니다. 가독성과 책임 분리 사이의 균형은 상황에 따라 맞춰가는 것이 중요합니다.

메서드 추출과 관련된 자료에 대해 말씀을 주셨는데, Java 코드의 특정 기능을 다루는 예시 중 isRunning 함수의 반환이 void로 되어 있어 실제 코드를 올바르게 반영하지 않은 것 같습니다. 다른 코드 베이스에 문제가 발생한다면 그에 맞춰 구체적인 상황을 고려한 변경이 이루어져야 할 것입니다.

SRP와 관련해 더 심화된 여러 예시들이 있으니 SRP에 대한 세부적인 논의와 적용 방법을 참고하시면 이해에 큰 도움이 될 것입니다. 또한, 강의 내에서 발견된 내용적 오류가 느껴지신다면, 직접적인 피드백을 강사님께 전달 드리는 것도 강의 개선에 도움이 될 것입니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

mit.inhrdev님의 프로필 이미지
mit.inhrdev

작성한 질문수

질문하기