!
*
'
(
)
;
@
&
=
+
$
,
/
?
#
[
]
%
인코딩이 필요하지 않다. → URL에 그대로 사용 가능하다.
대문자(A-Z)
소문자(a-z)
숫자(0-9)
-
_
.
~
%00 형태로 인코딩해야한다. (웹브라우저와 서버가 자동으로 처리한다.)
⇒ 개발자가 신경 쓸 필요가 없다.
↔ 직접 웹 서버를 만든다면 URL 디코딩을 직접 처리해주어야 한다.
가 → %EA%B0%80
16진수 정수 값 → ASCII 문자코드로 변환
게시판에 대한 새 요청 처리를 추가할 때, BoardHandler에 새 메서드 코드를 변경해야한다.
→ 게시글에 대해 새로운 요청 처리를 추가할 때마다 기존코드를 손대야한다.
⇒ 버그가 추가될 가능성이 높아진다.
그래서 사용하는 것이 Command Design Pattern이다.
앞으로 계속 새로운 요청, 새로운 명령이 추가될 가능성이 높은 경우, 기존 코드를 손대지 않고 해결하는 방법!
요청 처리를 담당하는 메서드를 별도의 클래스로 분리
com.bitcamp.board.handler.MemberListHandler 클래스 정의
com.bitcamp.board.handler.MemberDetailHandler 클래스 정의
com.bitcamp.board.handler.MemberUpdateHandler 클래스 정의
com.bitcamp.board.handler.MemberDetailHandler 클래스 정의
com.bitcamp.board.handler.MemberFormHandler 클래스 정의
com.bitcamp.board.handler.MemberAddHandler 클래스 정의
com.bitcamp.board.handler.MemberHandler 클래스 정의
com.bitcamp.board.MiniWebServer 클래스 정의
MiniWebServer class
...
} else if (path.equals("/board/list")) {
boardListHandler.list(paramMap, printWriter);
} else if (path.equals("/board/detail")) {
boardDetailHandler.detail(paramMap, printWriter);
} else if (path.equals("/board/update")) {
boardUpdateHandler.update(paramMap, printWriter);
} else if (path.equals("/board/delete")) {
boardDeleteHandler.delete(paramMap, printWriter);
} else if (path.equals("/board/form")) {
boardFormHandler.form(paramMap, printWriter);
} else if (path.equals("/board/add")) {
boardAddHandler.add(paramMap, printWriter);
} else if (path.equals("/member/list")) {
memberListHandler.list(paramMap, printWriter);
} else if (path.equals("/member/detail")) {
memberDetailHandler.detail(paramMap, printWriter);
} else if (path.equals("/member/update")) {
memberUpdateHandler.update(paramMap, printWriter);
} else if (path.equals("/member/delete")) {
memberDeleteHandler.delete(paramMap, printWriter);
} else if (path.equals("/member/form")) {
memberFormHandler.form(paramMap, printWriter);
} else if (path.equals("/member/add")) {
memberAddHandler.add(paramMap, printWriter);
} else {
errorHandler.error(paramMap, printWriter);
...
com.bitcamp.servlet.Servlet 인터페이스 정의
Servlet interface
// 요청을 처리하는 객체의 사용법을 정의한다.
public interface Servlet {
// HTTP Client의 요청이 들어올 때 마다 해당 요청을 처리하는 객체에 대해 호출되는 메서드이다.
void service(Map<String,String> paramMap, PrintWriter out) throws Exception ;
}
모든 Command 객체 class들이 모두 Servlet interface를 구현할 수 있도록 하고, Servlet의 규칙을 따라 메소드의 이름을 service()로 수정해준다.
public class BoardAddHandler implements Servlet{
private BoardDao boardDao;
public BoardAddHandler(BoardDao boardDao) {
this.boardDao = boardDao;
}
@Override
public void service(Map<String, String> paramMap, PrintWriter out) throws Exception {
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
...
} else if (path.equals("/board/list")) {
boardListHandler.service(paramMap, printWriter);
} else if (path.equals("/board/detail")) {
boardDetailHandler.service(paramMap, printWriter);
} else if (path.equals("/board/update")) {
boardUpdateHandler.service(paramMap, printWriter);
} else if (path.equals("/board/delete")) {
boardDeleteHandler.service(paramMap, printWriter);
} else if (path.equals("/board/form")) {
boardFormHandler.service(paramMap, printWriter);
} else if (path.equals("/board/add")) {
boardAddHandler.service(paramMap, printWriter);
} // board
else if (path.equals("/member/list")) {
memberListHandler.service(paramMap, printWriter);
} else if (path.equals("/member/detail")) {
memberDetailHandler.service(paramMap, printWriter);
} else if (path.equals("/member/update")) {
memberUpdateHandler.service(paramMap, printWriter);
} else if (path.equals("/member/delete")) {
memberDeleteHandler.service(paramMap, printWriter);
} else if (path.equals("/member/form")) {
memberFormHandler.service(paramMap, printWriter);
} else if (path.equals("/member/add")) {
memberAddHandler.service(paramMap, printWriter);
} else {
errorHandler.service(paramMap, printWriter);
}
...
Servlet interface를 구현한 모든 Command 객체 클래스들을 모두 Map에 넣어주어 코드를 줄인다.
public class MiniWebServer {
public static void main(String[] args) throws Exception {
BoardDao boardDao = new MariaDBBoardDao(con);
MemberDao memberDao = new MariaDBMemberDao(con);
// 서블릿 객체를 보관할 맵을 준비
Map<String, Servlet> servletMap = new HashMap<>();
servletMap.put("/", new WelcomeHandler());
servletMap.put("/board/list", new BoardListHandler(boardDao));
servletMap.put("/board/form", new BoardFormHandler());
servletMap.put("/board/delete", new BoardDeleteHandler(boardDao));
servletMap.put("/board/detail", new BoardDetailHandler(boardDao));
servletMap.put("/board/update", new BoardUpdateHandler(boardDao));
servletMap.put("/board/add", new MemberAddHandler(memberDao));
servletMap.put("/member/list", new MemberListHandler(memberDao));
servletMap.put("/member/form", new MemberFormHandler());
servletMap.put("/member/delete", new MemberDeleteHandler(memberDao));
servletMap.put("/member/detail", new MemberDetailHandler(memberDao));
servletMap.put("/member/update", new MemberUpdateHandler(memberDao));
servletMap.put("/member/add", new MemberAddHandler(memberDao));
ErrorHandler errorHandler = new ErrorHandler();
...
Servlet servlet = servletMap.get(path);
if (servlet != null) {
servlet.service(paramMap, printWriter);
} else {
errorHandler.service(paramMap, printWriter);
}
...
com.bitcamp.servlet.WebServlet 애노테이션 정의
WebServlet annotation
@Target(value=ElementType.TYPE) // annotation을 붙일 수 있는 범위를 설정한다.
@Retention(value=RetentionPolicy.RUNTIME) // annotation 값을 추출할 때를 지정 -> 프로그램이 실행 중일 때 추출하겠다.
// 다음 annotation은 HTTP 요청을 처리하는 객체에 대해 경로를 설정할 때 사용한다.
public @interface WebServlet {
String value(); // annotation의 필수 속성을 설정
}
WebServlet(value="/board/add")
public class BoardAddHandler implements Servlet{
private BoardDao boardDao;
...
Reflections 라이브러리를 프로젝트에 추가한다.
implementation 'org.reflections:reflections:0.10.2'
gradle.eclipse
를 실행하여 이클립스 IDE 설정 파일을 갱신한다.@WebServlet annotaion이 붙은 클래스를 찾는다.
MiniWebServer class
public class MiniWebServer {
public static void main2(String[] args) throws Exception {
// 클래스를 찾아주는 도구를 준비
Reflections reflections = new Reflections("com.bitcamp.board");
/*
// 지정된 패키지에서 @WebServlet 애노테이션이 붙은 클래스를 모두 찾는다.
// 검색필터 1) WebServlet 애노테이션이 붙어 있는 클래스의 이름들을 모두 찾아라!
QueryFunction<Store,String> 검색필터1 = TypesAnnotated.with(WebServlet.class);
// 검색필터 2) 찾은 클래스 이름을 가지고 클래스를 Method Area 영역에 로딩하여
// Class 객체 목록을 리턴하라!
QueryFunction<Store,Class<?>> 검색필터2 = 검색필터1.asClass();
// 위의 두 검색 조건으로 클래스를 찾는다.
Set<Class<?>> 서블릿클래스들 = reflections.get(검색필터2);
for (Class<?> 서블릿클래스정보 : 서블릿클래스들) {
System.out.println(서블릿클래스정보.getName());
}
*/
Set<Class<?>> servlets = reflections.get(TypesAnnotated.with(WebServlet.class).asClass());
for (Class<?> servlet : servlets) {
WebServlet anno = servlet.getAnnotation(WebServlet.class);
System.out.printf("%s ---> %s\n", anno.value(), servlet.getName());
}
}
public static void main(String[] args) throws Exception {
Connection con = DriverManager.getConnection(
"jdbc:mariadb://localhost:3306/studydb","study","1111");
BoardDao boardDao = new MariaDBBoardDao(con);
MemberDao memberDao = new MariaDBMemberDao(con);
// 서블릿 객체를 보관할 맵을 준비
Map<String,Servlet> servletMap = new HashMap<>();
// WebServlet 애노테이션이 붙은 클래스를 찾아 객체를 생성한 후 맵에 저장한다.
// 맵에 저장할 때 사용할 key는 WebServlet 애노테이션에 설정된 값이다.
//
Reflections reflections = new Reflections("com.bitcamp.board");
Set<Class<?>> servlets = reflections.get(TypesAnnotated.with(WebServlet.class).asClass());
for (Class<?> servlet : servlets) {
// 서블릿 클래스의 붙은 WebServlet 애노테이션으로부터 path 를 꺼낸다.
String servletPath = servlet.getAnnotation(WebServlet.class).value();
// 생성자의 파라미터의 타입을 알아내, 해당 객체를 주입한다.
Constructor<?> constructor = servlet.getConstructors()[0];
Parameter[] params = constructor.getParameters();
if (params.length == 0) { // 생성자의 파라미터가 없다면
servletMap.put(servletPath, (Servlet) constructor.newInstance());
} else if (params[0].getType() == BoardDao.class) {
servletMap.put(servletPath, (Servlet) constructor.newInstance(boardDao));
} else if (params[0].getType() == MemberDao.class) {
servletMap.put(servletPath, (Servlet) constructor.newInstance(memberDao));
}
}
ErrorHandler errorHandler = new ErrorHandler();
class MyHttpHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
System.out.println("클라이언트가 요청함!");
URI requestUri = exchange.getRequestURI();
String path = requestUri.getPath();
// String query = requestUri.getQuery(); // 디코딩을 제대로 수행하지 못한다!
String query = requestUri.getRawQuery(); // 디코딩 없이 query string을 그대로 리턴 받기!
byte[] bytes = null;
try (StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter)) {
Map<String,String> paramMap = new HashMap<>();
if (query != null && query.length() > 0) { // 예) no=1&title=aaaa&content=bbb
String[] entries = query.split("&");
for (String entry : entries) { // 예) no=1
String[] kv = entry.split("=");
// 웹브라우저가 보낸 파라미터 값은 저장하기 전에 URL 디코딩 한다.
paramMap.put(kv[0], URLDecoder.decode(kv[1], "UTF-8"));
}
}
System.out.println(paramMap);
Servlet servlet = servletMap.get(path);
if (servlet != null) {
servlet.service(paramMap, printWriter);
} else {
errorHandler.service(paramMap, printWriter);
}
bytes = stringWriter.toString().getBytes("UTF-8");
} catch (Exception e) {
bytes = "요청 처리 중 오류 발생!".getBytes("UTF-8");
e.printStackTrace(); // 서버 콘솔 창에 오류에 대한 자세한 내용을 출력한다.
}
// 보내는 콘텐트의 MIME 타입이 무엇인지 응답 헤더에 추가한다.
Headers responseHeaders = exchange.getResponseHeaders();
responseHeaders.add("Content-Type", "text/html; charset=UTF-8");
exchange.sendResponseHeaders(200, bytes.length);
OutputStream out = exchange.getResponseBody();
out.write(bytes);
out.close();
}
}
HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
server.createContext("/", new MyHttpHandler());
server.setExecutor(null);
server.start();
System.out.println("서버 시작!");
}
}