mojo's Blog

프로젝트 : 뉴스 기사 관리 웹 서비스 본문

JSP

프로젝트 : 뉴스 기사 관리 웹 서비스

_mojo_ 2022. 1. 3. 01:52

프로젝트 개요

 

이번 프로젝트에서 구현할 내용은 다음과 같다.

  • 부트스트랩을 이용한 화면 구현
  • 데이터베이스 연동
  • 이미지 파일 첨부를 포함한 뉴스 등록
  • 뉴스 목록에서 등록한 뉴스 확인
  • 뉴스 에러 처리

 

데이터베이스

데이터베이스는 이전에 설치하였던 H2 데이터베이스를 그대로 사용하며 접속 모드 역시 네트워크 모드를 그대로 사용한다.

데이터 구조가 단순하기 때문에 테이블은 단일 구조로 설계한다.

  • 테이블명: news

 

모델

news 테이블과 연동해 데이터를 처리하기 위한 클래스로 컨트롤러 이외의 다른 자바 애플리케이션에서도 사용이 가능한 독립적 구조를 가지고 있다.

  • News.java: 뉴스 테이블 구조와 매핑되는 DO 클래스
  • NewsDAO.java: 뉴스 데이터 처리를 위한 DAO 클래스

 

컨트롤러

향후 다른 프로젝트 개발에도 재활용 가능한 수준의 컨트롤러를 구현한다.

  • NewsController.java: 뉴스 관리 서비스 컨트롤러

 

본 프로젝트에서는 뉴스 등록, 목록, 보기, 삭제의 네 가지 기능을 제공하며, 화면은 2개로 구성된다.

  • newsList.jsp: 뉴스 관리 메인 화면으로 목록을 보여주고 삭제와 등록을 위한 UI 제공
  • newsView.jsp: 특정 뉴스를 선택했을 때 사진과 함께 기사 내용을 보여줌

 

모델 구현

 

1. 데이터베이스 생성

  • aid: 기사 고유 아이디인 기본키(PK), 자동 증가 칼럼
  • title: 기사 제목, 문자열
  • img: 사진 경로, 문자열
  • date: 등록 날짜와 시간, 타임스탬프 타입
  • content: 기사 내용, 긴 텍스트

날짜와 시간을 처리하는 방법은 데이터베이스마다 조금씩 다르고 자료형도 다르다.

어떤 데이터베이스는 날짜, 시간을 모두 포함한 타입을 제공하는 반면 어떤 데이터베이스는 날짜, 시간, 날짜/시간을 별도 타입으로 구별해서 제공하기도 한다.

H2 데이터베이스의 경우 후자에 해당되며 날짜의 시간을 함께 저장하기 위해서는 TIMESTAMP라는 타입을 사용해야 한다.

Date 타입을 사용할 경우 날짜 정보만 저장되니 주의한다.

 

 

2. News 클래스 구현

News.java 코드

package ch10;

public class News {
	private int aid;
	private String title;
	private String img;
	private String date;
	private String content;
	
	public int getAid() {
		return aid;
	}
	public void setAid(int aid) {
		this.aid = aid;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getImg() {
		return img;
	}
	public void setImg(String img) {
		this.img = img;
	}
	public String getDate() {
		return date;
	}
	public void setDate(String date) {
		this.date = date;
	}
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}	
}

 

  • database 로 설정한 칼럼들(aid, title, ...)을 변수로 설정한다.
  • 그 후에 setter()/getter() 를 활용하여 각 변수마다 set, get이 가능하도록 설정한다. 

 

 

3. NewsDAO 클래스 구현

NewsDAO.java 코드

package ch10;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;

public class NewsDAO {
	final String JDBC_DRIVER = "org.h2.Driver";
	final String JDBC_URL = "jdbc:h2:tcp://localhost/~/jwbookdb";
	
	public Connection open() {
		Connection conn = null;
		try {
			Class.forName(JDBC_DRIVER);
			conn = DriverManager.getConnection(JDBC_URL, "jwbook", "1234");
		} catch(Exception e) {
			e.printStackTrace();
		}
		return conn;
	}
	
	public void addNews(News n) throws Exception {
		Connection conn = open();
		
		String sql = "insert into news(title,img,date,content) values(?,?,CURRENT_TIMESTAMP(),?)";
		PreparedStatement pstmt = conn.prepareStatement(sql);
		
		/* try-catch-finally 구조 (try-with-resource 기법이 훨씬 간결함)
		 try{
		 	pstmt.setString(...);
		 	...
		 } catch(Exception e) {
		 	e.printStackTrace();
		 } finally {
		 	pstmt.close();
		 	conn.close();
		 }
		 */
		// try-with-resource 기법이 적용된 부분으로 예외 발생 시 해당 리소스를 자동으로 close
		try(conn; pstmt){
			pstmt.setString(1, n.getTitle());
			pstmt.setString(2, n.getImg());
			pstmt.setString(3, n.getContent());
			pstmt.executeUpdate();
		}
	}
	
	public List<News> getAll() throws Exception {
		Connection conn = open();
		List<News> newsList = new ArrayList<>();
		
		String sql = "select aid, title, SUBSTRING(date from 1 for 19) as cdate from news";
		PreparedStatement pstmt = conn.prepareStatement(sql);
		ResultSet rs = pstmt.executeQuery(); 
		
		try(conn; pstmt; rs){
			while(rs.next()) {
				News n = new News();
				n.setAid(rs.getInt("aid"));
				n.setTitle(rs.getString("title"));
				n.setDate(rs.getString("cdate"));
				newsList.add(n);
			}
		}

		return newsList;
	}
	
	public News getNews(int aid) throws SQLException {
		Connection conn = open();
		
		News n = new News();
		String sql = "select aid, title, img, SUBSTRING(date from 1 for 19) as cdate, content from news where aid=?";
		
		PreparedStatement pstmt = conn.prepareStatement(sql);
		pstmt.setInt(1,  aid);
		ResultSet rs = pstmt.executeQuery();
		
		rs.next();
		
		try(conn; pstmt; rs){
			n.setAid(rs.getInt("aid"));
			n.setTitle(rs.getString("title"));
			n.setImg(rs.getString("img"));
			n.setDate(rs.getString("cdate"));
			n.setContent(rs.getString("content"));
			// pstmt.executeQuery();
		}
		
		return n;
	}
	
	public void delNews(int aid) throws SQLException {
		Connection conn = open();
		
		String sql = "delete from news where aid=?";
		PreparedStatement pstmt = conn.prepareStatement(sql);
		
		try(conn; pstmt) {
			pstmt.setInt(1,  aid);
			// 처리된 테이블의 데이터 개수를 리턴함
			if(pstmt.executeUpdate() == 0) {
				throw new SQLException("DB Error");
			}
		}
	}

}

 

♣ open() :

상수형으로 지정한 JDBC_DRIVER, JDBC_URL 을 통해 JDBC 드라이버 로드데이터베이스 연결을 한 객체 conn 에 대해서 반환하도록 하는 메서드이다.

 

♣ addNews() :

파라메터로 받아온 News 객체 n에 대하여 해당 객체의 정보들을 가지고 statement 생성 및 SQL 문을 update 하도록 하는 메서드이다.

이때 try(conn; pstmt) 부분은 try-with-resource 기법이 적용된 부분으로 예외 발생 시 해당 리소스를 자동으로 close 한다. (Java 9 이상부터 가능)

 

♣ getAll() :

Database에 있는 모든 레코드들을 News 객체 n에 설정 한 후, News 자료형 List에 추가한다.

모든 레코드들에 대한 추가가 완료되었다면 추가한 List를 반환하도록 하는 메서드이다.

그리고 try(conn; pstmt; rs) 를 사용함으로써 try-with-resource 부분으로 자동으로 conn, pstmt, rs에 대한 close()가 가능하게 된다.

 

해당 메서드를 구현 시 문제 :

String sql = " ~~~ " 부분에서 PARSEDATETIME(date, 'yyyy-MM-dd hh:mm:ss') as cdate 를 하면 에러가 발생한다.

그에 대한 해결책 :

SUBSTRING 함수를 이용하여 간단하게 날짜 정보를 가져올 수 있다.

 

♣ getNews() : 

파라메터로 받아온 aid를 가지고 해당 aid에 대한 특정 뉴스 기사의 세부 내용을 보여주기 위한 메서드이다.

주목할 점은 sql = "select ... from news where aid=?" 와 pstmt.setInt(1, aid); 이다.

 

먼저 예를 들어보도록 하자.

sql = "select from student where aid=? and num=? and age=?" 와 같이 있다고 가정해본다.

그러면 sql 의 ?를 채우기 위해 pstmt에 set을 하는 순서는 다음과 같이 된다.

1번째 ? : pstmt.setInt(1, 5);  (aid=?  =>  aid=5)2번째 ? : pstmt.setInt(2, 100);  (num=?  =>  num=100)3번째 ? : pstmt.setInt(3, 20);  (age=?  =>  age=20)

 

즉, pstmt.setInt(1, aid) 를 호출 함으로써 aid=?  =>  aid=aid  가 됨을 알 수 있다. 

 

♣ delNews() : 

파라메터로 받아온 aid를 가지고 해당 기사를 삭제하도록 하는 메서드이다.

여기서 주목해야 할 점은 pstmt.executeUpdate() == 0 이 부분이다.

pstmt.executeUpdate() 의 반환값은 int 형으로 즉, delete 한 갯수를 반환하게 된다.

0일 경우 삭제하지 못하였으므로 이에 대한 예외 처리를 하였음을 알 수 있다.

 

 

4. 컨트롤러 구현

NewsController.java 코드

package ch10;

import java.io.IOException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import org.apache.commons.beanutils.BeanUtils;

/**
 * Servlet implementation class NewsController
 */
@WebServlet("/news.nhn")
public class NewsController extends HttpServlet {
	private static final long serialVersionUID = 1L;
	
	private NewsDAO dao;
	private ServletContext ctx;
	
	// 웹 리소스 기본 경로 지정
	private final String START_PAGE = "ch10/newsList.jsp";
	
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		dao = new NewsDAO();
		ctx = getServletContext();
	}
       

	/**
	 * @see HttpServlet#service(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
		request.setCharacterEncoding("utf-8");
		String action = request.getParameter("action");
		
		dao = new NewsDAO();
		
		Method m;
		String view = null;
		
		if(action == null) {
			action = "listNews";
		}
		
		try {
			m = this.getClass().getMethod(action, HttpServletRequest.class);
			view = (String)m.invoke(this, request);
		} catch(NoSuchMethodException e) {
			e.printStackTrace();
			ctx.log("요청 action 없음!!");
			request.setAttribute("error", "action 파라미터가 잘못되었습니다!!!");
			view = START_PAGE;
		} catch(Exception e) {
			e.printStackTrace();
		}
		
		if(view.startsWith("redirect:/")) {
			String rview = view.substring("redirect:/".length());
			response.sendRedirect(rview);
		} else {
			RequestDispatcher dispatcher = request.getRequestDispatcher(view);
			dispatcher.forward(request, response);
		}
	}
	
	public String addNews(HttpServletRequest request) {
		News n = new News();
		try {
			Part part = request.getPart("file"); 
			String fileName = getFilename(part);
			if(fileName != null && !fileName.isEmpty()) {
				part.write(fileName);
			}
			BeanUtils.populate(n, request.getParameterMap());
			
			n.setImg("/img/"+fileName);
			dao.addNews(n);
		} catch(Exception e) {
			e.printStackTrace();
			ctx.log("뉴스 추가 과정에서 문제 발생!!");
			request.setAttribute("error", "뉴스가 정상적으로 등록되지 않았습니다!!!");
			return listNews(request);
		}
		
		return "redirect:/news.nhn?action=listNews";
	}
	
	public String listNews(HttpServletRequest request) {
		List<News> list;
		try {
			list = dao.getAll();
			request.setAttribute("newsList", list);
		} catch(Exception e) {
			e.printStackTrace();
			ctx.log("뉴스 목록 생성 과정에서 문제 발생!!");
			request.setAttribute("error", "뉴스 목록이 정상적으로 처리되지 않았습니다!!!");
		}
		return "ch10/newsList.jsp";
	}
	
	public String getNews(HttpServletRequest request) {
		int aid = Integer.parseInt(request.getParameter("aid"));
		
		try {
			News n = dao.getNews(aid);
			request.setAttribute("news", n);
		} catch(SQLException e) {
			e.printStackTrace();
			ctx.log("뉴스를 가져오는 과정에서 문제 발생!!");
			request.setAttribute("error", "뉴스를 정상적으로 가져오지 못했습니다!!!");
		}
		
		return "ch10/newsView.jsp";
	}

	public String deleteNews(HttpServletRequest request) {
		int aid = Integer.parseInt(request.getParameter("aid"));
		try {
			dao.delNews(aid);
		} catch(SQLException e) {
			e.printStackTrace();
			ctx.log("뉴스 삭제 과정에서 문제 발생!!");
			request.setAttribute("error", "뉴스가 정상적으로 삭제되지 않았습니다!!!");
			return listNews(request);
		}
		
		return "redirect:/news.nhn?action=listNews";
	}
	
	private String getFilename(Part part) {
		String fileName = null;
		String header = part.getHeader("content-disposition");
		System.out.println("Header => "+header);
		
		int start = header.indexOf("filename=");
		fileName = header.substring(start+10, header.length()-1);
		ctx.log("파일명:"+fileName);
		
		return fileName;
	}
	
}

 

컨트롤러 호출 url 매핑을 '/news.nhn' 으로 설정하였다.

기본적으로 NewsDAO 객체 dao와 서버 로그 메시지 생성을 위해 ServletContext 객체를 선언해두었다.

 

♣ init() : 

super.init(config) 를 빼먹지 말아야 한다.

그리고 dao, ctx 에 대한 초기화 작업을 진행한다.

 

♣ service() : 

Method m 에 대해서 주의깊게 볼 필요가 있어 보인다.

자바 리플렉션을 사용하여 if(switch) 없이 요청에 따라 구현 메소드가 실행되도록 구성하는 역할이다.

일종의 Command 패턴을 적용하는 것으로 프로그램에서 불필요한 if 처리를 줄여주고 구조 변경에도 유리한 방법이다.

 

자바 리플렉션 API 이란?

자바 클래스 구조를 프로그램에서 직접 제어할 수 있는 라이브러리로, 구체적인 클래스 타입을 모르더라도 문자열 이름으로 클래스를 참조하거나 클래스의 메서드나 변수를 읽어와 실행하는 등의 작업이 가능하다.

 

자바 리플렉션을 이용하여 action으로 전달된 이름의 메서드를 자동으로 호출하는 부분으로 try { ... } 부분을 보면 된다.

 // 현재 클래스에서 action 이름과 HttpServletRequest를 파라미터로 하는 메서드를 찾는다.
m = this.getClass().getMethod(action, HttpServletRequest.class);
// 메서드를 실행한 후 리턴값을 받아온다.
view = (String)m.invoke(this, request);

 

뷰 이동 시 요청 처리 메서드를 호출한 다음 다음 리턴된 뷰 페이지로 이동하면 된다.

앞에서 여러번 구현했던 것처럼 forward를 하면 되는데, post 요청을 forward로 처리하면 웹 브라우저를 새로 고침 할 경우 이전 요청이 한 번 더 실행되는 문제가 있다.

예를 들어서 새로운 뉴스 기사를 등록할 때 입력 양식에서 <form method="post">와 같이 post 방식을 사용하게 되는데, 이때 뉴스 등록 후 목록 페이지로 포워딩하는 경우 페이지 새로 고침을 하게 되면 경고 메시지가 나오고 확인 버튼을 누르면 등록이 한 번 더 이루어지는 것이다.

따라서 컨트롤러는 페이지 이동 시 포워딩과 리디렉션 모두 지원할 수 있어야 한다.

페이지 리디렉션은 이미 알고 있는 것처럼 response.sendRedirect()를 사용하면 되지만 요청 처리 메서드는 일관적으로 뷰 페이지의 파일 경로를 리턴하게 되어 있기 때문에 일관된 방법으로 리디렉션을 지원하기 위한 별도의 규격화된 접근이 필요하다.

 

스프링 프레임워크에서는 return "redirect:/뷰 페이지 경로"와 같이 리턴하면 리디렉션이 이루어지는 구조를 제공한다.

다음과 같이 리턴에 따라 포워딩과 리디렉션이 구분되도록 한다.

// Post 요청에서는 리디렉션 방법으로 이동하도록 분기한다.
if(view.startsWith("redirect:/")) {
    // redirect 이후의 경로만 가져온다.

    String rview = view.substring("redirect:/".length()); 
    response.sendRedirect(rview);
} else {
    RequestDispatcher dispatcher = request.getRequestDispatcher(view);
    dispatcher.forward(request, response);
    // 지정된 뷰로 포워딩하며 포워딩 시 콘텍스트 경로는 필요 없다.

}

 

♣ listNews() :

News 자료형인 List 에 대해 데이터베이스에 있는 모든 데이터들을 List 에 할당하여 "newslist" 를 list 로 속성을 지정해준다.

속성 지정이 완료되면 기사들의 목록을 보여주기 위해 "ch10/newsList.jsp" 를 호출한다.예외가 발생할 경우 예외 처리를 해준다.

 

♣ addNews() :

이미지 파일 저장을 위해 request로부터 Part 객체를 참조하고 getFilename() 메서드를 통해 파일 이름을 가져온 다음 서버의 디스크에 저장한다.

이전에 배웠던 apache commons 라이브러리 BeanUtils를 이용하여 매핑한 다음 이미지 파일 이름을 경로와 함께 저장하고 dao의 addNews() 메서드를 호출해 등록한다.

정상적으로 데이터베이스에 News 객체 n이 추가된 경우 "redirect:/news.nhn?action=listNews" 를 호출하여 리디렉션 방식으로 처리하도록 한다.

예외가 발생할 경우 에러 메시지와 함께 listNews(request) 를 반환한다. 

 

♣ getNews() : 

선택한 aid를 정수형으로 변환하여 데이터베이스에 해당 aid에 대한 레코드를 News 객체 n에 할당하여 "news" 에 n 으로 속성을 세팅한다.

그 후에 정상적으로 세팅되면 선택한 aid 에 대한 기사의 세부정보를 보기 위해 "ch10/newsView.jsp" 를 반환하도록 한다.

예외가 발생할 경우 예외 처리를 해준다.

 

♣ deleteNews() : 

선택한 aid를 정수형으로 변환하여 데이터베이스에 해당 aid에 대한 레코드를 삭제하도록 한다.

삭제가 완료되면 리디렉션으로 현재 화면을 업데이트 한 화면으로 새로 보이기 위해 "redirect:/news.nhn?action=listNews" 를 반환한다.

에러 발생 시 예외 처리 및 listNews(request)를 반환한다.

 

♣ getFileName() : 

Part 객체로 전달된 이미지 파일로부터 파일 이름을 추출하기 위한 메서드이다.

여기서는 헤더 정보에서 단순히 파일 이름만 가져오지만 경우에 따라 공백 처리, 중복 파일 이름 처리 등 추가적 작업이 필요할 수 있다.

 

 

5. 뷰 구현

newsList.jsp 코드

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" 
	rel="stylesheet" 
	integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" 
	crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" 
	integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" 
	crossorigin="anonymous"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>뉴스 관리 앱</title>
</head>
<body>

	<div class="container w-75 mt-5 mx-auto">
		<h2>뉴스 목록</h2>
		<hr>
		<ul class="list-group">
			<c:forEach var="news" items="${newsList}" varStatus="status">
				<li class="list-group-item list-group-item-action
					d-flex justify-content-between align-items-center">
				<a href="news.nhn?action=getNews&aid=${news.aid}" Class=
				"text-decoration-none"> [${status.count}] ${news.title}, ${news.date} </a>
				<a href="news.nhn?action=deleteNews&aid=${news.aid}">
				<span class="badge bg-secondary"> &times; </span></a>
				</li> 
			</c:forEach>
		</ul>
		
		<hr>
		<c:if test="${error != null}">
			<div class="alert alert-danger alert-dismissible fade show mt-3">
				에러 발생: ${error}
				<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
			</div>
		</c:if>
		<button class="btn btn-outline-info mb-3" type="button" data-bs-toggle="collapse"
		data-bs-target="#addForm" aria-expanded="false" aria-controls="addForm">뉴스 등록</button>
		<div class="collapse" id="addForm">
			<div class="card card-body">
				<form method="post" action="/jwbook/news.nhn?action=addNews"
				enctype="multipart/form-data">
					<label class="form-label">제목</label>
					<input type="text" name="title" class="form-control">
					<label class="form-label">이미지</label>
					<input type="file" name="file" class="form-control">
					<label class="form-label">기사 내용</label>
					<textarea cols="50" rows="5" name="content" class="form-control"></textarea>
					<button type="submit" class="btn btn-success mt-3">저장</button>
				</form>
			</div>
		</div>
	</div>

</body>
</html>

 

newsView.jsp 코드

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>뉴스 관리 앱</title>
</head>
<body>

	<div class="container w-75 mt-5 mx-auto">
		<h2>${news.title}</h2>
		<hr>
		<div class="card w-75 mx-auto">
			<img class="card-img-top" src="${news.img}">
			<div class="card-body">
				<h4 class="card-title">Date: ${news.date}</h4>
				<p class="card-text">Content: ${news.content}</p>
			</div>
		</div>	
		<hr>
		<a href="javascript:history.back()" class="btn btn-primary"> << Back</a>
		
	</div>

</body>
</html>

 

 

※ 이미지 업로드를 위한 톰캣 설정

기본적으로 웹을 통해 접근할 수 있는 디렉터리 경로는 Document root, 즉 [webapp] 폴더 아래가 된다.

따라서 업로드되는 파일 역시 html에서 참조 가능한 경로에 있어야 한다.

이때 [ch10] 폴더 아래 [img]라는 폴더를 만들고 파일을 업로드 할 경우 문제가 발생한다.

이를 해결하기 위해 프로젝트를 reload하거나 톰캣을 다시 시작해야 하므로 이미지 업로드 경로는 다른 곳으로 지정하는 것이 좋다.

컨트롤러 코드를 보면 "c:/Temp/img" 폴더를 사용하도록 지정했는데 해당 폴더는 웹으로 접근할 수 없기 때문에 웹에서 접근 가능하도록 톰캣 설정을 추가해야 한다.

 

1. 톰캣이 실행되어 있따면 우선 종료하고 이클립스 Project Explorer에서 [servers] -> [Tomcat 9.0...]를 클릭하여 'server.xml' 파일을 연다.

 

 

2. 'server.xml' 파일은 톰캣을 실행하는 데 가장 기본이 되는 설정 파일이다.

이 파일의 제일 아래에서 <Context> 부분을 찾아 태그 끝 부분에 있는 </Host> 사이에 새로운 <Context> 항목을 다음과 같이 추가한다.

 

 

 

3. "c:/Temp/" 위치에 img 폴더가 존재해야 한다. 없다면 새로 생성한 후에 해당 폴더에 이미지 파일을 넣도록 한다.

 

 

※ 컨트롤러 실행

 

※ 뉴스 등록

 

 

※ 뉴스 삭제

 

※ 에러 처리

H2 Console에서 sql 쿼리를 이용해 직접 삭제한 후 해당 기사를 삭제하려고 하면 이미 DB에서 삭제된 뉴스이기 때문에 다음과 같이 에러가 출력된다.

 

 

'JSP' 카테고리의 다른 글

REST API  (0) 2022.01.05
리스너와 필터  (0) 2022.01.03
SQL 및 JDBC 기본 구조와 API  (0) 2022.01.02
데이터베이스와 JDBC  (0) 2021.12.31
MVC 패턴의 이해  (0) 2021.12.31
Comments