[ Spring ] MVC 패턴

한대희·2023년 11월 11일

Spring

목록 보기
2/4

✅ 웹 애플리케이션 이해

웹 서버, 웹 애플리케이션

  • 먼저 웹은 HTTP를 기반으로 통신을 한다.
  • 클라이언트 즉, 웹 브라우저에서 URL을 입력하면 인터넷을 통해 서버로 접근 하게 되고 서버에서는 요청한 데이터를 인터넷을 통해 다시 클라이언트로 보내주게 된다.
  • HTTP 메세지에 html, text, image, json, xml 등등 거의 모든 형태의 데이터를 담아 클라이언트와 서버가 통신한다.
  • 이렇게 클라이언트와 서버가 데이터를 주고 받을 때 HTTP라는 프로토콜을 기반으로 동작을 하게 된다.

웹 서버 ( Web Server )

  • 웹 서버라는 것은 HTTP를 기반으로 동작하는 서버이다.
  • 정적 리소스와 기타 부가기능을 제공하는 서버이다.
  • 여기서 정적 리소스라는 것은 특정 폴더, 디렉토리에 html,css,js, 이미지, 영상 파일들을 저장해 둔 것을 말한다.
  • 웹 서버는 이러한 파일들을 http 프로토콜로 제공해 주는 서버다.
  • 대표적인 웹 서버에는 NGINX, APACHE가 있다.

웹 애플리케이션 서버 ( WAS - Web Application Server )

  • WAS도 HTTP 기반으로 동작 한다.
  • WAS는 웹 서버의 기능을 포함하고 있고 프로그램 코드를 실행해서 애플리케이션의 로직을 수행할 수 있다.
  • 예를 들어 웹 서버는 정적 리소스만 제공하기 때문에 사용자에 따라 다른 데이터를 보내줄 수 없지만, WAS는 사용자 마다 다른 데이터를 보내줄 수 있는 것이다.
  • 앞으로 배울 서블릿, JSP, 스프링 MVC 모두 WAS에서 동작하는 것이다.
  • WAS의 예로는 Tomkat, Jetty, Undertow가 있다.

웹 서버와 웹 애플리케이션 서버를 구분하는 이유

  • 웹 애플리케이션 서버에서 웹 서버의 기능을 다 사용할 수 있기 때문에 웹 시스템을 구성할 때 웹 애플리케이션 서버와 DB만 있어도 시스템을 구성할 수 있다.
  • 하지만 이렇게 되면 웹 애플리케이션 서버가 너무 많은 역할을 담당하게 되어 서버 과부하가 우려 된다.
  • 따라서 정적 리소스는 웹 서버가 처리하게 하고, 애플리케이션 로직같은 동적인 처리는 WAS가 담당 하게 하는 것이 좋다.
  • 이렇게 역할을 구분 하게 되면 효율적인 리소스 관리가 가능하다. 예를 들면 정적 리소스가 많이 사용되면 Web서버를 더 늘리면 되고, 애플리케이션 리소스가 많이 사용되면 WAS를 늘리면 된다.
  • 또한 애플리케이션 로직이 동작하는 WAS 서버는 하드한 처리를 많이 하기 때문에 서버가 터질 수 있는 확률이 많지만 정적 리소스만 제공하는 웹 서버는 잘 터지지 않는다.
  • 따라서 WAS나 DB 장애시 Web서버가 정적 리소스로 존재 하는 오류 화면을 제공할 수 있다.

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
  • 만약 우리가 웹 애플리케이션 서버를 처음 부터 끝까지 구현해야 한다면 위의 Http메세지를 하나하나 풀어 해쳐야 한다.
  • 그 과정을 간단하게 살펴 보면 서버에 TCP/IP연결을 대기 시키고 소켓을 연결해야 하고, HTTP 메세지를 파싱해서 요청한 method가 뭔지 확인하고, Content-type 확인하고, body 내용 확인하고, 그거에 맞게 비지니스 로직 실행하고, Http응답 메시지 하나하나 다 작성해서 TCP/IP에 응답을 전달 하는 과정을 모두 거쳐야 한다.
  • 이러한 복잡한 과정을 단순하게 하기 위해 나온것이 Servlet이다.
  • 🔥 Servlet을 활용하면 개발자는 위의 과정에서 비지니스 로직만 신경 쓰면 된다.
  • Servlet은 아래와 같이 구성되어 있다.

@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 요청, 응답 흐름

  • 클라이언트가 HTTP 요청을 하면 WAS는 Request, Response 객체를 만들어서 서블릿 컨테이너 안에 있는 서블릿 객체에 파라미터로 전달한다.
  • 개발자는 전달받은 Request 객체에서 HTTP 요청 정보를 쉽게 꺼내서 사용하고, Response 객체에 HTTP 응답 정보를 쉽게 입력 할 수 있다.
  • 마지막으로 WAS가 Response 객체에 담긴 내용을 바탕으로 HTTP 응답 정보를 생성하여 클라이언트(브라우저)에게 반환 한다.

Servlet 컨테이너

  • Tomkat 처럼 Servlet을 지원하는 WAS를 서블릿 컨테이너 라고 한다.
  • 위의 코드 처럼 @WebServlet이 붙은 클래스가 호출 되면 WAS에서 해당 클래스를 서블릿 객체로 생성한다.
  • Servlet이 여러개 있으면 Servlet 컨테이너에 여러개의 Servlet객체가 들어 가게 되는 것이다.
  • Servlet 컨테이너는 Servlet 객체를 생성, 초기화, 호출, 종료 하는 생명주기를 관리해 준다.
  • Servlet 객체는 싱글톤으로 관리 되어 최초 로딩 시점에 Servlet 객체를 미리 만들어 두고 재활용 한다.
  • WAS가 Servlet 컨테이너에 전달하는 Request 객체는 고객 마다 다르지만 해당 Request를 받아 Response를 반환하는 Servlet객체는 하나만 있으면 되기 때문이다.
  • 이에 따라 고객의 요청이 올 때 마다 계속 객체를 생성하는 비효율을 방지 한다.
  • 또한 Servlet 컨테이너는 멀티 쓰레드를 지원하기 때문에 수많은 요청이 동시에 발생해도 처리할 수 있다.

Multi Thread (멀티 쓰레드)

Thread

  • 애플리케이션 코드를 하나하나 순차적으로 실행하는 것을 쓰레드 라고 한다.
  • 자바 메인 메서드가 처음 실행이 되면 main이라는 이름의 쓰레드가 실행이 되는 것
  • 쓰레드가 없다면 자바 애플이케이션 실행은 불가능 하다.
  • 쓰레드는 한번에 하나의 코드 라인만 수행 하기 때문에 동시 처리가 필요하다면 쓰레드를 추가로 생성해야 한다.

단일 요청 ( 쓰레드 하나 사용 )

  • 클라이언트로 부터 요청이 오면 WAS에 쓰레드가 할당이 되고 이 쓰레드가 Servlet 코드를 실행 시켜준다.
  • 쓰레드를 통해 응답이 완료가 되면 쓰레드도 휴식을 취한다.

다중 요청 ( 쓰레드 하나 사용 )

  • 예를 들어 클라이언트에서 요청이 2개 왔다고 가정해 보자.
  • 그러면 첫번째 요청에 쓰레드가 할당이 될 것인데 만약 뭔가 문제가 생겨서 응답하는 것까지 처리하는데 지연이 발생을 하면 어떻게 될까?
  • 첫번째 요청이 성공적으로 끝나야 쓰레드가 첫번째 요청에 대한 일을 마치고 두번째 요청을 받을 텐데 첫번째 요청이 지연 되면서 두번째 요청은 쓰레드를 기다리게 되는 상황이 된다.
  • 이렇게 되면 첫번째 요청, 두번째 요청 모두 죽어 버리는 문제가 발생한다.

다중 요청 ( 요청 마다 쓰레드 생성 )

  • 위의 문제를 해결하는 방법은 요청마다 쓰레드를 생성 하면 된다.
  • 첫번째 요청이 지연이 되더라도 두번째 요청도 쓰레드를 할당 받기 때문에 요청을 처리하는 것에 문제가 없게 된다.
  • 하지만 이렇게 하면 여러가지 다른 문제가 발생할 수 있다.
  • 첫번째로 쓰레드 생성 비용은 비싸기 때문에 고객의 요청이 올 때마다 쓰레드를 생성하면 응답 속도가 늦어지게 된다.
  • 두번째로 고객 요청이 너무 많아 수 많은 쓰레드가 생성이 된다면 CPU, 메모리의 임계점을 넘어가게 되어 서버가 죽을 수 있다.
  • 세번째로 쓰레드는 컨텍스트 스위칭 비용이 발생하게 된다.

쓰레드 풀

  • 위의 다중 요청시 발생하는 문제를 해결하기 위해 WAS는 내부에 Thread Pool 이라는 것을 사용한다.
  • 요청이 올 때마다 쓰레드를 새로 생성하고, 응답이 완료 되면 쓰레드가 사라지는 것이 아니라, 풀 안에 미리 쓰레드를 만들어 놓고 요청이 오면 풀 안에 있는 쓰레드를 사용 하고 응답이 완료 되면 쓰레드를 다시 풀 안으로 반납하는 방식이다.
  • 이 방식의 장점은 쓰레드가 미리 생성이 되어 있어 쓰레드를 생성하고 종료 하는 비용( CPU )이 절약 되고, 응답 시간이 빠르다.
  • 또한 생성 가능한 쓰레드의 최대치를 미리 만들어 두어 이 범위를 초과하는 요청이 들어 오면 요청을 거절하거나, 대기시킬 수 있어 기존 요청을 안전하게 처리할 수 있다.

쓰레드 풀 실무 팁

  • WAS의 주요 튜닝 포인트는 최대 쓰레드 수 이다.
  • 이 값을 너무 낮게 설정하면 동시 요청이 많아도 서버 리소스는 여유롭지만 클라이언트는 쓰레드 풀에 쓰레드가 반납 될 때 까지 대기 해야 하므로 응답이 지연 된다.
  • 이 값을 너무 높게 설정 하면 동시 요청이 많을 때 CPU와 메모리 리소스 임계점 초과로 서버가 다운 될 수 있다.

WAS의 멀티 쓰레드 지원

  • WAS는 멀티 쓰레드를 지원하기 때문에 개발자가 멀티 쓰레드 관련 코드를 신경쓰지 않아도 된다.
  • 마치 싱글 쓰레드로 프로그래밍을 하듯이 편리하게 개발하면 된다.
  • 주의할 점은 멀티 쓰레드 환경이므로 싱글톤 객체 ( 서블릿, 스프링 빈 ) 는 주의해서 사용해야 한다.
  • 그 이유는 예를 들어 스프링 빈들은 싱글톤이기 때문에 하나의 객체에 여러개의 요청이 들어 왔을 때 공유변수, 멤버 변수가 공유 되기 때문이다.

HTML, HTTP API, CSR, SSR

정적 리소스

  • 정적 리소스란 고정된 HTML 파일, CSS, JS, 이미지, 영상들을 말한다.
  • 주로 웹 브라우저에서 요청을 하게 되고, 요청을 받은 Web Server는 이미 생성되어 있는 정적 리소스 파일을 제공하게 된다.

HTML 페이지

  • 정적인 HTML말고 동적으로 필요한 HTML 파일을 생성해서 전달할 수 있다.
  • 정적 리소스가 아니기 때문에 WAS에서 처리하게 되고, 웹 브라우저에서 요청을 하면 WAS에서 동적으로 HTML을 생성해서 반환한다.
  • 대표적인 것이 JSP, 타임리프 등이 있다.

HTTP API

  • 서버에서 HTML 파일을 전달하는 것이 아니라 데이터를 전달하는 것을 말한다.
  • 주로 JSON 형식을 사용하게 된다.
  • 웹 브라우저에서 요청이 오면 WAS의 비지니스 로직을 통해 JSON 형태의 데이터를 반환한다.
  • 클라이언트는 받아온 데이터가 HTML파일이 아니기 때문에 그냥 보여주는 것이 아니라 별도의 처리 과정을 통해 JSON파일을 화면에 구현하는 처리를 해줘야 한다.
  • HTTP API 방식은 서버와 서버 사이에서 데이터를 주고 받을 때도 사용된다. ( 서버와 서버끼리는 HTML을 주고 받을 필요가 없기 때문 )

SSR ( 서버 사이드 렌더링 )

  • HTML 최종 결과를 서버에서 만들어서 웹 브라우저에 전달하는 것을 말한다.
  • 서버에서 HTML 파일을 다 만들어 HTTP 응답에 HTML 코드를 실어서 보내면 브라우저는 그냥 HTML파일을 화면에 렌더링 하면 된다.
  • 주로 정적인 화면에서 사용이 되고 관련 기술로 JSP, 타임 리프 등이 있다.

CSR ( 클라이언트 사이드 렌더링 )

  • HTML 결과를 자바스크립트를 사용해 웹 브라우저에서 동적으로 생성해서 사용하는 것을 말한다.
  • 주로 동적인 화면에서 사용이 되고, 수정 사항이 생겼을 때 페이지 전체를 다시 가져 오는 것이 아니라 부분적으로 변경할 수 있다.
  • 관련 기술로 React, Vue.js가 있다.

✅ 서블릿

서블릿 사용 하기

  • 원래 서블릿은 톰캣 같은 웹 애플리케이션 서버를 직접 설치한 다음 그 위에 서블릿 코드를 올려서 실행해야 한다.
  • 하지만 스프링 부트는 톰캣 서버를 내장하고 있어, 따로 톰캣 서버 설치 없이 편리하게 서블릿 코드를 실행할 수 있다.
  • 스프링 부트는 서블릿을 직접 등록해서 사용할 수 있도록 @ServletComponetScan 어노테이션을 지원한다. 아래와 같이 메인 메서드에 어노테이션을 붙이면 된다.
// 스프링에서는 ServletComponetScan이라는 어노테이션을 제공 하는데 이걸 입력해 주면 
   Spring이 자동으로 현재 내 패키지를 포함해서 하위 패키지 전부를 뒤져서 
   Servlet을 다 찾아서 자동으로 등록 실행할 수 있게 해준다.

@ServletComponentScan
@SpringBootApplication
public class ServletApplication {
	public static void main(String[] args) {
		SpringApplication.run(ServletApplication.class, args);
	}

}
  • 이제 서블릿을 아래와 같이 만들면 된다. @WebServlet 어노테이션을 붙여서 여기에 해당 서블릿의 이름과 url을 매핑한다.
  • 그 다음 서블릿은 HttpServlet을 상속 받아야 한다.

//  @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 자세히 알아보기

  • 개발자가 요청 받은 Http메세지를 직접 하나하나 파싱해서 사용할 순 있지만 매우 번거로운 일이다.
  • 따라서 서블릿은 개발자 대신 Http요청 메시지를 파싱해서 HttpServletRequest라는 객체에 담아 제공한다.
  • 아래와 같은 HTTP메세지에 대한 정보를 쉽게 가져와서 사용할 수 있게 해준다.
// 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)
  • HttpServletRequest가 제공하는 메서드를 확인해 보자.
// 먼저 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 요청 메세지의 Start Line 정보와 header정보를 알아 보았는데 이제는 쿼리 파라미터 라던가 컨텐츠 바디에 있는 실제 데이터를 조회하는 방법에 대해서 알아 보자.

HTTP 요청 데이터를 보내는 방법

  • 클라이언트에서 HTTP요청 메세지를 통해 서버로 데이터를 전달하는 방법은 크게 3가지가 있다.
  • Get방식 + 쿼리 파라미터, Post방식 + Html Form, HTTP message body에 직접 데이터를 담는 방법 이렇게 3가지다.

GET 방식 + 쿼리 파라미터 방식

  • 보통 GET 방식은 바디에 데이터를 보내지 않고 URL의 쿼리 파라미터에 데이터를 포함해서 전달한다.
  • Content Body에는 아무런 데이터도 담기지 않기 때문에 Content-Type에 관한 정보는 필요하지 않다.
/**
 * 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 방식

  • 메세지 바디에 쿼리 파라미터 형식으로 전달 한다.
  • 주로 회원 가입, 상품 주문 등에서 사용되는 방식이다.
  • 메세지 바디에 데이터가 들어가기 때문에 Content Type이 필요하다.
  • 해당 방식의 Content Type은 Content-Type:application/x-www-form-urlencoded 다.
  • content body에는 get방식의 쿼리 파라미터 형식과 똑같은 형식의 데이터가 들어 간다.
// 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>
  • 위의 form데이터를 이전에 get+쿼리 파라미터 방식에서 했던 servlet url에 그대로 보내보면 쿼리 파라미터 정보를 가져오는 것과 똑같이 전송한 form데이터를 가져오는 것을 확인할 수 있다.
  • form에 username은 hdh로 입력하고, age는 31로 입력해서 Post 요청을 보내 보자.
전체 파라미터 username=hdh
전체 파라미터 age=31
개별 파라미터 값 = hdh
개별 파라미터 값 = 31
복수 파라미터 값들 = hdh
  • content body에는 get방식의 쿼리 파라미터 형식과 똑같은 형식의 데이터가 들어 가기 때문에 쿼리 파라미터로 보내던, content-body에 데이터를 담아 보내던 둘다 request.getParameter로 값을 꺼낼 수 있는 것이다.
// 웹 브라우저가 생성한 form 데이터 메세지
POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
// 아래의 content-body를 보면 쿼리 파라미터와 형식이 같은 것을 확인할 수 있다.
username=hdh&age=30
  • 테스트 할때는 Postman을 활용하는 것이 좋다. Postman에서 body에 들어가서 x-www-form-urlencoded를 클릭해서 key와 value값들을 보내면 된다.

Http 메세지 바디에 데이터를 직접 담는 방식 ( 단순 텍스트 )

  • HTTP API에서 주로 사용 되고, HTTP Message body에 데이터를 직접 담아서 요청하는 방식이다.
  • 바디 안에 JSON,XML,TEXT 정보를 그대로 담아서 서버로 전달
  • InputStream을 사용해서 HTTP 메세지 바디의 데이터를 직접 읽을 수 있다.
  • 먼저 단순 텍스트를 body에 담아 보내 보자.
  • postman에서 body에 raw를 선택 후 text를 선택한 다음 텍스트를 보내보자.
  // 아래의 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 )

  • 위의 경우 처럼 단순 텍스트만 주고 받는 경우만 거의 없고 대부분 json으로 데이터를 주고 받는다.
  • 이번엔 json형식의 데이터를 body에 담아 보내 보자.
  • 텍스트와 json 형식은 메시지를 꺼내는 방식은 같은데, json의 경우 변환하는 코드가 필요하다.
  • json 데이터의 content-type은 application.json 이다.
  • json데이터의 경우 해당 데이터를 그대로 사용하는 것이 아니라 객체로 변환해서 사용하게 된다.
  • 따라서 전달받은 Json 데이터를 객체로 바꾸는 과정이 필요하다.
  • 먼저 java의 경우 데이터를 저장할 때 인스턴스를 사용한다. 따라서 json데이터를 객체로 변환 후 저장할 인스턴스를 구성해 보자.
// Lombok라이브러리를 사용하면
   getter와 setter 어노테이션을 붙인 경우 클래스 내부에서 따로 getter와 setter를 
   구성하지 않아도 된다.
@Getter
@Setter
// 전달 받은 json 데이터의 값들을 아래의 클래스의 인스턴스로 변환 할 것이다.
public class HelloData {
    private String username;
    private int age;
}
  • json데이터를 받을 서블릿을 만들어 보자.
@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");
    }
}
  • postman을 통해 json데이터를 보내면 정상적으로 출력이 되는 것을 확인할 수 있다.

HttpServletResponse 자세히 알아보기

기본 사용법

  • 개발자가 직접 Http 응답 메세지를 만들려면 너무 번거롭기 때문에 응답 메세지를 구성하는데 필요한 값들을 넣으면 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에 text로 응답하는 것은 이전의 했었고 간단하기 때문에 아래 코드만 살짝 보고 넘어가자.
//[message body]
PrintWriter writer = response.getWriter();
writer.println("ok");

Html 응답

  • Http 응답으로 HTML을 반환할 때는 content-type을 text/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 형식으로 응답

  • Http 응답으로 json을 반환할 때는 content-type을 application/json으로 지정해야 한다.
  • Jackson 라이브러리가 제공하는 objectMapper.writeValueString()을 사용하면 객체를 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);
    }
}

✅ 서블릿, JSP, MVC 패턴

회원 관리 앱 애플리케이션 만들기

  • 먼저 회원 entity를 만든다.
@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);
    }
}

서블릿으로 회원 관리 앱 애플리케이션 만들기

  • 먼저 서블릿으로 회원 등록 html 폼을 제공해 보자.
// 회원 가입 페이지 서블릿을 만든다.
// 서블릿은 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 데이터를 받는 서블릿을 만들어 보자.
// 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>");
    }
}
  • 이렇게 지금까지 서블릿과 자바 코드만으로 html을 만들어 보았다. 서블릿 덕분에 동적으로 원하는 html을 만들 수 있었다.
  • 하지만 자바 코드로 html을 만드는 코드는 너무나 번거로운 일이다.
  • 차라리 html문서에 동적으로 변경이 되는 부분만 자바 코드를 넣을 수 있다면 훨씬 편리할 것이다.
  • 이것을 가능하게 해주는 것이 템플릿 엔진이다. 템플릿 엔진을 사용하면 html문서에서 필요한 곳만 자바 코드를 적용해서 동적으로 변경할 수 있다.
  • 대표적인 템플릿 엔진으로는 JSP, Thymeleaf, Freemarker 등이 있다.

JSP로 회원 관리 앱 애플리케이션 만들기

  • 먼저 JSP를 사용할면 라이브러리를 추가 해야 한다.
  • build.gradle의 dependencies에 아래의 라이브러리를 추가 하자.
//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는 webapp폴더 아래 위치 해야 한다. webapp폴더 안에 jsp 폴더를 만들고 그 안에 members폴더를 만들고 그 안에 new-form.jsp파일을 만들어 보자.
  • webapp 밑에 폴더를 만들면 해당 폴더 이름을 url 경로로 사용할 수 있다.
  • webapp폴더 아래 jsp폴더를 만들고 그 아래 members 폴더를 만들고 그 안에 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>
  • 이제 localhost:8080/jsp/members/new-form.jsp로 접속하면 위의 html화면이 나올 것이다.
  • 이제 전송 받은 form 데이터를 저장해서 보여 주는 jsp파일을 만들어 보자.
// 서블릿에서 파라미터 가져오는 것과 같은 로직이 먼저 실행이 되고, 마지막에 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>
  • index.html이 열리는 localhost:8080에서 jsp 부분에 회원 가입을 하고 목록 조회를 해보면 회원 목록이 나오는 것을 확인할 수 있다.

JSP정리

  • jsp는 첫줄에 아래의 코드를 넣어 줘야 한다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
  • jsp파일을 보면 첫 줄을 제외하고는 거의 html과 똑같다. jsp는 서버 내부에서 서블릿으로 변환되기 때문에 이전에 만들었던 서블릿과 비슷한 형태로 변환이 된다.
  • url 경로를 통해 jsp파일을 열고 싶다면 http://localhost:8080/jsp/members/new-form.jsp 처럼 .jsp까지 입력 해줘야 한다.
  • jsp는 자바 코드를 그대로 사용할 수 있다.
// 아래의 블럭 사이에 자바 코드를 사용할 수 있다.
<%  %>

// 아래의 블럭 사이에서는 자바 코드를 출력할 수 있다.
<%=  %>

서블릿과 JSP의 한계

  • 서블릿으로 개발할 때는 뷰(view)화면 즉, html을 만드는 작업이 서블릿 내부에 자바 코드와 섞여 있기 때문에 복잡하다. ( html태그를 자바 코드로 하나하나 입력해 줘야 하기 때문에 굉장히 복잡함 )
  • 그에 따라 jsp를 사용해서 html 작업을 깔끔하게 가져가고, 중간에 동적으로 변경이 필요한 부분만 자바 코드를 적용했다. ( 이건 서블릿과 반대로 html파일에 자바 코드를 부분적으로 넣어서 좀 더 작성하기 편하다. )
  • 하지만 jsp도 부족함이 많다. 그 이유는 코드의 절반은 비지니스 로직 즉 , 자바 코드 이고 나머지 절반은 html로 보여주기 위한 뷰 영역이기 때문에 jsp가 너무 많은 역할을 한다.
  • 프로젝트가 커지면 커질 수록 수백 수천줄이 넘어가는 jsp파일이 만들어 질 것이다.
  • 이것을 해결하기 위해 나온 것이 MVC 패턴이다.

MVC 패턴

개요

  • MVC 패턴은 지금까지 서블릿이나 jsp에서 한번에 처리 하던 것을 컨트롤러(controller)와 뷰(view)라는 영역으로 역할을 나눈 것을 말한다.
  • 기존의 서블릿은 컨트롤러, jsp는 뷰가 된다.
  • 컨트롤러와 뷰 사이에는 모델이라는 계층이 존재 하게 되는데 컨트롤러는 모델에 데이터를 넘기고, 뷰는 모델에서 데이터를 가져와 렌더링 한다.

컨트롤러

  • Http 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 결과 데이터를 모델에 담는다.
  • 실무에서는 컨트롤러에서 비지니스 로직까지 담당하면 컨트롤러가 너무 많은 역할을 하기 때문에 일반적으로 서비스(Service)라는 별도의 계층을 만들어서 관리한다.
  • 실무적으로 컨트롤러의 역할은 Http 요청을 받아서 파라미터를 검증한 다음 비지니스 로직이 있는 서비스를 호출하는 역할을 한다.

모델

  • 뷰에 출력할 데이터를 담아 두는 곳
  • 뷰에서 화면을 렌더링 할 때 필요한 데이터를 모두 모델에서 전달 받기 때문에 뷰는 비지니스 로직이나 데이터 접근을 몰라도 되고, 그저 화면을 렌더링 하는 것에 집중할 수 있다.

  • 모델에 담겨 있는 데이터를 가져와 화면을 그리는 역할
  • HTML을 생성하는 부분을 말한다.

서비스

  • 컨트롤러에 비즈니스 로직을 둘 수도 있지만, 이렇게 되면 컨트롤러가 너무 많은 역할을 담당하게 된다.
  • 따라서 일반적으로 현업에서는 비즈니스 로직은 서비스(service)라는 별도의 계층을 만들어서 처리한다.
  • 그리고 컨트롤러는 HTTP요청을 받아서 비즈니스 로직이 있는 서비스를 호출하는 역할을 담당한다.

MVC 적용

  • 먼저 서블릿 mvc의 회원가입 버튼을 눌렀을 때 나오는 입력 form 화면을 구성해 보자.
  • 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);
    }

}
  • 위의 컨트롤러와 연결되는 jsp 파일
<%@ 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>
  • form 태그 전송 버튼을 클릭 했을 때 나오는 jsp를 보여 주기 위한 컨트롤러
@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);

    }


}
  • 위의 컨트롤러와 연결되는 jsp 파일
<%@ 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에는 더이상 자바 코드가 없고 그저 화면만 보여주는 역할만 담당하게 된다.

서블릿과 jsp로만 구성된 MVC 패턴의 한계

  • view로 이동하는 코드가 각각의 서블릿 마다 계속해서 반복적으로 이루어 져야 하는 문제
  • 기능이 복잡해 질 수록 컨트롤러에서 공통으로 처리해야 하는 부분이 점점 더 많이 생기게 된다.
  • 이 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능들을 처리해 주면 된다.
  • 그 방법으로 프론트 컨트롤러(Front Controller)라는 것이 있다.

✅ MVC 프레임워크 만들기

프론트 컨트롤러 패턴

개념

  • 프론트 컨트롤러 즉, 이전의 컨트롤러 방식을 살펴 보면 각각의 컨트롤러 마다 공통 로직이 들어가 있고 추가로 각각의 컨트롤러에 맞는 로직이 있었다.
  • 프론트 컨트롤러를 도입하면 각각의 클라이언트의 요청을 모두 프론트 컨트롤러가 받고 여기서 공통 로직을 처리하고 추가적으로 필요한 로직이 있다면 각각의 컨트롤러에서 처리하게 할 수 있다.
  • 정리하면 프론트 컨트롤러 역할은 하는 서블릿 하나가 클라이언트들의 모든 요청을 받고, 요청에 맞는 컨트롤러를 호출 해 주는 것
  • 따라서 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
  • 나중에 배울 스프링 웹 MVC의 DispatcherServlet이 바로 FrontController다.

도입

  • 먼저 서블릿과 비슷한 모양의 컨트롤러 인터페이스를 만든다.
  • 각각의 컨트롤러는 위의 인터페이스를 따르게 될 것이다.
  • 그러면 프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가질 수 있다.
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);
 }
  • 이제 frontController를 만들어 보자.
// 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);
    }
}
  • 이제 frontcontroller로 아래와 같이 요청을 보내면 프론트 컨트롤러에 매핑된 컨트롤러가 호출될 것이다.
  • frontcontroller의 urlPatterns에 /front-controller/v1뒤에 *을 넣어 놨기 때문에 /front-controller/v1 뒤로 어떤 요청이 와도 frontcontroller로 매핑이 되게 된다.
http://localhost:8080/front-controller/v1/members/new-form
profile
개발 블로그

0개의 댓글