웹 서버, 웹 애플리케이션
웹 서버 ( Web Server )
웹 애플리케이션 서버 ( WAS - Web Application Server )
웹 서버와 웹 애플리케이션 서버를 구분하는 이유
Servlet
Servlet이란 ?
서블릿이란 특정 url로 오는 클라이언트의 http 요청메세지를 받고, 다시 http 응답 메세지를 보내는 과정을 쉽게 만들어주는 객체라고 생각하면 된다.
Servlet이 뭔지 알아보기 위해 HTML Form 데이터를 전송하는 상황을 가정해 보자.
<form action="/save" method="post">
<input type="text" name="username"/>
<input type="text" name="age"/>
<button type="submit"> 전송 </button>
</form>
//🔥 위의 Form 데이터를 서버에 post 요청을 보내면 아래와 같은 Http 메세지로 보내게 된다.
POST /save HTTP/1.1
Host: localhost:8080
Content-Type:application/x-www-form-urlencoded
username=hdh&age=30
@WebServlet( name = "helloServlet" , urlPatterns ="/hello" )
public class HelloServlet extends HttpServlet {
@Override
protected void service ( HttpServletRequest request,
HttpServletResponse response ) {
// 애플리케이션 로직 작성
}
}
1. urlPatterns에 입력한 URL이 호출 되면 해당 Servlet의 service 메서드가
실행 된다.
2. Servlet을 만들 때는 HttpServlet을 상속 받아야 하고,
그 안에 HttpServlet이 제공하는 메서드인 service를 입력해 줘야 한다.
3. service의 인자로 들어오는 HttpServletRequest와 HttpServletResponse는
Http요청 정보과 응답 정보를 편라하게 사용할 수 있게 해 준다.
예를 들면 파라미터 값을 가져올 때 그냥 request.getParameter와 같이 쉽게
사용할 수 있는 것이다.
서블릿을 통한 HTTP 요청, 응답 흐름
Servlet 컨테이너
Multi Thread (멀티 쓰레드)
Thread
단일 요청 ( 쓰레드 하나 사용 )
다중 요청 ( 쓰레드 하나 사용 )
다중 요청 ( 요청 마다 쓰레드 생성 )
쓰레드 풀
쓰레드 풀 실무 팁
WAS의 멀티 쓰레드 지원
HTML, HTTP API, CSR, SSR
정적 리소스
HTML 페이지
HTTP API
SSR ( 서버 사이드 렌더링 )
CSR ( 클라이언트 사이드 렌더링 )
서블릿 사용 하기
// 스프링에서는 ServletComponetScan이라는 어노테이션을 제공 하는데 이걸 입력해 주면
Spring이 자동으로 현재 내 패키지를 포함해서 하위 패키지 전부를 뒤져서
Servlet을 다 찾아서 자동으로 등록 실행할 수 있게 해준다.
@ServletComponentScan
@SpringBootApplication
public class ServletApplication {
public static void main(String[] args) {
SpringApplication.run(ServletApplication.class, args);
}
}
// @WebServlet 어노테이션을 붙여서 여기에 해당 서블릿의 이름과 url을 매핑한다.
그 다음 서블릿은 HttpServlet을 상속 받아야 한다.
@WebServlet(name = "helloServlet", urlPatterns = "/helloServlet")
public class HelloServlet extends HttpServlet {
//🔥 ctrl + 0 를 눌러 HttpServlet의 service 내장 메서드 사용
HttpServletRequest를 사용하면 servlet에 http요청이 오면
Servlet Container가 request,response 객체를 만들어서 Servlet에 던져 준다.
여기까지 하면 Servlet이 호출 되었을 때 아래의 service 메서드가 호출이 된다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 이제 /hello로 접속을 하면 브라우저가 http요청 메세지를 만들어서
Servlet에 전달을 하는데 해당 요청 메세지를 출력해 보자.
// soutv를 하면 인텔리제이에서 자동으로 아래 처럼 매개변수를 출력해 준다.
System.out.println("request =" + request);
System.out.println("response =" + response);
// 콘솔에 HelloServlet.service가 찍히는 것을 확인할 수 있다.
soutm하고 텝 누르면 아래 처럼 클래스명.현재 메서드가 출력된다.
System.out.println("HelloServlet.service");
// 만약 쿼리 파라미터 까지 전달을 했다면, Servlet이 지원하는 getPrameter를 통해 값을 쉽게 가져올 수 있다.
// localhost:8080/ helloServlet?username=hdh로 요청을 하면 콘솔에 hdh가 출력될 것
String username = request.getParameter("username");
System.out.println(username);
// 이번에는 서버에서 브라우저로 응답을 해보자.
// 헤더에 응답 형식과, 인코딩 방식 입력하고 body에 hello 전달받은 username 을 보내보자.
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.getWriter().write("hello " + username);
}
}
HttpServletRequest 자세히 알아보기
// Start Line (http메서드, url, 쿼리 스트링, 스키마, 프로토콜)
POST /save HTTP/1.1
// 헤더
Host : localhost:8080
Content-Type : application/x-ww-form-urlencoded
// 바디
username=hdh&age=30
// 임시 저장
request.setAttribute(name, value)
// 임시 저장 된 값 불러오기
request.getAttrubute(name)
// 세션 관리 기능
request.getSession(create:true)
// 먼저 HTTP요청 메세지의 StartLine에 해당 하는 부분을 하니씩 출력해 보자.
@WebServlet(name="requestHeaderServlet", urlPatterns = "/request-header")
public class RequestHeaderServlet extends HttpServlet {
// request-header로 요청이 오면 아래 service가 실행이 될것이고
그러면 printStartLine메서드에 req를 전달할 것이다.
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
printStartLine(req);
}
// 전달받은 HttpServletRequest객체를 통해 Start라인 관련 정보를
아래 처럼 쉽게 확인할 수 있다.
private static void printStartLine(HttpServletRequest req) {
System.out.println("--- http요청 메세지 start라인 정보를 출력 시작---");
System.out.println("메서드 정보 = " + req.getMethod());
System.out.println("프로토콜 정보 = " + req.getProtocol());
System.out.println("스키마 정보 = " + req.getScheme());
System.out.println("요청 url 정보 = " + req.getRequestURL());
System.out.println("요청 uri 정보 = " + req.getRequestURI());
System.out.println("쿼리 파라미터 정보 = " + req.getQueryString());
System.out.println("https 사용 유무 = " + req.isSecure());
System.out.println("--- http요청 메세지 start라인 정보를 출력 끝 ---");
}
}
// http://localhost:8080/request-header?username=hdh로 요청
--- http요청 메세지 start라인 정보를 출력 시작---
메서드 정보 = GET
프로토콜 정보 = HTTP/1.1
스키마 정보 = http
요청 url 정보 = http://localhost:8080/request-header
요청 uri 정보 = /request-header
쿼리 파라미터 정보 = username=hdh
https 사용 유무 = false
--- http요청 메세지 start라인 정보를 출력 끝 ---
@WebServlet(name="requestHeaderServlet", urlPatterns = "/request-header")
public class RequestHeaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
printStartLine(req);
printHeaderUtils(req);
}
//Header 정보 조회
private void printHeaderUtils(HttpServletRequest request) {
System.out.println("--- Header 조회 시작 ---");
System.out.println("[Host 조회]");
System.out.println("Host 헤더 = " + request.getServerName());
System.out.println("Host 헤더 포트 = " + request.getServerPort());
System.out.println("[Accept-Language 조회]");
// 브라우저는 요청을 보낼때 Accept-Language에 원하는 언어의 우선순위를 입력해서 보내는데
// getLoacales()를 사용하면 해당 정보를 전부 가져 올 수 있다.
request.getLocales().asIterator()
.forEachRemaining(locale -> System.out.println("언어 우선순위 정보 전체" +
locale));
// 그 우선순위 중 가장 첫번째 것을 꺼낼려면 getLoacale()을 사용하면 된다.
System.out.println("우선순위 언어 중 첫번째 언어 = " + request.getLocale());
System.out.println("[cookie 조회]");
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
System.out.println(cookie.getName() + ": " + cookie.getValue());
}
}
System.out.println("[Content 조회]");
System.out.println("ContentType 조회 = " + request.getContentType());
System.out.println("ContentLength 조회 = " + request.getContentLength());
System.out.println("인코딩 방식 조회 = " + request.getCharacterEncoding());
System.out.println("--- Header 조회 끝 ---");
}
}
--- Header 조회 시작 ---
[Host 조회]
Host 헤더 = localhost
Host 헤더 포트 = 8080
[Accept-Language 조회]
언어 우선순위 정보 전체ko_KR
언어 우선순위 정보 전체ko
언어 우선순위 정보 전체en_US
언어 우선순위 정보 전체en
우선순위 언어 중 첫번째 언어 = ko_KR
[cookie 조회]
_ga: GA1.1.875511955.1684123037
_ga_DTT7CE66G0: GS1.1.1684153574.4.1.1684154616.0.0.0
_ga_GC1VKDEP9C: GS1.1.1685441913.4.0.1685441913.0.0.0
[Content 조회]
ContentType 조회 = null
ContentLength 조회 = -1
인코딩 방식 조회 = UTF-8
--- Header 조회 끝 ---
HTTP 요청 데이터를 보내는 방법
GET 방식 + 쿼리 파라미터 방식
/**
* 1. 파라미터 전송 기능
* http://localhost:8080/request-param?username=hdh&age=31
* 2. 동일한 파라미터 전송 가능
* http://localhost:8080/request-param?username=hdh&username=hdh2&age=31
*/
@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
public class RequestParamServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse
resp) throws ServletException, IOException {
// 전체 파라미터 조회 ( 🔥 getParameterNames() )
// 전달받은 쿼리 파라미터의 Key값을 순회하여 getParameter에 하나씩 넣어서
그 값을 가져 오는 것
// request.getParameterNames().asIterator()는 컬렉션을
Enumeration 인터페이스로 바꿔주는 것이다. 여기에는 파리미터의
key 값들이 들어있을 것이다.
// forEachRemaining은 각 요소에 지정된 작업을 수행하도록 하는 것.
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> System.out.println("전체 파라미터" + paramName +
"=" + request.getParameter(paramName)));
// 단일 파라미터 조회 ( 🔥 getParameter("키값 전달") )
// getParameter의 쿼리 파라미터의 key값을 전달한다.
String username = request.getParameter("username");
System.out.println("개별 파라미터 값 = " + username);
String age = request.getParameter("age");
System.out.println("개별 파라미터 값 = " + age);
// 복수 파라미터 조회 (🔥 getParameterValues("키값 전달")
// 하나의 key에 여러 값을 받았을 때는 getParameterValues로 받아야 한다.
// 만약 getParameter로 받으면 getParameterValues의 첫번째 값을 반환 한다.
// ?username=hdh&username=hdh2&age=31 이런식으로 username이라는 key에
복수의 값들을 전달할 수 있다.
// 참고로 복수 파라미터를 전달을 했는데 getParameter로 받으면
getParameterValues의 첫번째 값을 반환한다.
String[] usernames = request.getParameterValues("username");
for (String name : usernames) {
System.out.println("복수 파라미터 값들 =" + name);
}
resp.getWriter().write("ok");
}
}
전체 파라미터 username=hdh
전체 파라미터 age=31
개별 파라미터 값=hdh
개별 파라미터 값=31
복수 파라미터 값들 = hdh
복수 파라미터 값들 = hdh2
POST 방식 + HTML Form 방식
// form 데이터를 전송하는 html
// webapp 폴더에 basic 폴더를 만들고 그 안에 파일을 만듬
// 이렇게 하면 localhost:8080/basic/html파일명 으로 접속할 수 있다.
정적 컨텐츠가 제공이 되는 것
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/request-param" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>
전체 파라미터 username=hdh
전체 파라미터 age=31
개별 파라미터 값 = hdh
개별 파라미터 값 = 31
복수 파라미터 값들 = hdh
// 웹 브라우저가 생성한 form 데이터 메세지
POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
// 아래의 content-body를 보면 쿼리 파라미터와 형식이 같은 것을 확인할 수 있다.
username=hdh&age=30
Http 메세지 바디에 데이터를 직접 담는 방식 ( 단순 텍스트 )
// 아래의 servlet url에 postman을 통해 body에 데이터를 담아 보내 보자.
@WebServlet(name = "requestBodyStringServlet", urlPatterns = "/request-body-string")
public class RequestBodyStringServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest request, HttpServletResponseresponse)
throws ServletException, IOException {
// getInputStream을 통해 Content-body의 내용을 바이트 코드로
얻을 수 있다.
// 아래는 바이트 코드의 body 데이터를 string으로 변환 한것.
ServletInputStream inputStream = request.getInputStream();
System.out.println("inputStream +" + inputStream);
// 스프링이 제공하는 StreamUtils에 inputStream과 인코딩 방식을
넣어주면 바디 데이터를 꺼낼 수 있다.
String messageBody = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);
/* HelloData helloData = objectMapper.readValue(messageBody,
HelloData.class);
System.out.println("helloData.username = " + helloData.getUsername());
System.out.println("helloData.age = " + helloData.getAge());*/
response.getWriter().write("ok");
}
}
Http 메세지 바디에 데이터를 직접 담는 방식 ( JSON )
// Lombok라이브러리를 사용하면
getter와 setter 어노테이션을 붙인 경우 클래스 내부에서 따로 getter와 setter를
구성하지 않아도 된다.
@Getter
@Setter
// 전달 받은 json 데이터의 값들을 아래의 클래스의 인스턴스로 변환 할 것이다.
public class HelloData {
private String username;
private int age;
}
@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {
// 잭슨 라이브러리 사용해서 json데이터를 읽는다.
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest request, HttpServletResponse
response)
throws ServletException, IOException {
// 일단 Text를 받을 때 처럼 아래와 같이 코드를 입력 후
postman을 통해 {"username" : "hdh"} 라는 Json 형식의 데이터를 전달하면
{"username" : "hdh"} 이게 그대로 출력이 된다.
그 이유는 Json 데이터도 그냥 String이기 때문이다.
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);
// 그럼 이제 전달받은 json데이터를 HelloData.class의 타입으로 변환해 보자.
이때 스프링 부트에서 기본으로 제공하는 Jackson이라는 라이브러리를 사용해야 한다.
클래스 상단에 잭슨 라이브러이의 ObjectMapper의 인스턴스를 생성하자.
// 그러면 readValue를 통해 json데이터를 읽을 수 있게 된다.
여기에 전달 받은 Json 데이터와 변환할 클래스 타입을 입력한다.
객체로 변환 후 Getter 함수로 값을 꺼내온다.
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
System.out.println("helloData.username = " + helloData.getUsername());
System.out.println("helloData.age = " + helloData.getAge());
response.getWriter().write("ok");
}
}
HttpServletResponse 자세히 알아보기
기본 사용법
@WebServlet(name="responseHeaderServlet" , urlPatterns = "/response-header")
public class ResponseHeaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// setStatus를 통해 http 응답 코드를 넣을 수 있다.
200,300 등 과 같은 숫자를 직접 넣어도 되지만
// 아래 처럼 HttpServletResponse 인터페이스에 상수값으로 저장되어 있는 응답코드
값을 가져와 사용해도 된다.
response.setStatus(HttpServletResponse.SC_OK);
// setHeader를 통해 헤더 값 설정할 수 있다.
// content-type지정, 캐시 무효화, my header처럼 내가 원하는 임의의 헤더를
넣을 수도 있다.
// 이제 url을 통해 요청을 하고 개발자 도구에서 responseheader를
확인해 보면 아래의 값들이 들어 온것을 확인할 수 있다.
response.setHeader("Content-type","text/plain;charset=utf-8");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("my-header", "hello");
response.setHeader("your-header","world");
// 응답 편의 메서드에 응답 객체 전달
content(response);
cookie(response);
redirect(response);
}
// ✅좀더 편하게 하는 방법도 있다.
// 응답을 아래 처럼 더 쉽게 할 수도 있다.
private void content (HttpServletResponse resp) {
resp.setContentType("text/plain");
resp.setCharacterEncoding("utf-8");
}
// 응답 헤더에 쿠기 설정
private void cookie (HttpServletResponse resp) {
// 쿠키 객체를 생성해서
Cookie cookie = new Cookie("My-Cookie", "good");
cookie.setMaxAge(600); // 쿠키 유효시간 600초 의미
// 응답 메세지에 객체를 전달한다.
resp.addCookie(cookie);
}
// redirect 경로를 입력하면 응답 헤더에 Location 정보가 들어간다.
redirect 경로를 입력해 주면 처음에 302코드와 함께 응답 헤더에 Locatin 정보가
들어가고, 클라이언트는 해당 경로로 다시 요청을 하게 되고
요청이 정상적으로 수행이 되면 200코드가 반환이 되게 되는 것.
private void redirect (HttpServletResponse resp) throws IOException {
resp.sendRedirect("/hello-form.html");
}
}
Cache-Control: no-cache, no-store, must-revalidate
Connection:keep-alive
Content-Length:3
Content-Type:text/plain;charset=utf-8
Date:Sun, 31 Dec 2023 06:06:48 GMT
Keep-Alive:timeout=60
My-Header:hello
Pragma:no-cache
Http 응답 데이터
//[message body]
PrintWriter writer = response.getWriter();
writer.println("ok");
Html 응답
@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html")
public class ResponseHtmlServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponseresponse)
throws ServletException, IOException {
//Content-Type: text/html;charset=utf-8
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.println("<html>");
writer.println("<body>");
writer.println(" <div>안녕?</div>");
writer.println("</body>");
writer.println("</html>");
}
}
Json 형식으로 응답
@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json")
public class ResponseJsonServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest request, HttpServletResponse
response)
throws ServletException, IOException {
//Content-Type: application/json 설정
response.setHeader("content-type", "application/json");
response.setCharacterEncoding("utf-8");
// json객체를 변환할 클래스의 인스턴스를 가져옴
HelloData data = new HelloData();
data.setUsername("kim");
data.setAge(20);
// {"username":"kim","age":20}형태의 json 형식으로 변환
객체를 문자, 즉 Json(json도 그냥 문자다)으로 바꾸는 것
String result = objectMapper.writeValueAsString(data);
response.getWriter().write(result);
}
}
회원 관리 앱 애플리케이션 만들기
@Getter
@Setter
public class Member {
// 회원 정보는 id, usernamem, age로 구성된다.
private Long id;
private String username;
private int age;
public Member() {
}
public Member(String username, int age) {
this.username = username;
this.age = age;
}
}
public class MemberRepository {
// 회원이 저장될 store
private Map<Long, Member> store = new HashMap<>();
// 회원 id로 사용될 변수
private static Long sequence = 0L;
// 싱글톤 MemberRepository를 위해 내부에 인스턴스 생성
private static final MemberRepository instance = new MemberRepository();
// getInstance메소드를 통해서 인스턴스 리턴
public static MemberRepository getInstance() {
return instance;
}
private MemberRepository () {
}
// 회원 저장 메서드
// 회원을 저장하고 난뒤 저장된 회원 정보를 다시 리턴
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
// id로 회원을 찾는 메서드
public Member findById (Long id) {
return store.get(id);
}
// store에 있는 모든 값들을 꺼내서 새로운 ArrayList에 담아 준다.
public List<Member> findAll () {
return new ArrayList<>(store.values());
}
public void clearStroe() {
store.clear();
}
}
class MemberRepositoryTest {
MemberRepository memberRepository = MemberRepository.getInstance();
//@AfterEach를 사용하는 이유는 만약 테스트를 한번에 전부 실행하면
// 각각의 단위 테스트가 실행 되고 종료 되는 순서가 보장되지 않아 테스트 결과에
// 영향을 끼칠 수 있어서 각각의 단위 테스트가 종료 되면 clear를 해주는 것
@AfterEach
void afterEach() {
memberRepository.clearStroe();
}
@Test
void save() {
// 회원 정보가 저장이 되는지 확인
// 회원 객체 생성
Member member = new Member("hdh", 30);
// 회원 가입
Member saveMember = memberRepository.save(member);
// 회원 가입시 리턴 받은 회원 정보 중 회원 id로 회원을 찾음
Member findMember = memberRepository.findById(saveMember.getId());
// 저장된 회원과, id로 찾은 회원 정보가 일치하는지 확인
assertThat(findMember).isEqualTo(saveMember);
}
@Test
void findAll() {
// 가입된 회원 전체 조회 테스트
// 회원을 2명 만든다.
Member member1 = new Member("messi", 30);
Member member2 = new Member("ronaldo", 30);
// 회원 저장
memberRepository.save(member1);
memberRepository.save(member2);
// 전체 회원 데이터를 List로 받아옴
List<Member> result = memberRepository.findAll();
// 전체 회원 데이터가 2명이 맞는지 확인, 각각의 회원이 있는지 확인
assertThat(result.size()).isEqualTo(2);
assertThat(result).contains(member1,member2);
}
}
서블릿으로 회원 관리 앱 애플리케이션 만들기
// 회원 가입 페이지 서블릿을 만든다.
// 서블릿은 HttpServlet을 상속 받아야 한다.
@WebServlet(name="memberFormServlet" , urlPatterns = "/servlet/member/new-form")
public class MemberFormServlet extends HttpServlet {
// 이전에 MemberRepository를 싱글톤으로 만들었기 때문에 new 키워드로 인스턴스를 새로 생성하는 것이 아니라
// 메서드를 통해 생성해 둔 인스턴스를 가져 와야 한다.
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// html을 전달할 것이기 때문에 content-type과 인코딩 방식을 지정
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
// response.getWritrer()를 하면 writer를 가져올 수 있다.
// 🥊 이제 /servlet/member/new-form으로 접속하면 아래 html이 화면에 그려질 것이다.
PrintWriter w = response.getWriter();
w.write("<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>Title</title>\n" +
"</head>\n" +
"<body>\n" +
"<form action=\"/servlet/members/save\" method=\"post\">\n" +
" username: <input type=\"text\" name=\"username\" />\n" +
" age: <input type=\"text\" name=\"age\" />\n" +
" <button type=\"submit\">전송</button>\n" +
"</form>\n" +
"</body>\n" +
"</html>\n");
}
}
// html form의 post 요청을 받는 서블릿
@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/member/save")
public class MemberSaveServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// content-body에 담긴 데이터도 쿼리 파라미터 작성 형식과 같기 때문에 전송된 form형식의 데이터를 getParameter를 통해 가져온다.
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
// 전송된 form 데이터를 바탕으로 member객체를 만들고 저장
Member member = new Member(username, age);
memberRepository.save(member);
// Member 객체를 사용해서 화면용 html을 동적으로 만들어서 응답.
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter w = response.getWriter();
w.write("<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
"</head>\n" +
"<body>\n" +
"성공\n" +
"<ul>\n" +
" <li>id="+member.getId()+"</li>\n" +
" <li>username="+member.getUsername()+"</li>\n" +
" <li>age="+member.getAge()+"</li>\n" +
"</ul>\n" +
"<a href=\"/index.html\">메인</a>\n" +
"</body>\n" +
"</html>");
}
}
// 아래 url로 접속하면 저장된 모든 회원이 화면에 출력되는 서블릿
@WebServlet(name="memberListServlet" , urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
List<Member> members = memberRepository.findAll();
PrintWriter w = response.getWriter();
w.write("<html>");
w.write("<head>");
w.write(" <meta charset=\"UTF-8\">");
w.write(" <title>Title</title>");
w.write("</head>");
w.write("<body>");
w.write("<a href=\"/index.html\">메인</a>");
w.write("<table>");
w.write(" <thead>");
w.write(" <th>id</th>");
w.write(" <th>username</th>");
w.write(" <th>age</th>");
w.write(" </thead>");
w.write(" <tbody>");
// 아래 주석 처리 된 부분 처럼 구성하면 그냥 정적 html파일이 전송이 되는 것이므로
/*
w.write(" <tr>");
w.write(" <td>1</td>");
w.write(" <td>userA</td>");
w.write(" <td>10</td>");
w.write(" </tr>");
*/
// for문을 돌려서 현재 저장된 회원을 한명한명 출력한다.
for (Member member : members) {
w.write(" <tr>");
w.write(" <td>" + member.getId() + "</td>");
w.write(" <td>" + member.getUsername() + "</td>");
w.write(" <td>" + member.getAge() + "</td>");
w.write(" </tr>");
}
w.write(" </tbody>");
w.write("</table>");
w.write("</body>");
w.write("</html>");
}
}
JSP로 회원 관리 앱 애플리케이션 만들기
//JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상
implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //
스프링부트 3.0 이상
implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상
//JSP 추가 끝
<!-- jsp는 아래의 코드가 반드시 필요하다.-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/jsp/members/save.jsp" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>
// 서블릿에서 파라미터 가져오는 것과 같은 로직이 먼저 실행이 되고, 마지막에 html일 반환
// 했던 것 처럼 먼저 java코드를 상단에 입력
// 필요한 클래스들을 import
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
// jsp도 결국 서블릿으로 변환 되어 사용이 되기 때문에 request, response 그냥 사용 가능 하다.
MemberRepository memberRepository = MemberRepository.getInstance();
System.out.println("save.jsp");
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
System.out.println("member = " + member);
memberRepository.save(member);
%>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
성공
<ul>
<li>id=<%=member.getId()%></li>
<li>username=<%=member.getUsername()%></li>
<li>age=<%=member.getAge()%></li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
JSP정리
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
// 아래의 블럭 사이에 자바 코드를 사용할 수 있다.
<% %>
// 아래의 블럭 사이에서는 자바 코드를 출력할 수 있다.
<%= %>
서블릿과 JSP의 한계
MVC 패턴
개요
컨트롤러
모델
뷰
서비스
MVC 적용
// 서블릿 mvc부분의 회원 가입 버튼을 클릭하면 아래 url로 요청이 될 것이고,
아래의 서블릿으로 요청을 받게 될 것이다.
@WebServlet(name="mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
// dispatcher.forward() : 다른 서블릿이나 JSP로 이동할 수 있는 기능이다
// forward는 서버 내부에서 일어나는 호출이다. 무슨 말이냐면 redirect와 비교해서 이해해 보면 redirect는
// 응답이 클라이언트에 전달이 된 후, 클라이언트가 다른 경로로 다시 요청을 하는 것을 말한다. redirect의 경우
// 새로운 경로로 다시 요청하는 것이기 때문에 url경로로 바뀌게 된다. 쉽게 말해 호출이 2번 되는 것
// 하지만 forward의 경우 클라이언트는 한번 호출한 것이고, 그냥 서버 내부에서 발생하는 것
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//🔥 먼저 연결할 jsp파일의 경로를 입력한다.
WEB-INF 아래 jsp파일을 위치 시키면 url로 직접 jsp 파일에 접근 할 수 없고 반드시
컨트롤러 내부에서 forward 하거나 해야 jsp파일에 접근할 수 있다. was의 규칙이다.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
//🔥 dispatcher.forward는 다른 서블릿이나 jsp로 이동할 수 있는 기능이다.
forward는 서버 내부에서 일어나는 호출이다.
redirect와 비교해서 이해 해 보면 redirect는 응답이 클라이언트에 전달이 된 후,
클라이언트가 다시 다른 경로로 요청을 하는 것을 말한다. 따라서 url 경로가 바뀌게 된다.
쉽게 말해 호출이 2번 되는 것이다. 하지만 forward의 경우 클라이언트는 한번 호출한 것이고,
그냥 서버 내부에서 호출이 발생하는 것이기 때문에 url 경로가 바뀌지 않는다.
dispatcher.forward(request, response);
}
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
// form을 보면 action에 '/' 없이 save라고만 쓰여 있는 것을 확인할 수 있는데
이걸 상대 경로 라고 한다. 상대 경로로 입력하면 현재 url경로의 마지막 부분을 입력한 경로로
바꿔 준다.
// 현재 url 경로는 http://localhost:8080/servlet-mvc/members/new-form 일텐데
아래 form 태그가 제출이 되면 localhost:8080/servlet-mvc/members/save로 바뀐다.
<form action="save" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
// Model에 데이터를 보관 ( 이전의 서블릿과 다른 점 )
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
성공
<ul>
//🔥 jstl 문법에서는 ${}를 사용하여 아래와 같이 모델의 값을 쉽게 가져올 수 있다.
// 원래는 모델에서 getAttribute로 꺼내와야 하는데 이것을 간단하게 할 수 있는 것.
<li>id=${member.id}</li>
<li>username=${member.username}</li>
<li>age=${member.age}</li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
서블릿과 jsp로만 구성된 MVC 패턴의 한계
프론트 컨트롤러 패턴
개념
도입
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
// 회원 등록 컨트롤러
public class MemberFormControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse
response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
// 회원 저장 컨트롤러
public class MemberSaveControllerV1 implements ControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse
response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
// 회원 목록 컨트롤러
public class MemberListControllerV1 implements ControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse
response) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
// url뒤에 *이 있으면 그 자리에 어떤 것이 들어와도 아래 컨트롤러에 매핑 됨
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerSetvletV1 extends HttpServlet {
//🥊 각각의 컨트롤러의 매핑 url 경로담기 위해 Map을 만든다.
각각의 컨트롤러가 모두 ControllerV1 인터페이스를 따르기 때문에
아래 Map의 타입에 인터페이스만 넣으면 된다.
private Map<String, ControllerV1> controllerMap = new HashMap<>();
//🥊 생성자를 통해 해당 클래스가 만들어 질 때 Map에 경로의 이름과 각각의 컨트롤러 인스턴스가 생성 된다.
public FrontControllerSetvletV1() {
controllerMap.put("/front-controller/v1/new-form", new MemberFormControllerV1());
controllerMap.put("/front-controller/v1/save", new MemberSaveControllerV1());
controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
}
//🥊 해당 서블릿이 호출이 되면 아래 메서드가 실행 됨
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerSetvletV1.service");
//🥊 요청 uri를 가져 온다. ( 모든 요청은 FrontController에 한다.)
String requestURI = request.getRequestURI();
//🥊 전달 받은 url에 해당 하는 컨트롤러 인스턴스를 가져 온다. ( 생성자를 통해
등록되어 있음 )
ControllerV1 controller = controllerMap.get(requestURI);
//🥊 일치하는 컨트롤러가 없으면 Not Found 응답을 보내고
if(controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
//🥊 일치하는 컨트롤러가 있으면 해당 컨트롤러에 rquest, response를 보낸다.
controller.process(request,response);
}
}
http://localhost:8080/front-controller/v1/members/new-form