성장일기

여러분은 MVC 패턴에 대해서 얼마나 알고 계신가요? 단순히 먼저 접한 Model, View, Controller의 역할과 단순한 구현 방법에 대해서만 알고 계신가요?

 

목차

  1. 개요
  2. 간단한 Lotto 예제
  3. default 예제 코드
  4. MVC 패턴 이전에
  5. MVC ver.0
  6. MVC ver.1
  7. MVC ver.2
  8. MVC ver.3
  9. MVC ver.4
  10. MVC ver.5
  11. 마무리
  12. Github Repository Link

 


 

1. 개요

해당 글은 우테코 5기 프리코스의 미션 중 다리 건너기 미션을 진행하면서 생긴 의문점을 해결하기 위해 자료를 찾아보고, 공부한 과정을 정리 및 공유하기 위한 글입니다. 혹여나 잘못된 정보는 댓글로 남겨주시면 감사하겠습니다.

 

우테코 5기 프리코스 4주 차 미션의 다리 건너기 미션을 수행하는데, 미션을 위해 주어진 클래스를 보아하니 뭔가 MVC 패턴을 사용하도록 유도하는 듯한 느낌을 받았습니다. 저는 이전에 Spring으로 개발을 해본 적이 있어서 MVC 패턴을 접했었고, 제가 알고 있는 MVC 패턴으로 개발을 진행했습니다.

 

하지만 미션 중 프리코스 슬랙에 어떤 분이 테코 톡의 MVC 발표 영상을 공유해주었고, 제가 알고 있던 MVC 패턴과 상이하다는 것을 알게 되었습니다.

 

해당 테코 톡에서 제가 알고 있는 MVC 패턴과 달랐던 점은 View가 Model을 의존하고 있던 점이었습니다. 제가 생각하는 View가 Model을 의존하면 안 좋은 점은 View에서 Model 객체를 다룰 수 있는 여지를 남겨놓는 것입니다. View에서 Model객체의 데이터를 getter로 가져와서 출력만 한다고 해도, View에 Model 객체를 의존하고 있기 때문에 View에서 Model 객체를 다룰 수 있는 여지가 있습니다.

 

따라서 저는 구글링을 해보았고, 여러 글들에서 각각 상이한 MVC 패턴을 설명하고 있었습니다. 찾아본 결과  MVC 패턴은 워낙 오래전부터 생겨난 개발 방법론(아키텍쳐)이고, 시대를 거듭할수록 모바일에서, 웹에서 적용되면서 변화해왔던 것을 알 수 있었습니다.

 

MVC의 역사에 대해 공부하다 보니 MVC -> MVP -> MVVM 으로 변화해왔고, 자바 웹 진영에서는 서블릿, JSP, MVC v1, v2... 프레임워크로 변화해왔다는 것을 공부하게 되었습니다. 많은 사람들이 MVC 패턴이라고 부르면서 사용해온 것들이 생각만큼 표준화되어있지 않다고 생각했습니다.

 

저는 그래도 웹이나 안드로이드로 개발해본 적이 있어서 자료들을 찾아보고 이해하는데 어려움이 없었지만, 웹, 모바일 개발 경험이 없는 분들을 MVC 패턴 변화 과정이 어려울 수 있겠다고 생각했습니다.

 

따라서 공부한 점을 토대로 웹, DB, Servelet, 모바일 등 프레임워크 없이 기본 Java 프로그램만으로 MVC 패턴을 구현해보면서 상황을 부여하여 각 상황에서의 단점(아쉬운 점)을 보완하면서 MVC 패턴을 업그레이드시키면서 놀아보았고, 공부 과정을 공유하기 위해서 글을 정리해 보았습니다.

 

프로그램 예제는 우리에게 익숙한 우테코 프리코스 3주 차 미션의 로또 게임 미션을 간략화했습니다.

 

들어가기 전에,

  1. 기본적인 MVC의 개념과 특징 및 구현 방법에 대해서 설명하지 않습니다.
  2. Java 문법, 클린 코드, 객체지향이 핵심이 아니기 때문에 해당 부분은 깊게 신경 쓰지 않았으며 부족한 부분이 있을 수 있습니다.(getter, setter를 막 사용합니다.)
  3. MVC 패턴의 변화가 핵심이기 때문에 Model, View, Controller의 상관관계에 대해서만 관심을 가집니다. 따라서 예외 처리, null 체킹, 상수 화등을 전혀 고려하지 않고 정상 로직 작동에만 집중합니다.(Service 계층도 두지 않았습니다.)
  4. 부족한 부분이 있을 수 있습니다.
  5. 글이 길어져 전체 코드를 올리지 않고 각 버전의 MVC 패턴에서의 특징을 파악할 수 있을 정도의 코드만 올리고, 전체 코드는 깃허브를 통해 공개하도록 하겠습니다.(특히 로또를 구입하는 기능의 Controller, view에 대해서만 다루도록 하겠습니다.)
  6. 어떤 버전의 MVC 패턴이 더 낫다는 정답은 없습니다. 오히려 작은 규모의 프로젝트에서는 해당 패턴이 오버 프로그래밍일 수 있습니다. 예제에서는 필요 이상의 확장을 다루고 있습니다.

 

 

2. 간단한 Lotto 예제

1. 기능 목록

  1. 로또 구입
  2. 구입한 로또 개수 조회
  3. 구입한 로또 조회
  4. 종료

 

3가지 핵심 기능이 있는 아주 간단한 프로그램입니다.

 

2. 실행 예시

더보기

-----------------
1. 로또 구입
2. 구입한 로또 개수 조회
3. 구입한 로또 조회
4. 종료
번호를 입력해 주세요: 
1
구입할 로또 번호를 입력 해주세요: 
1,2,3,4,5,6
로또를 구입했습니다.
[1, 2, 3, 4, 5, 6]

-----------------
1. 로또 구입
2. 구입한 로또 개수 조회
3. 구입한 로또 조회
4. 종료
번호를 입력해 주세요: 
1
구입할 로또 번호를 입력 해주세요: 
2,3,4,5,6,7
로또를 구입했습니다.
[2, 3, 4, 5, 6, 7]

-----------------
1. 로또 구입
2. 구입한 로또 개수 조회
3. 구입한 로또 조회
4. 종료
번호를 입력해 주세요: 
2
구입한 로또의 개수는 2장 입니다.
-----------------
1. 로또 구입
2. 구입한 로또 개수 조회
3. 구입한 로또 조회
4. 종료
번호를 입력해 주세요: 
3
구입한 로또 목록입니다.
[1, 2, 3, 4, 5, 6]
[2, 3, 4, 5, 6, 7]

-----------------
1. 로또 구입
2. 구입한 로또 개수 조회
3. 구입한 로또 조회
4. 종료
번호를 입력해 주세요: 
4
프로그램을 종료합니다.

 

 

3. default 예제 코드

앞으로 모든 예제에서 쓰이고 바뀌지 않을 Lotto 클래스와 Lotto들을 저장할 수 있는 LottoRepository를 만들었습니다. 우리의 예제는 MVC 패턴 변화가 핵심이기 때문에 Controller, View만 변화해나갑니다. Service 계층도 두지 않았습니다.

 

 

4. MVC 패턴 이전에

로또를 구입하는 기능인 LottoBuyJsp 클래스만 살펴보도록 하겠습니다. 

 

MVC 패턴을 적용하기 이전의 코드입니다. Java 코드로 최대한 Jsp와 유사하게 짜보았습니다.(Jsp를 모르시는 분들은 Java로 웹 개발을 이렇게도 했었구나 하고 넘어가시면 됩니다.)

 

우테코 프리코스를 다 마치신 분들이라면 해당 코드에서 문제점을 쉽게 발견할 수 있을 거라고 생각합니다.

 

 

문제점

  1. LottoBuyJsp 클래스가 뷰를 렌더링 하는 책임과, 비지니스 로직을 처리하는 책임을 모두 가지고 있습니다. 또한 Java 코드와 데이터를 저장하는 Repository도 같은 클래스에 있습니다.
  2. 비지니스 로직의 변경이 있어도 해당 코드를 손대야 하고, UI를 변경할 일이 있어도 해당 코드를 손대야 하고, Repository를 바꿔야 할 경우도 해당 코드를 손대야 합니다. 작은 규모의 프로젝트도 복잡해 보이는데 서비스 규모가 커지면 커질수록 유지보수 하기 더 어려울 것입니다.

 

해당 코드를 가장 기본적인 MVC 구조로 개선해 보겠습니다.

 

 

5. MVC ver.0

사용자 입력과 출력 부분을 InputViewOuputView를 각각의 클래스로 분류하였습니다.

 

Controller는 InputView를 통해 요청을 받아서 비지니스 로직을 실행하고 프로그램 실행 흐름을 담당합니다. 그리고 결과를 OutputView에 전달해 결과를 출력합니다. 

 

해당 MVC 패턴은 개요에서 언급했던 테코 톡에 나와있는 MVC 패턴과 동일합니다. 가장 기본적인 MVC 패턴으로 많은 분들이 쉽게 접할 수 있는 패턴입니다.

 

+ Controller에 비지니스 로직을 둘 수도 있지만, Controller가 너무 많은 역할을 담당합니다. 따라서 비지니스 로직은 서비스(Service)라는 계층을 두어서 처리할 수도 있습니다. (앞서 언급했듯이 MVC 패턴에 집중할 것이기 때문에 예제를 최대한 간단히 하였고, 서비스 계층은 고려하지 않았습니다.)

 

 

개선한 점

  1. MVC 패턴 적용으로 Controller 로직과 View 로직을 확실하게 분리했습니다.
  2. 향후 수정이 발생하면 각각의 로직만 변경하면 됩니다.

 

아쉬운 점(서비스가 지속해서 확장함을 가정합니다.)

  1. 서비스가 확장하여 요청과 기능이 늘어날수록 Controller의 사이즈가 너무 커집니다.(run 메서드 내 if문 분기와 해당 메서드들이 계속 늘어나는 구조입니다.)
  2. 메서드들을 서비스 레이어로 둬서 나누더라도 서비스가 확장할수록(회원 기능, 결제 기능 등의 확장) if문 분기가 커질 뿐만 아니라 해당 Controller의 코드를 수정해야 합니다. 이는 Open - Closed Principal을 위배합니다.
  3. Controller도 커지지만, 뷰 화면을 위한 OutputView 클래스 역시 서비스가 늘어남에 따라 거대해집니다.

 

Contoller와 OutputView를 확장하기 좋게 개선해보겠습니다.

 

 

6. MVC ver.1

각 사용자 요청마다 기능을 수행할 수 있는 Controller를 분리하고, MainController를 두어서 한곳에 집중되어 있는 기존의 Controller의 책임을 분리했습니다.

 

OutputView 역시 각 요청의 응답에 맞는 View를 따로 분리했습니다. 따라서 LottoBuyController에서 해당 LottoBuyOutputView를 이용하여 랜더링합니다.

 

 

개선한 점

  1. 각 사용자 요청마다 컨트롤러를 따로 만들어서 컨트롤러 별로 요청을 수행하도록 개선했습니다. 따라서 새로운 요청의 기능이 생겨날 경우 새로운 컨트롤러를 만들어주면 됩니다.(억지로 Controller를 쪼갰지만 LottoController, MemberController, PayController 등 비슷한 도메인의 요청을 담당하는 Controller로 분류할 수 있습니다.)
  2. OutputView 역시 각 요청마다 랜더링 할 View를 따로 만들어 OutputView 역시 무거워지는 점을 개선했습니다.

 

아쉬운 점(서비스가 지속해서 확장함을 가정합니다.)

  1. 새로운 요청이나 기능에 따라 Controller를 만들어서 적용하면 되지만, 여전히 run() 메서드에 중복이 있고, run 메서드를 수정해줘야 하는 문제가 있습니다.
  2. LottoBuyOutputView 클래스를 보면 View 레이어에서 Lotto클래스 즉, 도메인 모델 객체를 의존하고 있습니다. 개요에서 언급했던, View에서 Domain Model 객체를 의존하고 있기 때문에 View에서 모델을 다룰 수 있는 여지가 있고, View에서 필요 이상의 모델의 정보에 대해서 알 수 있습니다.

 

이러한 아쉬운 점을 개선해보도록 하겠습니다.

 

 

7. MVC ver.2

Controller를 ControllableV2 인터페이스로 추상화하여 모든 Controller는 해당 인터페이스를 implement 해야 합니다. 사용자의 모든 요청은 FrontController가 먼저 요청을 받도록 하고, 사용자 요청에 따라 해당 요청을 수행할 수 있는 Controller를 찾아서 Controller를 실행시켜 주도록 했습니다. Controller들은 Map 저장해 두고 사용자 요청은 1,2,3,4의  입력 번호에 따라 달라지기 때문에 1,2,3,4를 키로 잡았습니다.

 

이전의 OutputView에서 Domain Model을 직접 의존하던 부분을 Map을 Model로 이용하여 key, value 방식으로 Controller에서 OutputView로 필요한 데이터들을 담아서 꺼내 쓸 수 있도록 개선했습니다.

 

 

개선한 점

  1. FrontCotroller를 만들어 모든 요청이 우선적으로 FrontController를 거치도록 하고, FrontController는 요청에 맞는 Controller를 찾아서 기능을 수행하게 됩니다. 따라서 이전 버전의 Controller의 run메서드 내의 if문 분기 로직조차 없앴습니다. 그리고 새로운 요청의 기능이 생겨날 경우 FrontController의 변경 없이 새로운 Controller를 만들어서 넣어주기만 하면 되기 때문에 Open - Closed Principal을 잘 지켰다고 볼 수 있습니다.

 

아쉬운 점(서비스가 지속해서 확장함을 가정합니다.)

  1. 각 컨트롤러가 OutputView를 알아야 합니다. 즉, View로 이동하는 코드를 항상 알아야 하고, 항상 호출되어야 합니다. 물론 이 부분을 메서드로 공통화해도 되지만, 메서드는 항상 직접 호출해야 합니다. 그리고 만약 해당 OutputView가 아닌 다른 랜더링을 위한 View로 바꾸고 싶을 때, 해당 Controller 코드를 고쳐야 하는 상황입니다. 이 역시 Open Cloed Principal을 위배합니다.

 

해당 아쉬운 점을 개선해보겠습니다.

 

 

8. MVC ver.3

OutputView도 Viewable 인터페이스를 이용하여 추상화했습니다. 따라서 모든 OutputView는 해당 인터페이스를 implements 해야 하고, OutputView들을 OutPutViewFactory에서 논리 이름으로 가져올 수 있도록 했습니다.

(View를 가져오는 방법은 Map을 사용하는 것이 아니라 자바 reflection을 이용하여 패키지 명과 클래스 명으로도 가져오도록 하는 방법도 있을 것 같습니다.)

 

이전의 Controller 인터페이스와 다른 점은 ViewModel을 반환하는 부분입니다. ViewModel은 해당 Controller가 출력할 때 필요한 View를 찾는 논리 이름(경로)을 지정해주어 View를 찾을 수 있습니다. 또한 View에서 필요한 Model을 담을 수도 있습니다.

 

이후 FrontController에서 해당 Controller에서 반환받은 ViewModel 객체를 이용하여 랜더링 합니다.

 

 

개선한 점

  1. OutputView를 Viewable 인터페이스를 이용해서 추상화하여 Controller의 OutputView 의존성을 제거하여 View를 바꾸는 일이 있더라도 Controller는 수정하지 않아도 됩니다.(View Path를 상수로 따로 관리하면 더 좋습니다.)

 

아쉬운 점(서비스가 지속해서 확장함을 가정합니다.)

  1. Controller에서 OutputView로 전달할 Model이 필요 없을 때는(단지 출력만) ViewPath만 반환하는 것이 이상적입니다.
  2. 개발자가 Controller를 사용할 때마다 ViewModel을 일일이 생성해서 값을 주입해서 반환해야 합니다. 개발자 입장에서는 Model이 필요한 경우 주어진 Model을 활용하여 값을 주입하고, 단지 ViewPath만 반환하는 것이 더 편합니다.

 

해당 아쉬운 점을 개선해보겠습니다.

 

 

9. MVC ver.4

이전의 Controller 인터페이스와 다른 점은 Model을 파라미터로 주입받아서 해당 Model을 이용하여 View에 전달할 데이터를 넣어주면 됩니다. 그리고 View의 논리 이름을 단지 반환하면 됩니다. Model은 어디서 주입해주는 걸까요?

 

FrontController에서 해당 Controller를 실행시켜주는 과정에서 Model을 주입시켜줍니다. 실행 결과로 viewPath를 받게 되고, 이후 기존에 사용했던 ViewModel을 생성하여 View를 랜더링 합니다. 

 

 

개선한 점

  1. 프로그래머가 일일이 ViewModel을 생성하고, 주입하고, 반환하는 작업을 Model을 파라미터로 받고 단지 View의 논리 이름을 반환하면 되는 구조로 바꿨습니다.

 

아쉬운 점(서비스가 지속해서 확장함을 가정합니다.)

  1. Controller에서 Model을 View로 넘기지 않아도 되는 경우에도 파라미터로 Model을 주입받아야 하기 때문에 이 부분이 맘에 안들 수도 있습니다. 이럴 경우에는 View의 논리 이름만 반환하는 Controller를 만들어주면 됩니다.
  2. 하지만 현재 FrontController는 한 버전의 Controller만 사용 가능하기 때문에 지금까지 개선해 왔던, 여러 버전의 Controller를 동시에 사용하지 못합니다.

 

해당 아쉬운 점을 개선해보겠습니다.

 

 

10. MVC ver.5

일단 앞선 버전의 아쉬운 점인 필요 없을 경우 Model을 받지 않고 View의 논리 이름만 반환하는 ControllerV5를 빠르게 만들어 보겠습니다.

기존에는 로또를 구입하는 기능의 Controller를 예시로 들었지만, View로 Model을 전달할 필요가 없고, 단지 게임 종료 문구만 출력하는 즉, View 논리 이름만을 반환하는 LottoEndController를 예시로 들었습니다.

 

하지만 ControllerV5를 사용할 경우 View로 Model을 전달할 필요가 있을 경우 전달할 방법이 없기 때문에 ControllerV5만 사용할 수 없습니다.

 

따라서 FrontController에서 여러 버전(V3, V4, V5)의 Controller를 사용할 수 있도록 개선해 보겠습니다. 여러 가지 방법이 있겠지만, 디자인 패턴 중 어댑터 패턴을 사용하도록 하겠습니다.

 

어댑터 패턴을 현 상황에 맞게 간단하게 설명하자면, FrontController는 Adapter(규격)를 의존하고 Controller(V3, V4, V5)들은 Adapter 규격에 맞는 각각의 Adapter(V3, V4, V5)를 만들어서 FrontController에게 제공해주면 됩니다. 이후 FrontController는 다른 규격의 Controller들을 동시에 다룰 수 있습니다.

 

Adapter 인터페이스를 정의했습니다. Adapter 규격은 기존의 ControllerV3와 유사하게 ViewModel을 반환하도록 정의하겠습니다.

 

이제 각 Controller(V3, V4, V5)의 Adapter를 만들어보겠습니다.

 

Adapter는 V3를 따르고 있기 때문에 ControllerV3의 어댑터는 크게 달라지는 점 없이 해당 ControllerV3에서 반환하는 ViewModel을 그대로 반환해주면 됩니다.

 

ControllerV4의 어댑터는 해당 ControllerV4에 model을 Map으로 넘겨줘야 하고, View의 논리 이름만을 반환하기 때문에 Adapter 규격에 맞게 ViewModel을 직접 만들어서 반환해주도록 합니다. 이게 어댑터의 역할입니다.

 

ControllerV5의 어댑터는 해당 ControllerV5는 View의 논리 이름만을 반환하기 때문에 Adapter 규격에 맞게 ViewModel을 직접 만들어서 반환해주도록 합니다.

 

모든 Controller의 Adapter를 만들었습니다. 이제 FrontController에서 모든 버전의 Controller의 Adapter를 통해서 요청을 처리하도록 바꿔보겠습니다.

 

FrontController의 생성자 부분입니다. Controller를 저장하고 있는 Map을 이제는 여러 버전의 Controller를 저장해 야하기 때문에 Object로 바꾸었습니다. (Object가 맘에 안 들면 기존 여러 버전의 Controllable 인터페이스를 한 단계 더 추상화하는 방법도 있습니다.) 그리고 각 버전의 Adapter도 List로 저장했습니다.

 

이제 사용자 요청이 들어오면 FrontController는 요청을 수행할 수 있는 Controller를 찾고, 해당 Controller를 수행할 수 있는 Adapter를 찾아 기능을 수행하면 됩니다. 해당 기능을 구현해보도록 하겠습니다.

 

사용자 요청이 들어오면 요청을 수행할 수 있는 Controller를 찾습니다. 해당 Controller는 여러 버전의 Controller 중 하나이기 때문에 FrontController에서 수행할 수 있는 Adapter를 Adapter 목록에서 찾습니다. Adapter의 기본 규격은 ViewModel을 반환하기  때문에 반환한 ViewModel을 이용하여 View를 랜더링 합니다.

 

 

11. 마무리

프리코스를 진행하면서 단지 제가 알고 있던 MVC 패턴과 상이한 부분에 대해서 찾아보다가 MVC에 대해서 딥하게 공부할 수 있었습니다. 그런데 공부했던 과정을 블로그에 최대한 쉽게 담으려고 했는데 생각보다 쉽지 않네요.

 

열심히 정리한 만큼 많은 사람들에게 도움이 되었으면 좋겠습니다!😊 

 

 

12. Github Respotiory Link

https://github.com/LimHeeSang/java_mvc

공유하기

facebook twitter kakaoTalk kakaostory naver band