231212_Maven, REST API, 스프링 프레임워크와 스프링부트
CHAPTER07-6 Maven 기반 프로젝트 구성
Maven 은 자바 빌드 도구로, 스프링 프레임 워크 개발에서 기본 빌드 도구로 활용되었다. 이후 Gradle가 나오고나서 안드로이드 앱 기발의 기본 빌드 도구가 되었다. Mavem과 Gradle은 현재 가장 대표적인 빌드 도구다. 이클립스에서 Maven 프로젝트를 사용할 땐 주로 웹 프젝트를 Maven기반으로 변환해 사용한다.
프로젝트에서 우클릭, Configure →Convert to Maven Project을 선택하면 POM을 생성하는 창이 뜬다. 필요한 사항을 등록한 뒤 Finish를 클릭하면 pom.xml 이 자동으로 생성된 것을 볼 수 있다. 이후 메이븐 리포지터리(https://mvnrepository.com/) 에 접속해서 JSTL을 검색해 코드를 붙여넣는다.
이하 더보기는 메이븐 리포지터리에서 코드를 복사하는 방법이다.



<!--라이브러리 추가-->
<dependencies>
<!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
생성된 pom.xml 파일에 라이브러리를 추가 끝났다면 pom.xml 우클릭-maven-update project 해야한다.
이전 수업에서 학생명단으로 작업할 때 사용했던 라이브러리들을 모두 추가했다. (모두 추가한 뒤 Maven 폴더를 열어봤을 때 아래와 같이 확인 된다. 이전에는 별도로 라이브러리를 폴더에 넣어서 사용했으나, 지금은 Maven에 사용하던 라이브러리를 코드로 넣었고 문제없이 작동되는걸 확인했다.)
CHAPTER12 REST API 개발
pom.xml 에 라이브러리를 추가했으나 최신 버전은 호환이 되지 않아 2.33 버전으로 수정해서 업데이트 했다. 이후, 12장 실습을 위해 ch10 폴더를 추가하고서 ch12챕터의 코드를 추가했다.
<!--12장-->
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.containers/jersey-container-servlet -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>2.33</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.inject/jersey-hk2 -->
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>2.33</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.media/jersey-media-json-jackson -->
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.33</version>
</dependency>
package ch12;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/api")
public class RestConfig extends Application{
public Map<String, Object> getProperties() {
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("jersey.config.server.provider.packages", "ch12");
return properties;
}
}
package ch12;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
@Path("/test")
public class RestApiExample {
// @GET
@POST
@Produces(MediaType.TEXT_PLAIN)
public String sayHello() {
return "Hello API Service"; //.../api/test
}
// .../api/test?msg="abc"
// @POST
@GET
public String sayHello(@QueryParam("msg") String msg) {
return msg+" API Service";
}
}
CHAPTER13 스프링 프레임워크와 스프링부트
pom.xml 에 아래 라이브러리를 추가했다.
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
application.properties 파일에 아래 내용을 추가했다.
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
이후 테스트를 위한 컨트롤러 (TestWebController.java) 를 작성한다. 위의 설정이 모두 끝난 뒤, 이클립스 콘솔에서 스프링부트 실행했다. 결과를 확인하려면 Run→Run on Server로 실행하는 것이 아닌, 웹 브라우저에서 경로(http://localhost:8080/test/hello)를 직접 작성해서 결과를 확인해야 한다. (경로가 test/hello 인 이유는 TestWebController.java 에서 확인 가능.)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
<h2>Hello World</h2>
<hr>
현재 날짜와 시간은 <%=java.time.LocalDateTime.now()%> 입니다.
<hr>
메시지: ${msg}
</body>
</html>
`
CHAPTER10 뉴스기사 관리 웹 서비스
뉴스기사 관리 웹 서비스에 사용된 파일들은 더보기 참고.
newsView.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js" integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous"></script>
<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">×</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>
newsList.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js" integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous"></script>
<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">×</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>
news.sql
drop table news;
CREATE TABLE news (
aid INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
title VARCHAR NOT NULL,
img VARCHAR NOT NULL,
date TIMESTAMP,
content VARCHAR NOT NULL
);
News,java
package ch10;
/**
* @author dinfree
*
*/
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;
}
}
NewsController.java
img를 저장하는 폴더를 찾지 못해 그 부분의 절대 경로를 업로드했다.
package ch10;
import java.io.IOException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.List;
import java.util.StringTokenizer;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
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;
import ch10.News;
import ch10.NewsDAO;
@WebServlet("/news.nhn")
@MultipartConfig(maxFileSize=1024*1024*2, location="D:\\Dev-Fullstack2023\\img")
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();
}
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
String action = request.getParameter("action");
dao = new NewsDAO();
// 자바 리플렉션을 사용해 if, switch 없이 요청에 따라 구현 메서드가 실행되도록 함.
Method m;
String view = null;
// action 파라미터 없이 접근한 경우
if (action == null) {
action = "listNews";
}
try {
// 현재 클래스에서 action 이름과 HttpServletRequest 를 파라미터로 하는 메서드 찾음
m = this.getClass().getMethod(action, HttpServletRequest.class);
// 메서드 실행후 리턴값 받아옴
view = (String)m.invoke(this, request);
} catch (NoSuchMethodException e) {
e.printStackTrace();
// 에러 로그를 남기고 view 를 로그인 화면으로 지정, 앞에서와 같이 redirection 사용도 가능.
ctx.log("요청 action 없음!!");
request.setAttribute("error", "action 파라미터가 잘못 되었습니다!!");
view = START_PAGE;
} catch (Exception e) {
e.printStackTrace();
}
// 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);
}
}
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);
}
// 입력값을 News 객체로 매핑
BeanUtils.populate(n, request.getParameterMap());
// 이미지 파일 이름을 News 객체에도 저장
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 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";
}
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";
}
// multipart 헤더에서 파일이름 추출
private String getFilename(Part part) {
String fileName = null;
// 파일이름이 들어있는 헤더 영역을 가지고 옴
String header = part.getHeader("content-disposition");
//part.getHeader -> form-data; name="img"; filename="사진5.jpg"
System.out.println("Header => "+header);
// 파일 이름이 들어있는 속성 부분의 시작위치를 가져와 쌍따옴표 사이의 값 부분만 가지고옴
int start = header.indexOf("filename=");
fileName = header.substring(start+10,header.length()-1);
ctx.log("파일명:"+fileName);
return fileName;
}
}
NewsDAO.java
getAll 에서 날짜부분 쿼리에서 에러가 떠 String sql = "select aid, title, date from news"; 로 수정했다. 그 외에 날짜가 관련되는 부분들을 전체적으로 수정하고, cdate는 date로 수정했다.
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.ArrayList;
import java.util.List;
public class NewsDAO {
final String JDBC_DRIVER = "org.h2.Driver";
final String JDBC_URL = "jdbc:h2:tcp://localhost/~/jwbookdb";
// DB 연결을 가져오는 메서드, DBCP를 사용하는 것이 좋음
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 List<News> getAll() throws Exception {
Connection conn = open();
List<News> newsList = new ArrayList<>();
String sql = "select aid, title, date 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("date"));
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, date, 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("date"));
n.setContent(rs.getString("content"));
pstmt.executeQuery();
return n;
}
}
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(conn; pstmt) {
pstmt.setString(1, n.getTitle());
pstmt.setString(2, n.getImg());
pstmt.setString(3, n.getContent());
pstmt.executeUpdate();
}
}
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에러");
}
}
}
}
뉴스 목록에서 이미지를 불러오지 못하는 문제가 있다. 수업 시간이 끝나서 문제점을 더 찾아보지는 못하고 다음 수업 때 이미지를 불러올 수 있는 방법이 있는지 다시 찾아볼 계획이다.
사용된 pom.xml 코드 전문
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jwbook</groupId>
<artifactId>jwbook</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.3</version>
</plugin>
</plugins>
</build>
<!--라이브러리 추가-->
<dependencies>
<!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.3.0</version>
</dependency>
<!--12장-->
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.containers/jersey-container-servlet -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>2.33</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.inject/jersey-hk2 -->
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>2.33</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.media/jersey-media-json-jackson -->
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.33</version>
</dependency>
</dependencies>
</project>