디자인 패턴 스터디를 진행하고 있는데,
내용을 정리하고자 한다.
MVC 패턴
정의
MVC는 애플리케이션을 세 가지 주요 컴포넌트로 분리하는 아키텍처 패턴
- Model: 데이터와 비즈니스 로직
- View: 사용자 인터페이스(UI)
- Controller: Model과 View 사이의 상호작용 관리
특징
- 관심사의 분리
- Model: 데이터 처리
- View: 사용자 인터페이스
- Controller: 로직 처리 및 조정
- 데이터 흐름
User → Controller → Model → View → User
- 각 컴포넌트 책임
- Model
// 데이터 구조와 비즈니스 로직 class PhotoModel { private photos: Photo[] = []; async fetchPhotos() { this.photos = await api.getPhotos(); } getFilteredPhotos(category: string) { return this.photos.filter(p => p.category === category); } }
- View
// 사용자 인터페이스 표현 const PhotoView = { render(photos: Photo[]) { return ` <div class="gallery"> ${photos.map(photo => ` <div class="photo-card"> <img src="${photo.src}" /> <h3>${photo.caption}</h3> </div> `).join('')} </div> `; } };
- Controller
// Model과 View 연결, 사용자 입력 처리 class PhotoController { constructor( private model: PhotoModel, private view: typeof PhotoView ) {} async initialize() { await this.model.fetchPhotos(); const photos = this.model.getFilteredPhotos('nature'); document.body.innerHTML = this.view.render(photos); } }
장점
- 코드 구조화 및 모듈화
- 유지보수 용이성
- 테스트 용이성
- 역할 분담 명확화
단점
- 작은 애플리케이션에서는 과도할 수 있음
- 복잡한 데이터 흐름
- 학습 곡선
BackBone.js
// Model
const PhotoModel = Backbone.Model.extend({
defaults: {
caption: '',
src: '',
metadata: ''
},
getSrc() {
return this.get('src');
}
});
const PhotoCollection = Backbone.Collection.extend({
model: PhotoModel
});
// View
const PhotoView = Backbone.View.extend({
template: _.template(`
<li class="photo">
<h2><%= caption %></h2>
<img class="source" src="<%= src %>"/>
<div class="metadata"><%= metadata %></div>
</li>
`),
events: {
'click': 'handleClick'
},
render() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
handleClick() {
this.trigger('photo:click', this.model);
}
});
// Controller (Router in Backbone)
const PhotoController = Backbone.Router.extend({
routes: {
'photos': 'showPhotos',
'photo/:id': 'showPhotoDetail'
},
initialize() {
this.collection = new PhotoCollection(photos);
this.view = new PhotoView({ collection: this.collection });
},
showPhotos() {
this.view.render();
}
});
Ember.js
// Model
export default class Photo extends Model {
@attr caption;
@attr src;
@attr metadata;
}
// Controller
export default class PhotosController extends Controller {
@action
handlePhotoClick(photo) {
// 클릭 처리
}
}
// View (Template)
// photos.hbs
<ul>
{{#each @model as |photo|}}
<li class="photo" {{on "click" (fn this.handlePhotoClick photo)}}>
<h2>{{photo.caption}}</h2>
<img class="source" src={{photo.src}} />
<div class="metadata">{{photo.metadata}}</div>
</li>
{{/each}}
</ul>
Angular.js
src/
├── app/
│ ├── models/ # Model
│ │ └── photo.model.ts
│ ├── views/ # View
│ │ ├── components/
│ │ │ └── photo-list/
│ │ │ ├── photo-list.component.html
│ │ │ ├── photo-list.component.css
│ │ │ └── photo-list.component.ts
│ ├── controllers/ # Controller (Services)
│ │ └── photo.service.ts
│ └── app.module.ts
// models/photo.model.ts
export interface Photo {
id: string;
caption: string;
src: string;
}
// controllers/photo.service.ts
@Injectable()
export class PhotoService {
getPhotos(): Observable<Photo[]> {
return this.http.get<Photo[]>('/api/photos');
}
}
// views/components/photo-list.component.ts
@Component({
templateUrl: './photo-list.component.html'
})
export class PhotoListComponent {
photos$ = this.photoService.getPhotos();
constructor(private photoService: PhotoService) {}
}
Vue.js
// models/types.ts
export interface Photo {
id: string;
caption: string;
src: string;
}
// controllers/store/modules/photos.ts
export const photos = {
state: () => ({
list: []
}),
mutations: {
setPhotos(state, photos) {
state.list = photos;
}
}
};
// views/components/PhotoList.vue
<template>
<div class="photo-list">
<div v-for="photo in photos" :key="photo.id">
<img :src="photo.src" :alt="photo.caption" />
</div>
</div>
</template>
// Model (Store)
const store = {
state: {
photos: [
{
caption: 'Sample Photo 1',
src: 'photo1.jpg',
metadata: 'Some metadata for photo 1'
},
// ...
]
},
mutations: {
selectPhoto(state, photo) {
state.selectedPhoto = photo;
}
}
};
// View + Controller (Component)
export default {
data() {
return {
photos: this.$store.state.photos
}
},
template: `
<ul>
<li v-for="photo in photos"
:key="photo.src"
class="photo"
@click="handleClick(photo)">
<h2>{{ photo.caption }}</h2>
<img class="source" :src="photo.src" />
<div class="metadata">{{ photo.metadata }}</div>
</li>
</ul>
`,
methods: {
handleClick(photo) {
this.$store.commit('selectPhoto', photo);
}
}
};// Model (Store)
const store = {
state: {
photos: [
{
caption: 'Sample Photo 1',
src: 'photo1.jpg',
metadata: 'Some metadata for photo 1'
},
// ...
]
},
mutations: {
selectPhoto(state, photo) {
state.selectedPhoto = photo;
}
}
};
// View + Controller (Component)
export default {
data() {
return {
photos: this.$store.state.photos
}
},
template: `
<ul>
<li v-for="photo in photos"
:key="photo.src"
class="photo"
@click="handleClick(photo)">
<h2>{{ photo.caption }}</h2>
<img class="source" :src="photo.src" />
<div class="metadata">{{ photo.metadata }}</div>
</li>
</ul>
`,
methods: {
handleClick(photo) {
this.$store.commit('selectPhoto', photo);
}
}
};
React.js (with Redux)
src/
├── models/ # Model
│ ├── types.ts # 타입 정의
│ └── api.ts # API 호출
├── views/ # View
│ ├── components/ # 재사용 가능한 컴포넌트
│ │ ├── PhotoCard.tsx
│ │ └── PhotoList.tsx
│ └── pages/ # 페이지 컴포넌트
│ └── PhotoGallery.tsx
├── controllers/ # Controller (Redux)
│ ├── actions/ # 액션 정의
│ │ └── photoActions.ts
│ ├── reducers/ # 리듀서
│ │ └── photoReducer.ts
│ └── store.ts # Redux 스토어
└── App.tsx
// models/types.ts
interface Photo {
id: string;
caption: string;
src: string;
}
// views/components/PhotoCard.tsx
const PhotoCard: React.FC<Photo> = ({ caption, src }) => (
<div className="photo-card">
<img src={src} alt={caption} />
<h3>{caption}</h3>
</div>
);
// controllers/actions/photoActions.ts
const fetchPhotos = () => async (dispatch) => {
const photos = await api.getPhotos();
dispatch({ type: 'SET_PHOTOS', payload: photos });
};
Express.js
src/
├── models/ # Model
│ └── PhotoModel.js
├── views/ # View
│ └── PhotoView.js
├── controllers/ # Controller
│ └── PhotoController.js
├── routes/
│ └── photoRoutes.js
├── public/
│ ├── css/
│ │ └── styles.css
│ └── images/
│ ├── photo1.jpg
│ └── photo2.jpg
└── app.js
// models/PhotoModel.js
class PhotoModel {
constructor() {
this.photos = [
{
id: 1,
src: '/images/photo1.jpg',
caption: 'Sample Photo 1',
metadata: 'Some metadata for photo 1'
},
{
id: 2,
src: '/images/photo2.jpg',
caption: 'Sample Photo 2',
metadata: 'Some metadata for photo 2'
}
];
}
findAll() {
return Promise.resolve(this.photos);
}
findById(id) {
const photo = this.photos.find(p => p.id === parseInt(id));
return Promise.resolve(photo);
}
}
module.exports = new PhotoModel();
// views/PhotoView.js
const renderPhotoList = (photos) => {
return `
<!DOCTYPE html>
<html>
<head>
<title>Photo Gallery</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div class="container">
<h1>Photo Gallery</h1>
<div class="photo-grid">
${photos.map(photo => `
<div class="photo-card">
<img src="${photo.src}" alt="${photo.caption}">
<h3>${photo.caption}</h3>
<p class="metadata">${photo.metadata}</p>
</div>
`).join('')}
</div>
</div>
</body>
</html>
`;
};
const renderPhotoDetail = (photo) => {
return `
<!DOCTYPE html>
<html>
<head>
<title>${photo.caption}</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div class="container">
<div class="photo-detail">
<img src="${photo.src}" alt="${photo.caption}">
<h2>${photo.caption}</h2>
<p class="metadata">${photo.metadata}</p>
<div class="actions">
<a href="/photos" class="btn">Back to Gallery</a>
</div>
</div>
</div>
</body>
</html>
`;
};
module.exports = {
renderPhotoList,
renderPhotoDetail
};
// controllers/PhotoController.js
const PhotoModel = require('../models/PhotoModel');
const PhotoView = require('../views/PhotoView');
class PhotoController {
async getAllPhotos(req, res) {
try {
const photos = await PhotoModel.findAll();
const html = PhotoView.renderPhotoList(photos);
res.send(html);
} catch (error) {
res.status(500).send('Error loading photos');
}
}
async getPhotoDetail(req, res) {
try {
const photo = await PhotoModel.findById(req.params.id);
if (!photo) {
return res.status(404).send('Photo not found');
}
const html = PhotoView.renderPhotoDetail(photo);
res.send(html);
} catch (error) {
res.status(500).send('Error loading photo');
}
}
}
module.exports = new PhotoController();
// routes/photoRoutes.js
const express = require('express');
const router = express.Router();
const PhotoController = require('../controllers/PhotoController');
router.get('/photos', PhotoController.getAllPhotos);
router.get('/photos/:id', PhotoController.getPhotoDetail);
module.exports = router;
// app.js
const express = require('express');
const photoRoutes = require('./routes/photoRoutes');
const app = express();
app.use(express.static('public'));
app.use('/', photoRoutes);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Java(Spring)
- 강력한 타입 시스템
- 의존성 주입을 통한 느슨한 결합
- AOP를 통한 횡단 관심사 분리
- 풍부한 생태계와 라이브러리 제공
// Model (Domain)
@Entity
@Table(name = "photos")
public class Photo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String caption;
private String imageUrl;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "photo", cascade = CascadeType.ALL)
private List<Comment> comments = new ArrayList<>();
// Getters, Setters, Constructors
}
// Repository
@Repository
public interface PhotoRepository extends JpaRepository<Photo, Long> {
List<Photo> findByUserId(Long userId);
Optional<Photo> findByIdAndUserId(Long id, Long userId);
}
// Service
@Service
@Transactional(readOnly = true)
public class PhotoService {
private final PhotoRepository photoRepository;
private final ImageUploadService imageUploadService;
@Autowired
public PhotoService(PhotoRepository photoRepository,
ImageUploadService imageUploadService) {
this.photoRepository = photoRepository;
this.imageUploadService = imageUploadService;
}
public List<PhotoDTO> getAllPhotos() {
return photoRepository.findAll()
.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
@Transactional
public PhotoDTO createPhoto(PhotoCreateRequest request) {
String imageUrl = imageUploadService.uploadImage(request.getImage());
Photo photo = Photo.builder()
.caption(request.getCaption())
.imageUrl(imageUrl)
.user(request.getUser())
.build();
Photo savedPhoto = photoRepository.save(photo);
return convertToDto(savedPhoto);
}
private PhotoDTO convertToDto(Photo photo) {
return PhotoDTO.builder()
.id(photo.getId())
.caption(photo.getCaption())
.imageUrl(photo.getImageUrl())
.userId(photo.getUser().getId())
.build();
}
}
// Controller
@RestController
@RequestMapping("/api/photos")
public class PhotoController {
private final PhotoService photoService;
@Autowired
public PhotoController(PhotoService photoService) {
this.photoService = photoService;
}
@GetMapping
public ResponseEntity<List<PhotoDTO>> getAllPhotos() {
List<PhotoDTO> photos = photoService.getAllPhotos();
return ResponseEntity.ok(photos);
}
@PostMapping
public ResponseEntity<PhotoDTO> createPhoto(
@Valid @RequestBody PhotoCreateRequest request) {
PhotoDTO photo = photoService.createPhoto(request);
return ResponseEntity.status(HttpStatus.CREATED).body(photo);
}
}
// View (Thymeleaf template)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Photo Gallery</title>
</head>
<body>
<div class="container">
<div class="gallery" th:if="${!photos.empty}">
<div class="photo-card" th:each="photo : ${photos}">
<img th:src="${photo.imageUrl}" th:alt="${photo.caption}"/>
<h3 th:text="${photo.caption}"></h3>
<div class="user-info" th:text="${photo.userName}"></div>
</div>
</div>
<div th:if="${photos.empty}">
<p>No photos available</p>
</div>
</div>
</body>
</html>
// DTO
@Getter
@Setter
@Builder
public class PhotoDTO {
private Long id;
private String caption;
private String imageUrl;
private Long userId;
private String userName;
// Constructors, etc.
}
// Configuration
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceLocations("file:uploads/");
}
}
Ruby on Rails
- 명명 규칙만 따르면 자동으로 MVC 연결
- 모델 관계를 선언적으로 정의
- 데이터베이스 스키마와 모델이 긴밀하게 연결
# 강력한 규칙성과 CoC(Convention over Configuration)
# app/models/photo.rb
class Photo < ApplicationRecord
belongs_to :user
has_many :comments
validates :caption, presence: true
end
# app/controllers/photos_controller.rb
class PhotosController < ApplicationController
def index
@photos = Photo.all
end
end
# app/views/photos/index.html.erb
<% @photos.each do |photo| %>
<%= render 'photo', photo: photo %>
<% end %>
Python(Django)
- MTV(Model-Template-View) 패턴 사용
- View가 Controller 역할, Template이 View 역할
- ORM이 모델과 긴밀하게 통합
# models.py
class Photo(models.Model):
caption = models.CharField(max_length=200)
image = models.ImageField(upload_to='photos/')
def get_absolute_url(self):
return reverse('photo-detail', args=[str(self.id)])
# views.py
class PhotoListView(ListView):
model = Photo
template_name = 'photos/list.html'
context_object_name = 'photos'
# templates/photos/list.html
{% for photo in photos %}
<div class="photo-card">
<img src="{{ photo.image.url }}">
<h3>{{ photo.caption }}</h3>
</div>
{% endfor %}
C#(ASP .NET)
- 강력한 타입 시스템
- Razor 뷰 엔진
- 의존성 주입이 기본으로 통합
// Model
public class Photo
{
public int Id { get; set; }
public string Caption { get; set; }
public string ImageUrl { get; set; }
}
// Controller
public class PhotoController : Controller
{
private readonly IPhotoService _photoService;
public PhotoController(IPhotoService photoService)
{
_photoService = photoService;
}
public IActionResult Index()
{
var photos = _photoService.GetAllPhotos();
return View(photos);
}
}
// View
@model IEnumerable<Photo>
@foreach (var photo in Model)
{
<div class="photo-card">
<img src="@photo.ImageUrl" alt="@photo.Caption">
<h3>@photo.Caption</h3>
</div>
}
PHP(Laravel)
- Eloquent ORM을 통한 강력한 모델 기능
- Blade 템플릿 엔진 내장
- 서비스 컨테이너를 통한 의존성 주입
// Model
class Photo extends Model
{
use HasFactory;
protected $fillable = ['caption', 'src'];
}
// Controller
class PhotoController extends Controller
{
public function index()
{
$photos = Photo::all();
return view('photos.index', compact('photos'));
}
}
// View (Blade template)
@foreach($photos as $photo)
<div class="photo-card">
<img src="{{ $photo->src }}">
<h3>{{ $photo->caption }}</h3>
</div>
@endforeach