MVC는 웹 애플리케이션을 만들 때 코드를 역할별로 나누는 방식이다.
여기서 역할은Model,View,Controller세 가지이다.
처음 보면 이름이 어렵고 복잡해 보일 수 있다.
하지만 핵심은 아주 단순하다.
서로 성격이 다른 일을 한 파일에 섞지 않기 위해 나누는 것이다.
웹 프로그램은 보통 한 가지 일만 하지 않는다.
사용자에게 화면도 보여 줘야 하고, 사용자가 입력한 값도 받아야 하고, 그 값을 가지고 계산하거나 검사도 해야 한다.
경우에 따라서는DB에 저장하거나,DB에서 다시 꺼내 오는 일도 필요하다.
즉, 화면을 보여 주는 일과 실제 처리를 하는 일이 함께 들어 있다.
문제는 이걸 한 파일 안에 전부 몰아넣으면 코드가 금방 복잡해진다는 점이다.
예를 들어 사용자가 점수를 입력하면 결과 화면을 보여 주는 기능이 있다고 하자.
처음에는 한 파일 안에서 입력도 받고, 점수도 계산하고, 결과도 바로 출력하면 간단해 보일 수 있다.
그런데 나중에 조건이 늘어나면 상황이 달라진다.
90점 이상이면A,80점 이상이면B처럼 분기해야 하고, 입력값이 비어 있으면 다시 입력 화면으로 보내야 한다.
결과 화면 문구도 바뀔 수 있다.
이렇게 되면 한 파일 안에서 입력 처리, 조건 판단, 화면 출력이 전부 섞이기 시작한다.
즉, 문제의 핵심은 코드가 길어지는 것 자체가 아니라, 서로 다른 성격의 코드가 한곳에 섞인다는 점이다.
이렇게 섞여 있으면 수정할 때도 불편하다.
화면 문구만 바꾸고 싶은데 처리 코드까지 같이 건드리게 될 수 있다.
반대로 처리 방식만 바꾸고 싶은데 화면 출력 코드까지 함께 봐야 할 수도 있다.
그러면 작은 수정도 점점 부담이 커진다.
그래서MVC는 처음부터 역할을 나누어 둔다.
보여 주는 일은 보여 주는 쪽에 맡긴다.
요청 흐름을 잡는 일은 흐름을 조정하는 쪽에 맡긴다.
실제 계산이나 데이터 처리는 처리 쪽에 맡긴다.
쉽게 말하면 일의 종류가 다르면 담당자도 나누는 것이라고 생각하면 된다.
이렇게 나누어 두면 나중에 기능이 커져도 어떤 파일이 무슨 역할을 하는지 훨씬 또렷하게 보인다.
Servlet은Controller역할을 맡아 요청 흐름을 잡는다.
JSP는View역할을 맡아 결과 화면을 보여 준다.
그리고 안쪽으로Service,VO,DAO,DB같은 구조가 더 이어질 수 있다.
Service는 처리 규칙이나 비즈니스 로직을 따로 분리할 때 사용하는 중간 계층이다.
이번 글에서는Service를 깊게 다루지 않고,Servlet,DTO,DAO,DB흐름을 중심으로 보면 된다.
처음에는 전체 구조를 한 번에 외우기보다,Servlet이 흐름을 잡고JSP가 결과를 보여 준다는 큰 틀부터 잡는 것이 중요하다.
왜 굳이 이렇게까지 나누어야 하는가
가장 먼저 드는 생각은 보통 이것이다.
그냥 한 군데에 다 쓰면 더 편한 거 아닌가 하는 생각이다.
처음에는 맞는 말처럼 들릴 수 있다.
기능이 아주 작을 때는 한 파일에 몰아넣는 게 더 쉬워 보인다.
하지만 웹 프로그램은 보통 계속 기능이 추가된다.
처음에는 입력과 출력만 있던 기능이, 나중에는 조건 검사도 들어가고, 예외 처리도 들어가고, 데이터 저장도 들어간다.
그 순간부터 한 파일에 모든 걸 몰아넣은 구조는 빠르게 복잡해진다.
예를 들어 결과 화면 색만 바꾸고 싶다고 해 보자.
그런데 화면 코드와 처리 코드가 한데 섞여 있으면, 화면만 수정하려다가 처리 로직이 있는 부분까지 같이 건드리게 될 수 있다.
반대로 계산 기준만 바꾸고 싶은데, 결과를 출력하는 코드와 섞여 있으면 화면 부분까지 같이 봐야 한다.
즉, 수정하고 싶은 곳이 딱 하나여도 실제로는 여러 부분을 함께 건드려야 하는 상황이 생긴다.
이게 가장 불편한 점이다.
MVC는 이런 문제를 줄인다.
어디를 고쳐야 하는지가 훨씬 분명해진다.
화면을 바꾸고 싶으면View쪽을 보면 된다.
요청 흐름을 바꾸고 싶으면Controller를 보면 된다.
실제 처리나 데이터 방식을 바꾸고 싶으면Model쪽을 보면 된다.
즉, 코드를 역할별로 나누어 두면 수정 범위도 같이 정리된다.
그래서MVC는 어렵게 만들기 위한 구조가 아니다.
오히려 반대이다.
나중에 더 쉽게 수정하고, 더 쉽게 관리하고, 더 쉽게 확장하려고 미리 정리해 두는 구조이다.
처음에는 파일이 나뉘어 있어서 복잡해 보일 수 있다.
하지만 조금만 기능이 늘어나면, 오히려 나누어 둔 쪽이 훨씬 덜 헷갈린다.
Model, View, Controller를 아주 쉬운 말로 보면
View는 사용자에게 보여 주는 부분이다.
입력 화면, 결과 화면, 오류 화면처럼 사용자가 눈으로 보는 화면이 여기에 해당한다.
즉, 보여 주는 역할이다.
Controller는 사용자의 요청을 먼저 받고, 그 요청을 어디로 보낼지 정하는 부분이다.
입력값을 읽고, 어떤 처리를 할지 판단하고, 마지막에 어떤 화면으로 보낼지 결정한다.
즉, 흐름을 조정하는 역할이다.
Model은 실제 처리를 담당하는 부분이다.
계산을 하거나, 데이터를 객체에 담거나,DB에 저장하고 조회하는 작업이 여기에 들어간다.
즉, 실제 일을 처리하는 역할이다.
이렇게 풀어 보면 결국MVC는 거창한 개념이 아니다.
화면 담당, 흐름 담당, 처리 담당을 나눈 구조라고 이해하면 된다.
처음에는 이 정도만 정확히 잡아도 충분하다.
다만 아주 작은 예제에서는 별도의Model파일을 만들지 않고Servlet이 간단한 판단이나 계산까지 함께 처리하기도 한다.
예를 들어 성적 처리나 계산 예제에서는Servlet안에서 조건 판단이나 계산이 바로 이루어진다.
기능이 커지고DB작업이 붙으면DTO,DAO,DB처럼Model역할이 더 분명하게 나뉜다.
MVC를 사람 역할에 비유하면 더 쉽다
처음 배우는 입장에서는 용어보다 비유가 더 잘 들어올 때가 많다.
그래서 사람 역할로 바꾸어 보면 이해가 훨씬 쉽다.
View는 안내판 같은 역할이다.
사용자에게 무엇을 보여 줄지 담당한다.
즉, 눈에 보이는 결과를 만드는 쪽이다.
Controller는 안내해 주는 직원 같은 역할이다.
사용자가 무엇을 요청했는지 듣고, 그 요청을 어디로 보내야 할지 정한다.
즉, 전체 흐름을 연결해 주는 쪽이다.
Model은 실제 업무를 처리하는 담당자 같은 역할이다.
계산을 하고, 데이터를 정리하고, 저장하고, 필요한 결과를 만들어 낸다.
즉, 실제 작업을 수행하는 쪽이다.
이 비유로 보면 왜 역할을 나누는지가 훨씬 쉽게 보인다.
안내판이 직접 계산을 하면 이상하다.
안내 직원이DB에 직접 저장까지 다 하면 역할이 너무 많아진다.
실제 업무 담당자가 사용자 화면까지 직접 꾸미면 그것도 역할이 섞인다.
그래서 각자 맡은 일을 나누는 것이 더 자연스럽다.
이 개념이 왜 뒤 예제에서 중요한가
앞으로 나올 예제에서는 이 역할 분리가 실제 코드로 보이게 된다.
입력 화면은 값을 보내는 역할만 한다.
Servlet은 그 값을 읽어서 흐름을 정한다.
결과 화면은 선택된 결과를 보여 준다.
즉, 방금 본 설명이 바로 뒤 예제에서 실제 코드 형태로 나타난다.
성적 처리 예제에서는 입력 화면이 점수와 이름을 보낸다.
Servlet은 점수에 따라 어느 결과 화면으로 갈지 정한다.
결과JSP가 등급을 보여 준다.
계산 예제에서는 입력값은 사용자가 보내고, 결과값은Servlet이 만들어서JSP로 넘긴다.
방명록 예제에서는 여기에DTO,DAO,DB까지 붙어서 구조가 더 확장된다.
장바구니 예제에서는 여러 번의 요청 사이에서도 데이터를 유지해야 하므로session이 등장한다.
즉, 지금 이 첫 개념 설명은 단순 이론이 아니다.
뒤에서 나올 예제들을 이해하기 위한 바닥이다.
이 바닥이 잡혀 있어야 왜Servlet이 따로 있고, 왜JSP가 따로 있고, 왜DAO가 또 따로 있는지가 자연스럽게 연결된다.
정리하면MVC는 복잡하게 보이는 구조가 아니라, 서로 다른 일을 맡는 코드를 미리 나누어 두는 구조이다.
처음에는 파일이 많아 보여도, 기능이 커질수록 이 방식이 훨씬 덜 헷갈리고 훨씬 관리하기 쉬워진다.
MVC를 웹 프로그램에 실제로 적용할 때 가장 먼저 만나게 되는 것이Servlet과JSP이다.
앞에서MVC를 화면, 흐름, 처리로 나누어 본 것처럼, 웹 예제에서는 이 역할이 더 구체적인 파일 형태로 나타난다.
이때 가장 먼저 잡아야 하는 것은Servlet은 보통 흐름을 잡는 쪽이고,JSP는 화면을 보여 주는 쪽이라는 점이다.
먼저Servlet은 사용자의 요청을 가장 먼저 받는 쪽이다.
사용자가 어떤 값을 보냈는지 읽고, 그 값을 가지고 어떤 처리를 해야 하는지 정한다.
마지막에 어떤 화면으로 보낼지도 결정한다.
즉,Servlet은 단순히 값을 받는 파일이 아니라, 요청을 해석하고 다음 흐름을 정하는 파일이다.
그래서MVC에서Controller역할과 잘 연결된다.
반대로JSP는 사용자에게 보이는 화면을 만드는 쪽이다.
결과 문장을 보여 주고, 계산 결과를 출력하고, 오류 메시지를 화면에 나타내는 식으로 최종 화면을 만든다.
즉,JSP는 보통 이미 처리된 결과를 화면으로 보여 주는 역할에 집중한다.
그래서MVC에서View역할과 연결된다.
여기서 중요한 점은,JSP가 처음부터 모든 판단을 직접 하는 구조로 가지 않는다는 점이다.
어떤 화면을 보여 줄지, 어떤 값을 출력해야 할지는 보통Servlet이 먼저 정해 준다.
그다음JSP는 전달받은 값을 화면에 보여 준다.
즉,Servlet과JSP는 따로 노는 것이 아니라, 요청을 처리하는 쪽과 결과를 보여 주는 쪽으로 역할을 나누어 함께 동작하는 구조이다.
사용자의 요청은 먼저
Controller로 들어간다.
Controller는 요청을 해석하고 필요한 처리를 거친 뒤, 결과를 보여 줄View로 흐름을 넘긴다.
사용자는 화면 하나만 보는 것 같아도, 서버 안에서는 먼저 흐름을 정하는 단계와 결과를 보여 주는 단계가 나뉘어 있다.
Servlet은Controller로 요청 흐름을 잡고,JSP는View로 결과 화면을 만든다.
그 안쪽으로Service,VO,DAO,DB같은 구조가 붙으면 실제 데이터 처리까지 이어진다.
처음에는 전체를 외우기보다Servlet이 흐름을 잡고JSP가 결과를 보여 준다는 큰 틀을 잡는 것이 더 중요하다.
왜 요청 흐름은 Servlet이 맡고 화면 출력은 JSP가 맡는가
처음 보면 그냥
JSP안에서 값도 읽고, 조건도 검사하고, 결과도 다 출력하면 더 쉬워 보일 수 있다.
하지만 그렇게 하면 화면 파일 안에 처리 로직이 점점 많아진다.
그러면 화면을 고칠 때도 처리 코드를 함께 봐야 하고, 처리 조건을 바꿀 때도 화면 코드를 같이 건드리게 된다.
즉, 역할이 다시 섞이게 된다.
예를 들어 점수에 따라 다른 결과를 보여 주는 기능이 있다고 하자.
이때JSP가 직접 점수를 검사해서A,B,C,D를 나누기 시작하면, 화면 파일 안에 조건문이 계속 들어가게 된다.
처음에는 간단해 보여도, 조건이 늘어날수록 화면 파일이 처리 로직까지 떠안게 된다.
그래서 조건 판단과 흐름 제어는Servlet이 맡고,JSP는 이미 정해진 결과를 보여 주는 쪽으로 두는 것이 더 자연스럽다.
이렇게 나누면 장점이 분명하다.
Servlet만 보면 요청이 어떻게 흘러가는지 보인다.
JSP만 보면 최종 화면이 어떻게 보이는지 보인다.
즉, 흐름을 보고 싶을 때는Servlet, 화면을 보고 싶을 때는JSP를 보면 된다.
이 분리가 바로MVC구조를 실제 웹 코드에서 느끼게 해 주는 부분이다.
forward는 왜 중요한가
Servlet과JSP를 연결할 때 자주 보게 되는 것이forward()이다.
여기서 이게 그냥 페이지 이동이랑 뭐가 다른지 헷갈리기 쉽다.
그래서 이 개념을 꼭 쉽게 잡아야 한다.
forward()는 서버 안에서 다음 화면으로 넘기는 방식이다.
즉, 브라우저가 새로운 주소로 다시 요청하는 것이 아니다.
사용자는 한 번 요청했지만, 서버 안에서는 그 요청을 이어서 다른 자원으로 넘겨 주는 것이다.
그래서 같은 요청 흐름 안에서 결과 화면으로 자연스럽게 연결된다.
이 말이 중요한 이유는request와도 연결되기 때문이다.
같은 요청 흐름 안에서 화면으로 넘기기 때문에,Servlet이request에 담아 둔 값들을JSP가 그대로 꺼내 쓸 수 있다.
즉,forward()는 단순 이동이 아니라 처리 결과를 다음 화면까지 이어 주는 연결 방식이라고 이해하면 된다.
반대로redirect는 브라우저가 새 주소로 다시 요청하는 방식이라서, 같은request를 그대로 이어서 쓰지 못한다.
그래서Servlet이 값을 읽고 처리한 뒤, 그 결과를JSP에 보여 줄 때forward()가 자주 사용된다.
WebServlet은 요청 주소와 Servlet을 연결한다
앞에서
Servlet은 사용자의 요청을 가장 먼저 받아 흐름을 정한다고 했다.
그 요청이 어떤Servlet으로 갈지 연결해 주는 설정이@WebServlet이다.
@WebServlet("/grade")는 브라우저에서/grade로 요청이 들어왔을 때GradeServlet을 실행하라는 뜻이다.
그래서eduForm.html의form action="/mvcedu/grade"요청은@WebServlet("/grade")가 붙은GradeServlet으로 연결된다.
여기서/mvcedu는 웹 애플리케이션의 기본 경로이고, 실제Servlet매핑 주소는/grade라고 보면 된다.
즉,@WebServlet은 요청 주소와 실행할Servlet을 이어 주는 표지판이다.
doGet()과 doPost()는 요청 방식에 따라 실행된다
요청 주소가
Servlet을 찾게 해 준다면, 요청 방식은doGet()과doPost()중 어떤 메서드가 실행될지 정한다.
즉,Servlet안에 메서드가 여러 개 있어도 아무 메서드나 실행되는 것이 아니다.
브라우저가 어떤 방식으로 요청했는지에 따라 실행되는 메서드가 달라진다.
GET방식 요청이 들어오면doGet()이 실행된다.
주소창으로 직접 요청하거나, 링크를 클릭하거나,form method="get"을 사용하면 보통doGet()흐름으로 들어간다.
반대로POST방식 요청이 들어오면doPost()가 실행된다.
form method="post"처럼 입력값을 요청 본문에 담아 보낼 때 주로 사용한다.
즉,Servlet이 마음대로 고르는 것이 아니라, 브라우저가 보낸 요청 방식에 따라 실행되는 메서드가 달라진다.
예제 1 - 성적 처리 흐름으로 확인하기
이제 위 개념이 실제 코드에서 어떻게 보이는지 첫 번째 예제로 확인할 수 있다.
이 예제는 성적 처리 예제이고, 구조가 아주 단순해서Servlet과JSP역할을 구분하기에 좋다.
핵심 흐름은 이렇다.
사용자는 입력 화면에서 이름과 점수를 넣는다.
그 요청은Servlet으로 간다.
Servlet은 점수를 보고 어떤 결과 화면으로 보낼지 결정한다.
그 뒤 선택된JSP가 결과를 보여 준다.
즉, 입력 화면 →Servlet→ 결과JSP흐름이다.
입력 화면은 무엇을 하는가
먼저 입력 화면은
eduForm.html이다.
이 파일은 사용자가 이름과 평균점수를 입력할 수 있게 만든다.
그리고post방식으로/mvcedu/grade에 요청을 보낸다.// eduForm.html <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>성적 입력</title> <style> article { width: 60%; padding: 20px; margin: 5px auto; background: radial-gradient(red, green, yellow); border: 1px solid grey; border-radius: 10px; } </style> </head> <body> <article> <h2>성적을 입력하시오.</h2> <hr> <form method="post" action="/mvcedu/grade"> <label for="input1">이름</label> <input type="text" id="input1" name="name"><!-- 이름 입력 --> <br> <label for="input2">평균점수</label> <input type="number" id="input2" min="0" max="100" name="score"><!-- 점수 입력 --> <br> <input type="submit" value="전송"><!-- 서버로 요청 --> <input type="reset" value="재작성"><!-- 입력 초기화 --> </form> </article> </body> </html>이 코드에서 중요한 점은 이 파일이 결과를 판단하지 않는다는 것이다.
그냥 이름과 점수를 받아 서버로 보내는 역할만 한다.
즉, 입력 화면은 데이터를 보내는 출발점이다.
입력 화면은 결과를 만드는 파일이 아니라, 결과를 만들기 위한 값을 보내는 파일이다.
Servlet은 무엇을 하는가
이제 요청을 받은
GradeServlet을 보면 역할이 더 분명해진다.
이 파일은 사용자가 보낸name과score를 읽고, 점수에 따라 어느JSP로 보낼지 결정한다.// GradeServlet.java package controller; import java.io.IOException; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @WebServlet("/grade") public class GradeServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); // 한글 처리 String name = request.getParameter("name"); // 이름 읽기 String score = request.getParameter("score"); // 점수 읽기 RequestDispatcher rd = null; // 이동할 화면 저장 if (name == null || name.equals("") || score == null || score.equals("")) { rd = request.getRequestDispatcher("/eduForm.html"); // 값이 없으면 입력 화면 } else { int jumsu = Integer.parseInt(score); // 문자열 점수를 숫자로 변환 if (jumsu >= 90) rd = request.getRequestDispatcher("/jspexam/gradeA.jsp"); // A 화면 선택 else if (jumsu >= 80) rd = request.getRequestDispatcher("/jspexam/gradeB.jsp"); // B 화면 선택 else if (jumsu >= 70) rd = request.getRequestDispatcher("/jspexam/gradeC.jsp"); // C 화면 선택 else rd = request.getRequestDispatcher("/jspexam/gradeD.jsp"); // D 화면 선택 } rd.forward(request, response); // 선택된 화면으로 서버 내부 이동 } }이 코드를 보면 왜
Servlet이Controller인지 분명해진다.
이 파일은 직접 결과 문장을 화면에 출력하지 않는다.
대신 요청을 읽고, 조건을 판단하고, 어디로 보낼지 결정한다.
바로 이 점이 핵심이다.
특히 중요한 줄은rd.forward(request, response);이다.
이 한 줄 때문에Servlet이 고른 결과 화면으로 요청이 이어진다.
즉, 사용자는 한 번 요청했지만, 서버 안에서는 그 요청이 알맞은JSP까지 연결된다.
serialVersionUID는 객체를 저장하거나 복원할 때 버전을 구분하기 위한 식별값이다.
지금 예제에서는HttpServlet을 상속할 때 자동으로 생기는 경고를 줄이기 위해 작성된 기본 코드 정도로 이해하면 된다.
앞에서doPost()는 입력값을 요청 본문에 담아 보내는 방식이라고 정리했다.
이때 한글 입력값을 제대로 읽으려면 문자 인코딩을 먼저 맞춰야 한다.
request.setCharacterEncoding("utf-8")은 요청 본문을UTF-8기준으로 읽겠다는 설정이다.
그래서name처럼 한글이 들어올 수 있는 값을getParameter()로 꺼내기 전에 먼저 실행한다.
이 순서가 바뀌면 한글이 깨질 수 있다.
JSP는 무엇을 하는가
이제 선택된 결과 화면은
JSP가 맡는다.
대표로gradeA.jsp를 보면 아래와 같다.// gradeA.jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>A 등급 화면</title> </head> <body> <h2>${ param.name }님은 A등급입니다. 우수한 성적이네요(^^)</h2><!-- 이름과 등급 결과 출력 --> <hr> <a href="${ header.referer }">성적 입력 화면으로</a><!-- 이전 화면으로 이동 --> </body> </html>이 파일은 점수를 다시 검사하지 않는다.
90점 이상인지,80점 이상인지를 여기서 판단하지 않는다.
그 판단은 이미Servlet에서 끝났다.
이 파일은 그냥 선택된 결과를 화면에 보여 주는 역할만 한다.
여기서${ param.name }은 요청으로 넘어온 이름을 꺼내는 표현이다.
사용자가 입력한 이름을 결과 문장에 넣어서 보여 준다.
이 예제에서param.name을 바로 꺼낼 수 있는 이유는Servlet이forward()로 같은 요청을 이어 주었기 때문이다.
${ header.referer }는 이전에 머물렀던 페이지 주소를 의미한다.
여기서는 결과 화면에서 다시 입력 화면으로 돌아가기 위해 사용한다.
다만referer헤더는 요청 상황에 따라 없을 수도 있으므로, 여기서는 학습 예제에서 이전 입력 화면으로 돌아가기 위해 사용하는 값으로 이해하면 된다.
eduForm.html에서 시작한 요청은GradeServlet으로 들어간다.
그다음 점수 조건에 따라gradeA.jsp,gradeB.jsp,gradeC.jsp,gradeD.jsp중 하나로 흐름이 나뉜다.
사용자가 보는 입력 화면은 하나지만, 서버 안에서는 결과에 맞는 화면을 선택하는 과정이 따로 존재한다.
실행 결과에서는 하나의 입력 화면에서 시작해 점수에 맞는 결과 화면으로 이동한다.
겉으로는 화면만 바뀌는 것처럼 보이지만, 내부에서는Controller가 조건을 판단하고 알맞은View를 선택하고 있다.
예제 1이 보여 주는 핵심
이 예제에서 꼭 잡아야 할 것은 세 가지이다.
입력 화면은 값을 보내는 역할이다.
결과를 판단하지 않는다.
Servlet은 요청을 읽고 흐름을 결정한다.
즉, 어떤 결과 화면으로 갈지를 정한다.
JSP는 선택된 결과를 화면에 보여 준다.
즉, 판단보다는 출력 역할에 가깝다.
결국 예제 1은Servlet은 흐름을 조정하고,JSP는 결과를 보여 준다는 점을 가장 단순하게 보여 주는 예제이다.
이 역할 분리를 정확히 이해하면, 다음 예제에서 나오는DTO,DAO,request,attribute,session같은 개념도 훨씬 자연스럽게 이어진다.
MVC앞부분 예제에서는 입력값을 받고, 처리하고, 화면에 결과를 보여 주는 흐름을 중심으로 보았다.
그런데 방명록처럼 다루는 데이터가 많아지면 값 하나만 처리하지 않는다.
방명록 글 한 건에는 글 번호, 작성자 이름, 작성일, 글 내용이 함께 들어간다.
이 값들을 계속 따로따로 들고 다니면 코드가 금방 복잡해진다.
그래서 먼저 필요한 것이DTO또는VO이다.
DTO와VO는 관련 있는 값을 하나의 객체로 묶는 역할을 한다.
즉, 여러 값을 하나의 의미 있는 단위로 정리해 주는 데이터 객체이다.
방명록 예제에서는 이 역할을VisitorDTO가 맡는다.
여기서 가장 먼저 확실히 나누어야 할 점이 있다.
DTO와VO는 반드시DB가 있어야만 사용하는 객체가 아니다.
DB에 저장하지 않더라도, 여러 값을 하나의 객체로 묶어서Servlet에서JSP로 넘길 때 사용할 수 있다.
즉,DTO의 핵심은DB저장이 아니라 관련 있는 값을 하나로 묶어 전달하는 것이다.
반대로DAO는DB가 붙을 때 의미가 커진다.
DAO는 데이터를 담는 객체가 아니다.
DAO는DB에 데이터를 저장하고, 조회하고, 수정하고, 삭제하는 작업을 맡는다.
즉,DB와 실제로 연결되는 중심 역할은DAO이다.
그래서 이 파트는 처음부터 이렇게 나누어 이해해야 한다.
DTO와VO는 데이터를 하나로 묶는 역할이다.
DAO는 그 데이터를DB에 저장하거나DB에서 다시 꺼내 오는 역할이다.
DB는 실제 데이터가 저장되는 공간이다.
정리하면DTO/VO는DB가 없어도 사용할 수 있는 데이터 묶음이고,DAO는DB작업이 필요할 때 등장하는 데이터 처리 담당자이다.
이 차이를 먼저 잡아야 뒤에서VisitorDTO,VisitorDAO,MySQLConnect,DB가 한꺼번에 나와도 역할이 헷갈리지 않는다.
DTO와 VO는 데이터를 하나로 묶는 객체이다
DTO와VO는 관련 있는 데이터를 하나의 객체로 묶는 역할을 한다.
처음에는 이름이 어렵게 보일 수 있지만, 초보자 기준에서는 데이터 상자라고 이해하면 쉽다.
방명록 글을 생각해 보자.
방명록 글 한 건에는 글 번호, 작성자 이름, 작성일, 글 내용이 들어간다.
이 값들은 따로따로 떨어진 값처럼 보여도 실제로는 하나의 방명록 글에 속한다.
그래서 이 값들을 하나의 객체에 묶으면 방명록 글 한 건이라는 단위로 다룰 수 있다.
이때 사용하는 객체가VisitorDTO이다.// VisitorDTO.java package model.domain; public class VisitorDTO { private int id; // 글 번호 private String name; // 작성자 이름 private String writeDate; // 작성일 private String memo; // 글 내용 public int getId() { return id; // 글 번호 반환 } public void setId(int id) { this.id = id; // 글 번호 저장 } public String getName() { return name; // 이름 반환 } public void setName(String name) { this.name = name; // 이름 저장 } public String getWriteDate() { return writeDate; // 작성일 반환 } public void setWriteDate(String writeDate) { this.writeDate = writeDate; // 작성일 저장 } public String getMemo() { return memo; // 글 내용 반환 } public void setMemo(String memo) { this.memo = memo; // 글 내용 저장 } }이 코드는
DB작업을 하지 않는다.
SQL도 없고,DB연결 코드도 없다.
그냥 방명록 글 한 건에 필요한 값들을 하나의 객체 안에 묶어 둔다.
즉,VisitorDTO자체가DB에 저장하는 일을 하는 것은 아니다.
VisitorDTO는 데이터를 담는 객체이다.
이 객체를DB에 저장할지, 화면으로 넘길지, 목록에 담아 둘지는 다른 흐름에서 결정된다.
DTO의 핵심은 저장이 아니라 묶음이다.
여러 값을 하나의 의미 있는 단위로 묶어 전달하기 위해 사용한다.
DTO와 VO는 여기서 어디까지 구분하면 되는가
DTO와VO는 실제로는 구분해서 설명할 수 있는 개념이다.
하지만 지금 단계에서는 둘의 차이를 깊게 파고들기보다, 현재 예제에서 어떤 역할로 쓰이는지를 먼저 잡는 것이 더 중요하다.
초보자 단계에서는 둘 다 관련 있는 데이터를 하나의 객체로 묶는 역할이라고 이해하면 충분하다.
중요한 것은 이름이DTO인지VO인지가 아니다.
왜 여러 값을 객체 하나로 묶어야 하는지 이해하는 것이 먼저이다.
방명록 예제에서는 글 번호, 작성자 이름, 작성일, 글 내용을VisitorDTO에 묶는다.
일부 구조도에서VisitorVO라고 표시되어도, 현재 코드 기준에서는VisitorDTO와 같은 데이터 객체 역할로 이해하면 된다.
이름이 조금 달라도 핵심은 같다.
그리고DTO는DB가 없어도 사용할 수 있다.
뒤에서 나오는 회원 정보 실습처럼 입력값을 객체에 담아request로JSP에 넘기는 흐름에서도DTO를 사용할 수 있다.
즉,DTO는DB전용 객체가 아니라 값을 묶어 전달하는 객체이다.
DTO와VO는 데이터를 직접 저장하는DB도 아니고,DB작업을 실행하는DAO도 아니다.
관련 있는 값을 하나의 객체로 정리하는 역할이다.
DTO의 멤버변수는 왜 private으로 작성하는가
DTO는 데이터를 담는 객체이다.
하지만 멤버변수를 아무 데서나 직접 바꾸게 두면 값 관리가 어려워진다.
예를 들어name,memo같은 값을public으로 열어 두면 다른 클래스에서dto.name = "홍길동"처럼 바로 값을 바꿀 수 있다.
겉으로는 편해 보일 수 있다.
하지만 값이 어디에서 어떻게 바뀌었는지 추적하기 어려워진다.
그래서DTO의 멤버변수는 보통private으로 만든다.
private은 클래스 바깥에서 멤버변수에 직접 접근하지 못하게 막는 접근 제한자이다.
대신 값을 넣을 때는setter, 값을 꺼낼 때는getter를 사용한다.
즉, 값을 읽고 쓰는 통로를 메서드로 정해 두는 것이다.// VisitorDTO.java public class VisitorDTO { private String name; // 외부에서 직접 접근하지 못하게 막기 public String getName() { return name; // 값 꺼내기 } public void setName(String name) { this.name = name; // 값 넣기 } }이렇게 작성하면 다른 클래스에서는
dto.name처럼 직접 접근하지 않는다.
대신dto.setName("홍길동"),dto.getName()처럼 메서드를 통해 값을 다룬다.
이 방식은 값에 접근하는 통로를 일정하게 만들어 준다.
나중에 값 검사가 필요해져도setter안에 규칙을 추가할 수 있다.
예를 들어 이름이 비어 있으면"이름 없음"으로 저장하는 규칙을setName()안에 넣을 수도 있다.
또 하나 중요한 점은JSP의EL표현식과도 연결된다는 것이다.
JSP에서${ visitor.name }처럼 쓰면 겉으로는name변수에 바로 접근하는 것처럼 보일 수 있다.
하지만 실제로는visitor객체의getName()같은getter를 기준으로 값을 읽는 방식으로 이해하면 된다.
그래서DTO를 만들 때는 멤버변수를private으로 두고,getter와setter를 함께 작성하는 방식이 기본 형태로 자주 사용된다.
왜 객체로 묶어야 하는가
값을 따로따로 들고 다니면 처음에는 쉬워 보인다.
하지만 값이 많아질수록 코드가 지저분해진다.
예를 들어 방명록 글을 저장하거나 조회한다고 생각해 보자.
글 번호, 작성자 이름, 작성일, 글 내용을 계속 따로 넘기면 메서드에 전달해야 하는 값이 많아진다.
또 어떤 값들이 같은 글에 속하는지도 계속 머릿속으로 묶어서 생각해야 한다.
그래서 관련 있는 값을 객체 하나로 묶는다.
방명록에서는 글 한 건에 필요한 값을VisitorDTO에 담는다.
이제 글 번호, 작성자 이름, 작성일, 글 내용을 따로따로 들고 다니지 않아도 된다.
VisitorDTO객체 하나에 담아서 다룰 수 있다.
이렇게 묶어 두면Servlet에서DAO로 넘길 때도 편하다.
새 글을 등록할 때는 이름과 글 내용을VisitorDTO에 담아VisitorDAO에 넘길 수 있다.
조회할 때는VisitorDAO가DB에서 읽은 한 행을VisitorDTO객체 하나로 만들 수 있다.
목록 조회라면 여러 개의VisitorDTO가 모여 하나의 목록이 된다.
여기서도 핵심은 같다.
VisitorDTO는 데이터를 저장하는 실행 담당이 아니다.
VisitorDTO는 데이터를 담는 상자이다.
그 상자를 받아 실제로DB에 저장하거나,DB에서 꺼내 오는 작업은DAO가 맡는다.
DTO는 데이터를 계산하거나 저장하는 객체가 아니라, 관련 있는 값을 하나의 의미 있는 객체로 정리하는 객체이다.
이 기준을 잡고 나면VisitorDTO,MeetingDTO,ReplyDTO처럼 이름이 달라져도 역할을 쉽게 파악할 수 있다.
DAO는 DB가 붙을 때 실제 작업을 맡는 클래스이다
DAO는DB를 직접 다루는 역할이다.
저장, 조회, 수정, 삭제 같은 작업을 맡는다.
여기서 중요한 점은DAO가 데이터를 담는 객체가 아니라는 것이다.
데이터를 담는 역할은DTO나VO가 한다.
DAO는 그 객체를 받아서DB에 저장하거나, 반대로DB에서 읽은 데이터를 다시 객체로 만들어 돌려준다.
쉽게 말하면 이렇다.
DTO나VO가 데이터 상자라면,DAO는 그 상자를 창고에 넣고 다시 꺼내 오는 담당자이다.
여기서 창고가 바로DB이다.
즉,DAO는 화면을 보여 주는 쪽도 아니고, 사용자의 입력을 직접 받는 쪽도 아니다.
오직DB와 연결해서 실제 데이터를 처리하는 역할에 집중한다.
방명록 등록 흐름을 보면 차이가 더 분명하다.
사용자가 입력한 이름과 글 내용은 먼저VisitorDTO에 담긴다.
그다음VisitorServlet은VisitorDAO의insert()메서드를 호출한다.
이때VisitorDAO가DB에insert작업을 실행한다.
방명록 목록 조회 흐름에서는 반대로 움직인다.
VisitorDAO가DB에서 여러 행을 읽어 온다.
그리고 각 행을VisitorDTO객체로 바꾸어 목록에 담아 돌려준다.
그 목록은 다시Servlet을 거쳐JSP에서 화면에 출력된다.
즉,DB작업의 중심은DAO이고,DTO는 그 작업에 필요한 데이터를 담아 주는 객체이다.
이 둘을 같은 역할로 보면 안 된다.
DTO와 DAO의 차이를 다시 비교하면
DTO와DAO는 이름이 비슷하게 보일 수 있지만 역할은 완전히 다르다.
초보자가 가장 많이 헷갈리는 지점이 바로 이 부분이다.
DTO는 데이터를 담는다.
글 번호, 작성자 이름, 작성일, 글 내용처럼 함께 움직이는 값을 하나의 객체에 넣는다.
DTO안에는 보통 멤버변수,getter,setter가 들어간다.
DAO는 데이터를 처리한다.
DB에 연결하고,SQL을 실행하고, 결과를 다시 객체로 만든다.
DAO안에는 보통Connection,PreparedStatement,ResultSet,executeQuery(),executeUpdate()같은JDBC관련 코드가 들어간다.
둘의 차이는 이렇게 정리할 수 있다.
DTO/VO: 값을 담는 객체이다.DAO:DB작업을 실행하는 클래스이다.DB: 실제 데이터가 저장되는 공간이다.예를 들어
VisitorDTO는 방명록 글 한 건을 담는 객체이다.
VisitorDTO안에는 글 번호, 작성자 이름, 작성일, 글 내용이 들어간다.
하지만VisitorDTO가 직접DB에 저장하지는 않는다.
반대로VisitorDAO는DB작업을 담당한다.
방명록 글을 저장하고, 목록을 조회하고, 검색하고, 삭제하려면 실제 저장 공간인DB와 연결해야 한다.
이 연결을 사용해SQL을 실행하는 쪽이VisitorDAO이다.
정리하면DTO는DB유무와 상관없이 사용할 수 있는 데이터 묶음이고,DAO는DB작업이 필요할 때 중심이 되는 클래스이다.
MySQLConnect는 DAO가 DB에 연결할 때 쓰는 공통 도구이다
DAO가DB작업을 하려면 먼저DB에 연결해야 한다.
이때 연결을 만들고 닫는 코드가 필요하다.
그 역할을 따로 빼 둔 클래스가MySQLConnect이다.
MySQLConnect는 화면을 만드는 파일이 아니다.
방명록 화면을 보여 주지도 않는다.
또 데이터를 담는DTO도 아니다.
대신DB에 접속할 수 있도록Connection객체를 만들어 주고, 사용이 끝난 연결을 닫는 역할을 한다.
즉,MySQLConnect는DAO가DB작업을 할 수 있도록 뒤에서 연결을 준비해 주는 공통 도구이다.
DAO는 이 연결을 사용해서 실제SQL작업을 실행한다.
Tomcat서버 안에서는 여러Servlet과JSP가 동작하고, 데이터가 필요할 때DB연결 객체를 통해 데이터베이스와 통신한다.
DAO는MySQLConnect같은 연결 코드를 이용해DB에 접근하고, 작업이 끝나면 연결을 닫는다.
연결을 만들고 닫는 흐름을 따로 관리해야DB작업 코드가 안정적으로 유지된다.// MySQLConnect.java package model.service; import java.sql.Connection; import java.sql.DriverManager; public class MySQLConnect { public static Connection connect() { Connection conn = null; // DB 연결 객체 try { Class.forName("com.mysql.cj.jdbc.Driver"); // MySQL 드라이버 로딩 String url = "jdbc:mysql://localhost:3306/edudb?characterEncoding=UTF-8&serverTimezone=Asia/Seoul"; conn = DriverManager.getConnection(url, "jdbctest", "jdbctest"); // DB 접속 } catch (Exception e) { System.out.println("DB접속오류 발생!!"); e.printStackTrace(); // 오류 내용 출력 } return conn; // 연결 객체 반환 } public static void close(Connection conn) { try { if (conn != null) { conn.close(); // DB 연결 닫기 } } catch (Exception e) { System.out.println(e); // 닫는 과정의 오류 출력 } } }이 코드를 보면
connect()는DB연결을 만든다.
반대로close()는 사용이 끝난 연결을 닫는다.
이렇게 연결 코드를 따로 빼 두면 여러DAO에서 같은 연결 코드를 반복해서 작성하지 않아도 된다.
예를 들어VisitorDAO,MeetingDAO처럼 여러DAO가 있어도MySQLConnect.connect()를 호출해서 같은 방식으로DB에 연결할 수 있다.
다만DB연결에 실패하면conn이null일 수 있다.
그래서 연결을 닫을 때는conn != null인지 확인한 뒤 닫는 방식이 안전하다.
위 코드의close()메서드도 이 점을 반영해null검사를 먼저 한다.
즉, 역할을 다시 나누면 이렇다.
MySQLConnect는DB연결을 만들고 닫는 담당이다.
DAO는 그 연결을 사용해서 실제SQL작업을 하는 담당이다.
DTO는 그 과정에서 필요한 데이터를 담는 객체이다.
이 구분이 잡히면 뒤에서 방명록 예제를 볼 때 흐름이 훨씬 또렷해진다.
사용자 입력은Servlet으로 들어오고, 관련 값은VisitorDTO에 담긴다.
VisitorDAO는MySQLConnect로 만든 연결을 이용해DB작업을 실행한다.
그리고 결과는 다시Servlet을 거쳐JSP로 전달된다.
핵심 정리
DTO,VO,DAO,DB는 한꺼번에 나오면 복잡해 보이지만 역할을 나누면 단순하다.
DTO와VO는 관련 있는 값을 하나의 객체로 묶는다.
DB가 없어도 사용할 수 있다.
방명록 예제에서는 글 번호, 작성자 이름, 작성일, 글 내용을VisitorDTO에 담아 방명록 글 한 건으로 다룬다.
DAO는DB작업을 맡는다.
데이터를 저장하고, 조회하고, 수정하고, 삭제하는 실제 작업은DAO가 담당한다.
그래서DB가 붙는 순간 구조에서 가장 중요한 처리 담당은DAO가 된다.
MySQLConnect는DAO가DB에 접근할 수 있도록 연결을 만들고 닫는 도구이다.
DAO가 실제 작업자라면,MySQLConnect는 작업자가DB에 들어갈 수 있게 연결 문을 열고 닫아 주는 역할이다.
정리하면DTO/VO는 데이터를 담는 객체이고,DAO는DB작업을 처리하는 클래스이며,DB는 실제 데이터가 저장되는 공간이다.
이 기준을 잡고 다음 방명록 예제로 들어가면VisitorDTO,VisitorDAO,MySQLConnect,DB가 각각 왜 필요한지 훨씬 쉽게 이해할 수 있다.
3 - 1. DB 연동 방명록 예제로 구조 확인하기
이제
DTO,DAO,DB가 실제로 어떻게 연결되는지 방명록 예제로 확인하면 된다.
이 예제는 사용자가 방명록 글을 작성하고, 목록을 보고, 검색하고, 삭제할 수 있으며, 코드상 수정 처리 흐름도 함께 포함된 구조이다.
이 예제에서 역할은 이렇게 나뉜다.
visitorMain.html은 사용자가 보는 입력 화면이다.
VisitorServlet은 요청을 읽고 등록, 목록, 검색, 삭제, 수정 중 어떤 흐름인지 판단한다.
VisitorDTO는 방명록 글 한 건의 데이터를 담는다.
VisitorDAO는 실제DB저장, 조회, 검색, 수정, 삭제를 처리한다.
visitorView.jsp는 결과 목록이나 메시지를 화면에 보여 준다.
즉, 이 예제는DTO,DAO,DB가 왜 나뉘는지 가장 직접적으로 보여 주는 예제이다.
사용자가
visitorMain.html에서 방명록 작성 영역을 열고 글을 입력하면 요청은VisitorServlet으로 전달된다.
VisitorServlet은 입력값을 방명록 데이터 객체에 담고,VisitorDAO에 저장 작업을 맡긴다.
현재 코드에서는 이 데이터 객체를VisitorDTO라는 이름으로 작성한다.
VisitorDAO는DB에 접근해 저장을 처리한 뒤 성공 여부를true또는false로 돌려준다.
Servlet은 그 결과에 따라visitorView.jsp에 보여 줄 메시지를 정한다.
목록 조회나 검색에서는 흐름이 반대로 움직인다.
VisitorServlet이VisitorDAO에 조회를 요청하면,VisitorDAO는DB에서 여러 행을 읽어 온다.
각 행은 방명록 데이터 객체 하나로 정리되고, 여러 객체가 목록으로 묶여visitorView.jsp까지 전달된다.
현재 코드 기준으로는 이 객체가VisitorDTO이다.
등록은 객체 하나를 저장하는 흐름이고, 조회는 여러 객체를 모아 화면에 전달하는 흐름이다.
visitorMain.html은 요청을 시작하는 화면이다
visitorMain.html은 사용자가 방명록 목록을 보거나, 글을 작성하거나, 검색할 수 있게 해 주는 화면이다.
중요한 점은 이 화면이 직접DB에 저장하지 않는다는 것이다.
이 화면은 사용자의 요청을/mvcedu/visitor로 보내는 출발점이다.// visitorMain.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>방명록</title> </head> <body> <header> <h1>방명록</h1> <nav> <button onclick="location.href='/mvcedu/visitor'">방명록 리스트 보기</button> <button onclick="displayTag('insert')">방명록 작성하기</button> <button onclick="displayTag('search')">방명록 검색하기</button> </nav> </header> <section> <article id="insert" style="display:none"> <h2>글을 남겨 주세요</h2> <hr> <form method="post" action="/mvcedu/visitor"> <input type="hidden" name="action" value="insert"><!-- 등록 요청 구분 --> 이름 : <input type="text" name="name" size="15"><br> 남기고자 하는 의견 : <br> <textarea name="memo" rows="10" cols="50"></textarea><br> <input type="submit" value="등록"> <input type="reset" value="재작성"> </form> </article> <article id="search" style="display:none"> <h2>검색하고자 하려는 단어를 입력하세요</h2> <hr> <form method="get" action="/mvcedu/visitor"> <input type="hidden" name="action" value="search"><!-- 검색 요청 구분 --> 검색어 : <input type="search" name="keyword" size="15"> <input type="submit" value="검색"> </form> </article> </section> <script> function displayTag(target) { document.querySelectorAll("section > *").forEach(function(elem) { elem.style.display = "none"; // 모든 입력 영역 숨기기 }); let dom = document.querySelector("#" + target); // 선택한 영역 찾기 dom.style.display = "block"; // 선택한 영역만 보이기 } </script> </body> </html>이 코드에서
action이라는 숨겨진 입력값을 눈여겨봐야 한다.
등록 폼에서는action=insert가 전달된다.
검색 폼에서는action=search가 전달된다.
이 값이 있어야VisitorServlet이 요청 종류를 구분할 수 있다.
즉, 화면은 단순히 입력창만 보여 주는 것이 아니라, 서버가 요청을 구분할 수 있는 값까지 함께 보낸다.
VisitorServlet은 요청 흐름을 나눈다
VisitorServlet은/visitor로 들어온 요청을 처리한다.
목록 조회, 검색, 삭제는doGet()에서 처리한다.
등록과 수정은doPost()에서 처리한다.// VisitorServlet.java package controller; import java.io.IOException; import java.util.List; import jakarta.servlet.RequestDispatcher; 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 model.service.VisitorDAO; import model.domain.VisitorDTO; @WebServlet("/visitor") public class VisitorServlet extends HttpServlet { private static final long serialVersionUID = 1L; private VisitorDAO dao; public void init() { dao = new VisitorDAO(); // Servlet이 처음 준비될 때 DAO 생성 } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); // 요청 종류 읽기 List<VisitorDTO> list = null; // 목록 저장 if (action == null) { list = dao.listAll(); // 전체 목록 조회 if (list.size() > 0) request.setAttribute("list", list); // 목록 저장 else request.setAttribute("msg", "방명록에 작성된 글이 없네요..."); } else if (action.equals("search")) { String keyword = request.getParameter("keyword"); // 검색어 읽기 list = dao.search(keyword); // 검색 결과 조회 if (list.size() > 0) request.setAttribute("list", list); else request.setAttribute("msg", keyword + "를 포함한 글이 없네요..."); } else if (action.equals("delete")) { boolean result = dao.delete(Integer.parseInt(request.getParameter("id"))); // 글 삭제 if (result) request.setAttribute("msg", "성공적으로 글이 삭제되었어요..."); else request.setAttribute("msg", "글을 삭제하는 동안 오류가 발생했어요..."); } RequestDispatcher rd = request.getRequestDispatcher("/jspexam/visitorView.jsp"); rd.forward(request, response); // 결과 화면으로 이동 } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); // 한글 처리 String action = request.getParameter("action"); // 등록 또는 수정 구분 String name = request.getParameter("name"); // 이름 읽기 String memo = request.getParameter("memo"); // 글 내용 읽기 if (action.equals("insert")) { VisitorDTO vo = new VisitorDTO(); // 방명록 글 객체 생성 vo.setName(name); // 이름 저장 vo.setMemo(memo); // 내용 저장 boolean result = dao.insert(vo); // DB 저장 요청 if (result) request.setAttribute("msg", name + "님의 글을 성공적으로 저장했어요^^"); else request.setAttribute("msg", name + "님의 글 저장 작업을 실패했어요ㅜㅜ"); } else { VisitorDTO vo = new VisitorDTO(); // 수정할 글 객체 생성 vo.setId(Integer.parseInt(request.getParameter("id"))); // 수정할 글 번호 vo.setName(name); vo.setMemo(memo); boolean result = dao.update(vo); // DB 수정 요청 if (result) request.setAttribute("msg", name + "님의 글이 성공적으로 수정했어요^^"); else request.setAttribute("msg", name + "님의 글 수정 작업을 실패했어요ㅜㅜ"); } RequestDispatcher rd = request.getRequestDispatcher("/jspexam/visitorView.jsp"); rd.forward(request, response); // 결과 화면으로 이동 } }
init()은Servlet객체가 처음 준비될 때 한 번 실행되는 초기화 메서드이다.
이 예제에서는VisitorDAO객체를 미리 만들어 두기 위해 사용한다.
그래서 요청이 들어올 때마다 새로DAO를 만드는 것이 아니라, 준비된dao를 사용해DB작업을 맡길 수 있다.
이 코드에서Servlet은 직접SQL을 작성하지 않는다.
대신 요청을 읽고, 필요한 데이터를VisitorDTO에 담고, 실제DB작업은VisitorDAO에 맡긴다.
등록 요청이면VisitorDTO객체를 만들고 이름과 글 내용을 담은 뒤dao.insert(vo)를 호출한다.
검색 요청이면 검색어를 읽고dao.search(keyword)를 호출한다.
삭제 요청이면 글 번호를 읽고dao.delete(id)를 호출한다.
수정 요청이면 글 번호, 이름, 내용을VisitorDTO에 담고dao.update(vo)를 호출한다.
수정은 새 글을 만드는 등록과 다르다.
이미 존재하는 글을 바꾸는 작업이기 때문에, 어떤 글을 수정할지 알려 주는id가 반드시 필요하다.
그래서 수정 흐름에서는id를 먼저 객체에 담고, 그 객체를DAO에 넘겨DB의 해당 행을 바꾼다.
즉,Servlet은 요청 흐름을 나누는 역할이고,DAO는 실제DB작업을 하는 역할이다.
VisitorDAO는 실제 DB 작업을 맡는다
VisitorDAO는 전체 목록 조회, 글 한 건 조회, 검색, 등록, 삭제, 수정 기능을 담당한다.
즉, 방명록 데이터와DB사이를 오가는 파일이다.// VisitorDAO.java package model.service; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import model.domain.VisitorDTO; public class VisitorDAO { public List<VisitorDTO> listAll() { Connection conn = MySQLConnect.connect(); // DB 연결 List<VisitorDTO> list = new ArrayList<>(); // 결과 목록 try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("select id, name, date_format(writedate, '%Y년 %m월 %d일') writedate, memo from visitor order by writedate desc");) { VisitorDTO vo; while (rs.next()) { vo = new VisitorDTO(); // 한 행을 객체로 변환 vo.setId(rs.getInt(1)); vo.setName(rs.getString(2)); vo.setWriteDate(rs.getString(3)); vo.setMemo(rs.getString(4)); list.add(vo); // 목록에 추가 } } catch (SQLException e) { e.printStackTrace(); } finally { MySQLConnect.close(conn); // 연결 닫기 } return list; // 목록 반환 } public VisitorDTO one(int id) { Connection conn = MySQLConnect.connect(); // DB 연결 VisitorDTO vo = null; // 글 한 건 저장 try (PreparedStatement pstmt = conn.prepareStatement("select id, name, date_format(writedate, '%Y년 %m월 %d일') writedate, memo from visitor where id = ?");) { pstmt.setInt(1, id); // 조회할 글 번호 바인딩 try (ResultSet rs = pstmt.executeQuery();) { if (rs.next()) { vo = new VisitorDTO(); // 조회 결과 객체 생성 vo.setId(rs.getInt(1)); vo.setName(rs.getString(2)); vo.setWriteDate(rs.getString(3)); vo.setMemo(rs.getString(4)); } } } catch (SQLException e) { e.printStackTrace(); } finally { MySQLConnect.close(conn); // 연결 닫기 } return vo; } public List<VisitorDTO> search(String keyword) { Connection conn = MySQLConnect.connect(); // DB 연결 List<VisitorDTO> list = new ArrayList<>(); // 검색 결과 목록 try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("select id, name, date_format(writedate, '%Y년 %m월 %d일') writedate, memo from visitor where memo like '%" + keyword + "%' ");) { VisitorDTO vo; while (rs.next()) { vo = new VisitorDTO(); // 검색 결과 한 행을 객체로 변환 vo.setId(rs.getInt(1)); vo.setName(rs.getString(2)); vo.setWriteDate(rs.getString(3)); vo.setMemo(rs.getString(4)); list.add(vo); } } catch (SQLException e) { e.printStackTrace(); } finally { MySQLConnect.close(conn); // 연결 닫기 } return list; } public boolean insert(VisitorDTO vo) { Connection conn = MySQLConnect.connect(); // DB 연결 boolean result = true; try (PreparedStatement pstmt = conn.prepareStatement("insert into visitor (name, writedate, memo) values (?, now(), ?)");) { pstmt.setString(1, vo.getName()); // 이름 바인딩 pstmt.setString(2, vo.getMemo()); // 글 내용 바인딩 pstmt.executeUpdate(); // 등록 실행 } catch (SQLException e) { result = false; e.printStackTrace(); } finally { MySQLConnect.close(conn); // 연결 닫기 } return result; // 성공 여부 반환 } public boolean delete(int id) { Connection conn = MySQLConnect.connect(); // DB 연결 boolean result = true; try (PreparedStatement pstmt = conn.prepareStatement("delete from visitor where id = ?");) { pstmt.setInt(1, id); // 삭제할 글 번호 int deleteNum = pstmt.executeUpdate(); // 삭제 실행 if (deleteNum != 1) result = false; } catch (SQLException e) { result = false; e.printStackTrace(); } finally { MySQLConnect.close(conn); // 연결 닫기 } return result; } public boolean update(VisitorDTO vo) { Connection conn = MySQLConnect.connect(); // DB 연결 boolean result = true; try (PreparedStatement pstmt = conn.prepareStatement("update visitor set name = ?, memo = ? where id = ?");) { pstmt.setString(1, vo.getName()); // 수정할 이름 pstmt.setString(2, vo.getMemo()); // 수정할 내용 pstmt.setInt(3, vo.getId()); // 수정할 글 번호 pstmt.executeUpdate(); // 수정 실행 } catch (SQLException e) { result = false; e.printStackTrace(); } finally { MySQLConnect.close(conn); // 연결 닫기 } return result; } }try-with-resources는 사용한 자원을 자동으로 닫는 문법이다
VisitorDAO코드에서try (...)안에 만든Statement와ResultSet은 작업이 끝나면 자동으로 닫힌다.
이런 방식을try-with-resources라고 한다.
DB작업에서는 연결, 명령 객체, 조회 결과처럼 사용 후 닫아야 하는 자원이 많기 때문에 이런 문법을 자주 사용한다.
다만 이 코드에서는Connection객체를try (...)안에 넣지 않았기 때문에,finally블록의MySQLConnect.close(conn)으로 따로 닫는다.
Statement와 PreparedStatement는 SQL 실행 방식이 다르다
Statement는 완성된SQL문자열을 그대로 실행할 때 사용한다.
예를 들어 전체 목록 조회처럼 이미 완성된select문을 실행할 때 사용할 수 있다.
PreparedStatement는?자리를 미리 만들어 두고, 나중에 값을 채워 실행할 때 사용한다.
사용자가 입력한 값이 들어가는SQL은PreparedStatement를 쓰는 편이 더 안전하고 관리하기 쉽다.
PreparedStatement의 ?는 나중에 값을 넣을 자리이다
PreparedStatement에서?는 실제 값을 나중에 넣기 위해 비워 둔 자리이다.
예를 들어insert into visitor (name, writedate, memo) values (?, now(), ?)에서 첫 번째?에는 작성자 이름이 들어가고, 두 번째?에는 글 내용이 들어간다.
이 값은pstmt.setString(1, vo.getName()),pstmt.setString(2, vo.getMemo())처럼 순서대로 채운다.
여기서 숫자1,2는 첫 번째?, 두 번째?를 의미한다.
이 방식은 문자열을 직접 이어 붙이는 방식보다 값이 들어갈 자리가 분명하고, 더 안전하다.
그래서 사용자가 입력한 값을SQL에 넣을 때는 가능하면?자리를 만들고,setString(),setInt()같은 메서드로 값을 채우는 방식이 자주 사용된다.
다만 현재search()예제처럼 검색어를 문자열로 직접 이어 붙이는 방식은 학습 흐름을 보여 주기 위한 단순한 형태이다.
사용자 입력값이 들어가는 실제 코드에서는 검색어도PreparedStatement의?로 바인딩하는 방식이 더 안전하다.
executeQuery()와 executeUpdate()는 실행 목적이 다르다
executeQuery()는select처럼 데이터를 조회할 때 사용한다.
실행 결과로ResultSet이 돌아오고,rs.next()를 사용해 조회된 행을 한 줄씩 읽는다.
그래서listAll(),one(),search()처럼 데이터를 가져오는 메서드에서 사용된다.
반대로executeUpdate()는insert,update,delete처럼 데이터를 변경할 때 사용한다.
실행 결과로 몇 행이 영향을 받았는지 숫자가 돌아온다.
그래서 등록, 수정, 삭제처럼DB내용을 바꾸는 메서드에서 사용된다.
즉, 조회는executeQuery(), 변경은executeUpdate()라고 먼저 구분하면DAO코드가 훨씬 쉽게 읽힌다.
이 코드는 길어 보이지만 메서드별로 보면 단순하다.
listAll()은 전체 목록을 가져온다.
one()은 글 번호에 맞는 글 한 건을 가져온다.
현재 본문 흐름에서는 목록, 검색, 등록, 삭제, 수정 흐름을 중심으로 보지만,one()은 글 한 건을 수정 화면에 불러오거나 상세 조회할 때 사용할 수 있는 메서드라고 이해하면 된다.
이때 글 번호도PreparedStatement의?에 바인딩해서 조회하므로, 어떤 값이 조건에 들어가는지 코드에서 분명하게 보인다.
search()는 내용에 특정 단어가 들어간 글을 찾는다.
insert()는 새 글을 저장한다.
delete()는 글 번호를 기준으로 삭제한다.
update()는 글 번호를 기준으로 이름과 내용을 수정한다.
중요한 점은DAO가DB에서 읽은 행을 바로 화면에 출력하지 않는다는 것이다.
DAO는 읽어 온 값을VisitorDTO객체에 담아Servlet에 돌려준다.
그다음Servlet이 그 목록을request에 담아JSP로 넘긴다.
즉, 역할이 계속 나뉘어 있다.
방명록 예제가 보여 주는 핵심
방명록 예제의 전체 흐름은 이렇게 정리할 수 있다.
사용자가visitorMain.html에서 요청을 보낸다.
VisitorServlet이 요청 종류를 판단한다.
입력값은VisitorDTO에 담긴다.
VisitorDAO가DB에 저장하거나 조회한다.
결과는 다시Servlet으로 돌아온다.
Servlet은 결과 목록이나 메시지를request에 담는다.
마지막으로visitorView.jsp가 화면에 보여 준다.
즉, 전체 흐름은 이렇다.
사용자 입력 →Servlet→VisitorDTO같은 데이터 객체에 값 묶기 →DAO가DB처리 → 결과를request에 담기 →JSP출력
이 흐름이 이해되면 뒤에서 나오는 계산 예제의request attribute, 회원 정보 객체 전달, 최종 미팅 예제도 훨씬 자연스럽게 이어진다.
Servlet과JSP역할을 구분하고,DTO와DAO가 왜 필요한지도 보았다.
그다음으로 꼭 잡아야 하는 것이 데이터가 어디서 왔고 어디에 담기는가이다.
여기서 가장 많이 헷갈리는 것이request.getParameter()와request.setAttribute()이다.
이름도 비슷하고 둘 다request를 쓰기 때문에 처음에는 같은 느낌으로 보일 수 있다.
하지만 둘은 출발점이 다르다.
먼저request parameter는 사용자가 보낸 값이다.
폼에 입력한 이름, 숫자, 연산자, 검색어 같은 값이 여기에 해당한다.
즉, 브라우저가 서버로 가져온 원래 입력값이다.
그래서Servlet에서는 사용자가 보낸 값을 읽을 때request.getParameter()를 쓴다.
쉽게 말하면parameter는 사용자가 들고 온 짐이라고 보면 된다.
반대로request attribute는 서버가 처리한 뒤 새로 담아 둔 값이다.
사용자가 처음부터 보낸 값이 아니라, 서버가 계산하거나 검사한 뒤 다음 화면에 보여 주려고 넣어 둔 데이터이다.
예를 들어 계산 결과, 오류 메시지, 조회 목록, 객체 같은 값이 여기에 해당한다.
그래서Servlet에서는request.setAttribute()를 써서 값을 담는다.
쉽게 말하면attribute는 서버가 추가로 넣어 준 결과물이라고 보면 된다.
즉, 둘의 차이는 아주 분명하다.
parameter는 사용자가 처음에 보낸 값이다.
브라우저에서 서버로 넘어온 입력 데이터이다.
attribute는 서버가 처리한 뒤 다음 화면에 넘기려고 담아 둔 값이다.
즉, 처리 결과나 안내 메시지 같은 값이다.
정리하면 입력은parameter, 처리 결과는attribute이다.
이 차이를 먼저 잡아 두면 왜 입력을 읽을 때와 결과를 넘길 때 메서드가 다른지 자연스럽게 이해된다.
왜 둘을 굳이 나누어 생각해야 하는가
이걸 따로 구분하지 않으면
Servlet코드를 볼 때 계속 헷갈리게 된다.
예를 들어 사용자가10과20을 입력해서 서버로 보냈다고 하자.
이 두 값은 사용자가 처음에 보낸 값이므로parameter이다.
그런데 서버가 계산해서30이라는 결과를 만들었다면, 이30은 사용자가 보낸 값이 아니다.
서버가 새로 만들어 낸 값이다.
즉, 같은 요청 안에 들어 있는 것처럼 보여도 출발점이 다르다.
그래서num1,num2,oper같은 입력은getParameter()로 읽는다.
result같은 계산 결과는setAttribute()로 담아서 다음 화면으로 넘긴다.
이걸 한 번 머리에 넣어 두면 뒤에서request관련 코드가 훨씬 또렷하게 보인다.
사용자가 입력해서 보낸 값은parameter이다.
서버가 처리해서 넣은 값은attribute이다.
이 기준만 정확히 잡혀도 계산 예제, 검색 예제, 목록 조회 예제를 읽을 때 훨씬 덜 헷갈린다.
4 - 1. 예제 2로 보는 결과 데이터 전달과 오류 처리
계산 예제는
parameter와attribute차이를 가장 분명하게 보여 주기 때문에 아주 좋은 예제이다.
사용자는 숫자와 연산자를 입력해서 보낸다.
그다음Servlet은 계산을 한다.
정상 계산이면 결과값을 만들고, 잘못된 계산이면 오류 메시지를 만든다.
마지막에는 그 결과를JSP에 보여 준다.
즉, 이 예제는 입력값을 읽는 단계와 처리 결과를 넘기는 단계를 동시에 보여 주는 구조이다.
입력 화면은 어떤 값을 보내는가
먼저 입력 화면은
calcForm.html이다.
calcForm.html은 두 개의 숫자와 하나의 연산자를 입력받아서/mvcedu/calc로 보낸다.// calcForm.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>계산 입력</title> </head> <body> <h1>연산할 두 개의 숫자를 입력하고 연산자를 선택하시오.</h1> <hr> <form method="get" action="/mvcedu/calc"> <input type="number" name="num1" placeholder="연산할 숫자1" required><!-- 첫 번째 숫자 --> <select name="oper"><!-- 연산자 선택 --> <option>+</option> <option>-</option> <option>*</option> <option>/</option> </select> <input type="number" name="num2" placeholder="연산할 숫자2" required><!-- 두 번째 숫자 --> <input type="submit" value="계산하기"><!-- 서버로 요청 --> </form> </body> </html>이 화면에서 중요한 것은
name="num1",name="oper",name="num2"이다.
이 이름이 바로 서버에서 값을 꺼낼 때 사용하는 기준이 된다.
사용자가 입력한 첫 번째 숫자는num1, 두 번째 숫자는num2, 연산자는oper라는 이름으로 서버에 전달된다.
이 값들은 모두 사용자가 보낸 것이므로parameter이다.
즉, 이 화면은 계산 결과를 직접 만들지 않는다.
단지 계산에 필요한 재료를 서버로 보내는 역할만 한다.
입력 화면은 결과를 만드는 파일이 아니라, 결과를 만들 재료를 보내는 파일이다.
Servlet은 어떻게 입력을 읽고 결과를 만드는가
이제
CalcServlet을 보면parameter와attribute차이가 코드에서 바로 보인다.// CalcServlet.java package controller; import java.io.IOException; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @WebServlet("/calc") public class CalcServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String q1 = request.getParameter("num1"); // 첫 번째 숫자 읽기 String q2 = request.getParameter("num2"); // 두 번째 숫자 읽기 int num1 = Integer.parseInt(q1); // 문자열을 숫자로 변환 int num2 = Integer.parseInt(q2); // 문자열을 숫자로 변환 String oper = request.getParameter("oper"); // 연산자 읽기 String view = ""; // 이동할 화면 저장 if (oper.equals("/") && num2 == 0) { request.setAttribute("msg", "나눗셈 연산시 두 번째 숫자는 0 일 수 없습니다!!"); // 오류 메시지 저장 view = "/jspexam/errorResult.jsp"; // 오류 화면 선택 } else { int result = 0; // 계산 결과 저장 switch (oper) { case "+": result = num1 + num2; break; case "-": result = num1 - num2; break; case "*": result = num1 * num2; break; case "/": result = num1 / num2; break; } request.setAttribute("result", result); // 계산 결과 저장 view = "/jspexam/calcResult.jsp"; // 결과 화면 선택 } RequestDispatcher rd = request.getRequestDispatcher(view); rd.forward(request, response); // 선택된 화면으로 이동 } }이 코드를 순서대로 보면
num1,num2,oper는 입력에서 왔기 때문에parameter이다.
반면result,msg는 처리 후에 만들어졌기 때문에attribute이다.
바로 이 차이를 코드가 그대로 보여 준다.
앞에서parameter는 사용자가 브라우저에서 보낸 값이라고 정리했다.
여기서 한 가지 더 중요한 점은request.getParameter()로 읽은 값은 기본적으로 문자열로 다뤄진다는 것이다.
브라우저에서 넘어오는 값은 숫자처럼 보여도 처음에는 문자열이다.
다만 요청에 해당 이름의 값이 없으면 문자열이 아니라null이 나올 수 있다.
그래서 필요한 경우에는 값을 사용하기 전에null여부를 먼저 확인해야 한다.
num1,num2처럼 계산에 사용해야 하는 값은Integer.parseInt()로 숫자로 바꾼 뒤 사용한다.
미팅 예제의 날짜도 마찬가지로 처음에는 문자열로 넘어오고, 저장할 때LocalDateTime.parse()와Timestamp.valueOf()를 거쳐 날짜와 시간 형식으로 바꾼다.
특히view라는 문자열 변수도 눈여겨볼 필요가 있다.
이 변수는 지금 어떤JSP로 갈지 저장하는 역할을 한다.
즉,Servlet은 단순히 계산만 하는 것이 아니라, 상황에 따라 다른 결과 화면으로 보내는 흐름 제어까지 맡고 있다.
결과 JSP는 무엇을 하는가
계산이 정상일 때는
calcResult.jsp가 결과를 보여 준다.// calcResult.jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>계산 결과</title> <style> span { color: #FF0066; } </style> </head> <body> <h2>연산 요청 결과</h2> <hr> 결과(스크립틀릿) : <span><%= request.getAttribute("result") %></span><!-- 결과 출력 --> <br> <br> 결과(EL) : <span>${ requestScope.result }</span><!-- 범위를 적은 출력 --> <br> <br> 결과(EL) : <span>${ result }</span><!-- 범위를 생략한 출력 --> <br> <br> <a href="${ header.referer }">입력화면</a> </body> </html>이 화면에서 중요한 점은 결과를 다시 계산하지 않는다는 것이다.
이미 계산은Servlet에서 끝났다.
이 파일은 그 결과를 꺼내서 보여 주기만 한다.
즉,View역할에 아주 충실한 구조이다.
${ requestScope.result }는request범위에서result를 찾겠다고 범위를 직접 적은 표현이다.
반면${ result }는 범위를 생략한 표현이다.
이 예제에서는Servlet이request에result를 담았기 때문에 두 표현 모두 같은 값을 출력한다.
오류가 났을 때는errorResult.jsp가 메시지를 보여 준다.// errorResult.jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>오류 결과</title> <style> span { color: #FF0066; } </style> </head> <body> <h2>요청을 처리하는 동안 오류가 발생했어요..</h2> <h3>오류의 원인 : <span><%= request.getAttribute("msg") %></span></h3><!-- 오류 메시지 출력 --> <h3>오류의 원인 : <span>${ msg }</span></h3><!-- EL 출력 --> <a href="${ header.referer }">입력화면</a> </body> </html>정상일 때는
result를 화면에 보여 준다.
오류일 때는msg를 화면에 보여 준다.
결국 두JSP모두Servlet이 담아 준 값을 꺼내서 보여 주는View라는 점은 같다.
같은 계산 요청이라도 처리 결과에 따라 흐름이 갈라진다.
정상 계산이면result가request에 담기고calcResult.jsp로 이동한다.
나눗셈에서 두 번째 숫자가0이면msg가request에 담기고errorResult.jsp로 이동한다.
처리 결과에 따라 다른JSP를 선택할 수 있다는 점이 핵심이다.
실행 결과에서는 정상 계산일 때와
0으로 나눌 때 결과가 다르게 나온다.
화면 모양만 달라진 것이 아니라,Servlet이 상황을 판단해서 다른 값과 다른 화면을 선택한 결과이다.
예제 2가 보여 주는 핵심
이 예제에서 꼭 잡아야 할 것은 네 가지이다.
사용자가 보낸 입력값은parameter이다.
즉,num1,num2,oper는 처음에 브라우저에서 온 값이다.
서버가 만든 계산 결과나 오류 메시지는attribute이다.
즉,result,msg는 처리 뒤에 새로 만들어진 값이다.
Servlet은 입력을 읽고, 처리하고, 어떤 화면으로 보낼지 정한다.
JSP는 전달받은 값을 화면에 보여 준다.
정리하면 예제 2는 입력은parameter, 처리 결과는attribute로 구분해서 생각해야 한다는 점을 가장 분명하게 보여 주는 예제이다.
4 - 2. 실습 문제 - MemberDTO 객체를 request에 담아 JSP로 넘기기
이 실습은 단순히 입력값을 받아 출력하는 문제처럼 보일 수 있다.
하지만 실제로는 사용자가 보낸request parameter를 읽고, 서버가 새로 만든 객체를request attribute로 넘기는 흐름을 연습하는 문제이다.
이 실습에서는memberForm.html에서 이름, 전화번호, 계정, 비밀번호를 입력해서 보낸다.
이 값들은 모두 사용자가 처음 보낸 입력값이므로request parameter이다.
그리고MemberServlet은 이 값을 바로 화면으로 보내지 않고,MemberDTO객체를 새로 만들어 그 안에 저장한 뒤request.setAttribute()로memberView.jsp에 넘긴다.
즉, 입력은request parameter이고, 서버가 새로 만든 객체는request attribute라는 흐름이 그대로 들어 있다.
사용자가 입력한 값은 먼저
MemberServlet에서request.getParameter()로 읽힌다.
그다음 네 개의 입력값은MemberDTO객체 하나로 묶인다.
MemberServlet은 이 객체를request에member라는 이름으로 저장하고, 같은 요청 흐름 안에서memberView.jsp로 넘긴다.
그래서 입력값은parameter, 서버가 만든 객체는request attribute로 구분해서 볼 수 있다.
문제
먼저 문제 원본은 아래와 같다.
문제에서 실제로 구현해야 하는 조건만 다시 정리하면 이렇다.
MemberDTO클래스를 작성한다.
회원 이름, 전화번호, 계정, 비밀번호를 저장할 멤버변수를 만든다.
각 멤버변수에 대한getter,setter를 작성한다.
memberForm.html에서 회원 정보를 입력받는다.
POST방식으로MemberServlet에 요청을 보낸다.
입력값이 비어 있으면"없음"으로 처리한다.
전달받은 값을MemberDTO객체에 저장한다.
저장한 객체를HttpServletRequest에 담는다.
memberView.jsp로forward()한다.
memberView.jsp에서는 스크립틀릿과EL방식으로 모두 출력한다.
MemberDTO 클래스 작성
가장 먼저 해야 할 일은 회원 정보를 담을 객체를 만드는 것이다.
이 문제에서는 이름, 전화번호, 계정, 비밀번호가 항상 함께 움직인다.
그래서 각각을 따로따로 넘기기보다 하나의 객체로 묶는 편이 훨씬 자연스럽다.
이 역할을 하는 클래스가MemberDTO이다.// MemberDTO.java package model.domain; public class MemberDTO { private String name; // 회원 이름 private String phone; // 전화번호 private String account; // 계정 private String password; // 비밀번호 public String getName() { return name; // 이름 반환 } public void setName(String name) { this.name = name; // 이름 저장 } public String getPhone() { return phone; // 전화번호 반환 } public void setPhone(String phone) { this.phone = phone; // 전화번호 저장 } public String getAccount() { return account; // 계정 반환 } public void setAccount(String account) { this.account = account; // 계정 저장 } public String getPassword() { return password; // 비밀번호 반환 } public void setPassword(String password) { this.password = password; // 비밀번호 저장 } }이 클래스가 중요한 이유는 이름과 전화번호와 계정과 비밀번호를 하나씩 흩어 놓지 않고 회원 정보 한 건이라는 단위로 묶어 주기 때문이다.
즉, 나중에Servlet에서 값을 다 읽은 뒤MemberDTO하나에 담아 넘길 수 있게 된다.
memberForm.html 작성
이제 사용자가 값을 입력할 화면을 만든다.
이 화면은 결과를 판단하지 않는다.
단지 이름, 전화번호, 계정, 비밀번호를 입력받아서 서버로 보내는 출발점 역할만 한다.// memberForm.html <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>회원 정보 입력</title> <style> * { box-sizing: border-box; } body { font-family: Arial, sans-serif; margin: 40px; } h1 { font-size: 48px; font-weight: bold; margin-bottom: 20px; } form { width: 350px; } input[type="text"], input[type="password"] { width: 100%; padding: 12px 14px; margin-bottom: 12px; border: 2px solid #ccc; border-radius: 16px; font-size: 22px; } input[type="submit"], input[type="reset"] { padding: 10px 18px; font-size: 22px; border-radius: 14px; border: 1px solid #999; cursor: pointer; margin-right: 10px; } </style> </head> <body> <h1>회원 정보를 입력하십시오.</h1> <hr> <form method="post" action="/mvcedu/member"> <input type="text" name="name" placeholder="이름을 입력하세요"><!-- 이름 입력 --> <input type="text" name="phone" placeholder="전화번호를 입력하세요"><!-- 전화번호 입력 --> <input type="text" name="account" placeholder="계정을 입력하세요"><!-- 계정 입력 --> <input type="password" name="password" placeholder="패스워드를 입력하세요"><!-- 비밀번호 입력 --> <div class="btn-area"> <input type="submit" value="등록"><!-- 서버로 전송 --> <input type="reset" value="재작성"><!-- 입력 초기화 --> </div> </form> </body> </html>여기서 중요한 부분은
method="post"와action="/mvcedu/member"이다.
사용자가 입력한 값은POST방식으로MemberServlet에 전달된다.
그리고 각 입력칸의name속성이 바로Servlet에서 값을 꺼낼 때 기준이 된다.
MemberServlet 작성
이제 사용자가 보낸 입력값을 받아서 처리하는
Servlet을 만든다.
이 파일이 이 실습의 중심이다.
왜냐하면parameter를 읽고, 비어 있는 값은"없음"으로 바꾸고,MemberDTO에 저장하고, 다시request에 담아JSP로 넘기는 흐름이 전부 여기 들어 있기 때문이다.// MemberServlet.java package controller; import java.io.IOException; import jakarta.servlet.RequestDispatcher; 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 model.domain.MemberDTO; @WebServlet("/member") public class MemberServlet extends HttpServlet { private static final long serialVersionUID = 1L; private String getValueOrDefault(String value) { if (value == null || value.trim().isEmpty()) { return "없음"; // 비어 있으면 없음 처리 } return value; // 값이 있으면 그대로 반환 } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); // 한글 처리 String name = getValueOrDefault(request.getParameter("name")); // 이름 추출 String phone = getValueOrDefault(request.getParameter("phone")); // 전화번호 추출 String account = getValueOrDefault(request.getParameter("account")); // 계정 추출 String password = getValueOrDefault(request.getParameter("password")); // 비밀번호 추출 MemberDTO dto = new MemberDTO(); // 회원 정보 객체 생성 dto.setName(name); // 이름 저장 dto.setPhone(phone); // 전화번호 저장 dto.setAccount(account); // 계정 저장 dto.setPassword(password); // 비밀번호 저장 request.setAttribute("member", dto); // request에 객체 저장 RequestDispatcher rd = request.getRequestDispatcher("/jspexam/memberView.jsp"); rd.forward(request, response); // JSP로 이동 } }먼저
request.getParameter()로 사용자가 입력한 네 가지 값을 읽는다.
이 값들은 사용자가 폼에서 직접 보낸 것이므로 모두parameter이다.
그다음getValueOrDefault()메서드로 값이 비어 있는지 검사한다.
만약 입력하지 않았거나 공백뿐이면"없음"으로 바꾼다.
즉, 이 문제에서 요구한 빈값 처리를 여기서 담당한다.
trim()은 문자열 앞뒤의 공백을 제거한다.
isEmpty()는 문자열이 비어 있는지 확인한다.
그래서 공백만 입력한 경우도 값이 없는 것으로 처리할 수 있다.
@Override는 부모 클래스에 있는 메서드를 자식 클래스에서 다시 작성했다는 표시이다.
여기서는HttpServlet의doPost()를 현재Servlet에 맞게 다시 작성한다는 뜻이다.
그 다음MemberDTO객체를 만들고, 읽어 온 값을 전부 저장한다.
여기서 중요한 점은 문자열 네 개를 따로따로 넘기지 않고, 회원 정보 객체 하나로 묶었다는 것이다.
마지막으로request.setAttribute("member", dto)를 호출한다.
이 줄이 이 실습에서 가장 중요하다.
사용자가 보낸parameter를 그대로 넘기는 것이 아니라, 서버가 새로 만든MemberDTO객체를attribute로 담는 줄이기 때문이다.
즉, 입력은parameter이고, 서버가 만든 객체는attribute이다.
memberView.jsp 작성
이제
Servlet이 넘겨 준MemberDTO객체를 화면에 출력할JSP를 만든다.
문제 조건에서는 스크립틀릿과EL방식으로 모두 출력하라고 했기 때문에 두 가지를 모두 보여 준다.// memberView.jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="model.domain.MemberDTO" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>회원 정보 출력</title> </head> <body> <% MemberDTO dto = (MemberDTO) request.getAttribute("member"); // request에 저장된 객체 꺼내기 %> <h1>회원 정보</h1> <hr> <ul> <li>회원 이름 : <%= dto.getName() %></li><!-- 스크립틀릿 출력 --> <li>회원 계정 : <%= dto.getAccount() %></li><!-- 스크립틀릿 출력 --> <li>회원 암호 : <%= dto.getPassword() %></li><!-- 스크립틀릿 출력 --> <li>회원 전화번호 : <%= dto.getPhone() %></li><!-- 스크립틀릿 출력 --> </ul> <ul> <li>회원 이름 : ${ member.name }</li><!-- EL 출력 --> <li>회원 계정 : ${ member.account }</li><!-- EL 출력 --> <li>회원 암호 : ${ member.password }</li><!-- EL 출력 --> <li>회원 전화번호 : ${ member.phone }</li><!-- EL 출력 --> </ul> </body> </html>앞에서
attribute는 서버가 처리한 결과를 담아 다음 화면으로 넘기는 값이라고 정리했다.
그런데request.getAttribute()로 값을 꺼낼 때는 형변환이 필요할 수 있다.
request에는 문자열뿐 아니라MemberDTO, 목록 객체처럼 여러 종류의 값을 담을 수 있다.
그래서getAttribute()는 꺼낸 값을 바로MemberDTO라고 확정해서 돌려주지 않고, 가장 넓은 타입인Object로 돌려준다.
따라서MemberDTO dto = (MemberDTO) request.getAttribute("member");처럼 다시MemberDTO타입이라고 알려 주는 형변환이 필요하다.
반면EL에서는${ member.name }처럼 더 간단하게 접근할 수 있다.
위쪽ul은 스크립틀릿 방식이다.
자바 코드로request.getAttribute("member")를 꺼낸 뒤getter를 호출해서 출력한다.
아래쪽ul은EL방식이다.
${ member.name }처럼 더 간단한 표현으로 객체의 각 값을 출력한다.
이 두 방식 모두 결국은 같은request attribute객체를 읽는 방식이다.
다만 비밀번호를 화면에 그대로 출력하는 것은 학습용 예제에서 값 전달을 확인하기 위한 방식이다.
실제 서비스에서는 비밀번호를 그대로 화면에 보여 주면 안 된다.
실행 결과 확인
정상 입력 결과에서는 회원 이름, 전화번호, 계정, 비밀번호가 결과 화면에 출력된다.
사용자가 입력한 값은MemberServlet에서 읽힌 뒤MemberDTO객체에 저장되고, 다시request에 담겨memberView.jsp로 전달된다.
빈값 입력 결과에서는 일부 입력값을 비운 상태로 전송했을 때"없음"으로 출력된다.
MemberServlet은 비어 있는 값을 그대로 두지 않고"없음"으로 바꾼 뒤MemberDTO객체에 저장한다.
실습하면서 확인할 점
이 실습에서 꼭 기억해야 하는 핵심은 세 가지이다.
사용자가 폼에서 보낸 값은parameter이다.
MemberDTO객체는 서버가 새로 만든 결과물이다.
그 객체를request.setAttribute()로 담아 다음 화면에 넘긴다.
정리하면 이 실습은 단순히 회원 정보를 출력하는 문제가 아니다.
request parameter와request attribute차이를 객체 전달 방식으로 확장해서 확인하는 실습 문제라고 이해하면 된다.
request와session은 둘 다 값을 잠깐 담아 두는 데 쓰일 수 있어서 처음에는 비슷해 보인다.
그래서 둘 다 데이터를 저장하는 거면 뭐가 다른지 헷갈리기 쉽다.
하지만 이 둘의 가장 큰 차이는 얼마나 오래 살아 있느냐이다.
먼저request는 한 번의 요청 동안만 살아 있는 저장 공간이다.
브라우저가 서버에 요청을 보내고, 서버가 응답을 마치면 그 요청은 끝난다.
request도 바로 그 요청과 함께 생겼다가, 그 요청이 끝나면 같이 끝난다.
즉, 짧게 쓰고 끝나는 데이터에 잘 맞는다.
계산 결과처럼 한 번 보여 주고 끝나는 값, 오류 메시지처럼 이번 화면에서만 잠깐 보여 주면 되는 값은request에 잘 어울린다.
반대로session은 같은 사용자의 여러 요청 사이에서도 값을 유지할 수 있는 저장 공간이다.
한 번 요청하고 끝나는 것이 아니라, 같은 사용자가 다음 요청을 보내더라도 이전에 저장한 값을 다시 꺼내 쓸 수 있다.
즉, 여러 번 이어서 기억해야 하는 데이터에 잘 맞는다.
장바구니처럼 한 번 클릭하고 끝나는 것이 아니라, 다음 클릭에서도 이전 값이 살아 있어야 하는 기능은session이 필요하다.
쉽게 비유하면 이렇게 이해하면 된다.
request는 한 번 쓰고 버리는 메모에 가깝다.
이번 요청에서만 잠깐 확인하고 끝난다.
반면session은 같은 사용자가 계속 꺼내 보는 보관함에 가깝다.
한 번 넣어 두면 다음 요청에서도 다시 꺼내 쓸 수 있다.
다만session도 영원히 남는 것은 아니고, 시간이 지나거나 서버가 세션을 끝내면 함께 사라질 수 있다.
즉,request는 한 번 요청하고 응답하면 끝나는 값에 적합하고,session은 여러 요청에서도 계속 유지되어야 하는 값에 적합하다.
왜 이 차이를 꼭 알아야 하는가
이 차이를 모르면 어떤 값을 어디에 넣어야 하는지 계속 헷갈리게 된다.
예를 들어 계산 결과처럼 이번 화면에서 한 번 보여 주면 끝나는 값을session에 넣으면 필요 이상으로 오래 남게 된다.
반대로 장바구니처럼 다음 요청에서도 계속 이어져야 하는 값을request에 넣으면, 요청이 끝나는 순간 사라져서 다음 클릭 때 이어지지 않는다.
즉, 값의 성격에 맞는 저장 공간을 골라야 한다.
가장 쉬운 판단 기준은 이것이다.
이번 요청이 끝나면 같이 사라져도 되는 값인가.
그러면request가 맞다.
다음 요청에서도 계속 살아 있어야 하는 값인가.
그러면session이 맞다.
이 기준만 정확히 잡아도 뒤에서 나오는 예제를 훨씬 쉽게 읽을 수 있다.
request는 언제 쓰는가
request는 이번 요청 안에서만 의미가 있는 값에 잘 맞는다.
계산 결과, 오류 메시지, 검색 결과, 조회 목록처럼 이번 화면에서 보여 주고 끝나는 값이 여기에 해당한다.
중요한 점은request에 담긴 값은 다음 요청으로 자동으로 이어지지 않는다는 것이다.
이번 요청에서Servlet이 값을 넣고, 같은 요청 흐름 안에서JSP가 그것을 꺼내 보는 구조라고 이해하면 된다.
즉,request는 이번 한 번만 잠깐 전달하는 용도에 가깝다.
session은 언제 쓰는가
session은 다음 요청에서도 계속 기억해야 하는 값에 잘 맞는다.
로그인 상태, 장바구니 정보, 같은 사용자의 선택 상태처럼 다음 요청에서도 이어져야 하는 값이 여기에 해당한다.
session은 같은 사용자 기준으로 이어진다.
그래서 사용자가 브라우저에서 여러 번 요청을 보내더라도, 서버 쪽에서는 같은 사용자의session안에 들어 있는 값을 다시 꺼내 쓸 수 있다.
즉,session은 한 번의 요청을 넘어서 이어지는 데이터를 보관하는 역할이다.
상품 객체가
HttpSession안에 저장되면, 현재 요청이 끝나도 장바구니 정보가 바로 사라지지 않는다.
같은 사용자가 다음 요청을 보내면 서버는 같은session에서 장바구니 객체를 다시 꺼낼 수 있다.
이 구조 때문에 상품 수량이 계속 누적될 수 있다.
5 - 1. 예제 3으로 보는 session 기반 장바구니 구조
장바구니 예제는
request와session차이를 가장 쉽게 보여 주는 예제이다.
왜냐하면 장바구니는 한 번 클릭하고 끝나는 기능이 아니라, 여러 번의 클릭이 계속 이어져야 하기 때문이다.
즉, 사과를 담은 뒤 그다음 요청에서 바나나를 담아도 사과 정보가 살아 있어야 하는 구조이다.
바로 이런 경우에session이 필요하다.
입력 화면은 어떤 역할을 하는가
먼저 입력 화면은
product.html이다.
product.html은 세 가지 상품 이미지를 보여 주고, 사용자가 이미지를 클릭하면/mvcedu/cart?pid=...형식으로 요청을 보낸다.
또 장바구니 비우기를 누르면/mvcedu/cart로 요청을 보낸다.// product.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>상품 선택</title> </head> <body> <h1>구매하고자 하는 상품을 선택하십시오.</h1> <hr> <a href="/mvcedu/cart?pid=p001"><img src="images/apple.gif" width="150" height="120"></a><!-- 사과 선택 --> <a href="/mvcedu/cart?pid=p002"><img src="images/banana.jpg" width="150" height="120"></a><!-- 바나나 선택 --> <a href="/mvcedu/cart?pid=p003"><img src="images/halabong.jpg" width="150" height="120"></a><!-- 한라봉 선택 --> <hr> <a href="/mvcedu/cart">장바구니 비우기</a><!-- 장바구니 비우기 --> </body> </html>이 화면에서 중요한 점은
form을 사용하지 않았더라도 결국은 사용자의 선택을 서버로 보낸다는 점이다.
사과 이미지를 누르면pid=p001이 전달되고, 바나나를 누르면pid=p002, 한라봉을 누르면pid=p003이 전달된다.
즉, 사용자가 지금 어떤 상품을 선택했는지를 서버가 알 수 있게 만들어 둔 구조이다.
그리고 장바구니 비우기 링크는pid없이/cart만 요청한다.
이 차이도 중요하다.
상품을 선택했을 때는 어떤 상품인지 전달되고, 비우기를 눌렀을 때는 상품 번호 없이 들어오기 때문에Servlet이 비우기 요청으로 구분할 수 있다.
ProductDTO는 왜 필요한가
session은 여러 요청 동안 값을 유지하는 저장 공간이다.
하지만 저장 공간만 있다고 해서 장바구니 기능이 바로 완성되는 것은 아니다.
그 안에 무엇을 어떤 형태로 넣어 둘지도 정해야 한다.
장바구니에서는 사과 개수, 바나나 개수, 한라봉 개수가 함께 움직인다.
이 값을 각각 따로 저장할 수도 있다.
하지만 그렇게 하면 관련 있는 데이터가 흩어져서 관리하기가 불편해진다.
그래서 이 예제에서는 상품 개수를 하나의 객체로 묶어서 저장한다.
그 객체가 바로ProductDTO이다.
즉, 이 예제에서session은 장바구니를 오래 기억하는 보관함이고,ProductDTO는 그 보관함 안에 넣어 두는 장바구니 데이터 묶음이다.
쉽게 말하면session은 저장하는 곳이고,DTO는 저장할 데이터를 정리한 객체이다.// ProductDTO.java package model.domain; public class ProductDTO { private int apple; // 사과 개수 private int banana; // 바나나 개수 private int halabong; // 한라봉 개수 public int getApple() { return apple; // 사과 개수 반환 } public void setApple(int apple) { this.apple += apple; // 사과 개수 누적 } public int getBanana() { return banana; // 바나나 개수 반환 } public void setBanana(int banana) { this.banana += banana; // 바나나 개수 누적 } public int getHalabong() { return halabong; // 한라봉 개수 반환 } public void setHalabong(int halabong) { this.halabong += halabong; // 한라봉 개수 누적 } }이 코드를 보면 사과 개수, 바나나 개수, 한라봉 개수를 한 객체 안에 모아 둔 구조이다.
특히setApple(),setBanana(),setHalabong()을 봐야 한다.
보통set메서드라고 하면 값을 그냥 덮어쓴다고 생각하기 쉽다.
그런데 이 예제에서는this.apple += apple;처럼 기존 값에 새 값을 더한다.
그래서 사과를 세 번 클릭하면1,2,3처럼 계속 누적된다.
모든set메서드가 원래 이렇게 누적되는 것은 아니다.
이 예제에서는 장바구니 기능에 맞게 누적되도록 만든 것이다.
ProductServlet은 왜 session을 쓰는가
이제
ProductServlet을 보면 왜session이 필요한지가 코드에서 직접 보인다.// ProductServlet.java package controller; import java.io.IOException; 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; import model.domain.ProductDTO; @WebServlet("/cart") public class ProductServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); // 현재 사용자의 session 가져오기 if (session.getAttribute("cart") == null) { session.setAttribute("cart", new ProductDTO()); // 처음이면 장바구니 생성 } ProductDTO p = (ProductDTO) session.getAttribute("cart"); // 기존 장바구니 꺼내기 String pid = request.getParameter("pid"); // 상품 번호 읽기 if (pid != null) { if (pid.equals("p001")) p.setApple(1); // 사과 1개 추가 else if (pid.equals("p002")) p.setBanana(1); // 바나나 1개 추가 else if (pid.equals("p003")) p.setHalabong(1); // 한라봉 1개 추가 } else { session.removeAttribute("cart"); // 장바구니 비우기 } request.getRequestDispatcher("/jspexam/productView.jsp").forward(request, response); // 결과 화면으로 이동 } }먼저
request.getSession()으로 현재 사용자의session을 가져온다.
즉, 이 사용자가 계속 쓰고 있는 보관함을 가져오는 것이다.
그다음session.getAttribute("cart") == null인지 검사한다.
아직 장바구니가 만들어진 적이 없는지 확인하는 것이다.
처음 방문한 사용자라면cart가 없을 수 있다.
그래서 없으면new ProductDTO()를 만들어session안에 넣는다.
즉, 장바구니를 처음 생성하는 단계이다.
그 다음ProductDTO p = (ProductDTO) session.getAttribute("cart");를 실행한다.
이 코드는session안에 저장되어 있는 장바구니 객체를 꺼내는 것이다.
session에서 꺼낸 값은 바로ProductDTO처럼 보이지 않기 때문에, 다시ProductDTO타입이라고 알려 주는 형변환 과정이 필요하다.
중요한 점은 이 객체가 이번 요청에서 새로 생긴 것이 아니라, 이전 요청에서 만들어 둔 것을 다시 꺼낼 수도 있다는 점이다.
바로 이 점 때문에 값이 이어질 수 있다.
이제pid를 읽는다.
사용자가 어떤 상품을 클릭했는지 확인하는 단계이다.
pid가p001이면 사과,p002이면 바나나,p003이면 한라봉이다.
그에 맞는set메서드를 호출해서 개수를1씩 누적한다.
만약pid가 없다면, 이건 상품 추가 요청이 아니라 장바구니 비우기 요청이다.
그래서session.removeAttribute("cart")를 호출해서cart자체를 없애 버린다.
즉, 비우기 요청일 때는 장바구니를 통째로 지우는 구조이다.
이 코드에서는 먼저cart가 없으면 장바구니 객체를 만들고, 이후pid가 없으면 다시 삭제한다.
동작 흐름을 단순하게 보기 위한 예제 구조이며, 실제로는pid여부를 먼저 확인해서 비우기 요청을 먼저 처리하도록 바꿀 수도 있다.
장바구니처럼 계속 유지되어야 하는 기능은request가 아니라session을 써야 한다.
productView.jsp는 어떤 값을 보여 주는가
이제 결과
View인productView.jsp를 보면,session에 저장된 값을 화면에 어떻게 보여 주는지 알 수 있다.// productView.jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="model.domain.ProductDTO"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>장바구니 결과</title> </head> <body> <% if (session.getAttribute("cart") != null) { %> <h3>선택된 상품 정보는 다음과 같습니다.</h3> <hr> <% ProductDTO cart1 = (ProductDTO) session.getAttribute("cart"); // 세션에서 장바구니 꺼내기 %> 선택된 사과의 개수 : <%= cart1.getApple() %> <br> 선택된 바나나의 개수 : <%= cart1.getBanana() %> <br> 선택된 한라봉의 개수 : <%= cart1.getHalabong() %> <br> <hr> 선택된 사과의 개수 : ${ sessionScope.cart.apple } <br> 선택된 바나나의 개수 : ${ sessionScope.cart.banana } <br> 선택된 한라봉의 개수 : ${ sessionScope.cart.halabong } <br> <% } else { %> <h3>장바구니를 비웠습니다.</h3> <% } %> <hr> <a href="${ header.referer }">상품 선택화면</a> </body> </html>이
JSP는 장바구니를 직접 계산하지 않는다.
몇 개가 들어 있는지 직접 세지 않는다.
그 일은 이미Servlet과ProductDTO가 해 두었다.
이 파일은session안에 저장된 장바구니 객체를 꺼내서, 그 안에 들어 있는 값을 화면으로 보여 준다.
${ sessionScope.cart.apple }은session범위에서cart객체를 찾고, 그 안의apple값을 읽는 표현이다.
겉으로는apple변수에 바로 접근하는 것처럼 보이지만, 실제로는getApple()같은getter를 통해 값을 읽는다고 이해하면 된다.
즉, 이 파일도 여전히View역할에 집중하고 있다.
상품을 여러 번 클릭할수록 수량이 누적되고, 장바구니 비우기를 누르면 저장된 값이 사라진다.
수량이 유지되는 이유는 상품 개수가request가 아니라session에 저장되어 있기 때문이다.
이 흐름은request와session차이를 실제 기능으로 보여 주는 좋은 예시이다.
예제 3이 보여 주는 핵심
이 예제에서 꼭 잡아야 할 것은 네 가지이다.
request는 한 번의 요청이 끝나면 같이 사라진다.
그래서 장바구니처럼 이어지는 데이터에는 맞지 않는다.
session은 같은 사용자의 다음 요청에서도 값을 다시 꺼내 쓸 수 있다.
그래서 장바구니처럼 누적되어야 하는 데이터에 잘 맞는다.
ProductDTO는 관련 있는 값을 하나의 객체로 묶고, 누적해서 관리하는 역할을 한다.
productView.jsp는 이미session에 저장된 값을 꺼내서 보여 준다.
정리하면 예제 3의 핵심은 장바구니처럼 여러 요청에서도 값이 유지되어야 하는 기능은session을 써야 한다는 점이다.
이 차이를 정확히 이해하면, 로그인 상태나 사용자 정보 유지 같은 개념도 훨씬 쉽게 연결할 수 있다.
이 예제는 지금까지 따로 보았던
MVC개념을 한 화면 안에서 한꺼번에 보여 주는 종합 예제이다.
미팅 글 목록 조회, 등록, 검색, 수정, 삭제를 처리하고, 여기에 댓글 등록과 댓글 목록 조회까지 함께 들어 있다.
즉, 이 예제는 화면을 보여 주는 파일, 요청을 나누는 파일, 데이터를 담는 파일,DB를 처리하는 파일이 어떻게 역할을 나누는지 실제 코드로 확인하게 해 주는 마지막 정리 예제이다.
앞에서는Servlet,JSP,DTO,DAO,request,session같은 개념을 하나씩 나누어 보았다.
하지만 실제 프로그램에서는 그 개념들이 따로 움직이지 않는다.
하나의 기능 안에서 같이 연결되어 동작한다.
이 예제는 바로 그 연결된 흐름을 보여 준다.
초보자 입장에서는 파일 수가 갑자기 많아 보여서 복잡하게 느껴질 수 있다.
하지만 실제로는 반대이다.
한 파일이 모든 일을 다 하지 않도록 역할을 나누어 두었기 때문에 오히려 흐름이 정리되어 있다.
이 점을 먼저 잡고 들어가면 훨씬 덜 헷갈린다.
이 예제를 먼저 큰 흐름으로 보면
사용자는
meetingView.jsp화면에서 목록을 보고, 새 미팅을 등록하고, 이름으로 검색하고, 기존 글을 수정하거나 삭제하고, 댓글도 달 수 있다.
즉, 사용자 입장에서는 하나의 화면 안에서 여러 기능을 쓰는 것처럼 보인다.
하지만 서버 안에서는 역할이 분명히 나뉜다.
meetingView.jsp는 사용자에게 보이는 화면을 만든다.
입력창과 버튼을 보여 주고, 결과 목록도 화면에 출력한다.
MeetingServlet은 어떤 요청이 들어왔는지 보고 등록인지, 검색인지, 수정인지, 삭제인지, 댓글 처리인지 구분한다.
MeetingDTO,ReplyDTO는 필요한 데이터를 객체에 담는다.
MeetingDAO는 실제DB조회, 등록, 수정, 삭제를 처리한다.
쉽게 말하면 이렇게 이해하면 된다.
화면에서 요청이 시작되고,Servlet이 그 요청을 해석하고, 필요한 값을 객체에 담고,DAO가 실제 데이터를 처리한 뒤, 마지막에 다시 화면으로 결과를 보여 준다.
댓글은 여기서 조금 다르게, 화면 전체를 다시 보여 주기보다 필요한 결과만JSON으로 바로 돌려준다.
즉, 전체 흐름은 이렇게 정리할 수 있다.
meetingView.jsp→MeetingServlet→MeetingDTO또는ReplyDTO→MeetingDAO→DB→ 다시 화면 또는JSON응답
meeting 테이블과 reply 테이블을 먼저 이해하기
최종 예제는
meeting테이블과reply테이블을 사용한다.
코드를 읽기 전에 어떤 데이터가 저장되는지 먼저 잡아 두면DAO코드가 훨씬 쉽게 보인다.
meeting테이블은 미팅 글 자체를 저장한다.
글 번호인id, 미팅 대상 이름인name, 미팅 목적이나 내용인title, 날짜와 시간인meetingdate가 핵심이다.
reply테이블은 댓글을 저장한다.
댓글 번호인id, 댓글 작성자 이름인name, 댓글 내용인content, 그리고 어떤 미팅 글에 달린 댓글인지 알려 주는refid가 핵심이다.
여기서refid가 중요하다.
댓글은 혼자 따로 존재하는 데이터가 아니라 특정 미팅 글에 연결되어야 한다.
그래서reply.refid에는 원본 미팅 글의id가 들어간다.
이 연결이 있어야 특정 글의 댓글 목록만 따로 조회할 수 있다.
JSON 응답은 왜 따로 설명해야 하는가
앞의 예제들은 대부분
Servlet에서JSP로forward()하는 방식이었다.
즉, 서버가 처리한 뒤 결과 화면 전체를 다시 보여 주는 흐름이다.
그런데 최종 예제의 댓글 기능은 조금 다르다.
댓글을 등록하거나 댓글 목록을 가져올 때는 화면 전체를 다시 이동하지 않는다.
대신 필요한 데이터만 서버에 요청하고, 서버는 그 결과를JSON형태로 바로 돌려준다.
JSON은 데이터를 주고받기 위한 가벼운 문자열 형식이라고 생각하면 된다.
화면 전체를 새로 만들 필요 없이, 댓글 결과처럼 필요한 데이터만 주고받을 때 자주 사용한다.
즉, 이 예제에서는 일반적인forward()흐름과 필요한 데이터만 받는JSON흐름을 함께 볼 수 있다.
댓글 기능은 화면 전체 이동 방식이 아니라, 필요한 데이터만 서버와 주고받는 방식이라는 점이 최종 예제의 중요한 확장 포인트이다.
XMLHttpRequest는 브라우저 화면 전체를 새로고침하지 않고 서버에 요청을 보내는 객체이다.
이 예제에서는 댓글 목록 조회와 댓글 등록 요청을 보낼 때 사용한다.
meetingView.jsp는 요청의 출발점이다
meetingView.jsp는 단순히 결과 목록만 보여 주는 파일이 아니다.
사용자가 새 미팅을 등록하고, 이름으로 검색하고, 기존 글을 수정하거나 삭제하고, 댓글을 작성하거나 댓글 목록을 확인하는 출발점이다.
즉, 화면이면서 동시에 여러 요청을 시작시키는 파일이다.
목록 출력 부분에서는Servlet이request에 담아 준list를 꺼내서 표로 보여 준다.
작성 폼에서는POST방식으로/mvcedu/meeting에 등록 또는 수정 요청을 보낸다.
검색 폼에서는GET방식으로 이름 검색 요청을 보낸다.
삭제 아이콘은 삭제 요청을 만든다.
댓글 아이콘과 제목 클릭은XMLHttpRequest를 사용해 댓글 등록 또는 댓글 목록 요청을 보낸다.
이 파일에서 중요한 부분은action값이다.
새 글을 등록할 때는action=insert가 들어간다.
수정할 때는action값에 수정할 글 번호가 들어간다.
댓글 등록은action=ireply, 댓글 목록 조회는action=lreply로 구분된다.
즉,MeetingServlet이 요청 종류를 구분할 수 있도록 화면에서 필요한 값을 함께 보내는 구조이다.// meetingView.jsp 핵심 흐름 <% ArrayList<MeetingDTO> list = (ArrayList<MeetingDTO>) request.getAttribute("list"); // 목록 꺼내기 if (list != null) { %> <table> <% for (MeetingDTO vo : list) { // 미팅 글 한 건씩 출력 %> <tr> <td class='<%= vo.getId() %>'><%= vo.getName() %></td> <td class='<%= vo.getId() %>' onclick="displayReply(<%= vo.getId() %>)"><%= vo.getTitle() %></td> <td class='<%= vo.getId() %>'><%= vo.getMeetingDate() %></td> <td><a href='/mvcedu/meeting?action=delete&id=<%= vo.getId() %>'>삭제</a></td> <td><button onclick="displayUpdateForm('<%= vo.getId() %>')">수정</button></td> <td><button onclick="insertReply('<%= vo.getId() %>')">댓글</button></td> </tr> <% } %> </table> <% } %> <form method="post" action="/mvcedu/meeting"> <input type="hidden" name="action" value="insert"><!-- 등록 또는 수정 구분 --> 미팅 대상 이름 : <input id="m_name" type="text" name="name"> 미팅 목적 : <textarea id="m_title" name="title"></textarea> 날짜와 시간 : <input id="m_dt" type="datetime-local" name="meetingDate"> <input type="submit" value="등록"> </form>이 핵심 구조만 봐도
meetingView.jsp의 역할이 보인다.
이 파일은 목록을 만들지 않는다.
목록은MeetingServlet과MeetingDAO가 준비한다.
meetingView.jsp는 준비된 목록을 보여 주고, 사용자의 다음 요청이 서버로 갈 수 있게 버튼과 폼을 제공한다.
수정 버튼을 누르면displayUpdateForm()이 실행된다.
위 핵심 코드에는 함수 본문을 줄여서 담았지만, 실제 전체 코드에서는displayUpdateForm()이 선택한 글의 값을 입력 폼에 다시 채운다.
또 숨겨진action값을insert가 아니라 수정할 글 번호로 바꾼다.
그래서 같은 폼을 사용하더라도action=insert이면 새 글 등록이고,action에 글 번호가 들어 있으면 기존 글 수정으로 처리된다.
댓글 기능은XMLHttpRequest로 동작한다.
화면 전체를 다시 요청하지 않고, 서버에 댓글 관련 요청만 따로 보낸다.// meetingView.jsp 댓글 요청 핵심 function displayReply(id) { var xhr = new XMLHttpRequest(); // 댓글 목록 요청 객체 생성 xhr.onload = function () { if (xhr.status == 200) { let jsondoms = JSON.parse(xhr.responseText); // JSON 문자열을 객체로 변환 let replyContent = ""; for (let i in jsondoms) { i = Number(i); replyContent += "[댓글" + (i + 1) + "] 작성자명 : " + jsondoms[i].name + ", 내용 : " + jsondoms[i].content + "\n"; } if (!replyContent) replyContent = "아직 작성된 댓글이 없네요...😂"; window.alert(replyContent); // 댓글 목록 출력 } } xhr.open("GET", "/mvcedu/meeting?action=lreply&id=" + id, true); // 댓글 목록 조회 요청 xhr.send(); } function insertReply(id) { let name = window.prompt("댓글 작성자의 성명을 입력하세요.."); // 댓글 작성자 입력 let content = window.prompt("댓글 내용을 입력하세요.."); // 댓글 내용 입력 let query = "action=ireply&name=" + name + "&content=" + content + "&refid=" + id; var xhr = new XMLHttpRequest(); // 댓글 등록 요청 객체 생성 xhr.onload = function () { if (xhr.status == 200) { let jsondom = JSON.parse(xhr.responseText); // 등록 결과 확인 if (jsondom.result == true) window.alert("댓글 작성이 완료되었습니다."); else window.alert("댓글 작성에 실패했습니다."); } }; xhr.open("GET", "/mvcedu/meeting?" + query, true); // 댓글 등록 요청 xhr.send(); }
displayReply()는 댓글 목록을 요청하고, 응답으로 받은JSON을 화면에서 읽어 알림창으로 보여 준다.
insertReply()는 댓글 작성자와 내용을 입력받아 서버로 보내고, 서버가 돌려준 성공 여부를 확인한다.
이 흐름은forward()로 전체 화면을 다시 보여 주는 방식과 다르다.
필요한 데이터만 주고받는 방식이다.
현재 예제는 흐름을 쉽게 보기 위해 입력값을 쿼리 문자열에 바로 붙인다.
실제 코드에서는 공백, 한글, 특수문자가 안전하게 전달되도록encodeURIComponent()같은 처리를 함께 고려해야 한다.
MeetingDTO와 ReplyDTO 구조
먼저 미팅 글 데이터를 담는 객체를 보자.
관련 있는 값을 하나의 객체로 묶어 주기 때문에 중요하다.// MeetingDTO.java package model.domain; public class MeetingDTO { private int id; // 글 번호 private String name; // 미팅 대상 이름 private String title; // 미팅 목적 private String meetingDate; // 날짜와 시간 public int getId() { return id; // 글 번호 반환 } public void setId(int id) { this.id = id; // 글 번호 저장 } public String getName() { return name; // 이름 반환 } public void setName(String name) { this.name = name; // 이름 저장 } public String getTitle() { return title; // 제목 반환 } public void setTitle(String title) { this.title = title; // 제목 저장 } public String getMeetingDate() { return meetingDate; // 날짜와 시간 반환 } public void setMeetingDate(String meetingDate) { this.meetingDate = meetingDate; // 날짜와 시간 저장 } }
MeetingDTO가 없으면 이름, 제목, 날짜와 시간을 각각 따로 들고 다녀야 한다.
그러면Servlet에서 꺼내고,DAO로 넘기고, 다시 결과를 받을 때 흐름이 지저분해진다.
그래서 이렇게 객체 하나로 묶는다.
즉,MeetingDTO는 미팅 글 한 건을 하나의 단위로 다룰 수 있게 만드는 상자라고 생각하면 된다.
이번에는 댓글 데이터를 담는 객체이다.
미팅 글과 댓글은 성격이 다르기 때문에 객체도 따로 존재한다.// ReplyDTO.java package model.domain; public class ReplyDTO { private int id; // 댓글 번호 private String name; // 댓글 작성자 private String content; // 댓글 내용 private int refid; // 원본 글 번호 public int getId() { return id; // 댓글 번호 반환 } public void setId(int id) { this.id = id; // 댓글 번호 저장 } public String getName() { return name; // 작성자 반환 } public void setName(String name) { this.name = name; // 작성자 저장 } public String getContent() { return content; // 댓글 내용 반환 } public void setContent(String content) { this.content = content; // 댓글 내용 저장 } public int getRefid() { return refid; // 원본 글 번호 반환 } public void setRefid(int refid) { this.refid = refid; // 원본 글 번호 저장 } }여기서 가장 중요한 값은
refid이다.
댓글은 혼자 따로 존재하는 것이 아니라, 어떤 미팅 글에 달린 댓글인지 연결되어 있어야 한다.
바로 그 연결 기준이refid이다.
MeetingServlet은 요청을 어떻게 나누는가
MeetingServlet은 사용자의 요청을 보고 어떤 기능인지 판단하고, 알맞은DAO메서드를 호출한 뒤, 마지막에 다시 화면으로 넘기거나JSON으로 응답한다.// MeetingServlet.java package controller; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; import java.util.List; 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 org.json.simple.JSONArray; import org.json.simple.JSONObject; import model.service.MeetingDAO; import model.domain.MeetingDTO; import model.domain.ReplyDTO; @WebServlet("/meeting") public class MeetingServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String targetName = request.getParameter("targetname"); // 검색 대상 이름 String id = request.getParameter("id"); // 글 번호 String action = request.getParameter("action"); // 요청 종류 MeetingDAO dao = new MeetingDAO(); // DAO 준비 if (targetName == null) { if (action != null && action.equals("delete")) { boolean result = dao.deleteM(Integer.parseInt(id)); // 글 삭제 if (result) request.setAttribute("msg", "글이 성공적으로 삭제되었습니다."); else request.setAttribute("msg", "글이 삭제되지 않았습니다."); } else if (action != null && action.equals("ireply")) { ReplyDTO vo = new ReplyDTO(); // 댓글 등록용 객체 생성 vo.setRefid(Integer.parseInt(request.getParameter("refid"))); vo.setName(request.getParameter("name")); vo.setContent(request.getParameter("content")); boolean result = dao.insertReply(vo); // 댓글 저장 response.setContentType("application/json; charset=utf-8"); PrintWriter out = response.getWriter(); HashMap<String, Boolean> hm = new HashMap<>(); hm.put("result", result); JSONObject obj = new JSONObject(hm); // 결과를 JSON 객체로 만들기 out.print(obj.toJSONString()); out.close(); return; // JSON 응답 후 종료 } else if (action != null && action.equals("lreply")) { List<ReplyDTO> list = dao.listReply(Integer.parseInt(request.getParameter("id"))); // 댓글 목록 조회 response.setContentType("application/json; charset=utf-8"); PrintWriter out = response.getWriter(); JSONArray jsonArray = new JSONArray(); for (ReplyDTO vo : list) { HashMap<String, String> hm = new HashMap<>(); hm.put("id", String.valueOf(vo.getId())); hm.put("name", vo.getName()); hm.put("content", vo.getContent()); JSONObject obj = new JSONObject(hm); // 댓글 한 건을 JSON 객체로 변환 jsonArray.add(obj); } out.print(jsonArray.toJSONString()); out.close(); return; // JSON 응답 후 종료 } request.setAttribute("list", dao.listM()); // 전체 목록 조회 결과 저장 } else { List<MeetingDTO> list = dao.searchM(targetName); // 이름 검색 if (list != null && list.size() == 0) request.setAttribute("msg", targetName + "님을 만나기로한 글이 없습니다."); else request.setAttribute("list", list); } request.getRequestDispatcher("/jspexam/meetingView.jsp").forward(request, response); // 결과 화면으로 이동 } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); // 한글 처리 String action = request.getParameter("action"); // insert 또는 수정할 글 번호 String name = request.getParameter("name"); // 이름 String title = request.getParameter("title"); // 제목 String meetingDate = request.getParameter("meetingDate"); // 날짜와 시간 MeetingDAO dao = new MeetingDAO(); MeetingDTO vo = new MeetingDTO(); // 미팅 글 객체 생성 vo.setName(name); vo.setTitle(title); vo.setMeetingDate(meetingDate); if (action.equals("insert")) { boolean result = dao.insertM(vo); // 새 글 등록 if (result) request.setAttribute("msg", name + "님의 글이 성공적으로 입력되었습니다."); else request.setAttribute("msg", name + "님의 글이 입력되지 않았습니다."); } else { vo.setId(Integer.parseInt(action)); // 수정 대상 글 번호 저장 boolean result = dao.updateM(vo); // 글 수정 if (result) request.setAttribute("msg", name + "님의 글이 성공적으로 수정되었습니다."); else request.setAttribute("msg", name + "님의 글이 수정되지 않았습니다."); } request.setAttribute("list", dao.listM()); // 변경 후 전체 목록 다시 조회 request.getRequestDispatcher("/jspexam/meetingView.jsp").forward(request, response); // 결과 화면으로 이동 } }이 코드는 초보자 입장에서
doGet()과doPost()로 나눠서 봐야 한다.
doGet()은 목록 조회, 검색, 삭제, 댓글 등록, 댓글 목록 조회를 처리한다.
doPost()는 새 미팅 글 등록과 기존 글 수정을 처리한다.
여기서는 흐름을 빠르게 확인하기 위해 댓글 등록도doGet()에서 처리한다.
다만 일반적으로 새 데이터를 저장하는 요청은POST방식이 더 자연스럽다.
댓글 처리 부분은 특별하다.
삭제나 검색처럼 마지막에forward()로 화면을 다시 보여 주는 흐름이 아니다.
댓글 등록과 댓글 목록 조회는JSON으로 바로 응답하고return으로 끝난다.
즉, 등록·검색·수정·삭제는 결과 화면을 다시 보여 주는 방식이고, 댓글은 필요한 데이터만 바로 보내 주는 방식이다.
댓글 등록 결과는 성공 여부 하나만 보내면 되기 때문에JSONObject하나로 응답한다.
댓글 목록은 여러 개의 댓글을 보내야 하므로 댓글 한 건을JSONObject로 만들고, 여러 객체를JSONArray에 담아 응답한다.
즉, 객체 하나는JSONObject, 여러 객체 목록은JSONArray로 생각하면 된다.
이 예제에서는JSON응답을 보내기 위해Content-Type을application/json으로 설정한다.
이렇게 하면 브라우저나 클라이언트가 응답 데이터를JSON형식으로 해석하기 쉽다.
MeetingDAO는 실제 DB 작업을 맡는다
MeetingDAO는 전체 목록 조회, 글 등록, 이름 검색, 수정, 삭제, 댓글 목록 조회, 댓글 등록을 모두 처리한다.
즉, 데이터베이스와 실제로 맞닿아 있는 부분이다.// MeetingDAO.java package model.service; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import model.domain.MeetingDTO; import model.domain.ReplyDTO; public class MeetingDAO { public List<MeetingDTO> listM() { Connection conn = MySQLConnect.connect(); // DB 연결 Statement stmt = null; ResultSet rs = null; List<MeetingDTO> list = new ArrayList<>(); try { stmt = conn.createStatement(); rs = stmt.executeQuery("SELECT id, name, title, DATE_FORMAT(meetingdate, '%Y/%m/%d %H:%i') AS meetingDate FROM meeting"); // 전체 목록 조회 while (rs.next()) { MeetingDTO dto = new MeetingDTO(); // 결과 한 행을 객체로 만들기 dto.setId(rs.getInt("id")); dto.setName(rs.getString("name")); dto.setTitle(rs.getString("title")); dto.setMeetingDate(rs.getString("meetingDate")); list.add(dto); // 목록에 추가 } } catch (SQLException e) { e.printStackTrace(); } finally { close(stmt, rs, conn); // 자원 정리 } return list; } public boolean insertM(MeetingDTO dto) { Connection conn = MySQLConnect.connect(); // DB 연결 PreparedStatement pstmt = null; try { pstmt = conn.prepareStatement("INSERT INTO meeting (name, title, meetingdate) VALUES(?,?,?)"); // 등록 SQL pstmt.setString(1, dto.getName()); pstmt.setString(2, dto.getTitle()); LocalDateTime d = LocalDateTime.parse(dto.getMeetingDate()); // 문자열 날짜 변환 pstmt.setTimestamp(3, Timestamp.valueOf(d)); // DB 날짜 형식으로 변환 pstmt.executeUpdate(); return true; } catch (SQLException e) { System.err.println("meeting insert 과정에서 오류 발생 " + e); return false; } finally { close(pstmt, null, conn); } } public List<MeetingDTO> searchM(String name) { Connection conn = MySQLConnect.connect(); // DB 연결 PreparedStatement pstmt = null; ResultSet rs = null; List<MeetingDTO> list = new ArrayList<>(); try { pstmt = conn.prepareStatement("SELECT id, name, title, DATE_FORMAT(meetingdate, '%Y/%m/%d %H:%i') AS md FROM meeting WHERE name = ?"); // 이름 검색 pstmt.setString(1, name); rs = pstmt.executeQuery(); while (rs.next()) { MeetingDTO dto = new MeetingDTO(); // 검색 결과 객체 생성 dto.setId(rs.getInt("id")); dto.setName(rs.getString("name")); dto.setTitle(rs.getString("title")); dto.setMeetingDate(rs.getString("md")); list.add(dto); } } catch (SQLException e) { e.printStackTrace(); } finally { close(pstmt, rs, conn); } return list; } public boolean updateM(MeetingDTO vo) { Connection conn = MySQLConnect.connect(); // DB 연결 PreparedStatement pstmt = null; try { pstmt = conn.prepareStatement("update meeting set name = ?, title = ?, meetingdate = ? where id = ?"); // 수정 SQL LocalDateTime d = LocalDateTime.parse(vo.getMeetingDate()); pstmt.setString(1, vo.getName()); pstmt.setString(2, vo.getTitle()); pstmt.setTimestamp(3, Timestamp.valueOf(d)); pstmt.setInt(4, vo.getId()); // 수정 대상 글 번호 pstmt.executeUpdate(); return true; } catch (SQLException e) { System.err.println("meeting update 과정에서 오류 발생 " + e); return false; } finally { close(pstmt, null, conn); } } public boolean deleteM(int id) { Connection conn = MySQLConnect.connect(); // DB 연결 PreparedStatement pstmt = null; try { pstmt = conn.prepareStatement("DELETE FROM meeting WHERE id = ?"); // 삭제 SQL pstmt.setInt(1, id); // 삭제할 글 번호 바인딩 if (pstmt.executeUpdate() != 0) return true; else return false; } catch (SQLException e) { System.err.println("delete 과정에서 오류 발생 " + e); return false; } finally { close(pstmt, null, conn); } } public List<ReplyDTO> listReply(int refid) { Connection conn = MySQLConnect.connect(); // DB 연결 PreparedStatement pstmt = null; ResultSet rs = null; List<ReplyDTO> list = new ArrayList<>(); try { pstmt = conn.prepareStatement("SELECT id, name, content FROM reply where refid = ?"); // 특정 글의 댓글 조회 pstmt.setInt(1, refid); // 원본 글 번호 바인딩 rs = pstmt.executeQuery(); while (rs.next()) { ReplyDTO dto = new ReplyDTO(); // 댓글 한 건을 객체로 변환 dto.setId(rs.getInt("id")); dto.setName(rs.getString("name")); dto.setContent(rs.getString("content")); dto.setRefid(refid); list.add(dto); } } catch (SQLException e) { e.printStackTrace(); } finally { close(pstmt, rs, conn); } return list; } public boolean insertReply(ReplyDTO dto) { Connection conn = MySQLConnect.connect(); // DB 연결 PreparedStatement pstmt = null; try { pstmt = conn.prepareStatement("INSERT INTO reply (name, content, refid) VALUES(?,?,?)"); // 댓글 등록 SQL pstmt.setString(1, dto.getName()); pstmt.setString(2, dto.getContent()); pstmt.setInt(3, dto.getRefid()); pstmt.executeUpdate(); return true; } catch (SQLException e) { System.err.println("reply insert 과정에서 오류 발생 " + e); return false; } finally { close(pstmt, null, conn); } } private void close(Statement s, ResultSet r, Connection conn) { try { if (r != null) r.close(); // 결과 집합 닫기 if (s != null) s.close(); // 명령 객체 닫기 MySQLConnect.close(conn); // 연결 닫기 } catch (SQLException e) { System.err.println("자원 정리 과정에서 문제 발생" + e); } } }이 파일은 한눈에 보면
SQL이 많아서 어렵게 느껴질 수 있다.
하지만 메서드별로 보면 생각보다 단순하다.
listM()은 전체 목록을 읽어 오는 메서드이다.
insertM()은 새 미팅 글을 저장한다.
searchM()은 이름을 기준으로 특정 글만 찾아온다.
updateM()은 기존 글을 수정한다.
deleteM()은 글 번호를 기준으로 삭제한다.
listReply()와insertReply()는 댓글 처리용 메서드이다.
listReply()도 원본 글 번호인refid를?에 바인딩해서 특정 글의 댓글만 조회한다.
deleteM()은 글 번호를 기준으로 데이터를 삭제한다.
이때DELETE FROM meeting WHERE id = ?처럼?자리를 만들고,pstmt.setInt(1, id)로 삭제할 글 번호를 바인딩한다.
이 방식은 값을 문자열로 직접 이어 붙이는 것보다 안전하고, 어떤 값이 어디에 들어가는지도 분명하다.
여기서 날짜 처리도 중요하다.
화면에서 넘어온 날짜는 문자열이다.
하지만DB는 날짜와 시간을 저장할 수 있는 형식을 원한다.
그래서 먼저LocalDateTime.parse()로 자바의 날짜 객체로 바꾸고, 다시Timestamp.valueOf()로 바꿔 저장한다.
즉, 화면 입력값이 그대로 저장되는 것이 아니라, 저장 가능한 형태로 한 번 바뀐 뒤 들어간다는 점이 중요하다.
코드를 처음 읽을 때 어디부터 봐야 하는가
초보자 입장에서는 파일이 여러 개일 때 순서를 놓치기 쉽다.
그래서 이 예제는 아래 순서로 보면 가장 이해가 쉽다.
먼저meetingView.jsp를 본다.
사용자가 무엇을 입력하고 어떤 버튼을 누르는지 본다.
즉, 요청이 어디서 시작되는지 먼저 확인한다.
그다음MeetingServlet을 본다.
그 요청이 들어오면 어떤 기준으로 등록, 검색, 수정, 삭제, 댓글 처리로 나뉘는지 본다.
그다음MeetingDTO,ReplyDTO를 본다.
흐름 속에서 어떤 값이 어떤 객체에 담기는지 확인한다.
마지막으로MeetingDAO를 본다.
실제DB에서 어떤 작업이 일어나는지 확인한다.
이 순서가 중요한 이유는 코드가 실행되는 흐름과 비슷하기 때문이다.
처음부터DAO만 보면SQL만 잔뜩 보여서 구조가 잘 안 잡힐 수 있다.
반대로 화면부터 시작해서 흐름을 따라가면 이 요청이 이렇게 들어와서 여기서 이렇게 처리되는구나가 더 잘 보인다.
실행 결과
1. 미팅 정보 등록
첫 번째 영상은 새 미팅 정보를 입력하고 등록한 뒤, 등록된 글이 목록에 반영되는 흐름을 보여 준다.
즉, 사용자가 입력한 값이Servlet로 전달되고,MeetingDTO에 담긴 뒤,MeetingDAO를 통해 저장되고, 다시 전체 목록이 화면에 보이는 구조이다.
등록 기능은 단순히 입력만 받는 것이 아니다.
입력값을 저장한 뒤 바뀐 전체 목록을 다시 조회해서 사용자에게 보여 준다.
그래서 사용자는 등록 직후 변경된 결과를 바로 확인할 수 있다.
2. 이름 검색 후 글 수정
두 번째 영상은 이름으로 원하는 글을 검색한 뒤, 해당 글을 수정하고 수정 결과가 목록에 반영되는 흐름을 보여 준다.
여기서는 검색과 수정이 따로 떨어진 기능이 아니라, 원하는 데이터를 찾은 다음 그 데이터를 바로 관리하는 흐름으로 이어진다는 점이 중요하다.
조회 기능은 단순히 보기만 위한 기능이 아니다.
검색으로 원하는 데이터를 찾고, 그 결과를 기준으로 수정 같은 다음 작업을 이어 갈 수 있다.
3. 댓글 작성 후 댓글 목록 확인
세 번째 영상은 특정 글에 댓글을 작성한 뒤, 등록된 댓글 목록을 다시 불러와 화면에 보여 주는 흐름을 담고 있다.
이 부분은 앞의 등록, 검색, 수정, 삭제와 성격이 조금 다르다.
왜냐하면 댓글 기능은 결과를JSON으로 바로 주고받는 흐름이 함께 들어가기 때문이다.
댓글 기능은 화면 전체를 다시 이동하지 않는다.
필요한 데이터만 서버에 요청하고, 서버가 돌려준JSON응답을 화면에서 해석한다.
그래서 이 예제는 단순한 화면 이동형 예제를 넘어서, 필요한 데이터만 따로 처리하는 흐름까지 함께 보여 준다.
4. 글 삭제
네 번째 영상은 선택한 글을 삭제하고, 삭제 후 목록에서 해당 글이 사라지는 흐름을 보여 준다.
삭제는 겉으로 보면 버튼 한 번 누르는 간단한 기능처럼 보인다.
하지만 내부에서는 삭제 대상 글 번호를 읽고,MeetingDAO가 해당 데이터를 제거한 뒤, 다시 바뀐 목록을 화면에 반영하는 단계가 이어진다.
삭제 기능도 결국은 조회와 마찬가지로 처리 후 결과 화면을 다시 보여 주는
MVC흐름 안에서 이해해야 한다.
삭제 요청이 끝나면 바뀐 목록을 다시 보여 주어 사용자가 결과를 확인할 수 있게 한다.
마무리 정리
이 최종 예제는 단순히 기능이 많은 예제가 아니다.
지금까지 나왔던MVC흐름을 한 번에 묶어서 보여 주는 정리용 예제이다.
meetingView.jsp는 사용자가 보는 화면을 맡고,MeetingServlet은 요청을 나누고 전체 흐름을 조정한다.
MeetingDTO와ReplyDTO는 의미가 다른 데이터를 각각의 객체로 묶고,MeetingDAO는 실제DB처리를 맡는다.
그리고 댓글 기능에서는JSON응답까지 함께 들어가면서, 같은 프로젝트 안에서도 기능에 따라 처리 방식이 달라질 수 있다는 점을 보여 준다.
즉, 이 예제는 앞에서 배운 개념이 실제 웹 기능 안에서 어떻게 합쳐져 움직이는지를 보여 주는 마지막 정리 예제라고 이해하면 된다.