MVC 패턴, 서비스가 콘솔 애플리케이션에서 필요한가?
MVC 패턴
- Model : 애플리케이션의 데이터와 비즈니스 로직 담당
- View : 사용자에게 보여지는 부분 (데이터의 시각화)
- Controller : Model로부터 데이터를 받아 View로 전달하는 작업 수행
콘솔 애플리케이션 (블랙잭)
화면 요구사항
게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)
pobi,jason
딜러와 pobi, jason에게 2장을 나누었습니다.
딜러카드: 3다이아몬드
pobi카드: 2하트, 8스페이드
jason카드: 7클로버, K스페이드
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
y
pobi카드: 2하트, 8스페이드, A클로버
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason카드: 7클로버, K스페이드
딜러는 16이하라 한장의 카드를 더 받았습니다.
딜러카드: 3다이아몬드, 9클로버, 8다이아몬드 - 결과: 20
pobi카드: 2하트, 8스페이드, A클로버 - 결과: 21
jason카드: 7클로버, K스페이드 - 결과: 17
## 최종 승패
딜러: 1승 1패
pobi: 승
jason: 패
특징
- 도메인과
view
의 결합이 높다.- 도메인(카드 더 받기) 라는 행위가 입출력(질문 후 y/n)과 긴밀하게 연결되어 있다.
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
y
pobi카드: 2하트, 8스페이드, A클로버
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason카드: 7클로버, K스페이드
MVC 패턴으로 구현한다면?
- 모델(model) : 크게 카드, 참가자, 게임 결과로 나눠진다.
- 뷰(view) : 입력을 받는
inputView
, 결과를 콘솔에 출력하는resultView
가 있다.
- 컨트롤러(controller) :
모델
로부터 데이터를 받아view
로 전달하는 작업을 수행한다.
MVC 패턴 적용시 문제점 : Massive-Controller (과도한 책임)
Controller에 객체 생성 책임이 존재한다
public class BlackjackController {
private final InputView inputView;
private final ResultView resultView;
public BlackjackController(InputView inputView, ResultView resultView) {
this.inputView = inputView;
this.resultView = resultView;
}
public void run() {
final Deck deck = new Deck(new ShuffleCardGenerator());
final Dealer dealer = Dealer.createEmpty();
final Players players = makePlayers();
spreadInitialCards(dealer, players, deck);
spreadExtraCards(dealer, players, deck);
showParticipantScore(dealer, players);
showWinningResult(dealer, players);
}
Controller
는 단순히View
를 통해 입력을 받고,Model
로부터 데이터를 받아View
로 출력하는 역할만 수행해야한다.- 그런데 내가 작성한
Controller
는 참가자를 생성하고, 카드를 생성하는 등 객체 생성의 책임까지 가지고 있다.- 객체 생성의 책임은 블랙잭의 도메인이 가져야한다. 게임을 시작할 때 필요한 객체를 적절히 초기화하는 책임은 비즈니스 로직이다. 단순 흐름 처리가 아니다.
- 해결 방법 : 블랙잭 게임이라는 도메인을 생성하여 참가자와 덱을 내부에서 생성하도록 변경한다.
Controller에 비즈니스 로직이 존재한다
private void spreadExtraCards(final Dealer dealer, final Players players, final Deck deck) {
spreadCardsToPlayers(players, deck);
spreadCardsToDealer(dealer, deck);
}
private void spreadCardsToPlayers(final Players players, final Deck deck) {
Players availablePlayers = players.findHitAvailablePlayers();
for (Gamer gamer : availablePlayers.getPlayers()) {
while (gamer.canHit() && wantHit(gamer)) {
final Hand hand = deck.spreadCards(SPREAD_SIZE);
gamer.receiveCards(new Hand(List.of(hand.getFirstCard())));
resultView.printParticipantTotalCards(gamer.getNickname(), gamer.showAllCards());
}
}
}
private void spreadCardsToDealer(final Dealer dealer, final Deck deck) {
while (dealer.canHit()) {
final Hand hand = deck.spreadCards(SPREAD_SIZE);
dealer.receiveCards(new Hand(List.of(hand.getFirstCard())));
resultView.printDealerExtraCard();
}
}
출력
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
y
pobi카드: 2하트, 8스페이드, A클로버
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason카드: 7클로버, K스페이드
Controller
에서 모든Player
들을 순회하면서 카드 추가 여부를 확인하고 있다.- 카드 추가 여부를 확인하는 코드는 비즈니스 로직이므로 도메인 객체가 수행해야한다. Controller의 담당이 아니다.
Players를 도메인에서 순회하는 방법
도메인인 BlackjackGame
에서 순회하는 것이 좋다.
문제는 추가 카드를 받을 수 있는 Player
마다 view
로 추가 여부를 물어봐야한다는 것이다.
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
y
pobi카드: 2하트, 8스페이드, A클로버
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason카드: 7클로버, K스페이드
따라서 BlackjackGame
에서 몇번째 플레이어까지 카드 추가 여부를 질문했는지 알기 위해서는 3가지 방법이 있다.
1. BlackjackGame
에 상태 필드 두기
public class BlackjackGame {
private final int turnCount = 0; // 상태 필드
순회한 Player
에 대해 turnCount++
하여 상태를 저장하는 방법이다. 적용하지 않은 이유는 BlackjackGame
이 가변 객체가 되어 재사용을 하거나 여러 스레드에서 동시에 사용될 때 안전하지 않다.
그리고 필요없는 필드가 추가된다.
2. 맥락을 저장하는 Context
객체 또는 dto
객체를 매개변수로 전달하기
private void spreadPlayersExtraCards(final BlackjackGame blackjackGame) {
for (Gamer gamer : players.getPlayers()) {
blackjackGame.spreadExtraCards(turn);
}
}
context
객체인 Turn
을 Controller
에서 생성하여 파라미터로 주입하는 방식이다.
적용하지 않은 이유는 상태를 가진 Turn
을 도메인에서 의존해야하기 때문이다. BlackjackGame
이라는 도메인에서 맥락(context) 객체를 의존하는 방향이 반대로 되어있다.
도메인에서 dto를 의존하지 말라는 말도 이러한 맥락이라고 생각한다.
3. 식별자를 전달하기
private void spreadPlayersExtraCards(final BlackjackGame blackjackGame, final List<String> names) {
for (String name : names)) {
blackjackGame.spreadExtraCards(name);
}
}
초기에 입력받은 Player
들의 이름을 Controller
에 저장하여, 이름이라는 식별자로 순회하는 방법이다.
문제점은 Controller
에서 초기 이름들을 계속 저장하고 있어야하기 때문에 Controller
가 상태를 갖게 된다는 점이다.
Controller가 흐름 제외 이외에 상태까지 관리해야하고, 여러 스레드에서 동시에 접근하는 경우 문제가 발생할 수 있다.
또한 도메인이 가진 이름과 Controller
가 가진 이름이 동일해야하기 때문에 결합도가 높다. 만약 요청마다 Controller
가 새로 생긴다면 ? 이름을 따로 저장해야해서 좋지 않다.
선택한 방법
위 3가지는 모두 문제가 있다고 판단하였기 때문에 채택하지 않았다.
따라서 Controller
에서 플레이어들을 순회하지만 추가 카드 가능 여부는 플레이어가 판단하도록 구현하였다.
private void spreadPlayersExtraCards(final BlackjackGame blackjackGame) {
Players players = blackjackGame.findExtraCardsAvailablePlayers();
for (Gamer gamer : players.getPlayers()) {
spreadExtraCards(blackjackGame, gamer);
}
}
private void spreadExtraCards(final BlackjackGame blackjackGame, final Gamer gamer) {
while (gamer.canGetMoreCard() && isMoreCard(gamer)) {
blackjackGame.spreadOneCardToPlayer(gamer);
resultView.printParticipantTotalCards(gamer.getNickname(), gamer.showAllCards()); // 뷰에서 입출력
}
}
하지만 Massive-Controller
문제가 여전히 해결되지 못하였다.
위 요구사항은 view
를 사용해서 상호작용하고, "카드 추가 여부"라는 도메인 로직이므로 Controller에 두는 것은 적절하지 않다.
MVC 패턴을 꼭 사용해야하는가?
MVC 패턴을 사용해야하는가? 라는 질문에 대한 답변을 보고 약간의 실마리를 얻을 수 있었다.
현대 데스크톱 애플리케이션에서는 MVC
보다 더 나은 대안이 있다. (MVVM
패턴)
대부분의 GUI 프레임워크는 MVC 패턴에서의 뷰와 컨트롤러 사이의 엄격한 분리를 지원하지 않으므로, 다른 방법을 사용해보아라.
Model View Controller pattern for a Java desktop application
I'm struggling to fully understand MVC pattern, I found a lot of information on the web but they are really confusing because it seems there are various ways to do it. What I understood is that th...
softwareengineering.stackexchange.com
Massive-Controller 문제 해결 : MVVM 패턴 ?
MVVM 패턴
Controller
의 책임 일부를ViewModel
로 이동할 수 있다.View
가 사용자 입력과 표시를 모두 처리한다.ViewModel
이Model
과View
사이의 중간 계층 역할을 한다.
View Model을 이용해서Controller
에 너무 많은 책임이 집중되는 Massive Controller
문제를 완화할 수 있다.
콘솔 애플리케이션에서는 MVVM 적용 어려움
MVVM
패턴은 GUI
애플리케이션을 위해 설계되었기 때문에, 콘솔 애플리케이션에 간소화하여 적용하는 것이 어렵다.
그리고ViewModel
설계도 복잡하여 적절한 패턴이 아니다.
Massive-Controller 문제 해결 : Service를 사용한다면?
Service
는 비즈니스 로직을 처리한다. Controller
의 도메인 로직을 Service
에 옮기면 문제가 해결될까?Service
는 여러 도메인 객체를 조합하여 비즈니스 로직을 만드는데 사용된다.
블랙잭 게임의 실행 로직을 Service
에 두면 도메인 로직이 서비스로 옮겨간 것 뿐이지 객체의 응집도가 떨어지는 것은 여전하다.
MVC 패턴을 꼭 사용해야하는가?
Controller라는 클래스 쓰지 말자
- 현재
Controller
는MVC
패턴의Controller
가 아니다. 흐름을 관리하는 책임 외에 너무 많은 책임을 가지고 있기 때문이다.- 따라서
Controller
라고 부르는 것은 혼란스럽게 만들 수 있다. 이름 쓰지 말자.
- 따라서
MVC 패턴 말고 단순 책임 분리를 하자
MVC
패턴을 굳이 사용할 필요가 없다. 블랙잭 요구사항과 같은 도메인과View
의 결합도가 높은 로직에는 적합하지 않다.
패턴은 목적이 아니라 도구이다.
단순 콘솔 애플리케이션
선택한 구조
- domain : 게임 비즈니스 로직, 규칙
- view : 입출력 처리
- GameManager : 전체 게임 흐름 처리, 도메인 연결, 뷰 호출
GameManager는 domain을 조합하여 전체 게임의 흐름을 처리하고, view를 호출하여 입출력을 수행한다.
도메인과 view(변경 쉬움) 만 분리하면 되는 것이 아닌가?
느낀점
미션을 구현하다가 구구 코치가 전파해주신 반 MVC 사상을 접하게되었다.
이 주제는 우테코에서 가장 큰 핫 이슈였다. 익숙한 Model-Controller-View
패턴이 잘못되었을 수 있다고?
콘솔 애플리케이션에 MVC
패턴을 적용하는 것이 맞나? (웹도 아닌데)
MVC 패턴에 익숙해져 더욱 충격을 받았던 것 같다.
여러 크루들과 코치분들과 이야기한 결과 내가 구현한 Controller
가 MVC
패턴의 Controller
가 아닌, 과도한 책임을 가진 잘못된 Controller
란 것을 알았다. 또한 블랙잭이라는 요구사항이 view
와 도메인이 긴밀하게 결합되어 있으므로 더욱 MVC
패턴에 적절하지 않다.
따라서 이번 기회에 MVC 패턴에 대해 정리하고, 적절한 설계란 무엇인지라는 고민을 하는 기회를 가졌다.
그리고 처음부터 틀을 잡고 개발하는 것보다, 객체지향에 맞춰 개발하되 점점 틀을 갖추어 개발하는 것이 좋다는 생각을 하였다.
우리가 블랙잭 미션에서 집중해야하는 가장 중요한 부분은 MVC
보다는 적절한 객체 설계이기 때문이다. 또한 틀을 잡고 개발하면 객체의 책임이 틀에 맞춰 분산될 수 있기 때문이다. (Controller, Service)
'우테코' 카테고리의 다른 글
[Lv1] 중간 회고 (0) | 2025.03.12 |
---|---|
[우아한테크코스] 7기 백엔드 합격 후기 (10) | 2025.01.03 |
[우아한테크코스] 최종 코딩테스트 후기 (6) | 2024.12.16 |
[우아한테크코스] 프리코스 4주차 - 편의점 미션 회고 (2) | 2024.11.17 |
[우아한테크코스] 프리코스 중간 회고 (5) | 2024.11.06 |