개발자입니다
[비트캠프] 73일차(15주차5일) - Servlet(URL 절대 경로와 상대 경로) 본문
[비트캠프] 73일차(15주차5일) - Servlet(URL 절대 경로와 상대 경로)
끈기JK 2023. 2. 17. 18:13
서블릿을 만드는 다양한 방법
《interface》Servlet 을 HelloServlet 이 구현하려면, 요청에 대한 작업을 수행하는 service() 뿐만 아니라, init(), destroy(), getServletInfo(), getServletConfig() 를 모두 구현해야 한다.
service() 만 구현하고 싶으면 《interface》Servlet 을 상속받은 《abstract》GenericServlet 을 구현체를 상속받아 HelloServlet 을 생성한다.
GenericServlet 은 init(), destroy(), getServletInfo(), getServletConfig() 를 구현해 놓았고 service() 만 미구현하였다. 이를 HelloServlet 이 상속받아 service() 를 구현한다.
service() 를 HTTP method 별로 구현하려면 GenericServlet 을 상속받은 《abstract》HttpServlet 을 상속받은 HelloServlet 을 생성한다.
HttpServlet 은 service() 안에서 GET 요청시 doGet(), POST 요청시 doPost() 를 실행하도록 정의하였다.
이를 HelloServlet 이 상속받아 doGet(), doPost() 등을 구현한다.
서버 이중화
C1, C2 가 Tomcat Server 1(active) 에 접속한다. 여기서 HelloServlet 실행한다.
C3, C4 가 Tomcat Server 2(active) 에 접속한다. 이때 ① 서버에 문제 발생! 하면
Tomcat Server(standby) 로 이전 한다.
Tomcat Server 2 가 실행한 HelloServlet 을 serialize (인스턴스를 그대로 유지한 상태로 다른 서버로 이전 = 인스턴스 값 유지한 상태로 이)해서 바이트 배열로 바꾼다. 이를 deserialize 해서 Tomcat Server(standby) 가 실행한 HelloServlet 로 이전한다.
*서블릿 객체를 다른 서버로 이전할 경우를 고려하여 GenericServlet 클래스는 java.io.Serializable 인터페이스를 구현한다.
Web Project 개발과 테스팅 흐름

Web Project(프로젝트 폴더)의 index.html 을 ① 편집 및 저장한다. Eclipse IDE 가 tmp0/wtpwebapps/myapp-server/ (톰캣 테스트 환경의 배치 폴더) 로 ② 자동 복사한다. 그 후 Web Browser 에서 index.html 을 ③ 다시 요청(refresh) 하면 ④ 응답(전송) 받는다. 페이지가 ⑤ 갱신 된다.
http://localhost:8080/web/ 만 입력하면 자동으로 root 에서 index.html 을 다운받아 보여준다.
서블릿 출력과 한글 문자 코드 인코딩

out.println("AB가각") 을 기본 문자 집합 ISO-8859-1 을 사용하여 인코딩한다.
'A' 'B' '가' '각' = 0041 0042 AC00 AC01 ← JVM 기본 문자집합 (UTF-16)
41 42 3F 3F ← ISO-8859-1 문자집합 으로 변환하여 전송한다.
Web Browser 화면 → A B ? ?
*ISO-8859-1 character set 에는 한글 문자에 대한 코드 값이 정의되어 있지 않다. 따라서 한글 문자는 '?' (못 바꾸겠다는 의미로)로 변환한다.
↓ 해결책
response.setContentType("text/html; charset=utf-8");
text/html : MIME Type. 응답 콘텐트가 무엇인지 웹브라우저에게 알려주는 용도
charset=utf-8 : 콘텐트의 문자 인코딩 지정. UTF16 변환하여 UTF8 로 출력
↓ 설정한 이후에
PrintWriter out = response.getWriter();
out.println("AB가각");
↓ UTF16 을 UTF8 규칙에 따라 변환(인코딩)
'A' 'B' '가' '각'
0041 0042 AC00 AC01 ← UTF-16
41 42 EAB080 EAB081 ← UTF-8 을 전송
A B 가 각 ← web browser 화면
요청 파라미터와 한글 인코딩

이름: AB가각
GET 요청 (UTF-8) 로 보내면 41 42 EA B0 80 EA B0 81 이고 이를 전송(UTF-8) 한다.
서버에서 getParameter("name") 통해 UTF-16 으로 값을 꺼내면 0041 0042 AC00 AC01 → A B 가 각
POST 요청 (UTF-8) 로 보내면 41 42 EA B0 80 EA B0 81 이고 이를 전송(ISO-8859-1) 한다.
서버에서 getParameter("name") 통해 UTF-16 으로 값을 꺼내면 00 을 앞에 다 붙여 → 0041 0042 00EA 00B0 0080 00EA 00B0 0081 이다. "ê°.."
post 요청으로 전송 받은 문자열이 UTF-8 로 인코딩 되었음을 알려줘야 한다.
→ request.setCharacterEncoding("UTF-8")
반드시 getParameter() 를 호출하기 전에
41 42 EA B0 80 EA B0 81
↓ 이를 적용해 전송(UTF-8) 하면
getParameter("name")
0041 0042 AC00 AC01 → "AB가각"
URL 과 getServletPath(), getPathInfo()

http://localhost:8080/web/board/list?menu=1
- /web : context root → getContextPath() = 웹 애플리케이션 경로
- /board : servlet path → getServletPath()
*Servlet Path?
@WebServlet("/board/*") // 서블릿 경로 - /list : 서블릿의 하위 경로 → getpathInfo()
- menu=1 : QueryString → getQueryString
URL 절대 경로와 상대 경로

웹브라우저 주소창 : http://localhost:8080/web/board 에서 /web/ 은 현재 경로, board 는 자원이다.
<a href="/list">...</a> → http://localhost:8080/list
<a href="list">...</a> 여기서 list 는 자원 → http://localhost:8080/web/list 에서 http://localhost:8080/web/ 는 현재 경로, list 는 자원
<a href="board/list">...</a> → http://localhost:8080/web/board/list board/list 는 자원

웹브라우저 주소창 : http://localhost:8080/web/board/index.html 에서 /web/board/ 는 현재 경로, index.html 은 자원이다.
<a href="/list">...</a> → http://localhost:8080/list
<a href="list">...</a> → http://localhost:8080/web/board/list 에서 http://localhost:8080/web/board/ 는 현재 경로, list 는 자원
<a href="board/list">...</a> → http://localhost:8080/web/board/board/list board/list 는 자원
Command Design Pattern

BoardServlet 메서드
form() ← board/form 요청
insert() ← board/insert 요청
list() ← board/list
view()
update()
delete()
service() ← board/*
만약 board/search 요청을 추가로 처리해야 한다면 BoardServlet 클래스를 변경해야 한다.
- 한 명령 → 한 메서드
- 향후 명령 추가할 가능성 있음
↓ Command 패턴 방식으로 설계를 변경
- 한 명령 → 한 클래스
- 명령 추가 → 클래스 추가
↓
기존 클래스는 손대지 않는다.
BoardFormServlet
BoardInsertServlet
BoardListServlet
BoardViewServlet
BoardUpdateServlet
BoardDeleteServlet
BoardSearchServlet
### 44. Web Application Server 구조로 전환하기
- 웹 기술 도입 - 웹 기술을 도입하여 애플리케이션 서버를 구현하는 방법
BoardHandler 파일명을 BoardServlet 으로 변경한다.
Servlet 패키지를 만들고 안에 board 패키지 만들어서 파일 이동한다.
그 안에서 Form, Insert, List, View, Update, Delete, Search 로 클래스를 나눈다. (GoF Design Pattern : Command Pattern)

목록 읽어들일 때 한글 깨짐 방지하기 위해 response.setContentType("text/html;charset=UTF-8"); 입력한다.
@WebServlet("/board/list")
public class BoardListServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private BoardDao boardDao;
public BoardListServlet() {
try {
InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
"bitcamp/myapp/config/mybatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
builder.build(mybatisConfigInputStream));
boardDao = new DaoGenerator(sqlSessionFactory).getObject(BoardDao.class);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println("<meta charset='UTF-8'>");
out.println("<title>비트캠프 - NCP 1기</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>게시판</h1>");
out.println("<div><a href='form'>새 글</a></div>");
out.println("<table border='1'>");
out.println("<tr>");
out.printf("<th>번호</th><th>제목</th><th>작성일</th><th>조회수</th>");
out.println("</tr>");
List<Board> boards = this.boardDao.findAll();
for (Board b : boards) {
out.println("<tr>");
out.printf("<td>%d</td><td><a href='view?no=%d'>%s</a></td><td>%s</td><td>%d</td>",
b.getNo(), b.getNo(), b.getTitle(), b.getCreatedDate(), b.getViewCount());
out.println("</tr>");
}
out.println("</table>");
out.println("</body>");
out.println("</html>");
}
}
@WebServlet("/board/view")
public class BoardViewServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private BoardDao boardDao;
public BoardViewServlet() {
try {
InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
"bitcamp/myapp/config/mybatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
builder.build(mybatisConfigInputStream));
boardDao = new DaoGenerator(sqlSessionFactory).getObject(BoardDao.class);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
int boardNo = Integer.parseInt(request.getParameter("no"));
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println("<meta charset='UTF-8'>");
out.println("<title>비트캠프 - NCP 1기</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>게시판</h1>");
Board b = this.boardDao.findByNo(boardNo);
if (b == null) {
out.println("<p>해당 번호의 게시글 없습니다.</p>");
} else {
this.boardDao.increaseViewCount(boardNo);
out.println("<form id='board-form' action='update' method='post'>");
out.println("<table border='1'>");
out.println("<tr>");
out.println(" <th>번호</th>");
out.printf(" <td><input type='text' name='no' value='%d' readonly></td>", b.getNo());
out.println("</tr>");
out.println("<tr>");
out.println(" <th>제목</th>");
out.printf(" <td><input type='text' name='title' value='%s'></td>", b.getTitle());
out.println("</tr>");
out.println("<tr>");
out.println(" <th>내용</th>");
out.printf(" <td><textarea name='content' rows='10' cols='60'>%s</textarea></td>", b.getContent());
out.println("</tr>");
out.println("<tr>");
out.println(" <th>암호</th>");
out.println(" <td><input type='password' name='password'></td>");
out.println("</tr>");
out.println("<tr>");
out.println(" <th>등록일</th>");
out.printf(" <td>%s</td>", b.getCreatedDate());
out.println("</tr>");
out.println("<tr>");
out.println(" <th>조회수</th>");
out.printf(" <td>%d</td>", b.getViewCount());
out.println("</tr>");
out.println("</table>");
}
out.println("<div>");
out.println(" <button id='btn-list' type='button'>목록</button>");
out.println(" <button>변경</button>");
out.println(" <button id='btn-delete' type='button'>삭제</button>");
out.println("</div>");
out.println("</form>");
out.println("<script>");
out.println("document.querySelector('#btn-list').onclick = function() {");
out.println(" location.href = 'list';");
out.println("}");
out.println("document.querySelector('#btn-delete').onclick = function() {");
out.println("var form = document.querySelector('#board-form');");
out.println(" form.action = 'delete';");
out.println(" form.submit();");
out.println("}");
out.println("</script>");
out.println("</body>");
out.println("</html>");
}
}
요청시 한글 깨짐 방지 위해 출력 보다 위에 request.setCharacterEncoding("UTF-8"); 입력한다.
1초 후 페이지 이동하기 위해 (또는 원하는 시간 이후 이동하기 위해)
1. response.setHeader("Refresh", "1;url=list") 화면 출력하고 1초 후 화면 이동시킨다. 숫자 1에 원하는 값 입력한다.
@WebServlet("/board/update") // @WebServlet("/board/*")
public class BoardUpdateServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private BoardDao boardDao;
public BoardUpdateServlet() {
try {
InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
"bitcamp/myapp/config/mybatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
builder.build(mybatisConfigInputStream));
boardDao = new DaoGenerator(sqlSessionFactory).getObject(BoardDao.class);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
Board board = new Board();
board.setNo(Integer.parseInt(request.getParameter("no")));
board.setTitle(request.getParameter("title"));
board.setContent(request.getParameter("content"));
board.setPassword(request.getParameter("password"));
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println("<meta charset='UTF-8'>");
out.println("<title>비트캠프 - NCP 1기</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>게시판</h1>");
Board old = boardDao.findByNo(board.getNo());
if (old == null) {
out.println("<p>해당 번호의 게시글이 없습니다.</p>");
} else if (!old.getPassword().equals(board.getPassword())) {
out.println("<p>암호가 맞지 않습니다!</p>");
} else {
boardDao.update(board);
out.println("<p>변경했습니다.</p>");
}
out.println("</body>");
out.println("</html>");
// 웹브라우저가 화면 출력을 완료한 후 1초 후에 다시 목록 화면을 호출하도록 명령한다.
// 어떻게? 응답 헤더에 Refresh 를 추가한다.
response.setHeader("Refresh", "1;url=list");
}
}
2. <head> 태그 사이에 <meta http-equiv='Refresh' content='1;url=list'>"); 입력한다.
@WebServlet("/board/insert")
public class BoardInsertServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private BoardDao boardDao;
public BoardInsertServlet() {
try {
InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
"bitcamp/myapp/config/mybatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
builder.build(mybatisConfigInputStream));
boardDao = new DaoGenerator(sqlSessionFactory).getObject(BoardDao.class);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8"); // getParameter 호출하기 전에 두어야 한다.
Board board = new Board();
board.setTitle(request.getParameter("title"));
board.setContent(request.getParameter("content"));
board.setPassword(request.getParameter("password"));
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println("<meta charset='UTF-8'>");
out.println("<meta http-equiv='Refresh' content='1;url=list'>");
out.println("<title>비트캠프 - NCP 1기</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>게시판</h1>");
boardDao.insert(board);
out.println("<p>등록했습니다.</p>");
out.println("</body>");
out.println("</html>");
}
}
@WebServlet("/board/form")
public class BoardFormServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private BoardDao boardDao;
public BoardFormServlet() {
try {
InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
"bitcamp/myapp/config/mybatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
builder.build(mybatisConfigInputStream));
boardDao = new DaoGenerator(sqlSessionFactory).getObject(BoardDao.class);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println("<meta charset='UTF-8'>");
out.println("<title>비트캠프 - NCP 1기</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>게시판</h1>");
out.println("<form action='insert' method='post'>");
out.println("<table border='1'>");
out.println("<th>제목</th>");
out.println("<td><input type='text' name='title'></td></tr>");
out.println("<tr>");
out.println("<th>내용</th>");
out.println("<td><textarea name='content' rows='10' cols='60'></textarea></td></tr>");
out.println("<tr>");
out.println("<th>암호</th>");
out.println("<td><input type='password' name='password'></td>");
out.println("</tr>");
out.println("</table>");
out.println("<div>");
out.println("<button>등록</button>");
out.println("<button id='btn-cancel' type='button'>취소</button>");
out.println("</div>");
out.println("</form>");
out.println("<script>");
out.println("document.querySelector('#btn-cancel').onclick = function() {");
out.println("location.href = 'list';");
out.println("}");
out.println("</script>");
out.println("</body>");
out.println("</html>");
}
}
@WebServlet("/board/delete")
public class BoardDeleteServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private BoardDao boardDao;
public BoardDeleteServlet() {
try {
InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
"bitcamp/myapp/config/mybatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
builder.build(mybatisConfigInputStream));
boardDao = new DaoGenerator(sqlSessionFactory).getObject(BoardDao.class);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
int BoardNo = Integer.parseInt(request.getParameter("no"));
String password = request.getParameter("password");
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println("<meta charset='UTF-8'>");
out.println("<title>비트캠프 - NCP 1기</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>게시판</h1>");
Board old = boardDao.findByNo(BoardNo);
if (old == null) {
out.println("<p>해당 번호의 게시글이 없습니다.</p>");
} else if (!old.getPassword().equals(password)) {
out.println("<p>암호가 맞지 않습니다!</p>");
} else {
boardDao.delete(BoardNo);
out.println("<p>삭제했습니다.</p>");
}
out.println("</body>");
out.println("</html>");
response.setHeader("Refresh", "1;url=list");
}
}
조언
*실무는 6시간 HTML, CSS, Javascript 작성하고 2시간 SQL 작성한다.
과제
StudentServlet, TeacherServlet 만들기
'네이버클라우드 AIaaS 개발자 양성과정 1기 > DBMS, SQL, JDBC, Servlet' 카테고리의 다른 글
[JDBC] 예제 소스 정리 - Servlet(서블릿, 필터, 리스너, 멀티파트, 썸네일, 리프레시, 리다이렉트, 쿠키, 세션) (0) | 2023.02.18 |
---|---|
톰캣 서버 start 시 발생하는 에러 해결 방법 (0) | 2023.02.18 |
[비트캠프] 72일차(15주차4일) - Mybatis(톰캣 서버) (0) | 2023.02.16 |
[JDBC] 예제 소스 정리 - Mybatis (0) | 2023.02.16 |
[비트캠프] 71일차(15주차3일) - JDBC(MyBatis), myapp-42~43(SqlSession, 프록시 패턴) (0) | 2023.02.15 |