콘솔 입력 객체의 역할에 맞지 않는 기능을 분리하도록 시도하기

2023. 3. 17. 14:28JAVA

개요

코드 스쿼드의 사다리게임을 콘솔 입력을 기반으로 구현하고 있었습니다. 제가 만든 사다리 게임에서는 사다리 게임에 참여하는 사람들의 이름과 사다리 게임의 도착지(또는 당첨 내용이라고 불림) 내용을 콘솔을 통해서 입력받아 게임을 진행합니다. 그런데 사용자로부터 콘솔 입력을 받는 객체가 부적절한 입력을 받았을때 검증하고 다시 입력받게 하거나 문자열로 입력받은 입력값들을 객체로 생성하여 반환하는 것을 하나의 메서드에서 수행하는 패턴을 발견하게 되었습니다. 이때 들었던 생각은 "사용자로부터 그저 입력을 받는 하나의 객체가 이렇게 복잡한 제어를 하는 것이 역할에 맞는가?"라는 생각이 들게 되었습니다. 따라서 이 글에서 소개하는 내용은 하나의 객체가 복잡한 과정(사용자로부터 입력받기, 부적절한 입력시 다시 입력받게 하기, 경고 메시지 출력, 입력받은 문자열 값을 기반으로 반환할 객체 생성 등)을 수행하는 메서드를 어떻게 객체를 생성하여 기능을 분리하였는지를 시도하는 내용을 소개합니다. 이 글은 저의 주관적인 경험을 바탕으로 객체 분리를 시도한 것이기 때문에 잘못된 설계일 수 있습니다.

 

문제 원인

문제가 된다고 생각하는 핵심적인 코드는 다음과 같았습니다.

public class LadderConsoleReader implements LadderReader {

    private static final int MINIMUM_PERSON = 2;
    private static final int MINIMUM_HEIGHT = 1;

    private final BufferedReader reader;
    private final LadderWriter ladderWriter;

    public LadderConsoleReader(BufferedReader reader,
        LadderWriter ladderWriter) {
        this.reader = reader;
        this.ladderWriter = ladderWriter;
    }

    @Override
    public Names readNameOfPeople() {
        Optional<Names> optionalNames = Optional.empty();
        while (optionalNames.isEmpty()) {
            ladderWriter.writeNamesOfPeopleIntro(); // "이름을 입력해주세요" 안내문
            optionalNames = readNamesOfPeopleTextAndToStringList();
        }
        return optionalNames.get();
    }

    private Optional<Names> readNamesOfPeopleTextAndToStringList() {
        Optional<Names> names = Optional.empty();
        try {
            String text = reader.readLine(); // text = "pobi, hounx, jk"
            names = Optional.of(new Names(text, MINIMUM_PERSON));
        } catch (InvalidNameFormatOfPeopleException | InvalidCountOfPeopleException e) {
            ladderWriter.write(e.getMessage()); // "잘못된 입력입니다." 출력
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return names;
    }

위 코드에서 LadderConsoleReader 객체의 역할은 사용자로부터 콘솔 입력을 받아서 문자열 입력에 대한 객체로 생성하여 반환하는 역할을 수행합니다. 예를 들어 위 코드에서 readNameOfPeople() 메소드와 도중에 호출하는 readNamesOfPeopleTextAndToStringList() 메서드는 다음과 같은 것들을 수행합니다.

  • 사용자가 부적절한 사람 이름 입력시 다시 입력받게 제어하기(while문을 통한 입력이 제대로 저장되었는지 검사)
  • 사용자로부터 한줄 입력받기 (reader.readLine)
  • Names 객체 생성 (new Names(text, MINIMUM_PERSON))
    1. 생성자 매개변수로 "pobi, hounx"와 같은 문자열을 전달합니다.
    2. 생성자 내부에서 "pobi, hounx"와 같은 문자열을 대상으로 쉼표를 기준으로 분리하여 ["pobi", "hounx"] 같은 형태의 문자열 타입 배열을 생성합니다.
    3. 문자열 타입 배열을 리스트 타입으로 변환합니다. (String[] -> List<String>)
    4. List<String> 타입의 생성한 리스트를 Names 클래스의 필드 멤버 List<String> names에 저장합니다.

위 코드의 수행과정을 그림으로 표현하면 다음과 같습니다.

위와 같이 readNameOfPeople() 메서드를 구현하면서 들었던 문제는 다음과 같았습니다.

  1. LadderConsoleReader 객체는 사용자로부터 문자열을 입력받는 역할이지 잘못된 입력에 대해서 다시 입력하라고 제어하는 책임은 역할에 맞지 않는다고 생각하였습니다.
  2. 입력받은 문자열을 기반으로 객체를 생성하여 반환하는 것은 LadderConsoleReader 객체 역할에 맞지 않는다고 생각하였습니다.
  3. 위 그림에서 3번 과정인 사용자로부터 한 줄을 입력받는 것이 온전한 LadderConsoleReader 객체가 수행하는 역할이라고 생각하였습니다.

따라서 위와 같은 생각으로 LadderConsoleReader의 readNameOfPeople() 메서드와 readNamesOfPeopleTextAndToStringList() 머스드는 단순히 사용자로부터 문자열을 입력받는 역할이고 사용자의 입력 순서를 제어하거나 객체를 생성하는 것은 다른 객체의 역할이라고 생각하였습니다.

 

즉, 하나의 객체가 메소드를 여러개로 나누었다고는 하지만 객체 내부에서만 메소드로 나누었을 뿐이지 그 객체 자체가 주어진 역할과는 맞지 않은 책임을 private 메소드로 호출하는 문제를 발견하였습니다.

 

해결 시도

LadderConsoleReader 객체에서 사용자가 부적절한 입력을 할시에 다시 입력받게 제어하는 기능을 분리하기 위해서 LadderGameConsoleController라는 이름의 객체를 생성하였습니다.

public class LadderGameConsoleController {

    private static final String ALL = "all";
    private static final String EXIT = "춘식이";

    private final LadderReader ladderReader;
    private final LadderWriter ladderWriter;

    public LadderGameConsoleController(LadderReader ladderReader, LadderWriter ladderWriter) {
        this.ladderReader = ladderReader;
        this.ladderWriter = ladderWriter;
    }

    public Names getNamesOfPeoples() {
        Optional<Names> optionalNames = Optional.empty();
        while (optionalNames.isEmpty()) {
            String[] nameArray = ladderReader.readNamesOfPeoples();
            optionalNames = createNames(nameArray);
        }
        return optionalNames.get();
    }

    // nameArray = ["pobi", "hounx"], -> Names = [new Name("pobi"), new Name("hounx")]
    private Optional<Names> createNames(String[] nameArray) {
        Optional<Names> optionalNames = Optional.empty();
        NamesFactory namesFactory = new NamesFactory();
        try {
            Names names = namesFactory.createNames(nameArray);
            optionalNames = Optional.of(names);
        } catch (InvalidNameFormatOfPeopleException | InvalidCountOfPeopleException e) {
            ladderWriter.write(e.getMessage()); // 적절하지 하지 입력 메시지
        }
        return optionalNames;
    }
    //...
}
  • LadderGameConsoleController 객체는 LadderReader와 LadderWriter 객체를 외부에서 주입받아서 멤버로 가지고 있습니다.
  • 사용자로부터 부적절한 입력을 받을시 다시 입력을 받게 하는 while문을 LadderConsoleReader 객체로부터 분리하여 getNamesOfPeoples() 라는 메서드를 생성하였습니다.
  • 사용자로부터 입력받는 부분은 구성요소인 ladderReader 객체에게 위임하여 받도록 하였습니다.
    • ladderReader 객체는 사용자로부터 문자열을 입력받고 간단한 정수나 문자열타입 배열로 변환하여 반환합니다.
  • Optional<Names> 타입의 optionalNames 객체를 검사하여 비어있는 동안에는 부적절한 입력이나 아직 입력을 받지 못한다고 생각하여 계속 입력받을 수 있도록 순서를 제어합니다.
  • createNames(String[] nameArray) 메서드를 호출하여 NamesFactory 객체에게 Names 객체를 생성하도록 지시합니다.
    • createNames 메서드의 반환타입은 사용자가 적절한 입력을 하였는지 하기 위해서 Optional 객체로 래핑하여 반환합니다.
    • Optional 객체의 값이 있느냐 없느냐의 유무에 따라서 다시 입력받을 수 있도록 제어합니다.

위와 같이 LadderGameConsoleController 객체의 getNamesOfPeople 메소드의 흐름을 그림으로 표현하면 다음과 같습니다.

기존 문제라고 생각했던 사용자의 입력 순서에 제어, 객체 생성과 같은 역할을 LadderConsoleReader 객체가 수행했다면 개선된 설계에서는 사용자의 입력 순서 제어는 LadderGameConsoleController 객체가 맡게 하고 객체 생성 같은 부분은 NamesFactory가 맡게 변경하였습니다. 그리고 사용자로부터 순수한 입력을 부분은 LadderCosnoleReader 객체가 수행하도록 두었습니다.

 

정리

기존 사용자로부터 콘솔 입력을 받는 객체가 사용자 입력 순서 제어, 객체 생성과 같은 책임을 수행하는 것을 패턴을 발견하였습니다. 이러한 과정들을 메소드로 분리하였다고는 하지만 객체 안에서 나누었을 뿐이지 객체 그자체가 이 책임들을 수행하는 것은 변치 않다고 생각합니다. 따라서 객체를 분리하여 기능을 분리해야 한다고 생각하였고 사용자 입력 순서 제어는 Controller 객체로 분리하였습니다. 그리고 객체를 생성하는 부분은 해당 도메인 Factory 객체를 생성하여 수행하는 방향으로 변경하였습니다.

 

이번 주제를 통하여 느낀점은 단순히 객체 내부에서 비대한 메소드를 나누었다고 책임이 나누어지지는 않았음을 느꼈습니다. 이러한 책임을 나누기 위해서는 객체를 분리하여 해당 책임을 분리한 객체에게 협력 시키는 형태로 설계해야겠다고 느꼈습니다. 

 

References

source code : https://github.com/yonghwankim-dev/be-java-ladder-max/blob/%EB%84%A4%EB%AA%A8%EB%84%A4%EB%AA%A8/src/main/java/kr/codesquad/ladder/controller/LadderConsoleReaderController.java

'JAVA' 카테고리의 다른 글

[모던 자바 인 액션] 람다 표현식  (0) 2023.10.12
JVM(Java Virtual Machine) 실행원리와 구조  (0) 2023.03.27
JAVA의 JVM, JRE, JDK 정리  (0) 2021.06.30
JAVA SE, JAVA EE, JAVA ME 차이  (0) 2021.06.30
Comparable vs Comparator in Java  (0) 2021.06.25