[Pattern] Android의 설계 패턴 1 - MVC
Eric Maxwell 선생님께서 작성하신 안드로이드의 패턴 간단 정리를 통해 그동안 머릿속에 있던 안드로이드 설계 패턴에 대한 파편화된 기억을 모아보고자 한다. 안드로이드에서 쓰이는 패턴은 크게 MVC, MVP, MVVM이 있지만 각기 장/단점이 있고, 늘 엔지니어들 사이에서 무엇이 나은가에 대한 논의는 계속해서 이루어지고 있다.
최근(2017 이후)에 이르러서는 구글이 제시하는 Jetpack을 통한 설계로 중간~대규모 프로젝트에는 거의 MVVM가 많이 쓰인다. 그렇다고 MVVM이 최고로 좋다!는 아니고 어떤 경우는 MVC, MVP가 좋을 때도 있다. 그리고 왜 MVVM 으로 귀결되는 추세를 보이는지 더 잘 알기위해선 MVC, MVP에 대해 당연히 알아야 한다.
MVC는 모델(Model)-뷰(View)-컨트롤러(Controller)를 부르는 말로, 안드로이드 프로그래밍은 초창기부터 이 패턴을 초석으로 발전해 왔다. 이제부터 Eric Maxwell 선생님께서 작성하신 훌륭한 예제인 Tic-Tac-Toe를 보며 이야기해보겠다. 틱택토는 O랑 X랑 서로 번갈아 샐을 선택하며 빙고를 먼저 만드는 사람이 이기는 게임이다.
https://github.com/ericmaxwell2003/ticTacToe
MVC
보통 Model View Controller 라고 불리는 세 가지 책임 집합으로 분리되어 동작하는 패턴을 말한다. 예제를 확인하기 전에 각 책임 집합 하나하나 먼저 알아보면 다음과 같다.
모델(Model)
모델은 앱의 상태 및 데이터와 비즈니스 로직이라고 보면된다. 즉 앱의 두뇌 역할을 한다. 모델을 클래스로 표현하면 클래스의 멤버 변수들은 상태, 멤버 메서드들은 비즈니스 로직이라고 하면 되겠다. 여기서 멤버 메서드는 컨트롤러와 상호작용하는데 쓰이는 API역할을 수행하며 Public 으로 작성되었다고 보면된다. 메서드를 호출할 때 전달하는 매개변수를 상호작용하는 데이터라고 보면된다!
예제에서는 Board 라는 클래스가 모델을 담당하고 있다.
뷰(View)
뷰는 모델을 표현한 것으로 보통 안드로이드에서는 UI로 보여진다. 사용자(User)와 상호작용하는 부분이기에 유저가 발생 시킨 여러 이벤트(클릭, 입력 등)들을 컨트롤러로 전달한다. 뷰는 모델이 어떻게 구성되어있는지 사용자의 상호작용에 따라 어떻게 반응해야하는 지 전혀 모르고 있다. 모르고 있다는 것은 그만큼 모델에 종속되지않아 유연하다는 뜻이다.
예제에서는 Xml 코드로 작성된 Layout코드이다.
컨트롤러(Controller)
컨트롤러는 모델과 뷰를 연결해주는 역할을 담당한다. 뷰에서 발생한 정보를 모델에 전달하고, 모델에서 처리된 정보를 다시 뷰에 업데이트한다. 안드로이드에서는 컨트롤러가 비교적 명확하게 구성되는데, 액티비티와 프레그먼트가 보통 컨트롤러 역할을 한다.
예제에서는 TicTacToeActivity(MainActivity)가 담당한다.
MVC의 동작
정리하자면 MVC는 다음과 같이 작동한다.
예제 코드를 살펴보기 앞서 뷰는 Xml로 작성된 파일로 안드로이드를 개발하는 사람(Jetpack Compose나 크로스 프레임워크 개발자 제외)은 매우 친숙한 뷰 설계 환경이기에 설명을 제외하도록 하겠다. 다만 뷰 Layout에는 각 샐이 있고 사용자는 샐을 눌러(터치) 해당 위치에 말을 둔다.
이제 "사용자가 화면 상의 샐을 터치하는 것"을 시작으로 MVC가 어떻게 동작하는지 코드로 살펴보겠다.
컨트롤러(Controller) 코드 일부 : TicTacToeActivity.java
public class TicTacToeActivity extends AppCompatActivity {
private Board model;
/* View Components referenced by the controller */
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tictactoe);
winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
winnerPlayerViewGroup = findViewById(R.id.winnerPlayerViewGroup);
buttonGrid = (ViewGroup) findViewById(R.id.buttonGrid);
model = new Board();
}
... 중략
public void onCellClicked(View v) {
Button button = (Button) v;
int row = Integer.valueOf(tag.substring(0,1));
int col = Integer.valueOf(tag.substring(1,2));
Player playerThatMoved = model.mark(row, col);
if(playerThatMoved != null) {
button.setText(playerThatMoved.toString());
if (model.getWinner() != null) {
winnerPlayerLabel.setText(playerThatMoved.toString());
winnerPlayerViewGroup.setVisibility(View.VISIBLE);
}
}
}
... 후략
}
컨트롤러에는 모델 인스턴스(model = new Board())가 있다. 이걸로 모델과 통신한다.
사용자가 샐을 터치하면 뷰는 무엇을 할지 모른다. 따라서 컨트롤러는 findViewById() 메서드를 이용해 각 뷰 컴포넌트들을 참조한뒤, 뷰에서 발생하는 터치 이벤트가 컨트롤러의 onCellClicked() 메서드를 호출하도록 연결되어있는 구조다. 컨트롤러는 onCellClicked() 메서드에서 모델(Board)의 mark 메서드를 호출하여 모델과 통신한다.
컨트롤러는 샐의 클릭이라는 정보를 받았고, 컨트롤러는 이 정보(데이터)를 적당히 처리할 모델의 API 함수를 알고 있다. 그 API 함수는 mark 메서드로 이는 모델에 구현이 되어있는 메서드이다. 이제 모델의 코드를 보겠다.
모델(Model) 코드 일부 : Board.java
public class Board {
private Cell[][] cells = new Cell[3][3];
private Player winner;
private GameState state;
private Player currentTurn;
... 중략
public Player mark( int row, int col ) {
Player playerThatMoved = null;
if(isValid(row, col)) {
cells[row][col].setValue(currentTurn);
playerThatMoved = currentTurn;
if(isWinningMoveByPlayer(currentTurn, row, col)) {
state = GameState.FINISHED;
winner = currentTurn;
} else {
// flip the current turn and continue
flipCurrentTurn();
}
}
return playerThatMoved;
}
... 후략
}
모델의 mark 함수는 Player 라는 객체를 반환한다. 이 객체는 해당 위치가 O인지 X인지 알고 있는 모델의 상태값이다. 모델은 사용자의 터치에 따라 변화한 정보로 Cell 배열을 업데이트(setValue)한다. Cell 은 모델이 관리하는 이 앱의 상태라고 볼 수 있다.
이제 다시 리턴받은 업데이트 된 모델의 정보를 기반으로 컨트롤러(액티비티)의 메서드 onCellClicked는 뷰(화면)을 갱신해준다.
MVC의 장단점
장점
MVC는 모델과 뷰를 완벽히 분리하여, 모델이 어디에도 종속되지않아 모델의 입장에서는 유연하다. 또한 유닛 테스트 단계에서 모델을 쉽게 테스트할 수 있다. (유닛 테스트 : 버그의 유무를 발견하기 위해 각종 조건을 입력해보는 테스트 코드를 작성하는 행위)
단점 (컨트롤러의 문제)
1. 컨트롤러가 안드로이드 API에 깊게 종속되어 유닛 테스트가 어렵다.
2. 뷰와 컨트롤러가 강하게 결합되어 있어서, 뷰 변경 시 컨트롤러도 변경해야하는 소요가 있다. 즉 유연하지 않고 모듈화가 되지 않는다.
3. 시간이 지남에 따라 코드가 늘어, 컨트롤러가 지나치게 비대해지는 경향이 있다. 즉 유지보수가 어려워진다. (MainActivity.java 코드만 몇천줄?)
끝
'Architecture & Engineering > Design Pattern' 카테고리의 다른 글
[GoF Design Pattern] 3. 데코레이터(Decorator) 패턴 (0) | 2023.06.18 |
---|---|
[GoF Design Pattern] 2. 옵저버(Obsever) 패턴 (0) | 2023.05.29 |
[GoF Design Pattern] 1. 전략(Strategy) 패턴 (0) | 2023.05.14 |
[Pattern] Android의 설계 패턴 2 : MVP by Java (0) | 2022.04.17 |
[Pattern] 옵저버 패턴 (Observer Pattern) by Kotlin (0) | 2022.03.28 |
댓글