Java/Java.JSP 프로그래밍

JSTL 을 활용한 게시판 기능 만들기

H_u 2024. 7. 24. 10:45
728x90
반응형
SMALL

1. 프로젝트 기본 구성

    • 회원가입: 사용자가 회원가입을 할 수 있는 기능
    • 회원탈퇴: 사용자가 회원탈퇴를 할 수 있는 기능
    • 로그인 및 로그아웃: 사용자가 로그인하고 로그아웃할 수 있는 기능회원 관리 기능
  1. 게시판 기능 게시글 작성, 수정, 삭제: 사용자가 게시글을 작성, 수정, 삭제할 수 있는 기능
    • 게시글 조회: 사용자가 게시글 목록을 조회하고 상세 내용을 볼 수 있는 기능
    • 댓글 작성 및 삭제: 게시글에 대한 댓글을 작성하고 삭제할 수 있는 기능
  2. 페이징 처리
    • 게시글 목록 페이징: 게시글 목록을 페이지 단위로 나누어 볼 수 있는 기능

2. 비기능적 요구사항

  1. Dynamic Web Project: 이클립스에서 Dynamic Web Project로 설정하여 개발(lib 직접 설정)
  2. 톰캣 10.1.25 환경: 톰캣 10.1.25 버전을 사용하여 애플리케이션 배포 및 실행
  3. MVC 패턴 사용: 모델, 뷰, 컨트롤러 구조를 따르는 코드 작성
  4. DAO 인터페이스 설계: 데이터베이스 접근을 위한 DAO 인터페이스와 구현 클래스 설계 및 구현
  5. DB 연결 풀링 (HikariCP): HikariCP를 사용하여 데이터베이스 연결 풀링 설정 및 사용
  6. JSTL 태그 사용: JSP 페이지에서 JSTL core 및 format 태그를 사용하여 코드 간결화
  7. 데이터베이스 정규화: 데이터베이스 테이블 설계 시 정규화 적용
  8. 캐스케이드 적용: 데이터베이스 외래 키 관계에서 캐스케이드 옵션 적용
  9. 게시글 조회 로직 수행시간 측정: 게시글 리스트 조회 시 로직 수행시간을 측정하여 출력

3. 프로젝트 기본 구조

프로젝트 명 : jsp_mvc_board
패키지 명 : com.tenco.tboard
context root : t-board
JSP 파일 위치 : WEB-INF/views

4. 데이터베이스(DB) 설계

DROP DATABASE IF EXISTS db_tboard;
CREATE DATABASE db_tboard;
USE db_tboard;

-- users 테이블 생성
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(50) NOT NULL,
    email VARCHAR(50) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
DESC users;
-- board 테이블 생성
CREATE TABLE board (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    title VARCHAR(255) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id)
        REFERENCES users (id)
        ON DELETE CASCADE
);
desc board;

-- comments 테이블 생성
CREATE TABLE comments (
    id INT AUTO_INCREMENT PRIMARY KEY,
    board_id INT,
    user_id INT,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id)
        REFERENCES users (id)
        ON DELETE CASCADE,
    FOREIGN KEY (board_id)
        REFERENCES board (id)
        ON DELETE CASCADE
);
desc comments;

-- 기존 데이터 삭제
-- truncate table comments;
-- truncate table board;
-- truncate table users;

-- users 테이블에 샘플 데이터 삽입
INSERT INTO users (username, password, email) VALUES ('user1', 'asd123', 'user1@example.com');
INSERT INTO users (username, password, email) VALUES ('user2', 'asd123', 'user2@example.com');
INSERT INTO users (username, password, email) VALUES ('user3', 'asd123', 'user3@example.com');

-- board 테이블에 샘플 데이터 삽입
INSERT INTO board (user_id, title, content) VALUES (1, '자바의 장점', '자바는 플랫폼 독립적이며, 풍부한 라이브러리를 제공합니다.');
INSERT INTO board (user_id, title, content) VALUES (2, '파이썬과 머신러닝', '파이썬은 머신러닝에 최적화된 언어로, 다양한 라이브러리를 지원합니다.');
INSERT INTO board (user_id, title, content) VALUES (3, '자바스크립트와 웹 개발', '자바스크립트는 웹 개발의 핵심 언어로, 프론트엔드와 백엔드 모두에서 사용됩니다.');

INSERT INTO board (user_id, title, content) VALUES (1, 'Spring 프레임워크', 'Spring은 자바 기반의 강력한 애플리케이션 프레임워크입니다.');
INSERT INTO board (user_id, title, content) VALUES (2, 'Django를 활용한 웹 개발', 'Django는 파이썬 기반의 웹 프레임워크로, 빠른 개발이 가능합니다.');
INSERT INTO board (user_id, title, content) VALUES (3, 'React로 인터랙티브 웹 만들기', 'React는 사용자 인터페이스 구축을 위한 자바스크립트 라이브러리입니다.');

INSERT INTO board (user_id, title, content) VALUES (1, 'JSP와 서블릿', 'JSP와 서블릿을 사용하여 동적인 웹 페이지를 개발할 수 있습니다.');
INSERT INTO board (user_id, title, content) VALUES (2, 'Flask의 장점', 'Flask는 가벼운 파이썬 웹 프레임워크로, 유연성과 확장성이 뛰어납니다.');
INSERT INTO board (user_id, title, content) VALUES (3, 'Node.js로 서버 개발', 'Node.js는 자바스크립트를 사용하여 서버 측 애플리케이션을 개발할 수 있습니다.');

INSERT INTO board (user_id, title, content) VALUES (1, '첫 게시글 작성', '첫 게시글을 작성했습니다. 앞으로 자주 올리겠습니다.');
INSERT INTO board (user_id, title, content) VALUES (2, '두 번째 게시글', '두 번째 게시글입니다. 댓글 많이 달아주세요.');
INSERT INTO board (user_id, title, content) VALUES (3, '안녕하세요', '안녕하세요, 오늘도 즐거운 하루 되세요.');

-- comments 테이블에 샘플 데이터 삽입
INSERT INTO comments (board_id, user_id, content) VALUES (1, 2, '자바의 장점에 대해 잘 읽었습니다.');
INSERT INTO comments (board_id, user_id, content) VALUES (1, 3, '좋은 글 감사합니다.');
INSERT INTO comments (board_id, user_id, content) VALUES (2, 1, '파이썬과 머신러닝에 관한 글 잘 봤습니다.');
INSERT INTO comments (board_id, user_id, content) VALUES (2, 3, '유익한 정보 감사합니다.');
INSERT INTO comments (board_id, user_id, content) VALUES (3, 1, '자바스크립트에 대해 더 알고 싶습니다.');
INSERT INTO comments (board_id, user_id, content) VALUES (3, 2, '도움이 많이 되었습니다.');

-- 데이터 확인
SELECT * FROM users;
SELECT * FROM board;
SELECT * FROM comments;
  • CASCADE는 데이터베이스 관리 시스템(DBMS)에서 외래 키 제약 조건을 설정할 때 사용되는 옵션 중 하나이다.
  • 이 옵션은 참조 무결성을 유지하기 위해 부모 테이블의 행이 변경될 때 자식 테이블의 행에 자동으로 동일한 변경을 적용하도록 한다.

CASCADE 옵션의 종류

  1. ON DELETE CASCADE
    • 부모 테이블에서 행이 삭제될 때, 해당 행을 참조하는 모든 자식 테이블의 행들도 자동으로 삭제된다.
    • 예를 들어, posts 테이블에서 특정 게시글이 삭제되면, 그 게시글에 달린 모든 댓글(comments 테이블)도 함께 삭제된다.
  2. ON UPDATE CASCADE
    • 부모 테이블의 기본 키 값이 업데이트될 때, 해당 기본 키를 참조하는 자식 테이블의 외래 키 값도 자동으로 업데이트다.

5. DB 접근 기술

Basic (사용 X)
package com.tenco.tboard.util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBUtilBasic {
	private static final String DB_URL = "jdbc:mysql://localhost:3306/db_tboard?useSSL=false&serverTimezone=Asia/Seoul";
	private static final String DB_USER = "root";
	private static final String DB_PASSWORD = "asd123";
	
	static {
		try {
			Class.forName("com.mysql.cj.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			System.out.println("DB 드라이버 로딩 실패");
			e.printStackTrace();
		}
	}
	
	public static Connection getConnection() throws SQLException {
		return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
	}
}
  • serverTimezone=Asia/Seoul
    • serverTimezone=Asia/Seoul로 설정하면, JDBC 드라이버가 데이터베이스 서버의 시간대를 한국 시간(서울)으로 해석. 이는 서버와 클라이언트의 시간대 차이로 인한 문제를 방지하기 위해 설정할 수 있다.
  • useSSL=false
    • useSSL 옵션은 데이터베이스 연결 시 SSL(Secure Sockets Layer)을 사용할지 여부를 설정.
    • useSSL=false로 설정하면, SSL을 사용하지 않고 데이터베이스에 연결.
    • SSL을 사용하지 않는 이유
      • 로컬 개발 환경: 개발 환경에서는 보안이 덜 중요한 경우가 많아 SSL을 비활성화하여 연결 설정을 단순화할 수 있다.
      • 성능: SSL을 사용하면 데이터 전송이 암호화되므로 성능에 약간의 영향을 줄 수 있다. 성능을 중시하는 경우 SSL을 비활성화할 수 있다.
    • 주의: SSL을 비활성화하면 데이터 전송이 암호화되지 않기 때문에 보안에 취약해질 수 있다. 특히, 운영 환경에서는 SSL을 사용하는 것이 좋다.
실제 사용 HikariCP 활용
<?xml version="1.0" encoding="UTF-8"?>
<Context>
	<Resource
		name="jdbc/tboard"
		auth="Container"
		type="javax.sql.DataSource"
		factory="com.zaxxer.hikari.HikariJNDIFactory"
		uniqueResourceName="MyTboard"
		minimumIdle="5"
		maximumPoolSize="10"
		connectionTimeout="3000"
		idleTimeout="60000"
		maxLifetime="180000"
		jdbcUrl="jdbc:mysql://localhost:3306/m_tboard?serverTimezone=Asia/Seoul"
		driverClassName="com.mysql.cj.jdbc.Driver"
		username="root"
		password="asd123" />
</Context>
  • context.xml 파일은 톰캣 서버에서 JNDI(Java Naming and Directory Interface) 리소스를 설정하는 데 사용된다.
  • 이 파일을 통해 데이터베이스 연결 풀을 정의하고 설정할 수 있다.
설정 항목 설명
<Resource> 태그 JNDI 리소스를 정의. 이 리소스를 통해 데이터베이스 연결을 관리.
name="jdbc/tboard" JNDI 리소스의 이름을 지정. 애플리케이션에서 이 이름으로 데이터베이스 연결을 조회할 수 있다.
auth="Container" 리소스의 인증을 컨테이너에서 관리하도록 설정.
type="javax.sql.DataSource" 리소스의 타입을 지정. 여기서는 javax.sql.DataSource 타입으로 설정하여 데이터베이스 연결 풀을 정의.
factory="com.zaxxer.hikari.HikariJNDIFactory" HikariCP (High-Performance JDBC Connection Pool)에서 제공하는 JNDI 팩토리 클래스를 사용하여 연결 풀을 생성.
minimumIdle="5" maximumPoolSize="10" 연결 풀의 설정. 최소 유휴 연결 수를 5로, 최대 연결 수를 10으로 설정.
idleTimeout="30000" maxLifetime="1800000" 연결의 유휴 타임아웃(30초) 및 최대 생존 시간(30분)을 설정.
dataSourceClassName="com.mysql.cj.jdbc.MysqlDataSource" 사용할 데이터베이스 드라이버 클래스 이름을 지정. 여기서는 MySQL 데이터 소스 클래스를 사용.
dataSource.url, dataSource.user, dataSource.password 데이터베이스 연결 정보. 데이터베이스 URL, 사용자 이름, 비밀번호를 설정.
package com.tenco.tboard.util;

import java.sql.Connection;
import java.sql.SQLException;

import javax.naming.InitialContext;
import javax.sql.DataSource;

public class DBUtil {
	
	private static DataSource dataSource;
	
	static {
		try {
			InitialContext ctx = new InitialContext();
			dataSource = (DataSource)ctx.lookup("java:comp/env/jdbc/tboard");
		} catch (Exception e) {
			System.out.println("DBUtil 초기화 실패 ");
			e.printStackTrace();
		}
	}
	
	public static Connection getConnection() throws SQLException {
		return dataSource.getConnection();
	}
	
}

DBUtil 클래스는 JNDI를 통해 데이터베이스 연결 풀에서 연결을 가져오는 유틸리티 클래스

  • InitialContext ctx = new InitialContext();로 JNDI 초기 컨텍스트를 생성한다.
  • ctx.lookup("java:comp/env/jdbc/tboard");로 context.xml에 정의된 데이터 소스를 찾는다. 여기서 "java:comp/env/jdbc/tboard"은 JNDI 리소스의 이름이다.

6. index.jsp 파일 생성

webapp/index.jsp 파일 생성

http://localhost:8080/t-board/index.jsp 요청 주소 확인
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JSP MVC 게시판</title>
<style type="text/css">
 body{
 	font-family: arial, sans-serif;
 	background-color: #f4f4f4;
 	color: #333;
 	margin: 0;
 	padding: 0;
 }
 
 .nav-list {
 	list-style-type: none;
 	padding: 0;
 }
 
 .nav-list li {
 	margin: 10px;
 	display: inline-block;
 }
 
  .nav-list li a {
 	text-decoration: none;
 	padding: 10px 20px; 
 	color: black;
 }
 
 .btn-primary {
 	background-color: #007bff;
 }
 
 .btn-secondary {
 	background-color: #6c757d;
 }
 
 
</style>
</head>
<body>
	<div class="container">
		<h2>JSP MVC 게시판 테스트 페이지</h2>
		<ul class="nav-list">
			<li class="btn btn-primary"><a href="/t-board/user/signup">회원가입</a></li>
			<li class="btn btn-primary"><a href="/t-board/user/signup">로그인</a></li>
			<li class="btn btn-secondary"><a href="/t-board/user/signup">로그아웃</a></li>
			<li class="btn btn-primary"><a href="/t-board/user/signup">게시판목록</a></li>
		</ul>
	</div>
</body>
</html>

회원 가입 및 로그인 기능 만들기

1. model 설계

User
package com.tenco.tboard.model;

import java.sql.Timestamp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

	private int id; 
	private String username;
	private String password; 
	private String email; 
	private Timestamp createdAt; 
	
}

2. repository 설계

UserRepository
package com.tenco.tboard.repository.interfaces;

import java.util.List;

import com.tenco.tboard.model.User;

public interface UserRepository {
	
	void addUser(User user);
	void deleteUser(int id);
	User getUserByusername(String username);
	User getUserByusernameAndPassword(String username, String password);
	List<User> getAllUsers();
}
UserRepositoryImpl 생성
package com.tenco.tboard.repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import com.tenco.tboard.model.User;
import com.tenco.tboard.repository.interfaces.UserRepository;
import com.tenco.tboard.util.DBUtil;

public class UserRepositoryImpl implements UserRepository {

	private static final String INSERT_USER_SQL = " INSERT INTO users (username, password, email) VALUES(?,?,?) ";
	private static final String DELETE_USER_SQL = " DELETE FROM users WHERE id = ? ";
	private static final String SELETE_USER_BY_USERNAME = " SELECT * FROM users WHERE username = ? ";
	private static final String SELETE_USER_BY_USERNAME_AND_PASSWORD = " SELECT * FROM users WHERE username = ? AND password = ? ";
	private static final String SELETE_ALL_USERS = " SELECT * FROM users ";

	@Override
	public void addUser(User user) {
		try (Connection conn = DBUtil.getConnection()) {
			conn.setAutoCommit(false);
			// username 중복 확인 필요
			// email 중복 확인 필요
			try (PreparedStatement pstmt = conn.prepareStatement(INSERT_USER_SQL)) {
				pstmt.setString(1, user.getUsername());
				pstmt.setString(2, user.getPassword());
				pstmt.setString(3, user.getEmail());
				pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void deleteUser(int id) {
		try (Connection conn = DBUtil.getConnection()) {
			conn.setAutoCommit(false);
			try (PreparedStatement pstmt = conn.prepareStatement(DELETE_USER_SQL)) {
				pstmt.setInt(1, id);
				pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public User getUserByUsername(String username) {
		User user = null;
		try (Connection conn = DBUtil.getConnection();
				PreparedStatement pstmt = conn.prepareStatement(SELETE_USER_BY_USERNAME)) {
			pstmt.setString(1, username);
			ResultSet rs = pstmt.executeQuery();
			if (rs.next()) {
				user = User.builder()
					.id(rs.getInt("id"))
					.username(rs.getString("username"))
					.password(rs.getString("password"))
					.email(rs.getString("email"))
					.createdAt(rs.getTimestamp("created_at"))
					.build();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return user;
	}

	@Override
	public User getUserByUsernameAndPassword(String username, String password) {
		User user = null;
		try (Connection conn = DBUtil.getConnection();
				PreparedStatement pstmt = conn.prepareStatement(SELETE_USER_BY_USERNAME_AND_PASSWORD)) {
			pstmt.setString(1, username);
			pstmt.setString(2, password);
			ResultSet rs = pstmt.executeQuery();
			if (rs.next()) {
				user = User.builder()
					.id(rs.getInt("id"))
					.username(rs.getString("username"))
					.password(rs.getString("password"))
					.email(rs.getString("email"))
					.createdAt(rs.getTimestamp("created_at"))
					.build();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return user;
	}

	@Override
	public List<User> getAllUsers() {
		List<User> userList = new ArrayList<>();
		try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(SELETE_ALL_USERS)) {
			ResultSet rs = pstmt.executeQuery();
			while (rs.next()) {
				User user = User.builder()
					.id(rs.getInt("id"))
					.username(rs.getString("username"))
					.password(rs.getString("password"))
					.email(rs.getString("email"))
					.createdAt(rs.getTimestamp("created_at"))
					.build();
				userList.add(user);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return userList;
	}

}

3. JSP 설계

signup JSP 파일 생성

- WEB-INF 폴더에 대한 이해
- 주소 설계 및 name 속성 반드시 확인
- 테스트 시 기본 값을 넣어 두자.
<%@ 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>
<meta charset="UTF-8">
<title>회원가입</title>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/common.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/signup.css">
</head>
<body>
	<div class="container">
		<h2>회원가입</h2>
		<form action="${pageContext.request.contextPath}/user/signup" method="post">
			<div class="form-group">
				<label for="username">Username : </label>
				<input type="text" name="username" id="username" value="고길동" required>
			</div>
			<div class="form-group">
				<label for="password">Password : </label>
				<input type="password" name="password" id="password" value="asd1234" required>
			</div>
			<div class="form-group">
				<label for="email">Email : </label>
				<input type="email" name="email" id="email" value="a@naver.com" required>
			</div>
			<div class="form-group">
				<input class="btn btn-primary" type="submit" value="회원가입" >
			</div>
		</form>
	</div>
</body>
</html>

WEB-INF 폴더란?

  • WEB-INF 폴더는 클라이언트(웹 브라우저)에서 직접 접근할 수 없다.
  • 서버 설정에 의해 보호되며, 외부에서 직접 요청할 수 없다. 이 폴더 내의 파일들은 서블릿이나 JSP 페이지를 통해서만 접근할 수 있다.

pageContext란?

  • pageContext는 JSP 페이지에서 가장 상위에 있는 내장 객체로, 그 하위에 다양한 범위의 객체들(request, response, session, application 등)에 접근할 수 있는 기능을 포함하고 있음.
  • pageContext 객체는 현재 JSP 페이지에서만 유효하다. 다른 JSP 페이지나 서블릿으로 이동할 때, pageContext의 속성은 자동으로 전달되지 않는다

Page Scope

  • 현재 JSP 페이지 내에서만 유효하다.
  • 다른 JSP 페이지나 서블릿으로 이동하면 유효하지 않는다.
  • 설정 예) pageContext.setAttribute("attrName", "value");
  • 접근 예:) pageContext.getAttribute("attrName");
signin 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>
<meta charset="UTF-8">
<title>로그인</title>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/common.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/signin.css">
</head>
<body>
	<div class="container">
		<h2>로그인</h2>
		<c:if test="${not empty errorMessage}">
			<p style="color: red;">"${errorMessage}"</p>
		</c:if>
		<form action="${pageContext.request.contextPath}/user/signin" method="post">
			<div class="form-group">
				<label for="username">Username : </label>
				<input type="text" name="username" id="username" value="고길동" required>
			</div>
			<div class="form-group">
				<label for="password">Password : </label>
				<input type="password" name="password" id="password" value="asd1234" required>
			</div>
			<div class="form-group">
				<input class="btn btn-primary" type="submit" value="로그인" >
			</div>
		</form>
	</div>
</body>
</html>

4. controller 설계

UserController
package com.tenco.tboard.controller;

import java.io.IOException;
import java.io.PrintWriter;

import com.tenco.tboard.model.User;
import com.tenco.tboard.repository.UserRepositoryImpl;
import com.tenco.tboard.repository.interfaces.UserRepository;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

@WebServlet("/user/*")
public class UserController extends HttpServlet {
	private static final long serialVersionUID = 1L;

	private UserRepository userRepository;

	// UserController --> UserRepository

	public UserController() {
		super();
	}

	@Override
	public void init() throws ServletException {
		userRepository = new UserRepositoryImpl();
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String action = request.getPathInfo();
		switch (action) {
		case "/signup":
			request.getRequestDispatcher("/WEB-INF/views/user/signup.jsp").forward(request, response);
			break;
		case "/signin":
			request.getRequestDispatcher("/WEB-INF/views/user/signin.jsp").forward(request, response);
			break;
		case "/logout":
			handleLogout(request, response);
			break;
		default:
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			break;
		}
	}
	
	/**
	 * 로그아웃 기능 처리
	 * @throws IOException
	 * http://localhost:8080/t-board/user/logout
	 */
	private void handleLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
		HttpSession session = request.getSession();
		session.invalidate();
		response.sendRedirect(request.getContextPath());
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String action = request.getPathInfo();
		switch (action) {
		case "/signup":
			handleSignup(request, response);
			break;
		case "/signin":
			handleSignin(request, response);
			break;
		default:
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			break;
		}
	}

	private void handleSignin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 데이터 추출
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		
		// 데이터 유효성 검사 생략
		User principal = userRepository.getUserByUsernameAndPassword(username, password);
		if (principal != null && principal.getPassword().equals(password)) {
			request.getSession().setAttribute("user", principal);
			response.sendRedirect(request.getContextPath() + "/board/list");
		} else {
			request.setAttribute("errorMessage", "잘못된 요청입니다.");
			request.getRequestDispatcher("/WEB-INF/views/user/signin.jsp").forward(request, response);
		}
	}

	private void handleSignup(HttpServletRequest request, HttpServletResponse response) throws IOException {
		// 데이터 추출
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		String email = request.getParameter("email");

		// 데이터 유효성 검사 생략

		User user = User.builder().username(username).password(password).email(email).build();

		int result = userRepository.addUser(user);

		if (result != 0) {
			response.sendRedirect(request.getContextPath() + "/user/signin");
		} else {
			response.setContentType("text/html; charset=UTF-8");
			PrintWriter out = response.getWriter();
			out.println("<script> alert('잘못된 요청입니다'); history.back();  </script>");
		}
	}

}

게시판 기능 만들어 보기

1. model 설계

Board
package com.tenco.tboard.model;

import java.sql.Timestamp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Board {
	
	private int id;
	private int userId;
	private String title;
	private String content;
	private Timestamp createdAt;
	
}

2. repository 설계

BoardRepository
package com.tenco.tboard.repository.interfaces;

import java.util.List;

import com.tenco.tboard.model.Board;

public interface BoardRepository {
	
	void addBoard(Board board);
	void updateBoard(Board board, int principalId);
	void deleteBoard(int id, int principalId);
	Board getBoardById(int id);
	List<Board> getAllBoards(int limit, int offset);
	int getTotalBoardCount();
	
}
BoardRepositoryImpl
package com.tenco.tboard.repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import com.tenco.tboard.model.Board;
import com.tenco.tboard.repository.interfaces.BoardRepository;
import com.tenco.tboard.util.DBUtil;

public class BoardRepositoryImpl implements BoardRepository {

	private final String SELECT_ALL_BOARDS = " SELECT * FROM board ORDER BY created_at DESC limit ? offset ? ";
	private final String COUNT_ALL_BOARDS = " SELECT count(*) AS count FROM board ";
	private final String INSERT_BOARD_SQL = " INSERT INTO board(user_id, title, content) VALUES (?,?,?) ";
	private final String DELETE_BOARD_SQL = " DELETE FROM board WHERE id = ? ";
	private final String SELECT_BOARD_BY_ID = " SELECT * FROM board WHERE id = ? ";
	private final String UPDATE_BOARD_SQL = " UPDATE board SET title = ?, content = ? WHERE id = ? ";

	@Override
	public void addBoard(Board board) {
		try (Connection conn = DBUtil.getConnection()) {
			conn.setAutoCommit(false);
			try (PreparedStatement pstmt = conn.prepareStatement(INSERT_BOARD_SQL)) {
				pstmt.setInt(1, board.getUserId());
				pstmt.setString(2, board.getTitle());
				pstmt.setString(3, board.getContent());
				pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void updateBoard(Board board) {
		try (Connection conn = DBUtil.getConnection()) {
			conn.setAutoCommit(false);
			try (PreparedStatement pstmt = conn.prepareStatement(UPDATE_BOARD_SQL)) {
				pstmt.setString(1, board.getTitle());
				pstmt.setString(2, board.getContent());
				pstmt.setInt(3, board.getId());
				pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void deleteBoard(int id) {
		try (Connection conn = DBUtil.getConnection()) {
			conn.setAutoCommit(false);
			try (PreparedStatement pstmt = conn.prepareStatement(DELETE_BOARD_SQL)) {
				pstmt.setInt(1, id);
				pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public Board getBoardById(int id) {
		Board board = null;

		try (Connection conn = DBUtil.getConnection();//
				PreparedStatement pstmt = conn.prepareStatement(SELECT_BOARD_BY_ID)) {
			pstmt.setInt(1, id);
			try (ResultSet rs = pstmt.executeQuery()) {
				if (rs.next()) {
					board = Board.builder()
						.id(rs.getInt("id"))
						.userId(rs.getInt("user_id"))
						.title(rs.getString("title"))
						.content(rs.getString("content"))
						.createdAt(rs.getTimestamp("created_at"))
						.build();
				}
			} catch (Exception e) {
				e.printStackTrace();
			}

		} catch (Exception e) {
			e.printStackTrace();
		}

		return board;
	}

	@Override
	public List<Board> getAllBoards(int limit, int offset) {
		List<Board> boardList = new ArrayList<>();

		try (Connection conn = DBUtil.getConnection();//
				PreparedStatement pstmt = conn.prepareStatement(SELECT_ALL_BOARDS)) {
			pstmt.setInt(1, limit);
			pstmt.setInt(2, offset);
			ResultSet rs = pstmt.executeQuery();

			while (rs.next()) {
				boardList.add(Board.builder()
					.id(rs.getInt("id"))
					.userId(rs.getInt("user_id"))
					.title(rs.getString("title"))
					.content(rs.getString("content"))
					.createdAt(rs.getTimestamp("created_at"))
					.build());
			}
			System.out.println("BoardRepositoryImpl - 로깅 : count " + boardList.size());

		} catch (Exception e) {
			e.printStackTrace();
		}

		return boardList;
	}

	@Override
	public int getTotalBoardCount() {
		int totalBoards = 0;

		try (Connection conn = DBUtil.getConnection();//
				PreparedStatement pstmt = conn.prepareStatement(COUNT_ALL_BOARDS)) {
			ResultSet rs = pstmt.executeQuery();
			if (rs.next()) {
				totalBoards = rs.getInt("count");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

		return totalBoards;
	}

}

3. JSP 설계

list.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>
<meta charset="UTF-8">
<title>게시글 목록</title>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/common.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/list.css">
</head>
<body>
    <h2>게시글 목록</h2>
    <div class="action">
        <a href="${pageContext.request.contextPath}/board/create">새글 작성하기</a>
        <a href="${pageContext.request.contextPath}">홈 화면</a>
    </div>

	<c:forEach var="board" items="${boardList}">
    <div class="board-item">
		<h3><a href="#">${board.title}</a></h3>
		<p>${board.content}</p>
		<!-- 게시글의 작성자가 세션 유저와 동일하다면 수정, 삭제 버튼을 보여주자 -->
        <a class="btn btn-edit" href="#">수정</a>
        <a class="btn btn-delete" href="#">삭제</a>
    </div>
    </c:forEach>
    <br>
    <div class="pagination">
    	
		<c:forEach var="i" begin="1" end="${totalPages}">
			<span><a href="${pageContext.request.contextPath}/board/list?page=${i}">${i}</a></span><br>
		</c:forEach>
    </div>
</body>
</html>
create.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>
<meta charset="UTF-8">
<title>새 글 작성하기</title>
	<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/create.css">
</head>
<body>
	<div class="container">
		<h2>새글 작성하기</h2>
		<form action="${pageContext.request.contextPath}/board/create" method="post">
			<div class="form-group">
				<label for="title">제목</label>
				<input type="text" name="title" id="title" value="코딩 테스트 연습">
			</div>
			<div class="form-group">
				<label for="content">내용</label>
				<textarea rows="30" cols="50" name="content" id="content">코딩 테스트 연습
				</textarea>
			</div>
			<div class="form-group">
				<input type="submit" value="작성하기" class="btn btn-submit">
				<a href="${pageContext.request.contextPath}/board/list?page=1" class="btn btn-back">목록으로 돌아가기</a>
			</div>
		</form>
	</div>
</body>
</html>
view.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시글 상세 보기 화면</title>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/view.css">
</head>
<body>
	<h2>상세 보기 화면</h2>
	<div class="container">
		<div class="board-content">
			<h2>
				<c:out value="${board.title}" />
			</h2>
			<p>
				<c:out value="${board.content}" />
			</p>
			<p>
				<fmt:formatDate value="${board.createdAt}" pattern="yyyy-MM-dd HH:mm" />
			</p>
		</div>
		<div class="actions">
			<c:if test="${board.userId == userId }">
				<a href="${pageContext.request.contextPath}/board/edit?id=${board.id}" class="btn btn-edit">수정</a>
				<a href="${pageContext.request.contextPath}/board/delete?id=${board.id}" class="btn btn-delete">삭제</a>
			</c:if>
			<a href="${pageContext.request.contextPath}/board/list?page=1" class="btn btn-back">목록으로 돌아가기</a>
		</div>

		<h3>댓글</h3>
		<!-- 댓글 리스트 작성 -->
		<div class="comment-item">
			<c:forEach var="comment" items="${commentList}">
				<p>
					<strong>${comment.username}</strong>
				</p>
				<p>${comment.content}</p>
				<p class="comment-date">
					<fmt:formatDate value="${comment.createdAt}" pattern="yyyy-MM-dd HH:mm" />
				</p>
			</c:forEach>
		</div>
		<!-- 댓글 작성 폼 -->
		<div class="comment-form">
			<form action="${pageContext.request.contextPath}/board/addComment" method="post">
				<textarea name="content" rows="5" placeholder="댓글을 작성해 주세요..."></textarea>
				<input type="hidden" name="boardId" value="${board.id}">
				<button type="submit" class="btn btn-submit">댓글 작성</button>
			</form>
		</div>
	</div>
</body>
</html>

4. controller 설계

package com.tenco.tboard.controller;

import java.io.IOException;
import java.util.List;

import com.tenco.tboard.model.Board;
import com.tenco.tboard.model.Comment;
import com.tenco.tboard.model.User;
import com.tenco.tboard.repository.BoardRepositoryImpl;
import com.tenco.tboard.repository.CommentRepositoryImpl;
import com.tenco.tboard.repository.interfaces.BoardRepository;
import com.tenco.tboard.repository.interfaces.CommentRepository;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

@WebServlet("/board/*")
public class BoardController extends HttpServlet {

	private static final long serialVersionUID = 1L;
	private BoardRepository boardRepository;
	private CommentRepository commentRepository;

	public BoardController() {
		super();
	}

	@Override
	public void init() throws ServletException {
		boardRepository = new BoardRepositoryImpl();
		commentRepository = new CommentRepositoryImpl();
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String action = request.getPathInfo();
		HttpSession session = request.getSession(false);
		if (session == null || session.getAttribute("principal") == null) {
			response.sendRedirect(request.getContextPath() + "/user/signin");
			return;
		}
		switch (action) {
		case "/delete":
			handleDeleteBoard(request, response, session);
			break;
		case "/update":
			showEditBoardForm(request, response, session);
			break;
		case "/create":
			showCreateBoardForm(request, response, session);
			break;
		case "/list":
			handleListBoards(request, response, session);
			break;
		case "/view":
			showViewBoard(request, response, session);
			break;
		case "/deleteComment":
			handleDeleteComment(request, response, session);
			break;

		default:
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			break;
		}
	}

	/**
	 * TODO 댓글 삭제 기능
	 * @param request
	 * @param response
	 * @param session
	 */
	private void handleDeleteComment(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
		// TODO Auto-generated method stub
		
	}

	/**
	 * TODO 상세 보기 화면 이동
	 * @param request
	 * @param response
	 * @param session
	 */
	private void showViewBoard(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
		try {
			int boardId = Integer.parseInt(request.getParameter("id"));
			Board board = boardRepository.getBoardById(boardId);
			if (board == null) {
				response.sendError(HttpServletResponse.SC_NOT_FOUND);
			}
			
			// 현재 로그인한 사용자의 ID
			User user = (User) session.getAttribute("principal");
			if (user != null) {
				request.setAttribute("userId", user.getId());
			} else {
				response.sendRedirect(request.getContextPath() + "/user/signin");
			}
			
			List<Comment> commentList = commentRepository.getCommentsByBoardId(boardId);
			
			request.setAttribute("board", board);
			request.setAttribute("commentList", commentList);
			request.getRequestDispatcher("/WEB-INF/views/board/view.jsp").forward(request, response);
			
			
		} catch (Exception e) {
			// TODO 스크립트 백
		}
	}

	/**
	 * TODO 수정 폼 화면 이동 (인증 검사 반드시 처리)
	 * @param request
	 * @param response
	 * @param session
	 */
	private void showEditBoardForm(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
	}
	
	/**
	 * TODO 게시글 삭제 기능 만들기
	 * @param request
	 * @param response
	 * @param session
	 */
	private void handleDeleteBoard(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
		// TODO Auto-generated method stub
		
	}

	/**
	 * 게시글 생성 화면 이동
	 * 
	 * @param request
	 * @param response
	 * @param session
	 * @throws IOException
	 * @throws ServletException
	 */
	private void showCreateBoardForm(HttpServletRequest request, HttpServletResponse response, HttpSession session)
			throws ServletException, IOException {
		request.getRequestDispatcher("/WEB-INF/views/board/create.jsp").forward(request, response);
	}

	/**
	 * 페이징 처리 하기
	 * 
	 * @param request
	 * @param response
	 * @throws IOException
	 * @throws ServletException
	 */
	private void handleListBoards(HttpServletRequest request, HttpServletResponse response, HttpSession session)
			throws ServletException, IOException {

		// 게시글 목록 조회 기능
		int page = 1; // 기본 페이지 번호
		int pageSize = 3; // 한 페이지당 보여질 게시글 수

		try {
			String pageStr = request.getParameter("page");
			if (pageStr != null) {
				page = Integer.parseInt(pageStr);
			}
		} catch (Exception e) {
			// 유효하지 않은 번호를 마음대로 보낼 경우
			page = 1;
		}
		int offset = (page - 1) * pageSize; // 시작 위치 계산 (offset 값 계산)

		List<Board> boardList = boardRepository.getAllBoards(pageSize, offset);

		// 전체 게시글 수 조회
		int totalBoards = boardRepository.getTotalBoardCount();

		// 총 페이지 수 계산
		int totalPages = (int) Math.ceil((double) totalBoards / pageSize);

		request.setAttribute("totalPages", totalPages);
		request.setAttribute("boardList", boardList);
		request.setAttribute("currentPage", page);

		// 현재 로그인한 사용자 ID 설정
		if (session != null) {
			User user = (User) session.getAttribute("principal");
			if (user != null) {
				request.setAttribute("userId", user.getId());
			}
		}

		request.getRequestDispatcher("/WEB-INF/views/board/list.jsp").forward(request, response);
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String action = request.getPathInfo();
		HttpSession session = request.getSession(false);
		if (session == null || session.getAttribute("principal") == null) {
			response.sendRedirect(request.getContextPath() + "/user/signin");
			return;
		}
		switch (action) {
		case "/create":
			handleCreateBoard(request, response, session);
			break;
		case "/edit":
			break;
		case "/addComment":
			handleCreateComment(request, response, session);
			break;

		default:
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			break;
		}
	}

	/**
	 * 댓글 생성 처리
	 * @param request
	 * @param response
	 * @param session
	 * @throws IOException 
	 */
	private void handleCreateComment(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
		// 유효성 검사 생략
		commentRepository.addComment(Comment.builder()
				.boardId(Integer.parseInt(request.getParameter("boardId")))
				.userId(((User)session.getAttribute("principal")).getId())
				.content(request.getParameter("content"))
				.build()
				);
		response.sendRedirect(request.getContextPath() + "/board/view?id=" + request.getParameter("boardId"));
	}

	/**
	 * 게시글 생성 처리
	 * 
	 * @param request
	 * @param response
	 * @param session
	 * @throws IOException 
	 */
	private void handleCreateBoard(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
		// 유효성 검사 생략
		boardRepository.addBoard(Board.builder()
				.title(request.getParameter("title"))
				.content(request.getParameter("content"))
				.userId(((User) session.getAttribute("principal")).getId())
				.build());
		response.sendRedirect(request.getContextPath() + "/board/list?page=1");
	}

}

댓글 설계

1. model 설계

Comment
package com.tenco.tboard.model;

import java.sql.Timestamp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Comment {
	
	private int id; 
	private int boardId; 
	private int userId; 
	private String content; 
	private Timestamp createdAt;
	private String username;
	
}

2. repository 설계

CommentRepository
package com.tenco.tboard.repository.interfaces;

import java.util.List;

import com.tenco.tboard.model.Comment;

public interface CommentRepository {
	
	void addComment(Comment comment);
	void deleteComment(int id);
	Comment getCommentById(int id);
	List<Comment> getCommentsByBoardId(int boardId);
}
CommentRepositoryImpl
package com.tenco.tboard.repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import com.tenco.tboard.model.Comment;
import com.tenco.tboard.repository.interfaces.CommentRepository;
import com.tenco.tboard.util.DBUtil;

public class CommentRepositoryImpl implements CommentRepository {

	private final String INSERT_COMMENT_SQL = " INSERT INTO comments (board_id, user_id, content) VALUES (?,?,?) ";
	private final String DELETE_COMMENT_SQL = " DELETE FROM comments WHERE id = ? ";
	private final String SELECT_COMMENT_BY_ID = " SELECT * FROM comments WHERE id = ? ";
	private final String SELECT_COMMENT_BY_BOARD_ID = //
			" SELECT c.*, u.username "//
					+ " FROM comments c "//
					+ " JOIN users u ON c.user_id = u.id "//
					+ " WHERE board_id = ? "//
					+ " ORDER BY created_at DESC ";

	@Override
	public void addComment(Comment comment) {

		try (Connection conn = DBUtil.getConnection()) {
			conn.setAutoCommit(false);
			try (PreparedStatement pstmt = conn.prepareStatement(INSERT_COMMENT_SQL)) {
				pstmt.setInt(1, comment.getBoardId());
				pstmt.setInt(2, comment.getUserId());
				pstmt.setString(3, comment.getContent());
				pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	@Override
	public void deleteComment(int id) {

		try (Connection conn = DBUtil.getConnection()) {
			conn.setAutoCommit(false);
			try (PreparedStatement pstmt = conn.prepareStatement(DELETE_COMMENT_SQL)) {
				pstmt.setInt(1, id);
				pstmt.executeUpdate();
				conn.commit();
			} catch (Exception e) {
				conn.rollback();
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	@Override
	public Comment getCommentById(int id) {
		Comment comment = null;

		try (Connection conn = DBUtil.getConnection();//
				PreparedStatement pstmt = conn.prepareStatement(SELECT_COMMENT_BY_ID)) {
			pstmt.setInt(1, id);
			try (ResultSet rs = pstmt.executeQuery()) {
				if (rs.next()) {
					comment = Comment.builder()
						.id(rs.getInt("id"))
						.boardId(rs.getInt("board_id"))
						.userId(rs.getInt("user_id"))
						.content(rs.getString("content"))
						.createdAt(rs.getTimestamp("created_at"))
						.build();
				}
			} catch (Exception e) {
				e.printStackTrace();
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
		return comment;
	}

	@Override
	public List<Comment> getCommentsByBoardId(int boardId) {
		List<Comment> commentList = new ArrayList<>();

		try (Connection conn = DBUtil.getConnection();//
				PreparedStatement pstmt = conn.prepareStatement(SELECT_COMMENT_BY_BOARD_ID)) {
			pstmt.setInt(1, boardId);
			try (ResultSet rs = pstmt.executeQuery()) {
				while (rs.next()) {
					Comment comment = Comment.builder()
						.id(rs.getInt("id"))
						.boardId(rs.getInt("board_id"))
						.userId(rs.getInt("user_id"))
						.content(rs.getString("content"))
						.createdAt(rs.getTimestamp("created_at"))
						.username(rs.getString("username"))
						.build();
					commentList.add(comment);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
		return commentList;
	}

}
728x90
반응형
SMALL