📌 커넥션풀을 하기 위해서 HikariCP를 주로 사용한다.
config.setConnectionTimeout(1000*19);
//해당시간이 지나면 끊고 다른 커넥션 선택
config.setMaximumPoolSize(100);
//톰켓(WAS)의 쓰레드 개수 (최대 동접자)
config.setMinimumIdle(1);
//처음부터 연결을 많이하면 비용문제가 생길수 있다.
👉 SQL에서 페이징은 데이터를 쪼개서 여러 페이지에 걸쳐 보여주는 기법으로, 대량의 데이터를 한 번에 가져오는 대신, 페이지 단위로 나누어 가져오는것.
👉 DB에서 적은양을 처리하면 네트워크 속도가 향상 및 JAVA메모리가 낮아서 서버 처리효율성이 좋아진다.
👉 LIMIT는 SQL에서 페이징 처리기법으로, 테이블에 저장된 데이터의 일부만 output할 수 있어서 출력속도가 월등하다.
select * from tbl_todo order by tno desc limit 0,10;
-- 1번부터 데이터가 있을때, 1번부터 10번까지 추출하기
select * from tbl_todo order by tno desc limit 10,10;
-- tno컬럼을 내림차순으로 10번부터 10개를 추출하기
📌 limit의 첫번째 구간은 skip과 같은 느낌인데, 해당 부분엔 식(5*2)를 넣을수 없고, 값 밖에 넣을수 없다.
📌 회사의 로직이 변경되면 기존에 내용은 변경되지 않더라도 필요한 로직만 변경할 수 있도록 비지니스 계층과 영속계층(DB)를 별도로 생성한다.
👉 이는 재사용성과 관련이 깊다.

📌 수평적 분할 : 각 사람마다 백엔드와 프론트엔드를 나누어 개발을 하기에 회사에서는 효율성이 떨어져 수평적 분할을 선호하지않는다.
📌 수직적 분할 : 프론트와 백엔드를 구분하여 개발한다.
package org.example.w2.common;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
public class Pageinfo {
private int page; //current page
private int start; //시작페이지
private int end; //마지막페이지
private boolean prev; //이전페이지가 있을지
private boolean next; //다음페이지가 있을지
public Pageinfo(int page, int size, int total){
this.page = page <= 0 ? 1 : page; //현재페이지가 0이면 안되니까, url에서 이상한 행동을 하는걸 막도록
end = (int)(Math.ceil(this.page/10.0) * 10);
start = end - 9; //시작페이지는 마지막-9
prev = start == 1 ? false : true; // 시작페이지가 1이면 이전페이지가 false
if(end*size < total) { //전체 데이터가 마지막페이지 * 페이지당 게시물 개수보다 크면
next = true; // 정상
}
else{
next = false;
end = (int)(Math.ceil(total/(double)size));
//만약 데이터가 131개가 있는데 13페이지까지 밖에 없으면 안되니까, 14페이지를 만들기 위해
}
}
}
👆 해당코드를 분석해보자
👉 위 클래스는 게시판의 페이지가 10개씩표출될때 다음페이지로 넘기기 위해 버튼으로 만들어놓은 코드.
👉 매개변수로 현재page, 한페이지당 몇개의 게시물을 표출하는 size, 전체 데이터의 개수 total을 받아 prev, next등이 계산된다.
@WebServlet(value = "/todo/list") //목록페이지
@Log4j2
public class TodoListController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String pageStr = req.getParameter("page"); //url주소창에 example.com/todo/list?page=21 에 해당하는 21의 값을 가져옴
log.info("pageStr: " + pageStr);
int page = StringUtil.getInt(pageStr, 1);
//페이지 이상하면 1페이로 넘어가기
try {
int total = TodoDAO.INSTANCE.getTotal();
//예외처리를 해줘야하는데 던지면 오버라이딩 상속에 문제가 생김 그래서 일단 try-catch
Pageinfo pageInfo = new Pageinfo(page, 10, total);
//현재 / 게시물개수 / 데이터개수
List<TodoVO> todoList = TodoDAO.INSTANCE.list(pageInfo.getPage());
//해당페이지를 주면 todoList의 값을 준다.
req.setAttribute("todolist", todoList);
req.setAttribute("pageInfo", pageInfo);
//더미데이터를 jsp로 옮기기
req.getRequestDispatcher("/WEB-INF/todo/list.jsp").forward(req, resp); //view
//list.jsp로 출발
}catch (Exception e) {
e.printStackTrace();
}
}
}
📌 코드의 작성순서는 해당 클래스의 모든 코드를 작성하는것이 아니지만 코드의 해석을 위주로 할것임.
👉 컨트롤러에서 jsp로 setAttribute메소드를 통해서 데이터가 이동되는데, 더미데이터를 통해 (Pageinfo pageInfo = new Pageinfo(1,10,130); / req.setAttribute("pageInfo",pageinfo); list.jsp에 데이터가 넘어가는지 확인을하는것이 중요하다.
👉 기존의 override를 통해 상속에 대한 예외를 던졌으나, 이후 추가적인 코드를 작성함에 따라 예외에 문제가 생겨 던질수없는 상황이 발발하여, 해당 클래스에서 try-catch를 사용하였다. 추가적으로 더 좋은 방법 모색이 필요함.
int total = TodoDAO.INSTANCE.getTotal();
👉 해당코드는 TodoDAO의 Enum에서 생성된 메소드인 getTotal을 사용하기 위해 변수를 생성했는데, 이 메소드는 데이터베이스에서 전체레코드의 개수를 가져오기 위함.
List<TodoVO> todoList = TodoDAO.INSTANCE.list(pageInfo.getPage());
👉 해당코드는 TodoVO라는 db table과 같은 구조를 가진 인스턴스로 구성되어있는 리스트를 해당하는 페이지에 대한 내용을 DAO -> 컨트롤러에서 가져오는 코드를 이후 setAttribute를 통해서 jsp로 이동된다.
package org.example.w2.common;
public class StringUtil {
public static int getInt(String str, int defaultValue){
if(str == null || str.length() == 0) { //URL에서 가져온 pageStr의 값은 문자열 '21' 이므로 정수로 변경해야함.
return defaultValue;
}
try {
return Integer.parseInt(str);
}catch (Exception e) {
return defaultValue;
}
}
}
👉 해당코드는 Controller 코드와 연관이 있는데, 페이지를 선택하게 되면 url에도
?page=1 과 같이 표출이되는데, 이부분의 데이터를 가져오는것을 이용한다.
String pageStr = req.getParameter("page");
int page = StringUtil.getInt(pageStr, 1);
👉 getParameter를 통해서 page의 파라미터에 해당하는 값을 추출하는데,
사용자가 12페이지를 보고있다면 example.com/todo/list?page=12 라는 url이 나오게된다.
그때 pageStr값은 문자열 '12'이므로. StringUtil의 클래스를 통해서 null 혹은 0 값이라면 default값인 1로 반환하고, 그것이 아니라면 int값으로 반환하게 된다.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@include file="../includes/header.jsp"%>
<h1>todo list page</h1>
<ul>
<c:forEach items = "${todolist}" var="todo">
<li>
<div>
<div>${todo.tno}</div>
<div>${todo.title}</div>
</div>
</li>
</c:forEach>
</ul>
<ul class="pagination">
<c:if test="${pageInfo.prev}"> <!-- 이전페이지 --> <!-- 현재 11에서 20페이지가 보인다고할때 start=11-->
<li class="page-item"><a class="page-link" href="/todo/list?page=${pageInfo.start-1}">Previous</a></li>
</c:if>
<c:forEach begin="${pageInfo.start}" end="${pageInfo.end}" var="num">
<li class="page-item ${pageInfo.page == num ? 'active':''}"><a class="page-link" href="/todo/list?page=${num}">${num}</a></li>
</c:forEach> <!-- 현재 page와 num이 같으면 파란색으로 변경-->
<c:if test="${pageInfo.next}"> <!-- 다음페이지 --> <!-- 현재 1에서 10페이지가 보인다고할때 end=10-->
<li class="page-item"><a class="page-link" href="/todo/list?page=${pageInfo.end+1}">Next</a></li>
</c:if>
</ul>
<%@include file="../includes/footer.jsp"%>
👉 JSTL 문법을 사용한 forEach문을 이용하여 items인 todolist의 객체를 활용하고, todo라는 변수(i)를 반복해서 사용한다.
todo.tno를 하게 되면 todolist에 있는 객체의 tno값을 출력하게 된다.
👉 부트스트랩을 이용하여 ul태그의 class를 pagination을 사용하고
3개의 단락으로 나누었다.
👉 prev 버튼 (11번째 페이지를 선택했을때 1~10)페이지를 누르기위함이고, pageInfo의 클래스에서 참 일경우 if문이 참이 되어 아래 li태그가 실행된다.
👉 num 버튼 페이지는 forEach의 시작점인 begin과 마지막인 end에 각 start,end를 넣고 반복되는 num변수를 생성하여 버튼을 눌렀을때 해당 url로 이동하도록 구성하였다.
👉 next 버튼도 prev와 반대되도록 구성하였다.
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class TodoVO { //db의 레코드 한줄한줄은 todovo의 인스턴스이다. 테이블 구조가 같으니까.
private int tno;
private String title;
private String writer;
private Timestamp regDate;
private Timestamp modDate;
private boolean delFlag;
}
👉 tbl_table과 같은 구성을하여 db에 있는 값을 그대로 가져와 list로 만들어서 한줄씩 출력하기위해 구성에 필요한 annotaion을 사용하였다.
@Log4j2
public enum TodoDAO {
INSTANCE;
//페이지의 total count값을 가져와야해서 db와 연결해줘야함
public List<TodoVO> list(int page) throws Exception{ //arraylist는 클래스 list 더 추상적이니까 사용함
int skip = (page -1) *10; //db에서의 limit
String query = """
select * from tbl_todo
where
tno > 0
and
delflag = false
order by tno desc
limit ?,10
""";
//limit 의 첫번째를 변수처리하기위해서 db안에서는 limit값에 값밖에 안들어가니까.
//1페이지는 1번부터 10번까지 있으므로 limit 0 10
//2페이지는 11번부터 20번까지 있으므로 limit 10 10
//그러면 limit (page-1)*10 10
@Cleanup Connection con = ConnectionUtil.INSTANCE.getDs().getConnection(); //connection pool을 사용하기 위함.
@Cleanup PreparedStatement ps = con.prepareStatement(query);
ps.setInt(1, skip); //첫번째 물음표의 값은 skip이야.
@Cleanup ResultSet rs = ps.executeQuery();
List<TodoVO> list = new ArrayList<>(); //데이터타입이 List<TodoVO> 인 ArrayList 생성
while (rs.next()) { //re.next() 레코드 한줄씩 넘기기
TodoVO vo = TodoVO.builder()
.tno(rs.getInt("tno")) //tno에다가 tno 컬럼값을 읽어서 넣자
.title(rs.getString("title"))
.writer(rs.getString("writer"))
.regDate(rs.getTimestamp("regdate"))
.modDate(rs.getTimestamp("moddate"))
.delFlag(rs.getBoolean("delflag"))
.build();
list.add(vo); //list에 한줄씩 추가하기
}
return list;
}
public int getTotal() throws Exception {
log.info("GetTotal");
String query = "select count(tno) from tbl_todo where tno > 0 and delflag = false";
@Cleanup Connection con = ConnectionUtil.INSTANCE.getDs().getConnection();
@Cleanup PreparedStatement ps = con.prepareStatement(query);
@Cleanup ResultSet rs = ps.executeQuery();
rs.next(); //앞에 쓸대없는 정보는 읽지않고 한개 넘기기 tno count 85016 읽기위해서
//rs의 count라는 한개의 행이 필요한데, 첫번째는 버리고 두번째에 그 카운트의 개수가 적혀있음.
int total = rs.getInt(1);
//getInt는 ResultSet에서 제공하는 메소드로 해당되는 인덱스의 값을 정수로 가져온다.
return total;
}
}
package org.example.w2.common;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
public enum ConnectionUtil {
INSTANCE;
//결과적으로 만들어내야하는것
private HikariDataSource ds;
//Connection pull 하기위함.
private ConnectionUtil() {
HikariConfig config = new HikariConfig();
config.setDriverClassName("org.mariadb.jdbc.Driver");
config.setJdbcUrl("jdbc:mariadb://localhost:13306/webdb");
config.setUsername("webdbuser");
config.setPassword("webdbuser");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
config.setConnectionTimeout(1000*19); //해당시간이 지나면 끊고 다른 커넥션 선택
config.setMaximumPoolSize(20); //톰켓(WAS)의 쓰레드 개수 (최대 동접자)
config.setMinimumIdle(1); //처음부터 연결을 많이하면 비용문제가 생길수 있다.
ds = new HikariDataSource(config);
}
//필요할때마다 가져갈수있도록
public HikariDataSource getDs(){
return ds;
}
}
👉 커넥션 풀을 이용하기 위해 HikariCP를 사용한다.