MVC는 사용자 인터페이스와 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소와 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있는 스타일의 아키텍처다. 이렇게 하면 사용자 인터페이스를 담당하는 계층의 응집력을 높일 수 있고 여러 개의 다른 UI를 만들어 그 사이에 결합을 낮출 수 있다.
모델-뷰-컨트롤러의 관계
- 컨트롤러 - 모델에 명령을 보내 모델의 상태를 변경할 수 있다. 뷰와 모델 사이에서 애플리케이션 흐름을 통제
- 모델 - 데이터의 상태에 변화가 있을 때 컨트롤러와 뷰에 이를 통보한다. 이를 통해 뷰는 최신 결과를 보여줄 수 있고, 컨트롤러는 모델의 변화에 따른 적용 가능한 명령을 내릴 수 있다..
- 뷰 - 사용자가 볼 결과물을 생성하기 위해 모델로부터 정보를 얻어 온다.
장점
- 느슨한 결합, 확장성: 각 컴포넌트의 결합이 약해 다른 부분에 영향을 주지 않고 수정할 수 있다. (유지보수성 좋음)
- 다수의 다른 뷰:: 하나의 모델을 위하여 다수의 다른 뷰를 쉽게 제공할 수 있다. 코드 중복이 적다.
단점
- 복잡도: 컴포넌트의 분리로 메커니즘 이해를 위한 복잡도가 올라갈 수 있다.
- 비효율성: 뷰에서 데이터를 접근하여야 하는 비효율적인 부분이 있다.
- 각 컴포넌트 구현을 위한 여러 기술에 대한 이해가 필요하다.
적용 시 유의할 점
이 유의점은 이 영상을 참고하여 적었습니다.
- Model은 Controller와 View에 의존하지 않아야 한다.
- View는 Model에만 의존해야 하고 Controller에는 의존하면 안 된다.
- Controller는 Model과 View에 의존해도 된다.
- View가 Model로부터 데이터를 받을 때는, 사용자마다 다르게 보여주어야 하는 데이터에 대해서만 받아야 한다.
- View가 Model로부터 데이터를 받을 때, 반드시 Controlelr에서 받아야 한다.
그림으로 나타내면 다음과 같다. 왼쪽 그림이 의존 관계를 나타낸 것이고 유의점 1, 2, 3번을, 오른쪽 그림이 5번을 설명해준다. 무엇보다 핵심은 비즈니스 로직과 뷰의 철저한 분리이다. 비즈니스 로직과 뷰가 각자만 변경되어도 서로를 터치할 일을 최대한 줄이거나 없애야 한다. 특히 대부분의 애플리케이션들은 비즈니스 로직에 비해 뷰의 변경 가능성이 많다. 뷰가 변경되어도 비즈니스 로직이 바뀌지 않으면 분리가 잘 이루어졌다고 말할 수 있을 것이다.
자동차 경주 게임 예시
자동차 경주 게임을 구현한다고 가정하자. 자세한 설명은 생략하고 차 이름을 입력받아 시도 횟수만큼 전진시킨 뒤에 결과를 출력하는 애플리케이션이다. 전진 조건은 9 이하 정수를 랜덤으로 받아 4 이상이면 전진한다. 실행 예시는 다음과 같다.
주요 클래스는 다음과 같다. 실제론 더 많은 클래스가 있지만 간략한 설명을 위해 줄인 것이다.
- Model
- Car
- RacingService
- RacingResult
- Controller
- RacingController
- View
- OutputView
먼저 MVC 패턴을 지키지 않았을 때를 생각하면 코드를 이렇게 짤 수도 있을 것이다.
RacingService.race() 리펙토링 전
public class RacingService {
public void race(int attemptNumber) {
// 입력 받은 이름으로 List<Car> cars를 생성했다고 가정
for (int i = 0; i < attemptNumber; i++) {
cars.forEach(car -> car.move()));
// 진행 바("-") 출력
System.out.print(car.getName + " : ");
//...
}
// 뷰 출력 로직
List<String> winnerNames = findWinnerNames();
OutputView.printWinners();
}
}
RacingService는 경주 비즈니스 로직을 처리한다는 관점에서 Model에 속하는데 View의 로직이 들어갔다. 이렇게 되면 View에 변경이 일어났을 때 RacingService의 race() 메서드까지 고쳐야 하는 것이다. 그리고 또 문제점이 Controller를 거치지 않고 Domain 단에서 데이터를 View에 출력하고 있다. MVC의 역할 분리를 위해선 데이터 플로우가 Domain -> Controller -> View의 순서로 계층적으로 전달되어야 한다. 이제 나름 MVC를 적용시킨 코드를 살펴보자.
RacingService.race() 리펙토링 후
public RacingResult race(int attemptNumber) {
// ...
RacingResult racingResult = new RacingResult();
for (int i = 0; i < attemptNumber; i++) {
cars.forEach(car -> car.move();
racingResult.addRecord(findCarDtos());
}
return racingResult;
}
리펙토링한 RacingService의 race() 코드이다. race() 내부에서 View를 사용하던 아까와는 다르게 RacingResult라는 경주 결과 데이터를 담고 있는 객체를 반환하고 있다. 이 클래스 안에는 시도 횟수마다의 Car들의 진행 상황이 다 담겨 있다. 이 메서드를 Controlelr에서 사용해보겠다.
RacingController
public class RacingController {
private final RacingService racingService = new RacingService();
public void start() {
// ...
// 시도횟수인 int attemptNumber를 입력 받았음
RacingResult racingResult = racingService.race(attemptNumber);
OutputView.printResultMessage(); // "실행 결과" 메세지 출력
racingResult.get().stream()
.forEach(OutputView::printRacingInfo); // 경주 상황 출력
List<String> winnerNames = racingService.findWinnerNames();
OutputView.printWinnersMessage(winnerNames); // 우승자 이름 출력
}
}
RacingController의 start() 함수를 보면 race()를 사용해 얻은 RacingResult 안의 데이터를 OutputView로 넘겨줌으로써 계층간 데이터 흐름도 지키고, Model과 View를 완전 분리해서 Controller에서 사용하고 있다. 이렇게 되면 경주 상황을 출력하는 뷰가 변경되어도 race() 메서드가 RacingResult를 반환하는 로직까지는 똑같기 때문에 RacingResult를 OutputView 안에서 어떻게 바꿀지만 고민하면 된다.
'방법론' 카테고리의 다른 글
소프트웨어와 복잡성 관리 (0) | 2024.09.01 |
---|---|
도메인 vs 엔티티 (feat. DDD) (1) | 2022.05.15 |
전략패턴과 OCP (2) | 2022.02.18 |
TDD(Test-Driven-Development) 강의 복습 (0) | 2022.02.13 |