1. MVC 아키텍처 개요
초기 웹 애플리케이션의 코드는 HTML과 Java가 혼재되어 가독성이 매우 떨어졌습니다. 예를 들어 JSP 페이지 내에 직접 스크립트릿을 사용하는 방식이었습니다.
<Ul>
<%
int max=Integer.parseInt()
%>
</Ul>
MVC(Model-View-Controller) 패턴은 이러한 문제를 해결하기 위해 등장했습니다. 각 계층의 책임을 명확히 분리하여 유지보수성을 향상시킵니다.
- Model: 비즈니스 로직과 데이터 처리를 담당합니다. 일반적으로
Service.java네이밍 규칙을 따릅니다. - View: 사용자에게 최종 결과를 표시하는 UI 계층입니다.
- Controller: Model과 View 사이의 중개자 역할을 하며, 요청을 분석하고 적절한 응답을 반환합니다.
MVC의 주요 장점은 다음과 같습니다.
- 팀 단위 개발이 용이하며, 각 개발자는 자신의 역할에 집중할 수 있습니다.
- 계층이 분리되어 있어 유지보수와 확장이 쉽습니다.
- 각 컴포넌트를 독립적으로 교체하거나 재사용할 수 있습니다.
2. 프로젝트 구조 및 규칙
2.1 엔터프라이즈 패키지 구조
프로젝트는 다음과 같은 패키지 구조로 구성됩니다.
controller: 요청 처리 및 응답 반환service: 비즈니스 로직dao: 데이터 접근 계층model: 데이터를 담는 JavaBean 객체
2.2 JavaBean 규약
JavaBean은 재사용 가능한 컴포넌트로, 데이터를 캡슐화하는 데 사용됩니다.
- public 클래스이며 기본 생성자를 제공해야 합니다.
- 모든 속성은 private으로 선언하고, getter/setter 메서드를 통해 접근합니다.
3. 시스템 구현 전략
PaintingDao를 개발하여 XML 파일로부터 데이터를 읽고 페이징 기능을 구현합니다.PaintingService는 DAO를 호출하여 비즈니스 로직을 수행합니다.PaintingController는 서비스 계층을 호출하고 요청을 적절한 뷰로 분기합니다.index.jsp는 JSTL과 EL을 활용하여 페이징된 데이터를 렌더링합니다.
4. 핵심 구현 상세
4.1 프로젝트 설정 및 데이터 소스
본 프로젝트는 MySQL 대신 XML 파일을 데이터 저장소로 사용합니다. XmlDataSource 클래스가 XML 파일 경로를 관리하고 파싱을 수행합니다.
4.2 페이징 로직
페이징은 XmlDataSource와 PageModel 클래스의 협력을 통해 이루어집니다. DAO의 pagination() 메서드는 다음과 같이 구현됩니다.
public PageModel pagination(int page, int rows) {
List<Painting> allData = XmlDataSource.getRawData();
return new PageModel(allData, page, rows);
}
서비스 계층에서는 가변 인자를 활용하여 카테고리별 필터링을 지원합니다.
public PageModel pagination(int page, int rows, String... category) {
if (rows == 0) {
throw new RuntimeException("유효하지 않은 rows 파라미터");
}
if (category.length == 0 || category[0] == null) {
return paintingDao.pagination(page, rows);
} else {
return paintingDao.pagination(Integer.parseInt(category[0]), page, rows);
}
}
컨트롤러에서는 요청 파라미터를 검증하고 기본값을 설정한 후, JSP로 포워딩합니다. JSP에서는 다음과 같이 데이터를 출력합니다.
<ul>
<c:forEach items="${pageModel.pageData}" var="painting">
<li>
<img src="${painting.preview}" class="img-li">
<div class="info">
<h3>${painting.pname}</h3>
<p>${painting.description}</p>
<div class="img-btn">
<div class="price">
<fmt:formatNumber pattern="0.00" value="${painting.price}"/>
</div>
<a href="#" class="cart">
<div class="btn">
<img src="image/cart.svg">
</div>
</a>
</div>
</div>
</li>
</c:forEach>
</ul>
4.3 관리자 백엔드
관리자 페이지는 3단 레이아웃으로 구성되며, 오른쪽 영역이 동적으로 변경됩니다. ManagementController는 method 파라미터를 기반으로 여러 요청을 분기 처리합니다.
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=utf-8");
String method = req.getParameter("method");
switch (method) {
case "list":
this.list(req, resp);
break;
case "delete":
this.delete(req, resp);
break;
case "show_create":
this.showCreatePage(req, resp);
break;
case "create":
this.create(req, resp);
break;
case "show_update":
this.showUpdatePage(req, resp);
break;
case "update":
this.update(req, resp);
break;
}
}
4.4 파일 업로드 처리
파일 업로드를 위해 Apache Commons FileUpload 라이브러리를 사용합니다. 폼은 enctype="multipart/form-data"로 설정해야 합니다.
private void create(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
try {
List<FileItem> formItems = upload.parseRequest(req);
Painting painting = new Painting();
for (FileItem item : formItems) {
if (item.isFormField()) {
String fieldName = item.getFieldName();
String value = item.getString("UTF-8");
switch (fieldName) {
case "pname":
painting.setPname(value);
break;
case "category":
painting.setCategory(Integer.parseInt(value));
break;
case "price":
painting.setPrice(Integer.parseInt(value));
break;
case "description":
painting.setDescription(value);
break;
}
} else {
String uploadPath = req.getServletContext().getRealPath("/upload");
String fileName = UUID.randomUUID().toString();
String extension = item.getName().substring(item.getName().lastIndexOf("."));
item.write(new File(uploadPath, fileName + extension));
painting.setPreview("upload/" + fileName + extension);
}
}
paintingService.create(painting);
resp.sendRedirect("/mmgallery/management?method=list");
} catch (Exception e) {
e.printStackTrace();
}
}
4.5 데이터 수정 및 삭제
수정 기능은 기존 데이터를 먼저 로드하여 폼에 미리 채워넣고, 사용자가 수정한 내용을 다시 저장하는 방식으로 동작합니다. hidden 필드에 ID 값을 저장하여 식별합니다.
삭제는 XPath를 이용해 XML에서 해당 노드를 찾아 제거합니다.
public static void delete(Integer id) {
SAXReader reader = new SAXReader();
try {
Document document = reader.read(dataFile);
List<Node> nodes = document.selectNodes("/root/painting[@id=" + id + "]");
if (nodes.isEmpty()) {
throw new RuntimeException("ID " + id + "에 해당하는 작품이 없습니다.");
}
Element paintingElement = (Element) nodes.get(0);
paintingElement.getParent().remove(paintingElement);
try (Writer writer = new OutputStreamWriter(new FileOutputStream(dataFile), "UTF-8")) {
document.write(writer);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
reload();
}
}
4.6 SweetAlert2를 활용한 미리보기
작품 미리보기 기능은 SweetAlert2 라이브러리를 사용하여 구현합니다.
<script type="text/javascript">
function showPreview(element) {
const previewUrl = $(element).data("preview");
const paintingName = $(element).data("pname");
Swal.fire({
title: paintingName,
html: "<img src='" + previewUrl + "' style='width:361px;height:240px'>",
showCloseButton: true,
showConfirmButton: true
});
}
</script>
5. 핵심 시사점
이 프로젝트를 통해 MVC 패턴을 활용한 Java 웹 애플리케이션 개발의 전반적인 흐름을 익힐 수 있었습니다. 특히 요청이 컨트롤러를 통해 서비스와 DAO로 전달되고, 최종적으로 JSP에서 렌더링되는 과정을 직접 구현해볼 수 있었습니다. XML을 데이터 저장소로 사용한 점이 데이터베이스 기반 프로젝트와의 차별점입니다.