eomcs-java-web
web
web #ex01
Servlet01 -> Servlet이 가지고 있는 5개의 메소드 // init(), destroy(), service(), getServletInfo(), getServletConfig()
// init(), destroy(), service() // 3개 // Servlet의 Lifecycle 메서드
// init() // 생성 // 서블릿 객체를 생성하고 초기 작업을 시행하게 하는 메서드
// 서블릿을 실행할 때 사용할 자원을 이 메서드에서 준비한다. // 서블릿 객체 생성 직후 1회만 메서드가 실행된다.
// 파라미터로 받은 ServletConfig 객체는 인스턴스 변수에 보관해 두었다가 필요할 때 사용
// destroy() // 소멸 // 소멸하기전에 서블릿 객체를 불러 마지막 작업을 수행하게 하는 메서드
// 웹 애플리케이션이나 서버를 종료할 때 이 서블릿이 만든 자원을 해제할 수 있도록
// 서블릿 컨테이너가 이 메서드를 호출한다. // 서블릿 객체 소멸 직전 1회만 메서드가 실행된다.
// init()에서 준비한 자원을 보통 이 메서드에서 해제한다.
// service() // 실행 // 서블릿 객체를 불러서 클라이언트가 요청한 작업을 수행 시키는 메소드
// 클라이언트가 이 서블릿의 실행을 요청할 때마다 호출된다.
// getServletConfig() // 서블릿에서 작업을 수행하는 중에 서블릿 관련 설정 정보를 꺼낼 때 이 메서드를 호출
// getServletInfo() // 서블릿 컨테이너가 관리자 화면을 출력할 때 이 메서드를 호출
// Servlet 클래스 사용 //
// 서블릿 클래스를 만든 후 배치 파일(web.xml; DD 파일)에 서블릿 정보를 등록해야만 실행될 수 있다.
// WEB-INF/web.xml // DD File // Deployment Descriptor File
// 서블릿 실행 방법 // http://서버주소:포트번호/웹애플리케이션이름/서블릿URL
// http://localhost:9999/bitcamp-java-web/ohora/haha
// 서블릿 구동 과정 //
// 1. 웹 브라우저(ClientApp)가 서블릿 실행을 요청한다.
// 2. 서블릿 컨테이너는 해당 URL의 서블릿 객체를 찾는다.
// 3.1 서블릿 객체가 없다면,
// 서블릿 클래스에 대해 인스턴스를 생성 - 서블릿 객체의 생성자를 호출 - init()를 호출 - service()를 호출
// 3.2 서블릿 객체가 있다면, service()를 호출
// 4. 웹 애플리케이션이 종료된다면, 생성된 모든 서블릿들의 destroy() 메서드를 호출
// Servlet 사용 주의 //
// 클라이언트마다 구분되어야 할 데이터는 서블릿 인스턴스 변수에 보관해서는 안된다.
// 서블릿 인스턴스는 오직 클래스 마다 한 개만 생성되어
// 모든 클라이언트가 같은 서블릿 인스턴스를 사용 // 인스턴스를 모든 클라이언트가 공유하기 때문
Servlet02 -> GenericServlet 추상 클래스
// javax.servlet.Servlet 인터페이스를 service() 메서드만 남겨두고 나머지 메서드들은 모두 구현
// 이 클래스를 상속 받는 서브 클래스는 service() 만 구현하면 된다.
// GenericServlet 추상 클래스는 java.io.Serialize 인터페이스를 구현하였다.
// serialVersionUID 변수 값을 설정해야 한다.
Servlet03 -> HttpServlet 추상 클래스 상속
// doGet(), doPost(), doHead(), doPut() 등을 호출하게 프로그램 되어 있다.
// service()를 오버라이딩 하는 대신에 doGet(), doPost(), doHead() 등을 오버라이딩 해야 한다.
// 호출과정 //
// 1. 웹브라우저가 톰캣 서버에 요청
// 2. 톰캣 서버가 서블릿 객체에 대해 service(ServletRequest, ServletResponse) 호출
// 3. service(HttpServletRequest, HttpServletResponse) 호출
// 4. doGet(HttpServletRequest, HttpServletResponse) 호출
Servlet04 -> @WebServlet 애노테이션
// web.xml 에 서블릿 정보를 설정하는 대신에 애노테이션을 사용하여 서블릿을 설정할 수 있다.
// metadata-complete="false" // web.xml의 태그에 false로 속성을 추가해야 한다.
web #ex02
Filter01 -> 서블릿 컨테이너가 관리하는 컴포넌트 // 서블릿, 필터, 리스너
// Filter 만들기 // javax.servlet.Filter 인터페이스 규칙에 따라 작성
// 필터 배포하기 // DD 파일(web.xml)에 설정하거나 애노테이션으로 설정하면 된다.
// 필터의 용도 // 서블릿을 실행하기 전후에 필요한 작업을 수행
// 서블릿 실행 전 //
// ex) 웹브라우저가 보낸 암호화된 파라미터 값을 서블릿으로 전달하기 전에 암호 해제하기
// ex) 웹브라우저가 보낸 압축된 데이터를 서블릿으로 전달하기 전에 압축 해제
// ex) 서블릿의 실행을 요청할 권한이 있는지 검사
// ex) 로그인 사용자인지 검사 // ex) 로그 남기기
// 서블릿 실행 후 //
// ex) 클라이언트로 보낼 데이터를 압축하기
// ex) 클라이언트로 보낼 데이터를 암호화시키기
// Filter이 가지고 있는 5개의 메소드 // init(), destroy(), doFilter()
// init() // 필터 객체를 생성한 후 제일 처음으로 호출 // 필터가 사용할 자원을 이 메서드에서 준비
// 필터 객체 생성 직후 1회만 메서드가 실행된다.
// destroy() // 웹 애플리케이션이 종료될 때 호출 // init()에서 준비한 자원을 해제
// 필터 객체 소멸 직후 1회만 메서드가 실행된다.
// doFilter() // 필터를 설정할 때 지정된 URL의 요청이 들어 올 때 마다 호출 //
// 서블릿이 실행되기 전에 필터가 먼저 실행 // 서블릿을 실행한 후 다시 필터로 리턴
Filter02 -> 필터 실행 순서
// 필터의 실행 순서를 임의로 조정할 수 없다.
// 필터의 실행 순서에 상관없이 각 필터가 독립적으로 동작하도록 작성해야 한다.
Listener01 -> 리스너 만들기
// 서블릿 컨테이너 또는 서블릿, 세션 등의 객체 상태가 변경되었을 때 보고 받는 옵저버
// "Observer" 디자인 패턴 적용
// ServletContextListener //
// 서블릿 컨테이너를 시작하거나 종료할 때 보고 받고 싶다면 이 인터페이스를 구현
// ServletRequestListener //
// 요청이 들어오거나 종료될 때 보고 받고 싶다면 이 인터페이스를 구현
// HttpSessionListener //
// 세션이 생성되거나 종료될 때 보고 받고 싶다면 이 인터페이스를 구현
// XxxListener
// 기타 다양한 인터페이스가 있어, 문서를 참고
// 리스너 배포하기 // DD 파일(web.xml)에 설정하거나 애노테이션으로 설정하면 된다.
// 리스너의 용도 // 서블릿 컨테이너나, 세션 등이 특별한 상태일 때 필요한 작업을 수행한다.
// ServletContextListener // 웹 애플리케이션을 시작할 때 Spring IoC 컨테이너 준비하기
// 웹 애플리케이션을 시작할 때 DB 커넥션 풀 준비하기
// 웹 애플리케이션을 종료할 때 DB 커넥션 풀에 들어 있는 모든 연결을 해제하기
// ServletRequestListener // 요청이 들어 올 때 로그 남기기
Listener02 ->리스너 테스트
web #ex03
Servlet01 -> 문자표 지정하기 전 // 기본적으로 아스키 코드(ASCII)로 변환하여 출력
// 자바(Unicode2;UTF-16) -> 출력문자(ASCII) // 자바코드를 자동적으로 아스키 코드로 변환한다.
Servlet02 -> 한글 깨짐 현상 처리
// res.setContentType("text/plain;charset=UTF-8"); // text/plain 으로 ContentType 지정 // UTF-16 -> UTF-8
Servlet03 -> HTML 출력하기
// res.setContentType("text/html;charset=UTF-8"); // text/html으로 ContentType 지정
// HTML 출력할 때, MIME 타입에 HTML을 지정하지 않으면 웹 브라우저는 일반 텍스트로 간주한다.
Servlet04 ->바이너리 데이터 출력하기
// /WEB-INF/photo.jpeg 파일의 실제 경로 알아내기
// 1. 서블릿의 환경 정보를 다루는 객체를 먼저 얻는다.
// ServletContext ctx = req.getServletContext();
// 2. ServletContext를 통해 웹 자원의 실제 경로를 알아낸다.
// getRealPath(현재 웹 애플리케이션의 파일 경로) // 실제 전체 경로를 리턴
// String path = ctx.getRealPath("/WEB-INF/photo.jpeg");
// FileInputStream in = new FileInputStream(path);
// 3. 바이너리를 출력할 때 MIME 타입을 지정해야 웹 브라우저가 제대로 출력할 수 있다.
// 웹 브라우저가 모르는 형식을 지정하면 웹 브라우저는 처리하지 못하기 때문에 다운로드 대화상자를 띄운다.
web #ex04
Servlet01 -> GET 요청 데이터 읽기 // 응답, 요청 프로토콜 https://te-ma.tistory.com/209 에서 상세 확인
// GET 요청 // 웹 브라우저에 URL을 입력한 후 엔터를 치면 GET 요청을 보낸다.
// 웹 페이지에서 링크를 클릭하면(자바스크립트 처리하지 않은 상태) GET 요청을 보낸다.
// 웹 페이지의 폼(method='GET' 일 때)에서 전송 버튼을 클릭하면 GET 요청을 보낸다.
// 웹 브라우저에서 웹 서버의 자원 요청하는 방법 //
// 1. 서블릿 클래스를 실행하고 싶을 때
// servlet class 실제 위치 : ex) wtpwebapps/eomcs-java-web/WEB-INF/classes/com/eomcs/web/ex04/servlet01.class
// 요청시 : http://localhost:9999/eomcs-java-web/ex04/s1
// 해당 서블릿을 서버에 등록할 때 사용한 URL을 지정해야 한다.
// 2. HTML, CSS, JavaScript, JPEG 등 정적 파일을 받고 싶을 때
// 정적 파일 실제 위치 : ex) wtpwebapps/eomcs-java-web/ex04/test01.html
// 요청 : ex) http://localhost:9999/eomcs-java-web/ex04/test01.html
// 3. /WEB-INF/ 폴더에 있는 정적 파일을 받고 싶을 때
// 정적 파일 실제 위치 : ex) wtpwebapps/eomcs-java-web/WEB-INF/ex04/test01.html
// 요청 : ex) WEB-INF 폴더 아래에 있는 파일은 클라이언트에서 요청할 수 없다.
// 웹 애플리케이션의 정보를 두는 폴더이기 때문이다.
// HTTP 요청 프로토콜 //
// method sp request-URI sp http_version CRLF // Request-Line 이라고 함.
// (general header | request header | entity header) CRLF
// CRLF // CRLF는 텍스트 줄의 끝을 의미
// message-body
// method는 GET, POST, PUT, DELETE, TRACE, CONNECT, HEAD, OPTIONS // 8가지이다. // 나머지는 밑에 설명
// GET 요청은 데이터를 request-URI에 붙여서 보낸다.
// ex) /java-web/ex04/s1?name=%ED%99%8D%EA%B8%B8%EB%8F%99&age=20
// 서블릿 URL // /java-web/ex04/s1
// 데이터(Query String) // name=%ED%99%8D%EA%B8%B8%EB%8F%99&age=20
// 데이터 형식 // 이름=값&이름=값&이름=값
// URL 인코딩 // 데이터를 보낼 때 7bit 네트워크 장비를 거치면 8비트 데이터가 깨진다.
// 이를 방지하고자 보내는 데이터를 7비트로 변환 // 원래 코드 값을 아스키(ASCII) 문자 코드로 변환
// ASCII 코드는 7비트이기 때문에 데이터를 주고 받을 때 깨지지 않는다.
// 단 8비트 데이터를 ASCII 코드(7비트)로 변환하면, 1데이터 손실이 일어나게 된다.
// 따라서 이 손실을 막기 위해, URL인코딩을 해야한다.
// URL 인코딩이란? 문자 코드의 값을 ASCII 코드화시키는 것
// http_version // HTTP 버전을 의미
// "HTTP" "/" 1*DIGIT "." 1*DIGIT 와 같이 정의되는데 현재 HTTP/1.0과 HTTP/1.1이 있다.
// header // header는 general-header, request-header, entity-header // 3가지 종류가 있다.
// General Header // Cache-Control, Connection, Date, Pragma, Trailer, Transfer-Enco, Upgrade, Via, Warning
// Request Header // Accept, Accept-Charset, Accept-Encoding, Accept-Language, Authorization, Expect, From,
// Host, If-Match, If-Modified-Since, If-None-Match, If-Range, If-Unmodified-Since, Max-Forwards,
// Proxy-Authorization, Range, Referer, TE, User-Agent 등
// Entity Header // Allow, Content-Encoding, Content-Language, Content-Length, Content-Location,
// Content-MD5, Content-Range, Content-Type, Expires, Last-Modified, extension-header
// name : content // 헤더마다 위와 같은 name을 가지고 있고, 해당하는 content를 넣어 표현한다.
// 각 값들은 공백이나, 탭으로 구분될 수 있고, 각 해더는 CRLF로 구분된다.
GET /java-web/ex04/s1?name=%ED%99%8D%EA%B8%B8%EB%8F%99&age=20 HTTP/1.1 Host: localhost:8080
Connection: keep-alive Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/73.0.3683.86 Safari/537.36 Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng, Accept-Encoding:
gzip, deflate, br Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7,la;q=0.6 빈 줄
// HTTP 응답 프로토콜 //
// HTTP-Version SP Status-Code SP Reason-Phrase CRLF // status-line이라 함.
// (general header | response header | entity header) CRLF
// CRLF
// message-body
// Status-Code // 3자리 숫자로 된 상태를 나타내는 코드
// 1xx // 요청을 받았고, 처리중임을 표현
// 2xx // 성공했음을 표현
// 200 // 요청을 정상적으로 처리했음
// 201 // 요청한 자원을 생성했음
// 3xx // 요청한 자원이 다른 장소로 옮겨졌음을 표현
// 301 // 요청한 자원이 다른 장소로 이동 했음
// 304 // 요청한 자원이 변경되지 않았음. 따라서 기존에 다운로드 받은 것을 그대로 사용하라고 요구함.
// 4xx // 잘못된 형식으로 요청했음을 표현
// 401 요청 권한이 없음.
// 403 // 요청한 자원에 대한 권한이 없어 실행을 거절함.
// 404 // 요청한 자원을 찾을 수 없음.
// 5xx // 서버쪽에서 오류가 발생했음을 표현
// 500 // 서버에서 프로그램을 실행하다가 오류 발생함.
// header // general header와 Entity header는 요청부분과 동일
// Response Header // Accept-Ranges, Age, ETag, Location, Proxy-Authenticate, Retry-After,
// Server, Vary, WWW-Authenticate
HTTP/1.1 200 OK Content-Type: text/plain;charset=UTF-8 Content-Length: 27 Date: Thu, 28 Mar 2019
05:46:08 GMT CRLF 이름=홍길동 나이=20
// URI (Uniform Resource Identifier) // 웹 자원의 위치를 가리키는 식별자 // URL과 URN이 있다.
// URL(Uniform Resource Locator) // scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
// ex) http://localhost:8080/ex04/s1?name=홍길동&age=20
// URN(Uniform Resource Name) // <URN> ::= "urn:" <NID> ":" <NSS>
// ex) urn:lex:eu:council:directive:2010-03-09;2010-19-UE
Servlet02 -> POST 요청 데이터 읽기
// POST 요청 // 웹 페이지의 폼(method='POST' 일 때)에서 전송 버튼을 클릭하면 POST 요청을 보낸다.
// 웹 브라우저가 보낸 데이터 읽기 // 데이터를 읽을 때는 GET 요청과 POST 요청이 같다.
// ServletRequest.getParameter("파라미터이름") // POST로 보낸 데이터는 영어(ISO-8859-1)로 간주한다
// 그래서 영어 코드를 자바에서 사용하는 UTF-8로 변환한다.
// ex) ABC가각 // 웹에서 보낸다고 가정하고 설명함.
// 실제 웹 브라우저가 "ABC가각" 문자열을 보낼 때 UTF-8 코드를 보낸다. // 414243EAB080EAB081
// 그런데 서블릿에서는 이 코드 값을 ISO-8859-1 코드라고 간주
// getParameter()로 값을 꺼내면 UTF-16으로 바꾼 값을 리턴 // 0041 0042 0043 00EA 00B0 0080 00EA 00B0 0081
// 바이트에 00을 붙여 문자열을 만들어 리턴 // 영어를 2바이트 유니코드로 바꿀 때 앞에 00을 붙이면 되기 때문
// ABCê°ê° // 변환된 코드를 화면에 출력하면 이렇게 나온다.
req.setCharacterEncoding("UTF-8");
String age = req.getParameter("age");
String name = req.getParameter("name");
res.setContentType("text/plain;charset=UTF-8");
// getParameter()를 최초로 호출하기 전에 클라이언트가 보낸 데이터의 인코딩 형식을 먼저 알려줘야 한다.
// getParameter가 이전에 한번이라도 호출 된 후라면 적용되지 않는다.
// GET 요청 vs POST 요청
// 1. 전송 데이터 용량
// GET // 게시글 조회나 검색어 입력 같은 간단한 데이터 전송
GET /ex01/s1?id=a@naver.com&pwd=1111 HTTP/1.1
HOST :
.
.
.
// 대부분의 웹서버가 request-line과 헤더의 크기를 8KB로 제한하고 있다.
// 위 예제처럼 GET은 값이 헤더에 들어가게 된다.
// 따라서 긴 게시글과 같은 큰 용량의 데이터를 GET 방식으로 전송할 수 없다.
// POST // 게시글 등록이나 첨부파일 같은 큰 데이터 전송
POST /ex01/s1 HTTP/1.1
HOST:
.
.
.
id=a@test.com&pwd=1111
// HTTP 요청 헤더 다음에 message-body 부분에 데이터를 두기 때문에
// 용량의 제한 없이 웹 서버에 전송할 수 있다.
// 위의 예제처럼 POST는 값이 message-body에 들어가게 된다.
// 즉 웹 서버가 제한하지 않는 한 전송 데이터의 크기에 제한이 없다.
// 웹 서버가 제한 한다? // 특정 사이트에서는 게시글의 크기나 첨부파일의 크기에 제한을 둔다.
// 2. 바이너리 데이터 전송
// GET // 바이너리 Data 전송 불가 // 어거지로 가능은 하나 그렇게 안쓴다.
// request-URI가 텍스트로 되어 있다. // 따라서 바이너리 데이터를 request-URI에 붙여서 전송할 수 없다.
// URL에 Data가 포함되기 때문이다.
// 꼭 GET 요청으로 바이너리 데이터를 보내고자 한다면? // 바이너리 데이터를 텍스트로 변환하면 된다.
// ex) 바이너리 데이터를 Base64로 인코딩하여 텍스트를 만든 후, GET 요청 방식대로 이름=값으로 보내면 된다.
// 그래도 결국 용량 제한 때문에 바이너리 데이터를 GET 요청으로 전송하는 것은 바람직하지 않다.
// POST // multipart 형식으로 보낼 때는 바이너리 Data 전송 가능 // 첨부파일 전송때 사용
// POST도 이름=값 형태로는 바이너리 값을 전송할 수 없다. // multipart 형식을 사용하면 전송 가능
// 3. 보안 // 보안이 가장 좋은건 POST가 아니라, HTTPS 방식을 사용하는 것이다.
// GET // URL에 전송 데이터가 포함되어 있기 때문에
// 사용자 아이디나 암호 같은 데이터를 GET 방식으로 전송하는 것은 위험하다.
// 웹 브라우저는 주소 창에 입력한 값을 내부 캐시에 보관해 두기 때문이다.
// POST // mesage-body 부분에 데이터가 있기 때문에 웹 브라우저는 캐시에 보관하지 않는다.
// 또한 주소 창에도 보이지 않는다. // 사용자 암호 같은 데이터를 전송할 때는 POST로 보내는 것이 바람직 하다.
// 보내는 데이터를 웹 브라우저의 캐시 메모리에 남기고 싶지 않을 때는 POST 방식을 사용한다.
// 특정 페이지를 조회하는 URL일 경우 POST 방식을 사용하면
// URL에 조회하려는 정보의 번호나 키를 포함할 수 없기 때문에 POST 방식이 적절하지 않다.
// 4. 쓰임
// GET // Request URL에 Data가 포함되어 있다. // 조회하는 페이지의 URL을 주고 받을 수 있어 편리하다.
// ex) /news/detail?no=117 // 뒤의 값이 나오기 때문에 즐겨찾기에 추가가 가능하다.
// POST // Request URL에 Data가 없다. // 특정 페이지로 바로 이동이 불가능하다.
// ex) /news/detail // 이 URL에 message body로 값을 보내기 때문에 URL은 고정되어 있다.
// URL은 계속 같은데, 넘겨지는 값에 따라 달라지기 때문에 북마크가 불가능하다.
Servlet03 -> 파일 업로드 처리하기
req.setCharacterEncoding("UTF-8");
String age = req.getParameter("age");
String name = req.getParameter("name");
String photo = req.getParameter("photo");
res.setContentType("text/plain;charset=UTF-8");
<form action="/eomcs-java-web/ex04/s3" method="post">
이름: <input type="text" name="name"><br>
나이: <input type="number" name="age"><br>
사진: <input type="file" name="photo"><br>
<input type="submit" value="전송">
</form>
// GET 요청으로 파일 전송하기 //
// GET 으로 파일을 전송할 수 없다. 단지 파일 이름만 전송한다.
// POST 요청으로 파일 전송하기 //
// POST 요청만 적용해서 보낸다면, form의 데이터 인코딩 형식은 "application/x-www-form-urlencoded"이다.
// 즉 "이름=값&이름=값&이름=값" 형식으로 데이터를 보낸다. // 첨부한 파일의 데이터를 보내지 못한다.
// 단지 파일 이름만 전송한다.
<form action="/eomcs-java-web/ex04/s3" enctype="multipart/form-data" method="post">
이름: <input type="text" name="name"><br>
나이: <input type="number" name="age"><br>
사진: <input type="file" name="photo"><br>
<input type="submit" value="POST 전송">
</form>
// POST 요청 + multipart/form-data 형식으로 파일 전송하기 //
// enctype="multipart/form-data"
// 파일 업로드할때 꼭 encodingtype을 "multipart/form-data"로 지정해야 한다.
// multipart/form-data 형식으로 데이터를 전송하지 않으면 첨부 파일의 데이터는 받을 수 없다.
// 파일을 업로드 하려면, 반드시 multipart/form-data 형식으로 POST 요청해야 한다.
Servlet04 -> apache 라이브러리 사용 // 멀티파트 파일 업로드 처리
// 멀티파트 형식으로 전송된 데이터는 별도의 처리과정이 필요하다.
// Apache 재단에서 제공하는 fileupload 라이브러리가 이 별도의 처리과정을 대신 해준다.
// mvnmvnrepository.com 또는 search.maven.org 접속 - 'commons-fileupload' 검색
// org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사
// - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'
private static final long serialVersionUID = 1L;
private String uploadDir;
@Override
public void init() throws ServletException {
this.uploadDir = this.getServletContext().getRealPath("/upload");
}
// init(ServletCondig)가 호출될 때 이 메서드(init())를 호출한다. // 파일을 저장할 디렉토리 경로를 준비
// 멀티파트 형식으로 보낸 첨부 파일 데이터를 읽는 방법 //
// Content-Type 헤더에 지정한 구분자를 사용하여 각 파트를 분리한 다음 데이터를 읽는다.
// 문제는 기존에 제공하는 getParameter()로는 멀티파트 형식으로 전송된 데이터를 읽을 수 없다.
// 해결방법 //
// 1. 개발자가 직접 멀티파트 형식을 분석하여 데이터를 추출한다. // 미친짓이다.
// 2. apache.org 사이트에서 제공하는 멀티파트 데이터 분석기를 사용 // 과거에는 많이 사용했었음.
// 3. Servlet 3.0 부터 제공하는 기능을 이용 // 과거에 쓰던 방식에 젖어 이 방법을 모르기도 한다.
// 4. Spring WebMVC를 사용한다면 해당 프레임워크에서 제공하는 기능을 이용 // 나중에 추후 설명
// req.setCharacterEncoding("UTF-8"); // 멀티파트 데이터를 처리할 때는 인코딩 설정이 적용되지 않는다.
// 멀티파트 형식으로 전송된 데이터는 getParameter()로 꺼낼 수 없다.
// 2번 방법 // 실무에서는 아직도 이 방법을 사용하기 때문에 짚고 넘어간다.
DiskFileItemFactory fileItemFactory = new DiskFileItemFactory();
ServletFileUpload multipartDataHandler = new ServletFileUpload(fileItemFactory);
HashMap<String, String> paramMap = new HashMap<>();
try {
List<FileItem> parts = multipartDataHandler.parseRequest((HttpServletRequest) req);
for (FileItem part : parts) {
if (part.isFormField()) {
paramMap.put(part.getFieldName(), part.getString("UTF-8"));
// DiskFileItemFactory // 멀티파트 데이터를 분석하여 FileItem 객체에 담아 줄 공장을 준비
// ServletFileUpload(fileItemFactory) // 공장 객체를 사용하여 클라이언트가 보낸 데이터를 처리할 객체
// HashMap<String, String> // 분석한 데이터를 보관할 맵 객체를 준비
// multipartDataHandler.parseRequest((HttpServletRequest) req); //
// 멀티파트 데이터 처리기를 이용하여 클라이언트 요청을 분석
// part.isFormField() // 파트의 데이터가 일반 데이터라면
// paramMap.put(part.getFieldName(), part.getString("UTF-8") //
// 클라이언트가 보낸 파라미터 이름, 파라미터의 값. 값 꺼낼 때 인코딩을 지정
} else {
String filename = UUID.randomUUID().toString();
File file = new File(this.uploadDir + "/" + filename);
System.out.println(file.getCanonicalPath());
part.write(file);
paramMap.put(part.getFieldName(), filename);
}
}
// 파트의 데이터가 파일이라면 upload/ 디렉토리에 파일을 저장한다.
// UUID.randomUUID().toString(); // 업로드 파일을 저장할 때 사용할 파일명을 준비한다. // 원래 파일명 사용 x
// 다른 클라이언트가 같은 이름의 파일을 업로드 하면 기존 파일을 덮어쓸 수 있기 때문
// new File(this.uploadDir + "/" + filename); // /java-web/upload/파일명 // 전체 파일 경로를 준비한다.
// part.write(file); // 파일 경로에 업로드 파일을 저장한다.
Servlet05 -> Servlet 3.0의 기본 라이브러리 사용
// 멀티파트 형식의 데이터를 처리할 서블릿으로 선언
// 사용방법은 두가지가 있다. // 1. DD 파일(web.xml)에 설정 2. 애노테이션으로 설정
// 1. DD 파일(web.xml)에 설정
<servlet>
<servlet-name>ex04.Servlet05</servlet-name>
<servlet-class>com.eomcs.web.ex04.Servlet05</servlet-class>
<multipart-config>
<max-file-size>10000000</max-file-size>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>ex04.Servlet05</servlet-name>
<url-pattern>/ex04/s5</url-pattern>
</servlet-mapping>
// 2. 애노테이션으로 설정
@MultipartConfig(maxFileSize = 1024 * 1024 * 10)
@WebServlet("/ex04/s5")
public class Servlet05 extends GenericServlet {
private static final long serialVersionUID = 1L;
private String uploadDir;
@Override
public void init() throws ServletException {
this.uploadDir = this.getServletContext().getRealPath("/upload");
}
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
HttpServletRequest httpReq = (HttpServletRequest) req;
res.setContentType("text/html;charset=UTF-8");
PrintWriter out = res.getWriter();
out.println("<html>");
out.println("<head><title>servlet04</title></head>");
out.println("<body><h1>파일 업로드 결과</h1>");
out.printf("이름=%s<br>\n", httpReq.getParameter("name"));
out.printf("나이=%s<br>\n", httpReq.getParameter("age"));
Part photoPart = httpReq.getPart("photo");
String filename = "";
if (photoPart.getSize() > 0) {
filename = UUID.randomUUID().toString();
photoPart.write(this.uploadDir + "/" + filename);
out.printf("사진=%s<br>\n", filename);
out.printf("<img src='../upload/%s'><br>\n", filename);
}
out.println("</body></html>");
}
}
// @MultipartConfig(maxFileSize=값) // 최대 FileSize를 설정한다.
// req.setCharacterEncoding("UTF-8"); //
// Servlet 3.0의 멀티파트 처리 기능을 이용할 때는 클라이언트가 보낸 데이터의 인코딩을 지정해야 한다.
// (HttpServletRequest) req; // 파라미터로 받은 ServletRequest를 원래의 타입으로 변환한다.
// HttpServletRequest : 요청Data 클라이언트 정보 // HttpServletResponse : 출력스트림, 응답정보 다루는 도구
// httpReq.getParameter("name") // 일반 폼 데이터를 원래 하던 방식대로 값을 꺼낸다.
// Part photoPart = httpReq.getPart("photo"); // 파일 데이터는 getPart()를 이용한다.
// 밑에 반복문은 업로드 한 후에 이름 무작위 부여와 저장에 대한 내용이다. // Servlet04에서 설명함.
Servlet06 -> 여러 개의 데이터를 같은 이름으로 보낸 경우 // 문제
<form action="s6" method="get">
<input type="checkbox" name="genre1">로맨틱<br>
<input type="checkbox" name="genre2">스릴러<br>
<input type="checkbox" name="genre3">호러<br>
<input type="checkbox" name="genre4">드라마<br>
<input type="checkbox" name="genre5">액션<br>
<input type="checkbox" name="genre6">SF<br>
<input type="submit" value="전송">
</form>
// HTML에서 서로 다른 이름으로 값을 보낼 경우 각각의 이름에 대해 값을 꺼내 확인해야 한다.
String genre1 = req.getParameter("genre1");
String genre2 = req.getParameter("genre2");
String genre3 = req.getParameter("genre3");
String genre4 = req.getParameter("genre4");
String genre5 = req.getParameter("genre5");
String genre6 = req.getParameter("genre6");
res.setContentType("text/plain;charset=UTF-8");
PrintWriter out = res.getWriter();
out.println("선택한 장르:");
if (genre1 != null) {
out.println("로맨틱");
}
if (genre2 != null) {
out.println("스릴러");
}
if (genre3 != null) {
out.println("호러");
}
if (genre4 != null) {
out.println("드라마");
}
if (genre5 != null) {
out.println("액션");
}
if (genre6 != null) {
out.println("SF");
}
Servlet06_2 -> 여러 개의 데이터를 같은 이름으로 보낸 경우 // 해결
// 같은 값을 여러 개 입력 받아야 하는 경우 같은 이름을 사용하자 // 단 value로 구분하자.
<form action="s6_2" method="get">
<input type="checkbox" name="genre" value="1">로맨틱<br>
<input type="checkbox" name="genre" value="2">스릴러<br>
<input type="checkbox" name="genre" value="3">호러<br>
<input type="checkbox" name="genre" value="4">드라마<br>
<input type="checkbox" name="genre" value="5">액션<br>
<input type="checkbox" name="genre" value="6">SF<br>
<input type="submit" value="전송">
</form>
// 그러면 위와 같이 한 번에 배열로 그 값들을 받을 수 있다.
// 배열로 받으면 반복문을 이용하여 보다 쉽고 간결하게 처리할 수 있다.
String[] genres = req.getParameterValues("genre");
String[] genreData = {"", "로맨틱", "스릴러", "호러", "드라마", "액션", "SF"};
res.setContentType("text/plain;charset=UTF-8");
PrintWriter out = res.getWriter();
out.println("선택한 장르:");
for (String genre : genres) {
out.println(genreData[Integer.parseInt(genre)]);
}
Servlet07 -> 빈 값과 null
<form action="s7" method="get">
a: <input type="text" name="a" value="aaaa"><br>
b: <input type="text" name="b"><br>
c: <input type="checkbox" name="c"><br>
<input type="submit" value="전송">
</form>
// a 입력상자에는 값을 넣고, b 입력상자에는 값을 입력하지 않고 전송을 한다.
// 파라미터 이름만 넘어갈 때 getParameter()의 리턴 값은 빈 문자열 객체이다.
// 즉 b는 null이 아닌 빈 문자열이 들어 있다.
// c 체크박스를 체크한 경우와, 체크하지 않은 경우
// 파라미터 이름 자체가 없으면 getParameter()는 null을 리턴한다.
// 즉 체크하지 않은 경우에는 null을 리턴한다.
Servlet08 -> 썸네일 이미지 만들기
// 돈 받고 프로그램 짤거면 사진에 대한 썸네일을 꼭 만들어야 한다. // 선택이 아닌 필수
// 클라이언트가 요청하지 않아도 3가지 버전은 필수로 만들어 놔야 한다.
// 1. 원본사진 2. 크기별 썸네일 1, 2 // 3장
// 썸네일은 원본 사진의 크기를 단순히 줄인 것이 아니다. // 화질을 떨구어 용량도 떨구어야 한다.
// mvnmvnrepository.com 또는 search.maven.org 접속 - 'thumbnailator' 검색
// org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사
// - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'
// 썸네일 만드는 라이브러리는 굉장히 많다. // 검색해서 사용하고 싶은 것 사용해도 된다.
// 썸네일 이미지 만들기 //
// ex) Thumbnails.of(this.uploadDir + "/" + filename).size(20, 20).outputFormat("jpg")
// .toFiles(Rename.PREFIX_DOT_THUMBNAIL);
// ex) Thumbnails.of(this.uploadDir + "/" + filename) .size(80, 80) .outputFormat("jpg")
// .toFiles(Rename.PREFIX_DOT_THUMBNAIL);
// ex) Thumbnails.of(this.uploadDir + "/" + filename) .size(160, 160) .outputFormat("jpg")
// .toFiles(Rename.PREFIX_DOT_THUMBNAIL);
// 원본 이미지 파일이 저장된 경로를 알려주고 // 어떤 썸네일 이미지를 만들어야 하는지 설정한다.
// 이미지 불러올 때 // 위에 ex) 순서대로
out.printf("<img src='../upload/thumbnail.%s.jpg'><br>\n", filename);
out.printf("<img src='../upload/%s' height='80'><br>\n", filename);
out.printf("<img src='../upload/%s'><br>\n", filename);
web #ex05
Servlet01 -> HttpServletRequest와 GET/POST // MyHttpServlet 상속 전
// HTTP 프로토콜로 통신을 하는 서블릿 컨테이너는 service() 메서드를 호출할 때
// ServletRequest의 값으로 HttpServletRequest를, ServletResponse의 값으로 HttpServletResponse를 전달한다.
// service() 메서드의 파라미터 값은 원래 HttpServletRequest와 HttpServletResponse이다.
// 이들 객체(HTTP객체)에는 HTTP 프로토콜을 다루는 메서드가 추가되어 있다.
// 따라서 HTTP 프로토콜을 다루려면 파라미터 값을 HTTP로 변환하여 사용하여야 한다.
HttpServletRequest httpReq = (HttpServletRequest) req; // HttpServletResponse httpRes = (HttpServletResponse) res;
if (httpReq.getMethod().equals("GET")) {
out.println("GET 요청입니다.");
} else if (httpReq.getMethod().equals("POST")) {
out.println("POST 요청입니다.");
} else {
out.println("이 서블릿이 다루지 못하는 요청 방식입니다.");
}
// HttpServletRequest.getMethod() // HttpServletRequest의 HTTP 프로토콜의 요청 방식을 리턴하는 메서드
// getMethod()를 사용해서, 요청이 GET인지, POST인지 구분이 가능하다.
Servlet02 -> HttpServletRequest와 GET/POST // MyHttpServlet 상속 후
// Servlet01의 문제점 // Http 프로토콜을 다루려고 할 때마다, Http로 형변환을 해야 한다.
@SuppressWarnings("serial")
public abstract class MyHttpServlet extends GenericServlet {
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
this.service(request, response);
}
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
}
}
// 서블릿 클래스를 만들 때 HTTP 프로토콜을 쉽게 다룰 수 있도록
// service(HttpServletRequest,HttpServletResponse) 메서드를 추가하였다.
// 서블릿 컨테이너가 service(ServletRequest req, ServletResponse res)를 호출하면,
// service(HttpServletRequest request, HttpServletResponse response)를 호출하게 만든 것이다.
// 단, Http를 파라미터로 받는 Service는 아무런 메서드가 구현되어있지 않기 때문에,
// 상속받아 사용하는 쪽에서 Service를 오버라이딩 해서 사용해야 한다. // Servlet02가 구현
// GenericServlet을 상속 받아 서블릿을 만들기보다 MyHttpServlet을 상속받아 사용한다면,
// 편하게 service()를 구현할 수 있다. // MyHttpServlet은 GenericServlet을 상속받는다.
Servlet03 -> HttpServletRequest와 Method // MyHttpServlet02 상속
// MyHttpServlet 클래스를 사용하여 서블릿을 만드는 것도 편하지만,
// 여기에다가 HTTP 요청 방식에 따라 메서드를 구분해 놓는다면 서브 클래스를 만들기가 더 편리할 것이다.
@SuppressWarnings("serial")
public abstract class MyHttpServlet2 extends GenericServlet {
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
this.service(request, response);
}
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String httpMethod = request.getMethod();
switch (httpMethod) {
case "GET":
doGet(request, response);
return;
case "POST":
doPost(request, response);
return;
case "PUT":
doPut(request, response);
return;
case "HEAD":
doHead(request, response);
return;
default:
error(request, response);
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
error(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
error(request, response);
}
protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
error(request, response);
}
protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
error(request, response);
}
private void error(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
out.println("해당 HTTP 요청을 처리할 수 없습니다.");
}
}
//미리 MyHttpServlet2에서 Client에서 넘어온 Method를 구분하여 메서드를 구분해 놓는다면,
// 요청 들어온 Method에 따라, 필요한 MyHttpServlet2에서 만든 메서드를 상속받아 사용하자.
// 서브 클래스에서 오버라이딩 할 메서드라면 private으로 선언하지 않게 한다.
// 서브 클래스에서 이 메서드를 오버라이딩 하지 않으면 오류를 출력하도록 한다.
// 클라이언트 요청이 들어오면 서블릿 컨테이너 - service(ServletRequest,ServletResponse) 호출
// service(HttpServletRequest,HttpServletResponse) 호출 - doXxx(HttpServletRequest,HttpServletResponse) 호출
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/plain;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("GET 요청입니다.");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/plain;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("POST 요청입니다.");
}
// Servlet03에서는 단순히 만들어놓은 메서드를 용도에 맞게 오버라이딩하여 사용한다.
Servlet04 -> HttpServlet 클래스를 활용하기
// HttpServlet는 기존 라이브러리에서 이미 MyHttpServlet2처럼 만들어 놓은 클래스이다.
// Http 프로토콜을 사용할 때에는 HttpServlet을 상속받아,
// 요청들어온 Method에 맞게 메서드를 오버라이딩하여 사용하자.
web #ex06
Servlet01 -> load on startup // 서블릿 객체 자동 생성 // 애노테이션으로 설정
// @WebServlet(value = "/ex06/s1", loadOnStartup = 1) // 객체를 미리 만들고 싶은 서블릿에 애노테이션 설정
// loadOnStartup=실행순서 // 미리 생성할 서블릿이 여러 개 있다면, loadOnStartup에 지정한 순서대로 생성
// 클라이언트가 실행을 요청하지 않아도 서블릿을 미리 생성할 때, loadOnStartup 프로퍼티 값을 지정한다.
// 서블릿을 미리 생성하는 경우 //
// 서블릿이 작업할 때 사용할 자원을 준비하는데 시간이 오래 걸리는 경우
// 웹 애플리케이션을 시작시킬 때 미리 서블릿 객체를 준비
// ex) DB 연결, 소켓 연결, 필요한 환경 변수 로딩, 스프링 IoC 컨테이너 준비 등
Servlet02 -> load on startup // 서블릿 객체 자동 생성 // web.xml에 설정
<servlet>
<servlet-name>ex06.Servlet02</servlet-name>
<servlet-class>com.eomcs.web.ex06.Servlet02</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ex06.Servlet02</servlet-name>
<url-pattern>/ex06/s2</url-pattern>
</servlet-mapping>
// DD파일에 미리 객체를 생성시키고 싶은 서블릿의 정보를 넣는다.
// 현재 프로젝트(v55_2)에서는 listener를 사용해서 IoC 컨테이너를 준비하지만,
// Servlet01과 Servlet02처럼 loadOnStartup 방법으로 준비할 수도 있다.
Servlet03 -> 서블릿 초기화 파라미터 // 애노테이션으로 설정
@WebServlet(value = "/ex06/s3", loadOnStartup = 1, initParams = {
@WebInitParam(name = "jdbc.driver", value = "org.mariadb.jdbc.Driver"),
@WebInitParam(name = "jdbc.url", value = "jdbc:mariadb://localhost/bitcampdb"),
@WebInitParam(name = "jdbc.username", value = "bitcamp"), @WebInitParam(name = "jdbc.password", value = "1111") })
@SuppressWarnings("serial")
public class Servlet03 extends HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletConfig config = this.getServletConfig();
resp.setContentType("text/plain;charset=UTF-8");
PrintWriter out = resp.getWriter();
}
}
// 서블릿이 사용할 값을 애노테이션 설정으로 지정할 수 있다.
// 서블릿 초기화 파라미터 // @WebInitParam()으로 설정된 값을 말한다.
// 그러나 위의 코드처럼 언제든 변경될 수 있는 값을 소스코드에 직접 적는 방식은 좋지 않다.
// 애노테이션으로도 이렇게 설정할 수 있다는 것을 보여주기 위한 예제이다.
// 보통 DD(web.xml) File에 보관한다. // 애노테이션으로 설정하지말고 DD File에 별도로 관리하자.
Servlet04 -> 서블릿 초기화 파라미터 // web.xml에서 설정
<servlet>
<servlet-name>ex06.Servlet04</servlet-name>
<servlet-class>com.eomcs.web.ex06.Servlet04</servlet-class>
<init-param>
<param-name>jdbc.driver</param-name>
<param-value>org.mariadb.jdbc.Driver</param-value>
</init-param>
<init-param>
<param-name>jdbc.url</param-name>
<param-value>jdbc:mariadb://localhost:3306/studydb</param-value>
</init-param>
<init-param>
<param-name>jdbc.username</param-name>
<param-value>study</param-value>
</init-param>
<init-param>
<param-name>jdbc.password</param-name>
<param-value>1111</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
// 별도로 web.xml에 지정하여 사용한다.
@Override
public void init() throws ServletException {
ServletConfig config = this.getServletConfig();
System.out.printf("driver=%s\n", config.getInitParameter("jdbc.driver"));
System.out.printf("url=%s\n", config.getInitParameter("jdbc.url"));
System.out.printf("username=%s\n", config.getInitParameter("jdbc.username"));
System.out.printf("password=%s\n", config.getInitParameter("jdbc.password"));
}
// init-param을 사용할 때에는 ServeltConfig를 사용한다.
Servlet05 -> 컨텍스트 초기화 파라미터 // web.xml에서 설정
<context-param>
<param-name>jdbc2.driver</param-name>
<param-value>org.mariadb.jdbc.Driver</param-value>
</context-param>
<context-param>
<param-name>jdbc2.url</param-name>
<param-value>jdbc:mariadb://localhost:3306/studydb</param-value>
</context-param>
<context-param>
<param-name>jdbc2.username</param-name>
<param-value>study</param-value>
</context-param>
<context-param>
<param-name>jdbc2.password</param-name>
<param-value>1111</param-value>
</context-param>
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext sc = this.getServletContext();
resp.setContentType("text/plain;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.printf("driver=%s\n", sc.getInitParameter("jdbc2.driver"));
out.printf("url=%s\n", sc.getInitParameter("jdbc2.url"));
out.printf("username=%s\n", sc.getInitParameter("jdbc2.username"));
out.printf("password=%s\n", sc.getInitParameter("jdbc2.password"));
}
// this.getServletContext(); // 컨텍스트 초기화 파라미터 값을 꺼내려면 ServletContext 객체가 필요하다.
// context-param을 사용할 때에는 ServletContext를 사용한다.
// 특정 Servlet에서만 사용한다면 init-param으로 선언한다. // Servlet04처럼
// 모든 Servlet에서 공통으로 사용한다면 context-param으로 선언한다. // Servlet05
web #ex07
Servlet01 -> 포워딩(forwarding) // 다른 Servlet에게 작업을 위임.
String op = request.getParameter("op");
if (!op.equals("+")) {
RequestDispatcher dispatcher = request.getRequestDispatcher("/ex07/s2");
dispatcher.forward(request, response);
return;
}
int a = Integer.parseInt(request.getParameter("a"));
int b = Integer.parseInt(request.getParameter("b"));
out.printf("%d + %d = %d\n", a, b, (a + b));
}
// URL에서 +는 공백으로 인식한다. // URL 인코딩시 +는 %2b로 처리한다.
// Servlet01, 02, 03 모두 ContentType을 설정해주어야 한다.
// op 뒤의 값이 "+"가 아니라면, /ex07/s2로 보낸다는 의미이다.
// ex) localhost:9999/bitcamp-java-web/ex07/s1?a=100&b=200&op=%2b
// 위 예시를 실행시, URL은 그대로인 상태에서 값이 300이 출력된다. // Servlet01 실행
// ex) localhost:9999/bitcamp-java-web/ex07/s1?a=100&b=200&op=-
// 위 예시를 실행시, URL은 그대로인 상태에서 값이 -100이 출력된다. // Servlet02 실행
// ex) localhost:9999/bitcamp-java-web/ex07/s1?a=100&b=200&op=*
// 위 예시를 실행시, URL은 그대로인 상태에서 해당 연산을 수행할 수 없습니다.가 출력된다. // Servlet03 실행
// 즉, URL은 그대로인 상태에서 이미 출력한 결과를 취소하는 것이다.
// 출력한 것이 취소 될 수 있다 ??? //
// PrintWriter 객체를 통해 출력하는 내용은 즉시 웹 브라우저로 전달되는 것이 아니라,
// 내부 출력 버퍼(보통 8KB 크기)에 보관한다.
// 서블릿의 service() 메서드 호출이 종료될 때 비로서 버퍼의 내용이 웹 브라우저로 전송된다.
// 물론 그 전에 버퍼가 꽉 차면 자동으로 출력된다.
// 그래서 다른 서블릿으로 실행을 위임하기 전에 서블릿이 출력한 내용을 취소할 수 있는 것이다.
// 포워딩은 서블릿을 실행한 후 리턴된다. // Servlet03까지 호출 됐어도, Servlet02, Servel01까지 실행함.
// 그러나, Servlet03에게 위임을 시켰기 때문에, Servlet02와 Servlet01의 출력 결과를 반영하지 않는다.
// 즉 Servlet03의 결과만 출력된다. // Servlet02, Servlet01의 결과는 Client에게 출력되지 않는다.
// 결론, URL에서 작업하는 Servlet이 바뀌어도 출력 결과만 바뀌고 URL은 바뀌지 않는다.
Servlet11 -> 인클루딩(including) // 다른 Servlet 작업을 포함.
// forward()는 다른 서블릿으로 위임할 때 현재 서블릿의 출력이 취소된다.
// include()는 다른 서블릿으로 실행을 위임하더라도 현재 서블릿의 실행 결과를 유지한다.
// forward() 코드의 경우, Servlet01에서 Servlet02로, Servlet02에서 Servlet03으로 넘어간다.
// Servlet03까지 가면, 해당 Servlet03의 내용만 출력된다.
// include()의 경우, Servlet11에서 Servlet11_error로, Servlet11_minus로, Servlet11_plus로 넘어간다.
// Servlet11_xxx로 넘어가더라도, Servlet11의 출력 결과를 유지한다.
// 인클루드의 경우 이전 서블릿에서 setContentType()을 설정해야 한다. // 최초 한번 설정하면 계속 유지
// 포워드는 현재 서블릿에서 설정한 setContentType()이 무시된다. // 서블릿 마다 설정을 해주어야 한다.
web #ex08
Servlet01 -> 리프래시(refresh) // java 코드로 설정 // 클라이언트에게 다른 URL을 요청하라는 명령
// refresh // ex) 결제완료 안내메세지 출력 후 메인페이지 요청
// 1. Client가 Servlet01에게 요청 // 2. Servlet01이 콘텐트+refresh url을 Client에게 응답
// 3. 콘텐트 출력
// 4. Client에서 일정시간 경과 후 Servlet02로 자동 요청 // 5. Servlet02이 Clinet에게 응답
// redirect // ex) 로그인 후 바로 메인페이지 요청
// 1. Client가 Servlet01에게 요청 // 2. Servlet01이 redirect url을 Client에게 응답
// 3. Client에서 응답 받는 즉시 Servlet02로 요청 // 4. Servlet02이 Clinet에게 응답
// forward // 오류 발생시 오류 처리 서블릿으로 포워딩
// 1. Client가 Servlet01에게 요청 // 2. Servlet01이 Servlet02에게 forward
// 3. Servlet02가 Client에게 응답
// include // 꼬리말 출력을 할 서블릿을 인클루딩
// 1. Client가 Servlet01에게 요청 // 2. Servlet01이 Servlet02에게 including
// 3. Servlet02가 Servlet01에게 리턴 // 4. Servlet01가 Client에게 응답
// refresh //
// 서버로부터 응답을 받고 내용을 출력한 후 특정 URL을 자동으로 요청하도록 만들 수 있다.
// 보통 웹 페이지를 자동으로 이동시키고 싶을 때 사용한다.
// ex) 로그인 후 메인페이지로 자동 이동, 메일을 전송한 후 메일 목록 페이지로 자동 이동
// ex) 게시글 등록한 후 게시글 목록으로 자동 이동, 결제 완료 후 결제 상태 페이지로 자동 이동
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("안녕하세요! - /ex08/s1");
for (int i = 0; i < 200; i++) {
out.println(i + " ===> 1234567890123456789012345678901234567890");
}
response.setHeader("Refresh", "3;url=s100");
}
// for문은 없다고 생각하고 이해 // for문은 이후에 넣어서 얘기함
// response.setHeader("Refresh", "3;url=s100"); // 응답 헤더에 Refresh 정보를 추가한다.
// 이미 println("안녕하세요...)를 출력한 상태에서 헤더를 어떻게 변경하는가?? //
// out.println()이 출력한 것은 출력스트림 버퍼에 보관되어 있다.
// 따라서 아직 클라이언트에게 응답한 상태가 아니다.
// 그래서 다음과 같이 출력을 한 후에 응답 헤더 값을 추가하거나 변경할 수 있는 것이다.
// 메서드 호출이 완료될 때 비로소 클라이언트로 응답헤더와 버퍼에 저장된 message-body가 출력된다.
// "반가워요 - /ex08/s100" // 따라서 3초 후에 Servlet100이 실행 된다.
public class Servlet100 extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("반가워요 - /ex08/s100");
}
HTTP/1.1 200
Refresh: 3;url=s100
Content-Type: text/plain;charset=UTF-8
Content-Length: 28 Date: Tue, 07 Apr 2020 06:46:25 GMT
Keep-Alive: timeout=20
Connection:keep-alive
안녕하세요! - /ex08/s1
// HTTP 응답 프로토콜을 확인해보면 Refresh 응답헤더가 적용 된 것을 확인할 수 있다.
// Refresh: 3;url=s100 // 웹 브라우저는 이 헤더의 정보에 따라 다시 요청한다.
// for 반복문을 넣고 돌린다면, 헤더를 추가하거나 변경하는 코드는 적용되지 않는다.
// 반복문으로 인해 8KB 버퍼가 꽉 차기 때문에 헤더를 설정하기 전에 이미 버퍼 내용이 출력된다.
// 즉 응답이 완료된다. // 응답을 완료한 다음에 응답 헤더의 값을 변경하거나 추가해도 적용이 되지 않는다.
Servlet02 -> 리프래시(refresh) // HTML 태그로 설정
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<html><head>");
out.println("<meta http-equiv='Refresh' content='3;url=s100'>");
out.println("</head><body>");
out.println("안녕하세요! - /ex08/s2");
out.println("</body></html>");
}
// HTML 태그로 설정하는 경우에는, <head> 태그 사이에 넣어야 한다.
Servlet03 -> 리다이렉트(redirect) // 응답할 때 콘텐트를 보내지 않고 바로 다른 페이지를 요청하라고 명령
// redirect //
// 클라이언트의 요청을 받은 후 콘텐트를 보내는 대신 다른 페이지의 URL을 알려줄 때 사용한다.
// 웹 브라우저는 응답 받는 즉시 해당 페이지를 요청한다.
// 웹 서버로부터 콘텐트를 받지 않았기 때문에 어떤 것도 출력하지 않는다. // 바로 다른 페이지로 이동
// 리프래시와 달리 서버는 콘텐트(message-body)를 보내지 않는다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<html><head>");
out.println("<title>리다이렉트</title>");
out.println("</head><body>");
for (int i = 0; i < 4000; i++) {
out.println("안녕하세요! - /ex08/s3");
}
out.println("</body></html>");
response.sendRedirect("s100");
}
HTTP/1.1 302
Location: s100
Content-Type: text/html;charset=UTF-8
Content-Length: 0
Date: Tue, 07 Apr 2020 07:04:42 GMT
Keep-Alive: timeout=20
Connection: keep-alive
// 전과 같이 for문이 있으면 redirect 적용되지 않는다. // 이유는 전과 같이 버퍼 때문에 응답이 되어버려서이다.
// for문이 없다고 가정하고 얘기함.
// HTTP 응답 프로토콜 //
// HTTP/1.1 302 // 요청한 자원이 다른 URL에 있음을 표시한다.
// Location: s100 // 다른 URL의 주소를 알려준다.
// refresh때와 다르게 안녕하세요! 같은 메세지가 없다.
// response.sendRedirect("URL"); // 클라이언트에게 URL을 알려줄 때 상대 경로를 지정할 수 있다.
// forward/include 와 달리 '/'는 컨텍스트 루트(웹 애플리케이션 루트)가 아닌 웹 서버 루트를 의미한다.
// forward/include 때의 Servlet02, 03의 경우에는 컨텍스트 루트였다.
// 리다이렉트를 하는 순간 이전까지 버퍼로 출력된 내용은 모두 버려진다.
// 이유 // 리다이렉트는 클라이언트에게 콘텐트를 보내지 않는다.
// 단, 버퍼가 꽉 차서 클라이언트에게 응답한 경우에는, 리다이렉트는 적용되지 않는다.
web #ex09
Servlet01 -> 보관소에 값 넣기 // ServletContext, HttpSession, ServletRequest
ServletContext sc = this.getServletContext();
sc.setAttribute("v1", "aaa");
HttpSession session = request.getSession();
session.setAttribute("v2", "bbb");
request.setAttribute("v3", "ccc");
response.setContentType("text/plain;charset=UTF-8");
PrintWriter out = response.getWriter();
// 보관소는 총 3가지가 있다. //
// ServletContext // this.getServletContext(); // ServletContext 객체는 애플리케이션이 시작될 때 생성된다.
// HttpSession // request.getSession(); // HttpSession 객체는 웹 브라우저에서
// '세션 아이디(예:고객번호, 스탬프 카드)'를 제공하지 않으면, getSession()을 호출할 때 생성된다.
// 웹 브라우저에서 '세션 아이디'를 제공하면, getSession()을 호출할 때 기존에 생성했던 세션 객체를 리턴한다.
// ServletRequest // request.setAttribute("v3", "ccc"); // ServletRequest 객체는 클라이언트가 요청할 때마다 생성
// ServletContext와 HttpSession은 객체를 생성 및 load를 하고 사용해야 한다.
// ServletRequest는 파라미터로 받았기 때문에 단순히 호출하면 된다.
Servlet02 -> 보관소에서 값 꺼내기
// ServletContext // this.getServletContext(); // Context를 먼저 불러와야 한다.
// HttpSession // request.getSession(); // ServletRequest에서 Session을 먼저 불러와야 한다.
// request.getAttribute("v3"); // ServletRequest에서 꺼내면 된다.
Servlet11 -> forward/include 서블릿끼리 ServletRequest 공유하는 것 테스트 // 값 넣기
// request.getRequestDispatcher("s12").forward(request, response);
// 포워드(인클루드 포함)할 때 이 서블릿이 파라미터로 받은 ServletRequest와 ServletResponse를 전달한다.
// 따라서 포워드/인클루드 서블릿들은 응답을 완료할 때까지 이 객체들을 공유하는 것이다.
Servlet12 -> forward/include 서블릿끼리 ServletRequest 공유하는 것 테스트 // 값 꺼내기
web #ex10
Servlet01 -> 쿠키(cookie) 보내기
// 쿠키 // 웹서버가 웹브라우저에게 맡기는 데이터 // 응답할 때 응답 헤더에 포함시켜 보낸다.
// 웹브라우저는 응답헤더로 받은 쿠키 데이터를 보관하고 있다가
// 지정된 URL을 요청할 때 요청 헤더에 포함시켜 웹 서버에게 쿠키를 다시 보낸다.
HTTP/1.1 200 Set-Cookie: name=hong
Set-Cookie: age=20
Set-Cookie: working=true
Set-Cookie: name2=홍길동 // 인코딩 하지 않은 예시
Set-Cookie: name3=%ED%99%8D%EA%B8%B8%EB%8F%99
Content-Type: text/plain;charset=UTF-8 Content-Length: 35 Date: Wed, 03 Apr
2019 01:03:37 GMT
// HTTP 응답 프로토콜 예시 //
// HTTP/1.1 200 Set-Cookie: // Set-Cookie 헤더에 '이름=값' 형태로 쿠키를 보낸다.
// 값은 반드시 문자열이여야 한다. // 객체 전송 불가능하다.
// name2=홍길동 // 홍길동은 URL인코딩하지 않은 상태이다. // 저렇게 보내면 안된다. // 코드에도 적어둠
// name3= // URL 인코딩을 한 상태의 예제이다.
// 쿠키 생성 // 이름과 값으로 생성
// 쿠키의 유효기간을 설정하지 않으면 웹브라우저가 종료될 때 까지 유지된다.
// 웹브라우저를 종료하면 유효기간이 지정되지 않은 쿠키는 모두 삭제된다.
// 쿠키의 사용범위를 지정하지 않으면 현재 경로에 한정한다.
// 쿠키를 보낼 때 URL이 /ex10/s1 이라면, 브라우저는 /ex10/* 경로를 요청할 때만 서버에게 쿠키를 보낸다.
Cookie c1 = new Cookie("name", "hong");
Cookie c2 = new Cookie("age", "20");
Cookie c3 = new Cookie("working", "true");
Cookie c4 = new Cookie("name2", "홍길동");
Cookie c5 = new Cookie("name3", URLEncoder.encode("홍길동", "UTF-8"));
response.addCookie(c1);
response.addCookie(c2);
response.addCookie(c3);
response.addCookie(c4);
response.addCookie(c5);
// c1 프로토콜 예 // Set-Cookie: name=hong
// c2, c3 // 값은 반드시 문자열이어야 한다 // 만약 문자열이 아닌 값을 보내려면,
// Base64와 같은 인코딩 기법을 이용하여 바이너리 데이터를 문자화시켜서 보내야 한다.
// c4, c5 // 또한 값은 반드시 ISO-8859-1 이어야 한다.
// 만약 UTF-8을 보내고 싶다면 URL 인코딩 같은 기법을 사용하여 ASCII 코드화시켜 보내야 한다.
// response.addCookie(쿠키); // 쿠키를 응답 헤더에 포함시키는 방법
Servlet02 -> 쿠키(cookie) 읽기 // 사용 범위가 지정된 쿠키 읽기
// Cookie[] cookies = request.getCookies(); // 쿠키 꺼내기
// 쿠키를 이름으로 한 개씩 추출할 수 없다. // 한번에 배열로 받아야 한다.
// 만약 요청 헤더에 쿠키가 없으면, 리턴되는 것은 null이다. // 사용할 때 유의하자.
if (c.getName().equals("name3")) {
out.printf(" => %s\n", URLDecoder.decode(c.getValue(), "UTF-8"));
}
// 쿠키 값이 URL 인코딩한 값이라면 원래의 값으로 되돌리기 위해 URL 디코딩 해야한다.
Servlet11 -> 쿠키(cookie) // 유효기간 설정하기
// 유효기간을 설정하면 웹브라우저를 종료해도 삭제되지 않는다.
// 단 유효기간이 지나면 웹서버에 보내지 않고 삭제한다.
// 웹 브라우저는 로컬 디스크에 쿠키를 보관한다.
Cookie c1 = new Cookie("v1", "aaa");
Cookie c2 = new Cookie("v2", "bbb");
c2.setMaxAge(30);
Cookie c3 = new Cookie("v3", "ccc");
c3.setMaxAge(60);
response.addCookie(c1);
response.addCookie(c2);
response.addCookie(c3);
// setMaxAge(시간) // 쿠키를 보낸 이후 시간 동안만 유효
HTTP/1.1 200 Set-Cookie: v1=aaa <---- 유효기간이 설정되지 않은 쿠키
Set-Cookie: v2=bbb; Max-Age=30; Expires=Wed, 08-Apr-2020 02:41:43 GMT
Set-Cookie: v3=ccc; Max-Age=60; Expires=Wed, 08-Apr-2020 02:42:13 GMT
Content-Type: text/plain;charset=UTF-8
Content-Length: 36
Date: Wed, 08 Apr 2020 02:41:40 GMT
// c1 : 시간설정 x // c2 : 30초 // c3 : 60초
Servlet12 -> 쿠키(cookie) // 유효기간이 설정된 쿠키 읽기
// 유효기간이 설정된 쿠키는, 유효기간이 지나기 전까지 브라우저를 껐다켜도 컴퓨터를 껐다켜도,
// 사라지지 않는다. // 유저 하드디스크에 보관하기 때문 또한 같은 브라우저에서만 사용된다.
// 시간이 남아있어도 다른 브라우저를 사용하면 나타나지 않는다. // 해당 브라우저에는 남아있음.
// 유효기간이 설정되지 않은 쿠키는 브라우저를 껐다 키면 사라진다. // 브라우저를 안끄면 계속 남아있음.
Servlet21 -> 쿠키(cookie) // 쿠키 사용 범위 지정
// 쿠키의 사용 범위를 지정하지 않으면 쿠키를 발행한 URL 범위에 한정된다.
// 즉 같은 URL로 요청할 때만 쿠키를 보내야 한다.
// ex) 쿠키를 발행한 URL : /ex10/s21 // 쿠키를 보낼 수 있는 URL : /ex10/*
// 쿠키를 보낼 수 없는 URL : /ex10 이외의 모든 URL
HTTP/1.1 200 Set-Cookie: v1=aaa
Set-Cookie: v2=bbb; Path=/bitcamp-java-web/ex10/a
Set-Cookie: v3=ccc; Path=/bitcamp-java-web
Content-Type: text/plain;charset=UTF-8
Content-Length:36
Date: Wed, 08 Apr 2020 02:51:12 GMT
// Path=값 // 값의 경로 안에 있는 것들로 제한한다.
// 사용 범위를 지정하지 않은 쿠키 //
// 쿠키를 발급한 서블릿과 같은 경로이거나 하위 경로의 서블릿을 요청할 때만 웹 브라우저가 서버에 쿠키를 보낸다.
// 사용 범위 지정한 쿠키 //
// 쿠키를 발급한 서블릿의 경로에 상관없이 지정된 경로의 서블릿을 요청할 때 웹 브라우저가 서버에 쿠키를 보낸다.
// 쿠키의 경로를 적을 때 웹 애플리케이션 루트(컨텍스트 루트)로 적어야 한다.
// 쿠키 경로는 서블릿 컨테이너가 사용하는 경로가 아니거 웹 브라우저가 사용하는 경로이다.
// 웹 브라우저에서 '/' 은 서버 루트를 의미한다.
// 서버 루트를 의미하는 '/' // 포트번호 다음에 오는 '/'를 말한다.
// 따라서 웹 브라우저가 사용하는 경로를 지정할 때는 조심해야 한다. '/'가 서버 루트를 의미하기 때문이다.
// 그래서 쿠키의 경로를 지정할 때는 웹 애플리케이션 루트(컨텍스트 루트)를 정확하게 지정해야 한다.
// Servlet22, 23, 24는 테스트용
web #ex11
Servlet01 -> 세션(session) //
// 세션(session) // 클라이언트를 식별하는 기술
// HTTP 프로토콜은 Stateless 방식으로 통신을 한다. // 연결한 후 요청하고 응답을 받으면 연결을 끊는다.
// 서버는 클라이언트가 요청할 때 마다 누구인지 알 수 없다.
// 이를 해결하기 위해 클라이언트가 접속하면 웹 서버는 그 클라이언트를 위한 고유 번호를 발급(쿠키 이용)한다.
// 이 고유 번호를 '세션 아이디'라 부른다.
// 웹 브라우저는 세션 아이디를 쿠키에 보관해 두었다가 그 서버에 요청할 때 마다 세션 아이디를 보낸다.
// 세션 아이디 쿠키는 유효기간을 설정하지 않았기 때문에 웹 브라우저를 종료하면 세션 아이디 쿠키는 삭제된다.
// 세션 아이디 쿠키의 사용 범위는 웹 애플리케이션이다.
// 따라서 같은 웹 애플리케이션의 서블릿을 실행할 때는 무조건 세션 아이디를 보낸다.
// 세션 아이디 발급 시기 //
// 새 세션을 생성할 때 세션 아이디를 발급한다.
// 새 세션 생성 시기 //
// 세션이 없는 상태에서 request.getSesssion()을 호출할 때 생성한다.
// 생성한 세션의 ID는 쿠키를 통해 응답할 때 웹 브라우저로 보낸다.
// 세션ID 무효화 //
// 1. 세션에 지정된 timeout 초과했을 때 // 2. session.invalidate() 호출
Servlet02 -> 세션(session) // 세션에서 값 꺼내기
// 세션 아이디 보내기 //
// 서버로부터 쿠키를 통해 받은 세션아이디는 웹 브라우저가 해당 서버에 요청할 때마다 쿠키로 세션 아이디를 보낸다.
GET /java-web/ex11/s2 HTTP/1.1
Host: localhost:9999
...
Cookie:JSESSIONID=9909D09693CE9E0B8D23BE824313C834
// 1-1. 세션 아이디가 있고, 유효하다면, 기존에 생성한 HttpSession 객체를 리턴한다.
// 1-2. 세션 아이디가 있고, 유효하지 않다면, 2번으로 진행함. // 없다고 취급함 유효하지 않으면 있으나 마나임.
// 2. 세션아이디가 없다면, 새 HttpSession 객체를 생성하여 리턴한다.
// 응답 프로토콜에 새로 생성한 HttpSession 객체의 세션ID를 쿠키로 보낸다.
// 세션에서 값 꺼내기 //
// 웹 브라우저를 종료하면 이전에 서버로부터 받은 세션 아이디 쿠기가 삭제된다.
// 그런 후에 웹 브라우저에서 이 서블릿을 요청하면 getSession() 메서드를 새 세션 객체를 생성한 후 리턴한다.
// 새 세션에 값을 넣어주지 않았다면 null을 출력한다.
Servlet11 -> Servlet12,13,14와 묶음이다.
// Servlet11은 이름을 입력하라는 창을 띄우고 버튼을 누르면 Servlet12로 넘어간다.
// Servlet12는 나이를 입력하라는 창을 띄우고 버튼을 누르면 Servlet13으로 넘어간다.
// Servlet13은 전화번호를 입력하라는 창을 띄우고 버튼을 누르면 Servlet14로 넘어간다.
// 14는 이전에 입력받은 이름과 나이를 세션에서 꺼내고 전화번호를 파라미터에서 꺼낸다.
// 왜 전화번호는 세션에서 꺼내지 않는가 ? //
// Servlet11을 잘 보면 이름을 입력후 버튼을 눌러 넘어간다. 입력 값은 12로 넘어간다.
// 값을 11에서 받고 넘어가는게 아니기 때문이다.
Servlet21 -> 세션(session) // 타임아웃 설정
// session.setMaxInactiveInterval(시간); // 마지막 요청으로부터 시간동안 요청이 없으면, 세션이 무효화 된다.
// session.setMaxInactiveInterval(10); // 마지막 요청으로부터 10초동안 요청이 없으면, 세션이 무효화 된다.
// 무효화된 세션은 사용할 수 없기 때문에 getSession()은 새 세션을 만들어 리턴한다.
Servlet22 -> 세션(session) // 타임아웃 설정 확인
// 마지막 요청으로부터 정해진 시간이 지나 세션이 무효화 된 이후 요청이 들어온다면,
// 새 세션을 만들어 리턴하면 값이 null로 표기된다.
Servlet31 -> 세션(session) // 무효화시키기 1
<session-config>
<session-timeout>1</session-timeout>
</session-config>
// DD File(web.xml)에 설정하는데, 여기서 time 기준은 분이다 // 위 예시는 1분
Servlet32 -> 세션(session) // 무효화시키기 // 값 꺼내기
// 1분이 지나고 해당 Session이 호출되면 새 세션 객체를 만들어 리턴된다.
Servlet33 -> 세션(session) // 무효화시키기 2
// session.invalidate(); // 세션을 무효화시킨다.
'IT Developer > Bitcamp' 카테고리의 다른 글
비트캠프 프론트엔드 및 벡엔드 개발자 Webapp js (1) | 2020.04.28 |
---|---|
비트캠프 프론트엔드 및 벡엔드 개발자 Webapp css (0) | 2020.04.27 |
비트캠프 프론트엔드 및 백엔드 개발자 Spring-webmvc, java-web-library (0) | 2020.04.17 |
비트캠프 프론트엔드 및 벡엔드 개발자 Webapp el, jsp, jstl (0) | 2020.04.13 |
비트캠프 프론트엔드 및 백엔드 개발자 #Project v55~60 (0) | 2020.03.31 |
비트캠프 프론트엔드 및 벡엔드 개발자 DB (0) | 2020.03.26 |
비트캠프 프론트엔드 및 벡엔드 개발자 net, netty, reflect (0) | 2020.03.11 |
비트캠프 프론트엔드 및 벡엔드 개발자 ioc, jdbc, mybatis, (0) | 2020.03.11 |