복합 패턴의 정의
복합 패턴(Compound Pattern)은 2개 이상의 패턴을 결합해서 일반적으로 자주 등장하는 문제들의 해법을 제공한다.
모델-뷰-컨트롤러(MVC)
모델-뷰-컨트롤러(Model-View-Controller, MVC)는 대표적인 복합 패턴이다.
- 뷰: 모델을 표현하는 방법을 제공함. 일반적으로 화면에 표시할 때 필요한 상태와 데이터는 모델에서 직접 가져옴.
- 컨트롤러: 사용자로부터 입력을 받으며 입력받은 내용이 모델에게 어떤 의미가 있는지 파악함.
- 모델: 모델에는 모든 데이터, 상태와 애플리케이션 로직이 들어있음. 뷰와 컨트롤러에서 모델의 상태를 조작하거나 가져올 때 필요한 인터페이스를 제공하고, 모델이 자신의 상태 변화를 옵저버들에게 연락해 주긴 하지만, 기본적으로 모델은 뷰와 컨트롤러에 별 관심이 없음.
모델-뷰-컨트롤러에 사용되는 패턴 알아보기
- 모델은 옵저버 패턴을 사용하여 자신의 상태가 바뀔 때마다 뷰와 컨트롤러에게 연락한다.
- 뷰는 전략 패턴을 사용하여 컨트롤러(행동)을 변경한다.
- 뷰는 컴포지트 패턴을 사용하여 자신 하위의 윈도우, 버튼 같은 다양한 구성 요소를 관리한다.
모델-뷰-컨트롤러 예제 코드
public interface BeatModelInterface {
void initialize();
void on();
void off();
void setBPM(int bpm);
int getBPM();
void registerObserver(BeatObserver o);
void removeObserver(BeatObserver o);
void registerObserver(BPMObserver o);
void removeObserver(BPMObserver o);
}
public class BeatModel implements BeatModelInterface, Runnable {
List<BeatObserver> beatObservers = new ArrayList<>();
List<BPMObserver> bpmObservers = new ArrayList<>();
int bpm = 90;
Thread thread;
boolean stop = false;
Clip clip;
@Override
public void initialize() {
try {
File resource = new File("src/main/java/chapter_12_combined/mvc/clap.wav");
clip = (Clip) AudioSystem.getLine(new Line.Info(Clip.class));
clip.open(AudioSystem.getAudioInputStream(resource));
} catch(Exception ex) {
System.out.println("Error: Can't load clip");
System.out.println(ex);
}
}
@Override
public void on() {
bpm = 90;
thread = new Thread(this);
stop = false;
thread.start();
}
@Override
public void off() {
stopBeat();
stop = true;
}
@Override
public void run() {
while (!stop) {
playBeat();
notifyBeatObservers();
try {
Thread.sleep(60000/getBPM());
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void setBPM(int bpm) {
this.bpm = bpm;
notifyBPMObservers();
}
@Override
public int getBPM() {
return bpm;
}
@Override
public void registerObserver(BeatObserver o) {
beatObservers.add(o);
}
public void notifyBeatObservers() {
for (int i=0; i<beatObservers.size(); i++) {
BeatObserver observer = (BeatObserver) beatObservers.get(i);
observer.updateBeat();
}
}
@Override
public void removeObserver(BeatObserver o) {
int i = beatObservers.indexOf(o);
if (i >= 0) {
beatObservers.remove(i);
}
}
@Override
public void registerObserver(BPMObserver o) {
bpmObservers.add(o);
}
public void notifyBPMObservers() {
for (int i=0; i<bpmObservers.size(); i++) {
BPMObserver observer = (BPMObserver) bpmObservers.get(i);
observer.updateBPM();
}
}
@Override
public void removeObserver(BPMObserver o) {
int i = bpmObservers.indexOf(o);
if (i >= 0) {
bpmObservers.remove(i);
}
}
public void playBeat() {
clip.setFramePosition(0);
clip.start();
}
public void stopBeat() {
clip.setFramePosition(0);
clip.stop();
}
}
BeatModel 클래스는 옵저버 패턴을 적용한 BeatModelInterface 인터페이스를 구현하는 모델 클래스이며, 모델에 등록된 여러 옵저버들을 등록/관리하고, 모델의 데이터가 변경되면 이를 옵저버들에게 알린다.
public interface BeatObserver {
void updateBeat();
}
public interface BPMObserver {
void updateBPM();
}
public class DJView implements ActionListener, BeatObserver, BPMObserver {
BeatModelInterface model;
ControllerInterface controller;
JFrame viewFrame;
JPanel viewPanel;
BeatBar beatBar;
JLabel bpmOutputLabel;
JFrame controlFrame;
JPanel controlPanel;
JLabel bpmLabel;
JTextField bpmTextField;
JButton setBPMButton;
JButton increaseBPMButton;
JButton decreaseBPMButton;
JMenuBar menuBar;
JMenu menu;
JMenuItem startMenuItem;
JMenuItem stopMenuItem;
public DJView(BeatModelInterface model, ControllerInterface controller) {
this.model = model;
this.controller = controller;
model.registerObserver((BeatObserver)this);
model.registerObserver((BPMObserver)this);
}
public void createView() {
viewPanel = new JPanel(new GridLayout(1, 2));
viewFrame = new JFrame("View");
viewFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
viewFrame.setSize(new Dimension(100, 80));
bpmOutputLabel = new JLabel("offline", SwingConstants.CENTER);
beatBar = new BeatBar();
beatBar.setValue(0);
JPanel bpmPanel = new JPanel(new GridLayout(2, 1));
bpmPanel.add(beatBar);
bpmPanel.add(bpmOutputLabel);
viewPanel.add(bpmPanel);
viewFrame.getContentPane().add(viewPanel, BorderLayout.CENTER);
viewFrame.pack();
viewFrame.setVisible(true);
}
public void createControls() {
JFrame.setDefaultLookAndFeelDecorated(true);
controlFrame = new JFrame("Control");
controlFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
controlFrame.setSize(new Dimension(100, 80));
controlPanel = new JPanel(new GridLayout(1, 2));
menuBar = new JMenuBar();
menu = new JMenu("DJ Control");
startMenuItem = new JMenuItem("Start");
menu.add(startMenuItem);
startMenuItem.addActionListener((event) -> controller.start());
stopMenuItem = new JMenuItem("Stop");
menu.add(stopMenuItem);
stopMenuItem.addActionListener((event) -> controller.stop());
JMenuItem exit = new JMenuItem("Quit");
exit.addActionListener((event) -> System.exit(0));
menu.add(exit);
menuBar.add(menu);
controlFrame.setJMenuBar(menuBar);
bpmTextField = new JTextField(2);
bpmLabel = new JLabel("Enter BPM:", SwingConstants.RIGHT);
setBPMButton = new JButton("Set");
setBPMButton.setSize(new Dimension(10,40));
increaseBPMButton = new JButton(">>");
decreaseBPMButton = new JButton("<<");
setBPMButton.addActionListener(this);
increaseBPMButton.addActionListener(this);
decreaseBPMButton.addActionListener(this);
JPanel buttonPanel = new JPanel(new GridLayout(1, 2));
buttonPanel.add(decreaseBPMButton);
buttonPanel.add(increaseBPMButton);
JPanel enterPanel = new JPanel(new GridLayout(1, 2));
enterPanel.add(bpmLabel);
enterPanel.add(bpmTextField);
JPanel insideControlPanel = new JPanel(new GridLayout(3, 1));
insideControlPanel.add(enterPanel);
insideControlPanel.add(setBPMButton);
insideControlPanel.add(buttonPanel);
controlPanel.add(insideControlPanel);
bpmLabel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
bpmOutputLabel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
controlFrame.getRootPane().setDefaultButton(setBPMButton);
controlFrame.getContentPane().add(controlPanel, BorderLayout.CENTER);
controlFrame.pack();
controlFrame.setVisible(true);
}
public void enableStopMenuItem() {
stopMenuItem.setEnabled(true);
}
public void disableStopMenuItem() {
stopMenuItem.setEnabled(false);
}
public void enableStartMenuItem() {
startMenuItem.setEnabled(true);
}
public void disableStartMenuItem() {
startMenuItem.setEnabled(false);
}
public void updateBPM() {
if (model != null) {
int bpm = model.getBPM();
if (bpm == 0) {
if (bpmOutputLabel != null) {
bpmOutputLabel.setText("offline");
}
} else {
if (bpmOutputLabel != null) {
bpmOutputLabel.setText("Current BPM: " + model.getBPM());
}
}
}
}
public void updateBeat() {
if (beatBar != null) {
beatBar.setValue(100);
}
}
@Override
public void actionPerformed(ActionEvent event) {
if (event.getSource() == setBPMButton) {
int bpm = 90;
String bpmText = bpmTextField.getText();
if (bpmText == null || bpmText.contentEquals("")) {
bpm = 90;
} else {
bpm = Integer.parseInt(bpmTextField.getText());
}
controller.setBPM(bpm);
} else if(event.getSource() == increaseBPMButton) {
controller.increaseBPM();
} else if (event.getSource() == decreaseBPMButton) {
controller.decreaseBPM();
}
}
}
DJView 클래스는 BeatObserver와 BPMObserver 옵저버 인터베이스를 구현하는 뷰 클래스이며, 전략 패턴을 통해 특정 모델과 컨트롤러를 선택하며, 옵저버를 선택한 모델에 등록한다.
이를 통해 뷰는 모델의 업데이트를 알림받을 수 있으며, 컨트롤러를 통해 특정 행동을 수행 할 수 있다. 또한 뷰는 다양한 GUI 요소(JComponent)들이 컴포지트 패턴을 통해 중첩되어 있어 GUI 요소들을 쉽게 관리할 수 있다.
public interface ControllerInterface {
void start();
void stop();
void increaseBPM();
void decreaseBPM();
void setBPM(int bpm);
}
public class BeatController implements ControllerInterface {
BeatModelInterface model;
DJView view;
public BeatController(BeatModelInterface model) {
this.model = model;
view = new DJView(model, this);
view.createView();
view.createControls();
view.disableStopMenuItem();
view.enableStartMenuItem();
model.initialize();
}
@Override
public void start() {
model.on();
view.disableStartMenuItem();
view.enableStopMenuItem();
}
@Override
public void stop() {
model.off();
view.disableStopMenuItem();
view.enableStartMenuItem();
}
@Override
public void increaseBPM() {
int bpm = model.getBPM();
model.setBPM(bpm + 1);
}
@Override
public void decreaseBPM() {
int bpm = model.getBPM();
model.setBPM(bpm - 1);
}
@Override
public void setBPM(int bpm) {
model.setBPM(bpm);
}
}
BeatController 클래스는 ConrollerInterface 인터페이스를 구현하는 컨트롤러 클래스이며, 뷰를 통해 입력된 행동을 구현된 알고리즘에 따라 모델과 뷰를 호출하며 수행한다.
웹 프레임워크에서의 MVC
웹 애플리케이션에서는 클라이언트 측(브라우저) 애플리케이션과 서버 측 애플리케이션이 존재함 → 모델, 뷰, 컨트롤러가 어디에 있는지에 따라 설계 방식이 달라진다.
- 신 클라이언트(thin client): 대부분의 모델과 뷰, 그리고 컨트롤러가 모두 서버로 들어가고 브라우저는 뷰를 화면에 표시하고 컨트롤러로 입력을 받아오는 역할만 수행하는 접근법.
- 단일 페이지 애플리케이션(single page applicaton): 대부분의 모델과 뷰, 그리고 컨트롤러까지 클라이언트에 들어가는 접근법.
Spring Web MVC, Django, ASP.NET MVC, AngularjS, EmberJS, JavaScriptMVC, Backbone 등의 수많은 웹 MVC 프레임워크는 각자 고유한 방식으로 모델, 뷰, 컨트롤러를 클라이언트와 서버에 나눠서 배치한다.
핵심 정리
- 모델-뷰-컨트롤러(MVC)는 옵저버, 전략, 컴포지트 패턴으로 이루어진 복합 패턴임.
- 모델은 옵저버 패턴을 사용해서 의존성을 없애면서도 옵저버들에게 자신의 상태가 변경되었음을 알릴 수 있음.
- 컨트롤러는 뷰의 전략 객체임. 뷰는 컨트롤러를 바꿔서 또다른 행동을 할 수 있음.
- 뷰는 컴포지트 패턴을 사용해서 사용자 인터페이스를 구현함. 보통 패널이나 프레임, 버튼과 같은 중첩된 구성 요소로 이루어짐.
- 모델, 뷰, 컨트롤러는 위의 3가지 패턴으로 서로 느슨하게 결합되므로 깔끔하면서도 유연한 구현이 가능함.
- 새로운 모델을 기존의 뷰와 컨트롤러에 연결해서 쓸 때는 어댑터 패턴을 활용하면 됨.
- MVC는 웹에도 적용됨.
- 클라이언트-서버 애플리케이션 구조에 MVC를 적용시켜 주는 다양한 웹 MVC 프레임워크가 있음.
GitHub Code Reference: https://github.com/Krapi0314/Design_Patterns
GitHub - Krapi0314/Design_Patterns: Java Implementation of GoF's Design Patterns
Java Implementation of GoF's Design Patterns. Contribute to Krapi0314/Design_Patterns development by creating an account on GitHub.
github.com
Reference: http://www.yes24.com/Product/Goods/108192370
헤드 퍼스트 디자인 패턴 - YES24
유지관리가 편리한 객체지향 소프트웨어 만들기!“『헤드 퍼스트 디자인 패턴(개정판)』 한 권이면 충분하다.이유 1. 흥미로운 이야기와 재치 넘치는 구성이 담긴 〈헤드 퍼스트〉 시리즈! 하나
www.yes24.com
'객체지향 및 기반 언어 > 디자인 패턴' 카테고리의 다른 글
[Design Pattern] Chapter 14 기타 패턴 (0) | 2022.11.06 |
---|---|
[Design Pattern] Chapter 13 실전 디자인 패턴 (0) | 2022.11.05 |
[Design Pattern] Chapter 11 프록시 패턴 (0) | 2022.10.29 |
[Design Patterns] Chapter 10 상태 패턴 (0) | 2022.10.23 |
[Design Patterns] Chapter 09 반복자 패턴과 컴포지트 패턴 (0) | 2022.10.23 |