지하철 노선도 미션

개발새발log·2022년 12월 3일
0

프리코스

목록 보기
3/3
post-thumbnail

우테코 최종 코테를 대비하며 과거 미션들을 구현해보고 있다.
✨그 두번째, 지하철 노선도 미션 !!✨

최근에 학습한 것: 동작 파라미터화

문득 프리코스에 지원한 사람들이 스트림으로 알잘딱깔센!하게 코드를 작성하는 걸 보고, 나도 잘 써보고 싶다는 욕심이 생겼다. (고백하자면.. 이전에는 사실 IDE의 힘을 빌려 적당히 때려 맞추는(?) 식으로 쓰는 감이 있었기 때문에🙄)

스트림을 제대로 쓸 수 있도록 공부하자고 다짐하고, 얼마 전에 모던 자바 인 액션을 구입해서 읽기 시작했다.

챕터2 동작 파라미터화 코드 전달하기와 챕터3 람다 표현식을 읽고 "동작 파라미터화"라는 세계에 눈을 떴다!!👀✨ 객체지향 프로그래밍에 절여 있던 뇌.. 신대륙을 발견한 기분이라고 해야하나?! (덕분에 나중에 함수형 프로그래밍 언어도 알아볼 의향이 생겼다)

챕터3을 읽으면서 사실 마냥 쉽게 다가오진 않아서, "의식적으로 함수형 인터페이스를 활용하자"는 개인적인 목표를 품고 있었는데, 이번 미션에서 우연찮게 활용할 기회가 됐다.

함수형 인터페이스들과 아직 낯가려서 정리해둔 주요 인터페이스 목록을 참고해가며 개발했다.

2020년도 우테코 미션 풀어보기

📌 미션 레포: 지하철 노선도

처음 미션을 읽으면서 동공 지진이..ㅎㅎ 생각보다 너무 거대한 시스템이여서 당황했다.

일단 구현해야 할 기능들이 많다. 크게는 역 관련 기능, 노선 관련 기능, 구간 관련 기능, 전체 노선도 출력 기능으로 나뉜다. 기본 코드에서 Repository가 주어지다 보니, 컨트롤러를 활용하자는 사고로 자연스럽게 이어졌다.

가장 단순하게 떠오르는 구현 방식은 사용자가 "1"을 입력하면 역 관련 기능 컨트롤러를 생성하고.. "2"를 입력하면 노선 관련 기능 컨트롤러를 생성하고.. 이렇게 if ~ else 혹은 switch 문을 활용한 컨트롤러 객체 생성 방식이였지만, 그렇게 가고 싶지는 않았다. 그래서 어떻게 하면 유연하게 컨트롤러 객체를 생성할 수 있을까?를 중점적으로 고민하기 시작했다.

가장 큰 미션: 반복 줄이기

👉 반복을 줄이기 위해 활용한 방식을 공유하겠습니다

1️⃣ 유연하게 컨트롤러 객체를 생성하기

지난 몇주간 우테코 미션들을 해결하며, enum과 제법 친해졌다. 그러다보니 그동안 잘 활용한, 입력과 enum 값을 매핑하는 방식으로 컨트롤러를 생성하는 방식을 떠올렸다. 객체를 생성하는 동작을 파라미터화 하기 위해, Supplier를 활용하는 게 좋겠다 싶어서 다음과 같은 enum 값들을 선언했다.

  • 역 관련 기능 : STATION_CONTROL
  • 노선 관련 기능 : LINE_CONTROL
  • 구간 관련 기능 : SECTION_CONTROL
  • 전체 노선도 출력 기능 : LINES_PRINTER
  • 프로그램 종료 : QUIT
STATION_CONTROL("1", StationController::new),
LINE_CONTROL("2", LineController::new),
SECTION_CONTROL("3", SectionController::new),
LINES_PRINTER("4", SubwayPrinter::new),
QUIT("Q", ProgramFinish::new);

두번째 필드는 Supplier<Controller> 형식으로, 새로운 컨트롤러 객체를 생성하는 동작을 수행한다.

여러가지 컨트롤러 객체들을 유연하게 매핑하기 위해 Controller라는 상위 인터페이스를 선언했다. 모든 컨트롤러에서 공통적으로 구현하는 execute라는 메소드를 가진 인터페이스다.

public interface Controller {
    void execute();
}

⬇️ 결과적으로, 아래와 같은 클래스 구조를 띄게 되었다:

enum을 활용해서 적절한 Controller 객체를 생성하기

enum을 활용한 객체 생성 구현 방식은 아래와 같다:

  • ControllerMapper : 사용자의 입력에 맞는 enum의 컨트롤러 객체를 반환한다
public class ControllerMapper {

    public static Controller executeByUserChoice(String choice){
        return MainControls
                .getMatchingControls(choice)
                .generatedController();
    }

}
  • MainControls 중
public static MainControls getMatchingControls(String choice){
    return Arrays.stream(MainControls.values())
            .filter(controls -> controls.choiceMatches(choice))
            .findAny()
            .orElseThrow(() -> new IllegalArgumentException(INVALID_MAIN_CHOICE.getMessage()));
}

public boolean choiceMatches(String choice){
    return this.choice.equals(choice);
}

public Controller generatedController(){
    return controllerMaker.get();
}

(이미 자주 등장한) 이 링크의 아이디어를 참고했다

결과적으로 client 코드에서는 입력에 따른 적절한 컨트롤러의 execute 메소드를 호출하는 데서 끝난다.

Controller controller = getMatchingController(inputView, outputView);
controller.execute();

2️⃣ 컨트롤러 내에서 입력에 따른 메소드 매핑

아래와 같이, 사용자의 입력에 따라 특정 함수를 수행하도록 하는 기능을 구현하면서 두번째 불만 사항이 생겼다.

## 역 관리 화면
1. 역 등록
2. 역 삭제
3. 역 조회
B. 돌아가기

## 원하는 기능을 선택하세요.
1

## 등록할 역 이름을 입력하세요.
잠실역

[INFO] 지하철 역이 등록되었습니다.

바로 아래와 같은 구조의 반복적인 if가 생기는 형태의 코드다:

private void executeChoice(String choice){
	if (choice.equals("1")){
    	createStation();
        return;
    }
    if (choice.equals("2")){
    	deleteStation();
        return;
    }
    if (choice.equals("3")){
    	getStationsList();
        return;
    }
    if (choice.equals("B")){
        return;
    }
    throw new IllegalArgumentException("error!");
}

우테코는 .. 이런 극악무도한 코드를 보면 지나칠 수 없게 만들었다 .. 😵‍💫

분명 이것도 객체 내에서 함수를 호출하는 동작을 파라미터화할 수 있을 거 같은데 싶어서 고민됐다. 입력에 따른 특정 함수 호출을 매핑하는 방식을 중점적으로 찾아보다가 우연히 이 글에서 힌트를 얻었다 !

아래와 같이 Map<String, Runnable>을 활용하는 방식이다.
찾아보니 Runnable은 어떠한 객체도 return하지 않는 함수형 인터페이스라, 그저 특정 함수들을 호출하는 데 사용하면 되겠다 싶었다.

private final Map<String, Runnable> commandMap = new HashMap<>();

private void mapInit() {
    commandMap.put(CREATE, this::createStation);
    commandMap.put(DELETE, this::deleteStation);
    commandMap.put(GET_LIST, this::getStationsList);
    commandMap.put(BACK, this::goBack);
}

특정 입력을 key로 가지는 commandMap에서, 입력에 맞는 메소드(value)를 실행하록 동작한다.

  • runCommand 메소드 중:
String choice = getUserInput();
commandMap.getOrDefault(choice, this::throwException).run();

여기까지 완성한 뒤 심정 한장 요약

반복되는 동작을 넘겨주는 동작 파라미터화의 위력을 느낄 수 있었던 미션이다🥹 흐흑..

3️⃣ UI 로직의 반복 줄이기

반복 멘트 추출하기

원래는 저 멘트 출력하는 메소드들을 다 따로 만들었는데, 이렇게 하다보니 컨트롤러를 구현할수록 손쓸 수 없이 메소드들이 많다고 느꼈다. 이를 위해 반복되는 부분을 추출해서 외부에서 주입받을 수 있도록 수정했다.

ex. 
등록할 역/노선 이름을 입력하세요
삭제할 역/노선 이름을 입력하세요
역/노선 목록
# OutputView 일부

public void printCreationResult(String subject){
    System.out.printf(System.lineSeparator() + INFO + " " + CREATED, subject);
    System.out.println();
}
public void printDeletionResult(String subject){
    System.out.printf(System.lineSeparator() + INFO + " " + DELETED, subject);
    System.out.println();
}

이렇게 바꾸니 view에서 도메인을 전보다 덜 안다는 장점도 딸려왔다.

도메인의 공통 기능 추출

  • 기존 코드
// Station 목록 출력
public void printStationsList(List<Station> stations) {
   for (Station station : stations) {
        printSingleStation(station);
    }
}
public void printSingleStation(Station station){
    System.out.println(INFO + " " + station.getName());
}

// Line 목록 출력
public void printLinesList(List<Line> lines) {
   for (Line line : lines) {
        printSingleLine(line);
    }
}
public void printSingleLine(Line line){
    System.out.println(INFO + " " + line.getName());
}

보다시피, 역 출력이나 노선 출력이나 겹치는 부분이 많다.
둘 다 해당 노선/역의 이름을 출력한다는 점에서 같다.

Line과 Station의 상위 클래스 Domain을 만들어서 해결했다. Domain에는 name이라는 필드를 가지고 있고, 이를 반환하는 메소드가 있다.

결과적으로 출력부가 다음과 같이 바뀔 수 있었다.

public void printDomainList(List<Domain> subjects) {
   for (Domain subject : subjects) {
        printSingleDomain(subject);
    }
}

public void printSingleDomain(Domain subject){
    System.out.println(INFO + " " + subject.getName());
}

✨new!✨ 가변 인수의 활용

아래는 노선 도메인을 표현하는 Line 클래스의 일부다:

private final String name;
private final LinkedList<Station> stations = new LinkedList<>();

public Line(String name, Station...stations) {
    this.name = name;
    Arrays.stream(stations)
            .forEach(this::addStation);
}

노선의 이름을 표현하는 name과, 여러가지 역들을 포함하는 stations 필드를 가지고 있다.

노선을 생성하기 위해 노선의 이름과 여러가지 역들을 한번에 생성하는 게 도메인 중심적이라고 생각됐기에, 해결하고 싶은 문제는 어떻게 n개의 가변적인 원소를 가진 linked list를 만들 것인가였다.

문제를 해결하기 위해서 받아들이는 인수의 개수가 가변적인, 가변 인수를 받아들이는 생성자를 활용하기로 했다. 결과적으로 몇 개의 역이 들어오는지 모르는 상황에서, n개의 역을 가진 linked list를 만들 수 있게 되었다.

가변 인수의 존재만 알고 직접 사용해볼 기회는 없었는데, 이렇게 활용할 수 있다는 게 개인적으로 흥미로웠다 👀

WRAP-UP

뿌듯한 점

  • 최근에 학습한 함수형 인터페이스과 스트림을 적극 활용했다
    - (위에서 한참 찬양했듯) 동작 파라미터화를 위해 함수형 인터페이스을 활용해서 유연성을 최대한 가져가려 했다.
    - 확실히 스트림을 활용하니 번거로움이 줄고 가독성이 늘었다.

고민되는 지점

  • (위에서 언급한) 출력 로직을 단순화 하기 위한 목적으로 상위 클래스를 도입하는 방식이 괜찮을까? 개인적으로 오버 프로그래밍은 아닐까 싶기도..

  • 컨트롤러에서 입력부의 반복

# get어쩌구Input 

inputView.print어쩌구Opening();
try{
	String input = readInput();
    // 검증 + 생성 메커니즘
    // return 객체 / 비즈니스 로직 처리 (void)
}catch(Exception ex){
	outputView.printErrorMessage(ex.getMessage());
    // 메소드 재호출
}

이런 로직의 반복인데, InputView의 책임 범위를 입력 받아서 검증하는 것까지 묶으면 이 동작 역시 파라미터화 시킬 수 있을텐데.. 현재 InputView는 입력 전 메세지(ex.원하는 기능을 선택하세요.) 출력만 담당해서, 이런 사소한 작업(?)들을 하는 메소드의 반복이다. InputView의 책임이 어디까지인가 재고해볼 필요가 있을 것 같다..!!


👉 소스 Repo

profile
⚠️ 주인장의 머릿속을 닮아 두서 없음 주의 ⚠️

0개의 댓글