Servlet 기반 뉴스 모듈 구현 가이드

프로젝트 구조

디렉터리 설명
src/main/java/…/servlet/indexServlet 홈페이지 컨트롤러
src/main/java/…/servlet/NewsServlet 뉴스 상세 페이지 컨트롤러
src/main/java/…/service/NewsService 뉴스 비즈니스 로직
src/main/java/…/dao/NewsDao 뉴스 데이터 액세스 객체
src/main/java/…/bean/News 뉴스 테이블 매핑 클래스
src/main/webapp/index.jsp 홈페이지 뷰
src/main/webapp/login.jsp 로그인 페이지
src/main/webapp/news.jsp 뉴스 상세 뷰
src/main/webapp/WEB-INF/web.xml 웹 애플리케이션 배포 설정
pom.xml Maven 빌드 설정

web.xml 설정

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <welcome-file-list>
    <welcome-file>index</welcome-file>
  </welcome-file-list>
</web-app>

login.jsp - 로그인 페이지

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page isELIgnored="false" %>
<!DOCTYPE html>
<html>
<head>
    <title>로그인</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
        body { margin:0; padding:0; }
        .login-header {
            min-width:800px; height:100px;
            display:flex; align-items:center;
            font-size:30px; font-weight:900; font-style:italic;
            color:#4487db; padding:0 0 0 25px;
        }
        .login-center {
            min-width:800px; height:300px; background:#2881f9;
            display:flex; justify-content:center; line-height:200px;
            color:#fff; font-size:23px; font-weight:600;
        }
        .layout {
            position:relative; height:100vh; background:#f2f5fa;
        }
        .login-panel {
            position:absolute; background:#fff;
            border-radius:20px 20px 0 0;
            left:0; right:0; width:500px; height:300px; margin:auto; bottom:0;
        }
        .login-form {
            width:100%; height:100%;
            display:flex; flex-direction:column; align-items:center;
        }
        .form-label { margin:35px 0 5px; font-size:17px; color:#878b8d; font-weight:600; }
        .input-field input {
            outline:none; border:2px solid #f2f2f2; height:32px; width:260px;
            margin:8px; padding:0 5px; border-radius:5px; font-size:17px; color:#626769;
        }
        .input-field input::placeholder { color:#a5a5a5; }
        .input-field input[type="submit"] {
            border:2px solid #f2f2f2; height:37px; width:275px;
            background:#4088f7; margin:8px; border-radius:5px;
            font-size:17px; font-weight:600; color:#f4faff;
        }
        .register-link a {
            text-decoration:none; color:#4088f7;
            margin-left:220px;
        }
    </style>
</head>
<body>
<div class="layout">
    <div class="login-header">뉴스 사이트 관리 시스템</div>
    <div class="login-center">관리자 로그인</div>
    <div class="login-panel">
        <form class="login-form" action="${pageContext.request.contextPath}/login" method="post">
            <div class="form-label">로그인</div>
            <div class="input-field">
                <input type="tel" name="username" placeholder="아이디">
            </div>
            <div class="input-field">
                <input type="password" name="password" placeholder="비밀번호">
            </div>
            <div class="input-field">
                <input type="submit" value="제출">
            </div>
            <div class="register-link">
                <a href="${pageContext.request.contextPath}/register.jsp">회원가입</a>
            </div>
        </form>
        <div>${message}</div>
    </div>
</div>
</body>
</html>

IndexServlet - 홈페이지 컨트롤러

package com.example.servlet;

import com.example.service.NewsService;
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 java.io.IOException;
import java.sql.SQLException;
import java.util.List;

@WebServlet("/index")
public class IndexServlet extends HttpServlet {

    private NewsService newsService = new NewsService();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        try {
            List newsList = newsService.fetchPaginatedNews(1, 7);
            request.setAttribute("newsList", newsList);
        } catch (SQLException e) {
            throw new RuntimeException("홈페이지 데이터 로딩 실패", e);
        }
        request.getRequestDispatcher("/index.jsp").forward(request, response);
    }
}

NewsServlet - 뉴스 상세 컨트롤러

package com.example.servlet;

import com.example.model.News;
import com.example.service.NewsService;
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 java.io.IOException;
import java.sql.SQLException;

@WebServlet("/news")
public class NewsDetailServlet extends HttpServlet {

    private NewsService newsService = new NewsService();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String newsId = request.getParameter("id");
        if (newsId == null || newsId.isEmpty()) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "뉴스 ID 누락");
            return;
        }

        try {
            News news = newsService.findNewsById(Integer.parseInt(newsId));
            request.setAttribute("newsDetail", news);
            request.getRequestDispatcher("/news.jsp").forward(request, response);
        } catch (SQLException | NumberFormatException e) {
            throw new RuntimeException("뉴스 데이터 로딩 실패", e);
        }
    }
}

index.jsp - 홈페이지 뷰

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page isELIgnored="false" %>
<!DOCTYPE html>
<html>
<head>
    <title>뉴스 홈</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
        body { margin:0; display:flex; justify-content:center; }
        a { text-decoration:none; color:#222; }
        a:hover { color:#00F; }
        .page-wrapper { display:flex; justify-content:center; flex-direction:column; }
        .navbar { background:#f9fafe; padding:10px; display:flex; justify-content:space-between; }
        .navbar img { height:25px; }
        .navbar a {
            display:inline-block; border:1px solid #3377ff; height:26px; line-height:26px; width:60px;
            text-align:center; border-radius:13px; color:#3377ff; background:#eef8fd;
            box-shadow:0 1px 3px 0 #dde9fd; font-size:13px; font-weight:900;
        }
        .category-bar { background:#3071f2; display:flex; color:#fff; height:40px; line-height:40px; }
        .category-item { width:60px; text-align:center; }
        .content-area { width:1200px; display:flex; justify-content:center; flex-direction:column; }
        .headline-section { padding:30px 0 0; }
        .headline-title a { font-size:20px; font-weight:900; line-height:40px; color:#555; }
        .headline-list { display:flex; flex-wrap:wrap; flex-direction:column; height:170px; padding:30px 0 0; }
        .headline-item { height:40px; }
        .headline-item a { font-size:13px; color:#555; }
        .headline-item a:hover { color:#00F; }
        .hot-news-header { font-size:18px; font-weight:900; color:#00F; margin:10px 0; }
        .news-grid { display:flex; flex-wrap:wrap; }
        .news-card { position:relative; }
        .news-card a { position:relative; display:block; }
        .news-image { width:380px; height:220px; overflow:hidden; display:flex; justify-content:center;
                       border-radius:5px; align-items:center; box-shadow:0 0 2px 0 #ccc; margin:10px; }
        .news-image img { width:100%; }
        .news-caption { position:absolute; bottom:20px; left:20px; color:#fff; width:360px;
                        font-weight:900; font-size:17px; background:#0004; padding:2px 5px; }
        .sub-news-list { display:flex; flex-wrap:wrap; height:200px; margin:20px 0 0; }
        .sub-news-item { width:380px; height:80px; border-bottom:1px solid #ddd; margin:10px; }
        .sub-news-item a { display:flex; flex-direction:row-reverse; }
        .sub-news-thumb { width:100px; height:60px; overflow:hidden; border-radius:5px; }
        .sub-news-thumb img { width:100px; }
        .sub-news-title { width:285px; font-size:13px; font-weight:900; }
    </style>
</head>
<body>
<div class="page-wrapper">
    <div class="navbar">
        <div><img src="https://inews.gtimg.com/newsapp_bt/0/15822349472/0" alt="로고"></div>
        <div><a href="${pageContext.request.contextPath}/login.jsp">로그인</a></div>
    </div>
    <div class="category-bar">
        <div class="category-item">속보</div>
        <div class="category-item">테크</div>
        <div class="category-item">경제</div>
        <div class="category-item">연예</div>
    </div>
    <div class="content-area">
        <div class="headline-section">
            <div class="headline-title">
                <a href="${pageContext.request.contextPath}/news?id=${newsList[2].id}">${newsList[2].title}</a>
            </div>
            <div class="headline-title">
                <a href="${pageContext.request.contextPath}/news?id=${newsList[3].id}">${newsList[3].title}</a>
            </div>
            <div class="headline-list">
                <c:forEach var="item" items="${newsList}" begin="0" end="8" step="1">
                    <div class="headline-item">
                        <a href="${pageContext.request.contextPath}/news?id=${item.id}">${item.title}</a>
                    </div>
                </c:forEach>
            </div>
        </div>
        <div class="hot-news-section">
            <div class="hot-news-header">인기 뉴스</div>
            <div class="news-grid">
                <c:forEach var="item" items="${newsList}" begin="0" end="2" step="1">
                    <div class="news-card">
                        <a href="${pageContext.request.contextPath}/news?id=${item.id}">
                            <div class="news-image">
                                <img src="${pageContext.request.contextPath}${item.img}" alt="${item.title}">
                            </div>
                            <div class="news-caption">${item.title}</div>
                        </a>
                    </div>
                </c:forEach>
            </div>
            <div class="sub-news-list">
                <c:forEach var="item" items="${newsList}" begin="0" end="5" step="1">
                    <div class="sub-news-item">
                        <a href="${pageContext.request.contextPath}/news?id=${item.id}">
                            <div class="sub-news-thumb">
                                <img src="${pageContext.request.contextPath}${item.img}" alt="${item.title}">
                            </div>
                            <div class="sub-news-title">${item.title}</div>
                        </a>
                    </div>
                </c:forEach>
            </div>
        </div>
    </div>
</div>
</body>
</html>

news.jsp - 뉴스 상세 뷰

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page isELIgnored="false" %>
<!DOCTYPE html>
<html>
<head>
    <title>뉴스 상세</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
        body { margin:0; display:flex; justify-content:center; }
        a { text-decoration:none; color:#222; }
        a:hover { color:#00F; }
        .page-wrapper { display:flex; justify-content:center; flex-direction:column; }
        .navbar { background:#f9fafe; padding:10px; }
        .navbar img { height:25px; }
        .category-bar { background:#3071f2; display:flex; color:#fff; height:40px; line-height:40px; }
        .category-item { width:60px; text-align:center; }
        .content-area { width:1200px; display:flex; justify-content:center; flex-direction:column; align-items:center; }
        .news-container { width:800px; border:1px solid #ccc; margin:50px 0; padding:10px; border-radius:5px; }
        .news-header { font-size:35px; font-weight:600; }
        .news-author { font-size:17px; font-weight:900; margin:5px 0 0; }
        .news-timestamp { font-size:13px; margin:0 0 5px; color:#ddd; }
        .news-main-image { width:100%; overflow:hidden; border-radius:10px; }
        .news-main-image img { width:100%; }
    </style>
</head>
<body>
<div class="page-wrapper">
    <div class="navbar">
        <div><img src="https://inews.gtimg.com/newsapp_bt/0/15822349472/0" alt="로고"></div>
    </div>
    <div class="category-bar">
        <div class="category-item">속보</div>
        <div class="category-item">테크</div>
        <div class="category-item">경제</div>
        <div class="category-item">연예</div>
    </div>
    <div class="content-area">
        <div class="news-container">
            <div class="news-header">${newsDetail.title}</div>
            <div class="news-author">${newsDetail.writer}</div>
            <div class="news-timestamp">${newsDetail.updateTime} 발행</div>
            <div class="news-main-image">
                <img src="${pageContext.request.contextPath}${newsDetail.img}" alt="${newsDetail.title}">
            </div>
            <div class="news-content">${newsDetail.content}</div>
        </div>
    </div>
</div>
</body>
</html>

NewsService - 비즈니스 로직 계층

package com.example.service;

import com.example.model.News;
import com.example.dao.NewsDao;
import java.sql.SQLException;
import java.util.List;

public class NewsService {

    private NewsDao newsDao = new NewsDao();

    public List fetchPaginatedNews(int pageNumber, int pageSize) throws SQLException {
        return newsDao.selectPaginated(pageNumber, pageSize);
    }

    public int addNews(News news) throws SQLException {
        return newsDao.insert(news);
    }

    public int updateNews(News news) throws SQLException {
        return newsDao.updateById(news);
    }

    public News findNewsById(Integer id) throws SQLException {
        return newsDao.selectById(id);
    }
}

NewsDao - 데이터 액세스 계층

package com.example.dao;

import com.example.model.News;
import com.example.utils.DatabaseUtil;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import java.sql.SQLException;
import java.util.List;

public class NewsDao {

    public List selectPaginated(int pageNumber, int pageSize) throws SQLException {
        QueryRunner queryRunner = new QueryRunner(DatabaseUtil.getDataSource());
        String sql = "SELECT * FROM t_news LIMIT ?, ?";
        return queryRunner.query(sql,
                new BeanListHandler<News>(News.class),
                (pageNumber - 1) * pageSize, pageSize);
    }

    public int insert(News news) throws SQLException {
        QueryRunner queryRunner = new QueryRunner(DatabaseUtil.getDataSource());
        String sql = "INSERT INTO t_news (id, title, img, content, writer, type, status, update_time, create_time) " +
                     "VALUES (null, ?, ?, ?, ?, 1, 1, NOW(), NOW())";
        return queryRunner.update(sql,
                news.getTitle(), news.getImg(), news.getContent(), news.getWriter());
    }

    public int updateById(News news) throws SQLException {
        QueryRunner queryRunner = new QueryRunner(DatabaseUtil.getDataSource());
        String sql = "UPDATE t_news SET title = ?, img = ?, content = ?, update_time = NOW() WHERE id = ?";
        return queryRunner.update(sql,
                news.getTitle(), news.getImg(), news.getContent(), news.getId());
    }

    public News selectById(Integer id) throws SQLException {
        QueryRunner queryRunner = new QueryRunner(DatabaseUtil.getDataSource());
        String sql = "SELECT * FROM t_news WHERE id = ?";
        return queryRunner.query(sql, new BeanHandler<News>(News.class), id);
    }
}

News 모델 클래스

package com.example.model;

import java.util.Date;

public class News {
    private int id;
    private String title;
    private String img;
    private String content;
    private String writer;
    private int type;  // 0: 헤드라인, 1: 인기, 2: 일반
    private int status; // 0: 미발행, 1: 심사중, 2: 발행완료
    private Date updateTime;
    private Date createTime;

    // Getter 및 Setter 메서드
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }

    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 getContent() { return content; }
    public void setContent(String content) { this.content = content; }

    public String getWriter() { return writer; }
    public void setWriter(String writer) { this.writer = writer; }

    public int getType() { return type; }
    public void setType(int type) { this.type = type; }

    public int getStatus() { return status; }
    public void setStatus(int status) { this.status = status; }

    public Date getUpdateTime() { return updateTime; }
    public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }

    public Date getCreateTime() { return createTime; }
    public void setCreateTime(Date createTime) { this.createTime = createTime; }

    @Override
    public String toString() {
        return "News{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", img='" + img + '\'' +
                ", content='" + content + '\'' +
                ", writer='" + writer + '\'' +
                ", type=" + type +
                ", status=" + status +
                ", updateTime=" + updateTime +
                ", createTime=" + createTime +
                '}';
    }
}

DatabaseUtil - 데이터베이스 연결 유틸리티

package com.example.utils;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class DatabaseUtil {

    private static DataSource dataSource = new ComboPooledDataSource();

    public static DataSource getDataSource() {
        return dataSource;
    }

    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
}

Maven 의존성 (pom.xml)

<!-- Servlet API -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
<!-- JSTL -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.40</version>
</dependency>
<!-- C3P0 Connection Pool -->
<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.2</version>
</dependency>
<!-- Apache Commons DbUtils -->
<dependency>
    <groupId>commons-dbutils</groupId>
    <artifactId>commons-dbutils</artifactId>
    <version>1.7</version>
</dependency>
<!-- File Upload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.3</version>
</dependency>

데이터베이스 테이블 생성 SQL

DROP TABLE IF EXISTS t_news;
CREATE TABLE t_news (
    id INT UNSIGNED AUTO_INCREMENT NOT NULL COMMENT '기본 키',
    title VARCHAR(150) NOT NULL COMMENT '뉴스 제목',
    img VARCHAR(200) NOT NULL COMMENT '뉴스 이미지 경로',
    content TEXT NOT NULL COMMENT '뉴스 본문',
    writer VARCHAR(50) NOT NULL COMMENT '작성자',
    type TINYINT NOT NULL COMMENT '뉴스 타입',
    status TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '상태',
    update_time DATETIME NOT NULL COMMENT '수정 시간',
    create_time DATETIME NOT NULL COMMENT '생성 시간',
    PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='뉴스 테이블';

태그: Servlet JSP JDBC C3P0 Apace Commons DbUtils

5월 24일 17:30에 게시됨