Java/Java 강의

[Java] MVC 패턴의 오해와 고찰 | feat. Smalltalk-80, Dolphin Smalltalk MVP

Chipmunks 2024. 3. 25.
728x90

 

모두가 다르게 말하는 디자인 패턴이 있다?

벌써 5개월 전이다.

온라인으로 자바 과제를 하는 코스에 참가해, 많은 학생과 코드 리뷰를 활발히 했었다.

자바 과제는 입력 / 비즈니스 요구사항 처리 / 출력의 형식에서 벗어나지 않았다.

 

코드 리뷰가 활발하다 보니 몇몇 기술 유행이 불처럼 번졌다.

그 중 하나가 바로 MVC(Model-View-Controller) 디자인 패턴이었다.

많은 학생이 MVC 패턴을 채택한 이유가 뭘까.

직관적이고 객체 지향적으로 분리되고 과제 구현에 최적화됐기 때문이라 생각한다.

 

MVC 패턴을 코드 리뷰할 때, 신기한 점을 발견했다.

학생마다 MVC 패턴을 다르게 알고 있는 게 아닌가?

심지어 내가 이해한 MVC 패턴과도 달랐다.

 

왜 이런 일이 발생한걸까?

(5개월 전 커뮤니티에서 논의한 걸 정리한 글입니다.)

 

누구나 알고 있는 MVC 패턴

MVC 패턴을 간략히 소개하면 다음과 같다.

Model, View, Controller로 데이터 / 논리 제어와 사용자 인터페이스를 구현하는 데 널리 사용되는 소프트웨어 디자인 패턴이다. [1]

소프트웨어 비즈니스 로직과 화면(UI)을 분리하는 데 목적이 있다.

 

과제를 예를 들면, 다음처럼 분리할 수 있다.

  • 입력과 출력 : View
  • 도메인 데이터 : Model
  • 요구사항 처리 : Controller

[1] 출처 : https://developer.mozilla.org/ko/docs/Glossary/MVC

 

여기까진 누구나 고개를 끄덕였을 것이다.

그러나 문제는 MVC 패턴을 '구현'하는 데 있었다.

 

쟁점. View 는 다른 요소를 알지도, 대화하지도 않는다. 이는 MVC 패턴의 원칙을 어기는 것이다.

아래 View 코드에 남긴 리뷰를 소개한다.

// src/main/java/.../view/OutputView.java

    public static void printResult(ResultDTO resultDTO) {
        ...
    }
DTO 는 모델 계층은 아니지만, 모델 계층과 밀접한 관계를 갖고 있다.
View 는 자기 자신을 제외한 다른 요소를 알지도, 대화하지도 않는다.
따라서 View 계층에서 직접 DTO 로 소통하면 안된다.
이는 MVC 패턴의 원칙을 어기는 것이다.

 

DTO는 Data Transfer Object의 약자로, 계층간 데이터를 주고 받기 위한 객체다.

코드에서 ResultDTO 객체를 가리킨다.

결과값을 표현한 필드와 그에 대한 Getter 메소드로 이뤄져 있다.

 

여기서 관심 있게 봐야 할 점은, 'View 는 다른 요소를 알지도, 대화하지도 않는다.' 이다.

다시 위로 올라가 MVC 패턴의 그림을 보자.

View 가 Model, Controller 와 소통하고 있다.

이상하지 않은가?

어떻게 View 가 다른 요소와 소통하지 않는다는 걸까?

( 다형성으로 다른 요소의 구현 클래스를 모르게 할 순 있다. )

 

이제, 하나 하나 씩 고찰을 시작해보자.

 

여담으로 DTO 가 모델 계층과 밀접한 관계를 가진다는 점도 논의를 했습니다.

MVC 패턴이라는 주제와 맞지 않아, 제 글을 인용하여 줄입니다.

 

저는 다르게 생각하고 있어요.
DTO 는 MVC 계층 간의 데이터를 주고 받기 위한 객체입니다.
이는 단방향이 아니라, 양방향으로도 주고 받을 수가 있습니다.
즉, Controller 에서 데이터 전송을 목적으로 DTO 를 View 에 전달하는 것은 어색하지 않습니다.

또한, DTO 는 모델 계층과 밀접한 관계를 가질 수도 있지만, 그렇지 않은 DTO 도 있습니다.
Model 과 소통하기 위한 DTO 는 Model 계층과 밀접하지만,
View 와 Controller 끼리 소통하기만 하는 DTO 는 그렇지 않기 때문이죠.

DTO 를 보내는 출신지가 Model 이라고 해서, View 가 Model 을 알고 있다라고 해석하는 건 무리가 있지 않나 싶어요.
예컨데, 위 메소드에서 ResultDTO 라는 객체로 Model 이 어떤 클래스인지 알 수가 없습니다.
더불어 Model 을 조작하는 것도 불가능하고요.

현실로 예를 들자면, 어부(모델)가 물고기(DTO)를 시장(컨트롤러)에 전달합니다.
시장(컨트롤러)은 물고기(DTO, 또는 또다른 DTO인 회가 될 수도)를 손님(뷰)에게 전달합니다.

손님(뷰)은 어부(모델)가 줬다는 사실을 물고기(또는 회, DTO)형태를 보며 눈치를 챌 순 있겠지요.
하지만 그게 정말 어부(모델)가 준 건지, 정확히 어떤 어부(모델)가 준 건지, 또는 다른 낚시꾼(모델)이 준 건지, 또는 시장(컨트롤러)에서 자체적으로 구해온 건지 전혀 알 수가 없습니다.

(중략)

 

고찰 1. 디자인 패턴과 아키텍처의 혼용

앞선 코드 리뷰에서 눈치챈 독자가 있을지도 모른다.

MVC 디자인 패턴 소개에서 보지 못했던, '계층' 이란 단어가 나타났다.

'계층' 이란 건 어디서 나타난 걸까?

 

아무래도 '서버' 개발자를 희망하는 학생답게

웹 시스템에서 차용하는 '계층 아키텍처(Layered Architecture)' 를 배운 걸로 보인다.

대단하지 않은가? 벌써 시스템 아키텍처까지 공부하다니!

 

계층 아키텍처를 간략히 소개하면 다음과 같다.

하나의 수평 계층에 여러 컴포넌트(Component)가 있다.

한 계층은 특정 역할(프레젠테이션, 비즈니스 로직 등)을 수행하고, 책임을 가진다.

상위 계층과 하위 계층 간의 상호작용으로 시스템이 동작한다.

따라서 한 계층은 다른 계층의 역할과 책임을 알 필요가 없다.

[2] 출처 : https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html

 

 

다른 시스템 아키텍처 중, 우리 눈에 띈 아키텍처가 있다.

바로 MVC 패턴을 차용한 'MVC 아키텍처(MVC Architecture)' 이다.

MVC 패턴에서의 객체 역할을 '시스템' 으로 확장했다.

Model 컴포넌트, View 컴포넌트, Controller 컴포넌트로 구성된다.

[3] 출처 : https://www.tutorialspoint.com/mvc_framework/mvc_framework_introduction.htm

 

여기서 중요한 건, Model '계층' 이라고 하지 않았다는 거다.

Model '컴포넌트' 라고 했다!

Layered Architecutre 와 MVC Architecture 는 다른 시스템 아키텍처다.

 

자, 컴포넌트를 더 알아보자.

아무래도 '서버' 개발자다 보니, 컴포넌트가 서버로만 이뤄져있다고 생각할 수도 있다.

아니다!

'클라이언트'도 컴포넌트에 포함된다.

 

시스템은 서버와 클라이언트를 모두 포함하고,

데이터베이스나 스토리지 등 다양한 컴포넌트까지 포함한다.

시스템은, 여러 컴포넌트 간의 상호작용으로 목표하는 기능을 수행하는 것을 의미한다.

 

이제 코드 리뷰를 다시 한 번 보고 와보자.

어떤 사고의 흐름으로 된 건지 생각해보고 오자!

 

 

 

 

정리하자면 다음과 같다.

 

1. 객체 수준의 디자인 패턴과 시스템 수준의 아키텍처를 혼용했다.

2. 특정 시스템 아키텍처만 사용하는 개념을 혼용했다.

 

아래는 다른 분들의 의견이다.

저도 일단 처음에 가지고 온 인용은 잘못됐다고 생각합니다. DTO 자체가 데이터 전달 객체이기 때문에 View에서 다른 Layer로 값을 가져오기 위해 사용하는 역할을 수행하는 것이 맞습니다. 그리고 모델은 DTO에 의존해선 안됩니다. 그 역은 상관없지만, 모델이 DTO에 의존하게 되는 순간 배보다 배꼽이 더 커져버리는 격이죠. 각 Layer간의 커플링이 생기는 동작을 유도할 수도 있습니다.

MVC에서 컨트롤러는 제가 알기로 컨트롤러 패턴에서 유래된 것으로 알고 있습니다. 컨트롤러는 Client측에서 발생한 동작을 해당하는 도메인의 로직과 연결시켜주는 역할을 합니다. 따라서 컨트롤러가 어떻게 보면 전체적인 흐름을 관장하는 것은 맞긴 합니다. View와 Model 사이에 커플링을 최소화하는 것이 Controller의 취지니까요 하지만, Controller가 팽창된 컨트롤러, 즉 흐름 관장 이외에 많은 일을 수행하는 것은 안티 패턴입니다. 그래서 비지니스 로직은 도메인에서 집중하고, 다른 기능들은 Util로 빼는 것이죠.

요즘 추세는 MVC 자체가 Layerd Architecture와 거의 혼용되고 있긴 한데 이해하는데 문제는 없다고 생각합니다. 결론적으로는 View-Model의 커플링을 줄이기 위함에 있으니까요.

 

추가적으로 의논하고 싶은 사항이 있는데요..!! 사실 저는 MVC 패턴이 Layered Architecture 안에 속하는 개념이라고 생각하고 큰 고민 없이 Controller, Service, Domain 단으로 구조를 분리하여 개발해왔습니다.
이렇게 짜다 보니 개발은 용이하지만 객체 끼리 메세지를 주고 받으며 협력 하는 느낌은 들지 않는데 이와 관련해서 다들 의견이 있으신가요?

 

고찰 2. 정말 View 는 다른 객체와 소통하지 않을까?

이제부터 객체 수준의 디자인 패턴만 생각해 보자!

 

다시 한 번 MVC 패턴의 그림을 봐보자.

[1] 출처 : https://developer.mozilla.org/ko/docs/Glossary/MVC

 

View 는 Controller 에게 입력을 전달한다.

Controller 는 Model 을 수정하거나, 직접 View 에게 수정을 요청한다.

수정한 Model 은 View 에게 업데이트를 요청한다.

 

Controller 와 View 의 상호작용을 살펴보자.

게임 진행을 담당하는 Controller 와 입출력을 담당하는 InputView, OutputView 와의 상호작용이다.

Car Model 을 표현하는 CarView 도 생성하고 있다.

public interface GameController {
    void run();
}

public class GameControllerImpl implements GameController {
    private final OutputView outputView;
    private final InputView inputView;
    private final List<CarView> carViews;

    public GameControllerImpl(final OutputView outputView, final InputView inputView) {
        this.outputView = outputView;
        this.inputView = inputView;

        carViews = new ArrayList<>();
    }

    @Override
    public void run() {
        final Cars cars = getCars();
        createCarViews(cars);
        ...
        Console.close();
    }

    public Cars getCars() {
        outputView.printInputCarNameMessage();
        final String carNames = inputView.readCarNames();
        return CarsFactory.create(carNames);
    }
	
    public void createCarViews(Cars cars) {
        final DistanceStyle dashDistanceStyle = DistanceStyleFactory.create(DistanceStyles.DASH);
        cars.getCars().stream()
            .map(car -> new CarView(car, outputView, dashDistanceStyle))
            .forEach(carViews::add);
    }
    
    ...
}

public class ConsoleInputView extends InputView {
    @Override
    public String read() {
        return Console.readLine();
    }
}

public class Application {
    public static void main(String[] args) {
        final OutputView outputView = new ConsoleOutputView();
        final InputView inputView = new ConsoleInputView();

        final GameController gameController = new GameControllerImpl(outputView, inputView);
        gameController.run();
    }
}

 

객체지향적으로 바라본다면

InputView 객체는 GameController 객체와 메시지를 주고받으며 소통하고 있다.

인터페이스 또는 추상클래스로 다형성 뒤에 숨어, 자신의 진짜 정체를 숨길 뿐이다.

소통한다는 사실은 변함이 없다.

 

심지어 Model 과 View 도 소통을 한다.

다만, Controller 와 다른 점은 '일방향' 이라는 점이다.

어떻게 Model 의 값이 변하면 Model 을 표현하는 View 도 변하게 할까?

 

답은 '옵저버 패턴(Observer Pattern)' 이다.

여태 MVC 패턴을 설명하다, 갑자기 다른 패턴이 나와버렸다.

하나의 디자인 패턴 안에, 여러 디자인 패턴을 포함할 수 있다.

 

Model 은 데이터 변경을 관찰하고 알리는 데 옵저버 패턴을 이용하고,

View 는 컴포지트 패턴(Composite Pattern)으로 복합 객체를 단일 객체로 취급할 수 있고,

Controller 는 전략 패턴(Strategy Pattern)으로 다양한 사용자 입력을 처리하는 방식을 교체할 수 있다.

 

다시 옵저버 패턴으로 돌아와보자.

옵저버 패턴을 간략하게 설명하면 다음과 같다.

한 객체(Observable)의 값이 변경했을 때 알림(통지)을 받을, 관찰하는 객체(Observer)를 등록한다.

실제 값이 변경했을 때, 모든 관찰하는 객체에 전달한다.

이를 반영하는 건, 관찰하는 객체에 책임을 위임한다.

 

코드로 한 번 살펴보자.

public interface Observable {
    void registerObserver(final Observer observer);

    void removeObserver(final Observer observer);

    void notifyObservers();
}

public interface CarModel extends Observable {
    int getDistance();

    String getName();
    
    void moveForward();
}

public class Car implements CarModel {
    private final String name;
    private final List<Observer> observerList;
	...

    public Car(final String name) {
        this.name = name;
        this.observerList = new ArrayList<>();
		...
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void moveForward() {
        ...
        notifyObservers();
    }

    @Override
    public int getDistance() {
        return distance;
    }

    @Override
    public void registerObserver(final Observer observer) {
        observerList.add(observer);
    }

    @Override
    public void removeObserver(final Observer observer) {
        observerList.remove(observer);
    }

    @Override
    public void notifyObservers() {
        observerList.stream()
                .forEach(Observer::update);
    }
    
    ...
}

public interface Observer {
    void update();
}

public class CarView implements Observer {
    private final OutputView outputView;
    private final CarModel carModel;
    private final DistanceStyle distanceStyle;

    public CarView(final CarModel carModel, final OutputView outputView, final DistanceStyle distanceStyle) {
        this.carModel = carModel;
        carModel.registerObserver(this);

        this.outputView = outputView;

        this.distanceStyle = distanceStyle;
    }

    @Override
    public void update() {
        final String name = carModel.getName();
        final int distance = carModel.getDistance();
        final String distanceString = distanceStyle.getDistanceString(distance);
        outputView.printCarNameAndDistanceString(name, distanceString);
    }
}

public class GameControllerImpl implements GameController {
    ...

    public void printExecutionResult(final int tryCount, final Cars cars) {
		...
        for (int i = 0; i < tryCount; i++) {
            cars.moveForward(); // 값 변경
            ...
        }
    }
    
    ...
}

public class Cars {
    private final List<Car> cars;
    ...

    public Cars(final List<Car> cars) {
        this.cars = cars;
    }

    public void moveForward() {
        cars.stream()
            .forEach(Car::moveForward);
    }
    ...
}

 

코드가 생각보다 길다.

차근차근 따라와 보자.

 

CarView 생성자에서 CarModel 에 자기 자신을 등록(registerObserver)하는 걸 볼 수 있다.

Car(CarModel)에서 값을 변경하면, notifyObservers 메소드로 등록된 모든 View 에게 업데이트를 요청한다.

update 메소드를 구현한 CarView 가 그 요청을 처리한다.

 

자, Model 은 View 를 등록하고, View 에게 값이 변경되면 알려준다.

View 는 Model 의 정체가 무엇인지 모르지만

어쨌든 메시지를 받아 처리하고 있다.

 

결국,

View 는 화면에 표시하는 책임을 수행하기 위해,

다른 객체(Controller, Model)와 소통하고 있다는 사실을 알 수 있습니다.

 

(여담)

고찰 2 의 코드가 사실 완전 MVC 패턴 그림과 똑같지는 않다.

왜냐면 Car 객체에게 메소드를 호출하기 때문이다.

정확하게 그림처럼, CarView 와 Car 을 떼어내려면 어떻게 해야 할까?

 

Controller 는 View 와 Model 을 알고 있다.

Controller 에서 View 를 생성할 때 registerObserver 까지 해줘야 한다.

또한 update 메소드는 '특정 값' 만을 매개 변수로 받아와야 했을 것 이다.

예제 코드에선 편의상 CarModel 이라는 인터페이스로 서로의 접근을 제어하긴 했다.

 

고찰 3. MVC 패턴의 반례?

위 이야기를 나누던 중

한 분이 MVC 패턴의 관한 기술 영상을 소개해주셨다.

 

결론부터 말하자면 저도 작성자분 말씀에 동의합니다~
view는 model과 완전 독립해야하고, controller는 view와 통신하며 model과 view를 이어주는 역할을 하니까요. view는 Controller를 알고 있는게 맞죠.
controller 에서 데이터 전송을 목적으로 DTO 를 View 에 전달하는 건 view를 model에 의존적이게 만드는 것과는 거리가 있다고 생각합니다.
아래 영상 참고하면 도움이 될거라 생각해요!

 

이 영상을 보다가도, 역시 View 에서 의문이 드는 점을 발견했다.

2. View 는 Model 에만 의존해야 하고, Controller 에는 의존하면 안된다.
( View 내부에 Model 의 코드만 있을 수 있고, Controller 의 코드가 있으면 안 된다. )

 

이에 대한 반례가 생각났기 때문인데...

'헤드퍼스트 디자인 패턴' 교재를 오랜만에 한 번 찾아봤다.

View 와 Model 과 옵저버 패턴으로 되어 있다는 사실은 금방 알 수 있다.

 

그런데...

여기선 View 가 Controller 를 참조하고 있다..!?

UI 입력이 있을 때 Controller 에게 메시지를 보내기 때문이다.

 

...
5. 뷰에서 모델한테 상태를 요청합니다.
뷰에서 화면에 표시할 상태는 모델로부터 직접 가져옵니다. 예를 들어, 모델에서 뷰한테 새로운 곡이 재생되기 시작했다고 알려주면, 뷰에서는 모델한테 곡 제목을 요청하고, 그것을 받아서 화면에 표시합니다. 컨트롤러에서 뷰를 바꾸라는 요청을 했을 때도 뷰에서 모델한테 상태를 알려달라고 요청할 수도 있겠지요.
...

 

public class DJView implements ActionListener, BeatObserver, BPMObserver {
  BeatModelInterface model;
  ControllerInterface controller;

  public DJView(ControllerInterface controller, BeatModelInterface model) {
    this.controller = controller;
    this.model = model;
    model.registerObserver((BeatObserver)this);
    model.registerObserver((BPMObserver)this);
  };

  public void createView() { ... };

  public void updateBPM() {
    // 모델의 상태가 변경되면 updateBPM() 메소드가 호출된다. BPM 값은 모델에게 직접 물어본다.
    int bpm = model.getBPM();
    if (bpm == 0) {
      bpmOutputLabel.setText("offline");
    } else {
      bpmOutputLabel.setText("Current BPM: " + model.getBPM());
    }
  }

  public void updateBeat() {
    // 모델에서 새로운 박자가 연주될 때 마다 updateBeat() 메소드가 호출
    beatBar.setValue(100);
  }

  public void actionPerformed(ActionEvent event) {
    if (event.getSource() == setBPMButton) {
      int bpm = Integer.parseInt(bpmTextField.getText());
      controller.setBPM(bpm);
    } else if (event.getSource() == increaseBPMButton) {
      controller.increaseBPM();
    } else if (event.getSource() == decreaseBPMButton) {
      controller.decreaseBPM();
    }
  }

}

 

ControllerInterface 를 필드로 생성자로 주입받고 있다.

View 가 Controller 에 의존하게 되는 셈이다.

 

이쯤 되자, 머리가 혼란스러워진다.

왜 여기는 다르게 설명하고, 저기는 다르게 말하고 있을까?

무엇이 이렇게 만든걸까?

 

고찰 4. MVC 패턴에서 Model 의 옵저버 패턴은 필요 없다.

헤드퍼스트 예시를 드니, 다른 분이 의견을 주셨다.

 

@다람쥐 오랜만에 '헤드퍼스트 디자인 패턴'책을 펴봤네요. 책에서 언급하는 MVC 패턴의 구조는 옵저버 패턴, 전략 패턴, 컴포지드 패턴 3가지가 복합적으로 이루어진 패턴이라고 설명하고 있어요 그 중에서도 옵저버 패턴 부분만 언급해 볼께요.

MVC 에서 옵저버 패턴의 부분을 간단한 문장으로 요약해보자면 "모델의 상태가 변화할 때, 이를 뷰에게 알린다." 라고 요약할 수 있습니다. 간단한 예시는 "버튼을 누른다.", "특정 수치 조절"정도 이겠네요.

그렇다면 우리가 다루고 있는 View와 비교해 볼께요. 예로 들면 "값을 입력받는다", "값을 출력한다" 정도 일 겁니다. 이러한 점에서 이전의 View는 "무언가 변화를 주었을 때 바로 Model에게 정보를 제공" 할 정도의 역할은 필요가 없죠
"값을 입력받는다" : Controller에서 View에게 정보를 요청하고 Cotroller에게 다시 건네주면 되니까요.
"값을 출력한다" : 그냥 정해진 형식에 맞춰 출력하면 됩니다.

그래서 MVC 모델에서 '옵저버 패턴' 부분은 굳이 따져 이야기하자면 "필요 없다"라고 이야기 할 수 있다고 생각합니다. 정확한 그림은 아니지만 참고합니다. (주저리 주저리 썼는데, 이해 안되시는 부분 다시 이야기해주세요.)

Updates 가 X 처리 된 MVC 패턴 사진

 

또 다른 분과 의견을 나눠서 결론(?)이 난 상태라

따로 답은 안했었다.

여기서나마 답을 해보겠다.

 

의견을 정리해 본다면 다음과 같다.

"Controller 가 다 해줄 수 있는데, Model 의 옵저버 패턴은 필수가 아니라 옵션일 뿐이다!"

 

그렇다면, Controller 의 책임이 더 많아진다고 생각한다.

흐름 로직을 수행하는 것 뿐 아니라, Model 의 변경사항을 View 에게 전달해야 하는 책임이 생긴다.

Model 의 변경사항마다 Controller 에서 하나 하나 코드를 추가해야 한다는 말과 같다.

Model 의 로직을 하나 하나 분석해 가면서 말이다.

 

Model 과 View 사이에 '등록'만 하면, Controller 는 더이상 신경을 쓰지 않아도 된다.

View 에 업데이트할 시점을 Model 이 정하기만 하면 될 뿐이다. 

( 사실 Controller 도 Model 에 옵저버로 등록할 수 있다. )

 

아래는 의견을 주신 분과 또 다른 분과의 대화다.

 

OO님께 질문이 있습니다.

이러한 점에서 View는 "무언가 변화를 주었을 때 바로 Model에게 정보를 제공"할 정도의 역할은 필요가 없죠 

말씀하신 내용은 View -> Model 이지만 첨부된 이미지에는 Model -> View 로 보여요.

전자라면 OO님 말씀에 동의하지만, 후자일 경우 data1, data2 가 모델에서 제공하는 정보라고 생각하는데, OO님은 어떻게 생각하실까요? 

 

XX님 말씀이 맞는 것 같습니다. 화살표 자체가 "직접적인 field로 가지고 있어야 한다"라고 생각해서 X라고 쳤는데, 다시 보니까 안그래도 될 거 같네요
 다시 보니 data1, data2 등의 정보를 View에 제공한다는 의미로 보는 것이 올바를 것 같습니다.

제가 책에서 본 그림과 유사에서 인용해 보았는데, 아예 다른 그림이라고 보는게 맞을 것 같네요.

 

 

고찰 5. MVC 패턴이, 정말 내가 아는 MVC 패턴이 맞는 걸까?

당시 MVC 패턴이란 무엇일까, 고민을 하며 일주일을 보냈다.

그러던 중 MVC 패턴의 역사(?)까지 파헤쳤다. [4]

Smalltalk-80 에서의 MVC 패턴, Dolphin Smalltalk 에서의 MVP 패턴 등등을 정리한 글이다.

이 고찰에선, 위 내용을 정리해보겠다.

 

MVC / MVP 패턴은 비슷하다고 한다.

그러나 각 패턴은 달성하고자 하는 목표가 다르다고 한다.

 

Smalltalk-80 MVC 패턴은 1978~1989년도에 Trygve Reenskaug 분이 고안했다.

Smalltalk-80 MVC 패턴의 주요 목적은, 사용자가 데이터를 표현한 다양한 View 를 편집하도록 인터페이스를 제공하는 데 있다.

Smalltalk-80 은 참고로, 클래스 라이브러리 명이다.

 

일반적으로 알려진 MVC 패턴의 구조와 다른 점이 있다.

일반적인 MVC 패턴은 View -> Controller 의 연결이 간접적이지만, 여기선 직접적으로 연결되어 있다.

즉, View 와 Controller 가 서로 결합되어있다는 점이다.

이에 관한 이야기는 추후에 나온다.

 

View 의 책임은 주로 출력을 처리하고

Controller 는 주로 입력을 처리한다.

Model 과 상호작용하는 건 View 와 Controller 모두의 책임이다.

아까 전, Controller 도 Model 에 옵저버로 등록할 수 있다는 사실 기억이 나는가?

바로 이런 관점에서 말한 것 이었다!

 

Controller 는 사용자 입력을 받고 Model 에 반영하기도 하며,

View 도 스스로 업데이트하며 Model 에 반영하기도 한다.

둘 다 필요에 따라 데이터에 접근하고 수정할 수 있다.

 

사용자가 데이터를 입력하면, Controller 가 이를 적절하게 응답한다.

데이터 변경 / 메소드 호출 등으로 Model 과 상호작용할 수도 있고,

메뉴 축소 / 스크롤 등으로 View 에 시각적인 변경을 요청할 수도 있다.

 

여기까지 잘 따라왔는지 궁금하다.

정말로 중요한 이야기가 곧 나온다!!

 

Smalltalk-80 MVC 에서 Controller 에 관한 오해

Smalltalk-80 MVC 에서 Controller 에 관한 오해가 있다고 한다.

바로, Controller 의 목적이 View 와 Model 을 분리하기 위함이라는 점이다.

아니다.

이미 Model 은 옵저버 패턴으로 View 와 분리되어 있다.

심지어 Controller 까지도!

 

자, 그럼 Controller 는 누구와 소통하는 걸까?

바로 '최종 사용자(End User)' 다.

즉 MVC 패턴의 그림은 다음과 같아야 한다.

여태껏 등장하지 않았던 사용자가 등장하게 된다.

 

사용자가 포함된 MVC 패턴

 

 

즉, Smalltalk-80 MVC 패턴에서의 Controller 는

최종 사용자의 입력을 받는 책임을 갖고 있다,

라고 볼 수 있다.

 

Taligent MVP 패턴

다음으론 MVP(Model-View-Presenter) 패턴을 보자.

MVP 패턴은 MVC 패턴의 변형이며, 애플리케이션의 도메인, 프레젠테이션, 사용자 입력으로 분리하게 된다.

 

Taligent MVP 패턴은 본 글과 크게 중요치 않은 내용이므로, 넘어가도 된다.

 

MVP 패턴은 원래 Smalltalk-80 MVC 패턴의 영향을 받은 Taligent 프로그래밍 모델이 기반이다.

Taligent 에서 근무하던 1996년에 Mike Potel 이 공식적으로 처음 설명했다.

( Taligent 는 Apple Computer, IBM 과의 합작 투자로 1995년 말 IBM 이 전액 출자한 자회사였고, MVP 패턴의 많은 요소는 Apple 에서 개발되었다고 한다. )

 

[5] 출처 : https://stefanoborini.com/book-modelviewcontroller/02-mvc-variations/05-variations-on-the-triad/08-taligent-mvp.html

 

기존 Smalltalk-80 MVC 패턴에 비해 추가된 게 있다.

바로, Selection, Commands, Interfactors, Presenter 다.

 

Selection 은 Model 내의 데이터 중 어느 부분을 편집할 지 지정하는 구성요소다.

특정 기준을 충족하는 행, 열 또는 개별 요소를 정의할 수 있다.

 

Commands 는 데이터를 활용한 작업을 정의할 수 있는 구성 요소다.

Model 내의 데이터 삭제, 인쇄 또는 저장 작업을 예를 들 수 있다.

 

Interfactors 는 마우스 움직임, 키보드 입력, 체크박스 또는 메뉴 항목 선택 등등,

Model 작업과 사용자 이벤트가 매핑되는 방식을 다룬다.

 

Presenter 는 어플리케이션 내 다른 구성 요소의 전반적인 상호 작용을 조정하는 구성 요소다.

적절한 Model, Selection, Commawnds, View, Interactors 생성 등을 관리한다.

전체를 조율하는 관리자 역할을 하며, 사용자 이벤트를 가로채는 건 Interactor 가 한다.

즉, Interactor 가 기존 Controller 의 역할을 하는 셈이다.

 

Smalltalk-80 Controller 처럼 주어진 뷰의 각 위젯마다 Presenter 가 필요하지 않다.

일반적으로 View 하나당 한 Presenter 가 있지만, 경우에 따라 논리적으로 관련한 여러 View 를 관리할 수 있다.

 

Dolphin Smalltalk MVP 패턴

Taligent MVP 패턴의 구성요소는 너무 많다.

Dolhpin 개발 팀은 이를 줄여, Model, View, Presenter 로만 남겼다.

처음엔 MVC 디자인을 고려했지만, Model 이 View 에 직접 접근할 수 있는 모델이 싫었고,

사용자 이벤트에 응답하는 Controller 개념이, 기본 위젯이 사용자 이벤트를 직접 처리하는 최신 개발 플랫폼과 맞지 않았다고 한다.

View / Domain 분리에 대한 접근 방식에서 겪었던 유연성 문제를 해결하려고, Taligent MVP 패턴을 채택했다고 한다.

 

Dolphin MVP 패턴에서 View 는 운영체제에서 생성한 사용자 이벤트를 가로챈다.

이미 Windows 운영 체제에서, 기본 위젯이 기존 Controller 의 역할을 했다.

View 가 Model 을 직접 업데이트 하여 사용자 이벤트에 응답했지만,

대부분 사용자 이벤트는 Presenter 에 위임되었다.

MVC 패턴과 마찬가지로, View 는 옵저버 패턴으로 Model 의 변경 사항을 반영하고, 화면을 업데이트하여 응답한다.

 

Smalltalk-80 MVC v.s. Dolphin Smalltalk MVP

이제 마지막이다!

두 패턴을 비교해보자.

원글에서도 겉으로 보기엔, 둘의 차이점을 알기가 어렵다고 한다.

 

MVC 패턴에서의 Controller 의 주요 목적은, '사용자 입력을 가로채는 것' 이었다.

Model 을 업데이트하는 역할은, 고유한 부분이 아니라 주요 목적의 '부산물' 정도였다.

 

반면, Dolphin Smalltalk MVP 패턴에서의 Presenter 의 주요 목적은, 'Model 을 업데이트' 하는 것이다.

View 가 위임한 이벤트를 가로채는 Presenter 의 역할은, 고유한 부분이 아니라 주요 목적의 '부산물' 이었다.

 

MVC 패턴은 데이터를 표현하는 Model 과 편집기 View 와 상호 작용할 수 있도록 논리를 분리하는 거였다.

화면에 데이터를 표시하는 작업과, 키보드 / 마우스 장치에서 사용자 입력을 해석하는 작업은 기술적으로 무척 다르기 때문에,

Controller 로 분리하게 되었다.

자연스레 Controller 가 입력을 책임받았기에, Model 을 업데이트하는 책임도 맡게 된 것이다.

 

이제, Dolphin Smalltalk MVP 패턴을 보자.

사용자의 입력을 가로채는 역할이 Controller 에서 View 로 이동했다!

Controller 내지는 Interactor 의 필요성이 완전히 제거됐다.

 

Taligent 팀은 Presenter 의 원래 아이디어를 응용 프로그램 수준의 Controller 로 보았지만,

Dolphin 팀은 이를 VisualWorks 의 응용 프로그램 모델을 대체하는 것으로 잘못 간주하여, Controller 자리에 Presenter 를 유지했다.

 

따라서, Dolphin Smalltalk MVP 패턴과 Smalltalk-80 MVC 패턴이 '유사하게' 보였던 것이었다.

그러나 Presenter 와 Controller 는 해결하고자 하는 목적이 달랐다.

 

헤드 퍼스트 디자인 패턴으로 돌아가보자

다시 헤드퍼스트 디자인 패턴의 코드를 봐보자.

 

public class DJView implements ActionListener, BeatObserver, BPMObserver {
  BeatModelInterface model;
  ControllerInterface controller;

  public DJView(ControllerInterface controller, BeatModelInterface model) {
    this.controller = controller;
    this.model = model;
    model.registerObserver((BeatObserver)this);
    model.registerObserver((BPMObserver)this);
  };

  public void createView() { ... };

  public void updateBPM() {
    // 모델의 상태가 변경되면 updateBPM() 메소드가 호출된다. BPM 값은 모델에게 직접 물어본다.
    int bpm = model.getBPM();
    if (bpm == 0) {
      bpmOutputLabel.setText("offline");
    } else {
      bpmOutputLabel.setText("Current BPM: " + model.getBPM());
    }
  }

  public void updateBeat() {
    // 모델에서 새로운 박자가 연주될 때 마다 updateBeat() 메소드가 호출
    beatBar.setValue(100);
  }

  public void actionPerformed(ActionEvent event) {
    if (event.getSource() == setBPMButton) {
      int bpm = Integer.parseInt(bpmTextField.getText());
      controller.setBPM(bpm);
    } else if (event.getSource() == increaseBPMButton) {
      controller.increaseBPM();
    } else if (event.getSource() == decreaseBPMButton) {
      controller.decreaseBPM();
    }
  }

}

 

이 코드는, 우리 과제와 다른 점이 있다.

바로 GUI(Graphic User Interface) Swing 을 쓴다는 점이다.

Swing 은 JDK 에 포함된 GUI 개발 라이브러리다.

앞서 MVP 패턴에서 말한, '위젯이 사용자의 입력을 가로채는' 형태다.

 

그렇다면, 위 코드는 MVC 패턴이 아니라 'MVP 패턴'이라고 볼 수 있지 않을까?

라고 조심스레 결론을 내려본다.

 

고찰 2에 소개한 내 코드도, InputView 의 책임을 Controller 에게 옮겨야,

Smalltalk-80 MVC 패턴이라고 부를 수 있을 것 이다.

Controller 가 사용자와 상호작용으로 입력을 가져오는 역할을 담당하기 때문이다.

 

여담. 웹 어플리케이션을 위한 MVC 패턴 적용 역사

웹 어플리케이션을 위한 MVC 패턴도 소개하고 있다.

Java 진영이라 관심 있게 읽었다.

 

초기 MVC 패턴과 유사하게, 어플리케이션의 도메인, 클라이언트 측 프레젠테이션, 서버 측 입력 처리를 구성 요소로 분리했다고 한다.

HTTP 프로토콜과 무상태(Stateless) 특성에 객체 지향 설계가 적용되면서 자연스럽게 나타났다고 한다.

요청을 처리하고 라우팅할 때 Smalltalk-80 컨트롤러와 동일한 목적을 지닌 코드가 생성되었다.

사용자에게 보여지는 HTML 의 텍스트 특성으로, 국제화 / 지역화된 콘텐츠도 어플리케이션 로직에서 분리하기 위해 템플릿 기반 접근 방식이 탄생했다. 위의 기술이 객체 지향에 따라 웹 어플리케이션을 위한 MVC 패턴이 나타났다고 한다.

 

결국 Java의 'Model 2' 아키텍처에 사용되었다고 한다.

1990년대 중반부터 후반까지, 웹 어플리케이션이 주로 CGI(Common Gateway Interface) 표준을 사용하여 개발되었다.

CGI 응용 프로그램은 컴파일되거나, 해석한 코드의 형태로 인바운드 HTTP 요청을 처리한다.

이 때, 웹 서버에 의해 별도의 프로세스로 생성된다.

 

1997년 Sun Microsystems 는 HTTP 요청을 JVM(Java Virtual Machine) 내에서 별도의 스레드로 처리해 CGI 기반 어플리케이션을 개선하려는 목적으로 Java Servlet 1.0 스펙을 발표했다고 한다.

곧이어 1999년에 Sun 이 JSP(Java Server Page) 1.0 스펙을 도입해 프레임워크를 강화햇다.

1996년 12월에 나온 Microsoft 의 ASP(Active Server Page) 와 개념이 유사했고, 특수 HTML 템플릿을 생성할 수 있는 Servlet 생성도 제공했다.

 

'Model 1' 은 요청이 어플리케이션의 모델(JavaBeans)과 상호 작용하는 JSP로 직접 라우팅된다.

Model 과 View 를 분리한 셈이다.

'Model 2'는 보다 복잡한 어플리케이션을 위해 Model - Controller - View 분리로, 요청이 모델과 상호 작용하는 Java Servlet 으로 라우팅된 후, View 를 브라우저로 렌더링하기 위해 JSP 에서 제어할 수 있게 됐다.

 

원래 Smalltalk-80 MVC 패턴의 Controller 가 사용자의 하드웨어 장치(마우스, 키보드 등) 입력을 처리하는 대신,

웹 기반 MVC Controller 가 HTTP 요청을 위임받아 처리한다는 점이다.

 

여기서 'Model 2' 가 서버측 아키텍처로 볼 수 있다는 기사가 나왔다고 한다.

이후 Struts 으로 MVC 프레임워크로 발전하게 된다.

 

마치며

글에서 Fowler Pattern 과 Supervising Controller Pattern, Passive View Pattern 등이 있지만

현재 2024년에 결코 들어 본 적이 없으므로...

관심 있으신 분은 더 찾아보기 바란다.

 

객체 수준의 디자인 패턴과 시스템 수준의 아키텍처의 차이를 배웠다.

MVC 패턴의 역사로 Smalltalk-80 MVC 패턴부터, Dolphin Smalltalk MVP 패턴까지 살펴봤다.

이제 MVC 패턴을 이야기하면, 조금은 아는 척 할 수 있을지도!?

 

MVC 패턴을 직접 적용하고 작성하면서

나처럼 의문이 많이 든 사람에게

명쾌한 해답이 되었으면 좋겠다.

 

자료를 많이 찾아보지 않았지만, [4] 만큼 역사와 배경, 이유까지 자세히 서술한 곳은 못 봤다.

한 곳에서만 조사했다 보니 틀렸을 수도 있다.

댓글로 많은 의견 부탁드립니다.

 

마지막으로 아래는 커뮤니티에서 정리한 글을 인용했다.

어쩌다보니, 정리한 걸, 다시 정리한(?) 기분이다.

저도 이번 주 내내 MVC 패턴이란 무엇일까.. 계속 고민을 하며 지냈어요.
해외 커뮤니티에서도 MVC 패턴이란 정말 무엇일까, 열띤 토론이 있더라고요.
그 글들을 하나씩 정독해가면서 MVC 패턴의 역사(?)도 파헤치게 되었어요.
그러던 중 Smalltalk-80 에서의 MVC 패턴, Dolphin Smalltalk 에서의 MVP 패턴 등등을 정리한 글을 정독했습니다.

위 글에서 Smalltalk-80 구조에서 뷰에서 컨트롤러의 연결이 있다고 하는데요.
이는 MVC 패턴의 고유한 부분이 아니며, 구현의 부산물이라고 합니다.
오히려 Dolphin Smallkal 에서의 MVP 패턴과 유사하다고 하네요.

뷰의 책임은 주로 출력을 처리하고, 컨트롤러의 책임은 주로 입력을 처리하는 것이라고 합니다.
모델과 상호작용하는 것은 뷰와 컨트롤러의 공동 책임입니다.
컨트롤러는 사용자 입력에 대한 응답의 결과로 모델과 상호 작용하고, 뷰는 자체 업데이트의 결과로 모델과 상호 작용합니다.
둘 다 필요에 따라 모델 내의 데이터에 액세스하고 수정할 수 있습니다.

사용자가 데이터를 입력하면 컨트롤러는 사용자의 입력을 가로채 적절하게 응답합니다.
일부 사용자 작업은 데이터 변경 또는 메소드 호출과 같은 모델과의 상호 작용으로 이어지고,
다른 사용자 작업은 메뉴 축소, 스크롤 막대 강조 표시 등과 같은 뷰의 시각적 변경을 일으킬 수 있습니다.

또한 MVC 에 대한 일반적인 오해를 설명했는데, 인상깊게 봤어요.
MVC 구성 요소간의 일반적인 오해 중 하나를 소개했습니다.
바로 컨트롤러의 목적이 뷰와 모델을 분리하는 것이 아니라고 합니다..!
즉, 뷰와 모델의 다리 역할을 하는 게 아닙니다.
MVC 패턴으로 모델(도메인) 계층을, 시각화하는 프레젠테이션 문제에서 분리를 하는데요.
이는 컨트롤러가 아니라 옵저버 패턴으로 달성한다고 합니다.
컨트롤러는 뷰와 모델 사이가 아닌, 최종 사용자와 애플리케이션 사이의 중재자로 생각하는 게 옳다고 합니다.
따라서 사용자 입력을 처리하는 것도 최종 사용자와 가까운 컨트롤러의 역할이 되지요.

겉으로 보기에는 Smalltalk-80 MVC 패턴과 Dolphin Smalltalk MVP 패턴과의 차이점을 식별하기 어렵습니다.
뷰와 모델의 역할도 동일하고, 컨트롤러와 프레젠터 모두 모델 업데이트에 관여합니다.
두 패턴 모두 옵저버 패턴으로 모델에 변경사항이 있을 때 뷰를 업데이트합니다.
위 글에서도, MVC 와 MVP 패턴을 어떻게 구별하는 걸 이해하는 데, 왜 그렇게 많은 혼란이 일어나는지 공감을 해주신 게 킬링포인트였어요. 🤣

MVC 패턴의 컨트롤러는 사용자와 가깝고, 사용자의 입력을 가로 채는 게 주요 목적이라고 합니다.
모델을 업데이트하는 건, 컨트롤러의 고유한 역할이라기 보다는, 이 기능의 부산물입니다.

반면, Dolphin Smalltalk Model-View-Presenter(MVP) 패턴 내에서 Presenter 의 주요 목적은 모델을 업데이트 하는 것 입니다.
View 가 위임한 이벤트를 가로채는 Presenter 의 역할은 모델을 업데이트하는 기능의 부산물입니다.

MVC 패턴의 원래 아이디어에선, 데이터(모델)와 편집기(뷰)를 분리하여 서로 상호작용하는 게 주 목적이었다고 합니다.
화면에 데이터를 표시하는 작업은 키보드나 마우스의 장치에서 사용자 입력을 해석하는 작업과 기술적으로 매우 다르기에,
이런 작업을 단일 객체로 처리하면 불필요하게 복잡해졌다고 합니다.
따라서, 입력과 출력에 대한 관심은 뷰와 컨트롤러로 분리되었습니다. 컨트롤러에는 편집기의 입력 책임이 할당되었기에,
자연스레 사용자로부터 입력을 받으면 모델을 업데이트하는 책임을 맡게 되었습니다.

반면 Dolphin Smalltalk MVP 패턴에선, 사용자의 입력을 가로채는 역할이 뷰로 이동했습니다.
이에 관해서 이전에 MVP 패턴을 고안한 Taligent 팀과 Dolphin 팀의 오해도 있으니 글을 참고해보세요~
그 이후에 나오는 Supervising Controller 패턴 등에서 컨트롤러는,
이제 뷰에서 사용자 이벤트를 받아 처리하는 역할을 담당한다고 합니다.

 

참고 문헌

  1. MVC - MDN Web : https://developer.mozilla.org/ko/docs/Glossary/MVC
  2. Software Architecture Patterns Ch1. Layered Architecture : https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html
  3. MVC Framework : https://www.tutorialspoint.com/mvc_framework/mvc_framework_introduction.htm
  4. Interactive Application Architecture Patterns : https://lostechies.com/derekgreer/2007/08/25/interactive-application-architecture/
    1. 5개월 전에 봤던 도메인이 이전됐다. 이미지 주소가 이전 호스트로 되어 있어 엑박이 뜬다.
  5. Taligent/IBM Model-View-Presenter (MVP) : https://stefanoborini.com/book-modelviewcontroller/02-mvc-variations/05-variations-on-the-triad/08-taligent-mvp.html

 

댓글