[Inflearn|인프런] 스프링부트 개념정리(이론) - 2

수짱·2022년 10월 18일
0

SpringBoot

목록 보기
1/1

해당 내용은 https://inf.run/xFxo 를 참고하여 작성되었습니다.

왜 벨로그에 쓰냐구..? 티스토리 복구가 아직도 안됐거든 😉 쓰다보니 벨로그가 더 좋은 것 같기도 .. ?


3강 - Spring Boot 동작원리


1. 내장 톰캣을 가진다.

톰캣을 설치할 필요없이 바로 실행이 가능하다.

소켓(Socket)이란?

운영체제가 가지고 있는 것. 소켓통신은 계속해서 연결되어 있기 때문에 부하가 크다. main 스레드에서 요청을 받으면 다른 스레드를 만들어 통신을 연결한다.
소켓과 반대로 HTTP 통신(문서를 전달하는 통신) 은 연결을 지속하지 않고 끊어버리는 Stateless 방식을 사용한다. 이 방식은 부하가 적지만 다시 연결될 때는 같은 클라이언트인지 확인할 수 없다. 이 방식을 보완하여 만들어진 것이 "웹서버" 이다.
http는 소켓을 기반으로 system call을 통해 만들어졌다.

톰캣(Tomcat)이란?

먼저, 톰캣과 웹서버의 차이를 알아야 한다.

HTTP에서는 URL에 필요한 자원을 요청하여 받을 수 있다. HTTP는 갑이 을의 IP 주소를 모른다. 이를 해결하기 위해서는 연결이 지속되는 소켓이 필요하다. 그러나, HTTP는 단순하게 요청시 정적인 응답(html 문서, 자원 등 => 이 자원들은 static 자원)만 해주는 구조이다. 따라서 연결이 지속 될 필요가 없다.

반면에, 톰캣은? 웹서버로 아파치를 사용한다. 아파치는 자바코드를 이해하지 못한다. 그렇기에 아파치에 톰캣을 달아준다. 톰캣을 달아줌으로써 아파치가 이해하지 못하는 요청이 온다면 해당 요청의 제어권을 톰캣으로 준다. 그리고 이 톰캣은 파일의 자바코드를 컴파일하고 컴파일이 된 데이터를 html 문서에 넣어준다. 그 다음 아파치에게 만든 html 파일을 돌려주고 아파치는 html로 응답을 전달한다.

만약 JSP 요청을 받았을 때 JSP 파일을 톰캣을 들리지 않고 해당 파일을 그대로 웹브라우저에 응답으로 돌려주기만 했다면 웹브라우저는 정적인 파일밖에 이해하지 못하므로 JSP은 웹브라우저에서 내용이 깨지게 된다.

따라서, 아파치는 요청한 파일을 응답하고 톰캣은 요청한 파일중 자바 파일이 있다면 그것을 컴파일 후 html 문서로 만들어서 아파치에 다시 돌려주는 역할을 한다.



2. 서블릿 컨테이너

근데 스프링에서 URL을 통해 자원에 접근하는 방식을 막아두었다. 그래서 URL 방식은 사용을 하지 못한다.
따라서 식별자(URI)를 통해 자원을 요청해야 한다.

즉, 특별한 파일 요청을 할 수없다. 요청시에는 무조건 자바를 거치게 되며 아파치는 무조건 톰캣에게 제어권을 넘기게 된다.

(여기서 갑자기 시간이 1년을 점프하신다 ㅋㅋ 삼성노트 업데이트!)

java와 상관없는 html, css와 관련된 요청이 오면 아파치(정적 웹서버)만 실행이 된다.

만약 최초 java 자원 요청이 온다면

  1. 최초 자바 자원 요청시 서블릿 객체 생성 => init() 메소드 호출
  2. service 호출 전에 스레드가 생성된다.
  3. 생성된 스레드가 service() 호출 => post, get, put, delete 방식인지 check
  4. 만약 get으로 요청이 왔다면 get() 메소드가 호출 된다.
    3.1 db연결, 데이터, html 담아서 응답

두번째 java 자원 요청이 온다면

  1. 서블릿 객체를 생성하지 않고 재사용한다. init()도 호출하지 않는다. 단, 새로운 스레드가 생성된다.
  2. 새로운 스레드가 다시 service() 메소드를 호출하여 check후 method를 호출한다.

자바의 메모리 영역은 크게 보면 static, heap, stack이 있다.
*class A가 생성되고 Hello() 를 가진다. new A()로 객체를 생성하게 되면 heap에 Hello 메소드가 뜨게 된다.

  • 떠있는 Hello()를 호출하게 되면 stack공간에 Hello()메소드 내부 코드를 처리하는데 이 공간을 Hello 메소드 스택 공간이다. 이 공간은 독립적이다.
    - 독립적이란 뜻은 Hello 메소드를 호출하는 사람마다 독립적이라는 의미이다.

    • 따라서 서블릿 객체가 한 개여도 스레드는 여러개이므로 메소드는 따로 사용할 수 있다.

  • 톰캣기본설정이 만약 스레드20개 까지라면 21번째 요청은 더이상 스레드를 만들 수 없으므로 대기하게 된다.

  • 대기하다가 첫번째 스레드가 사용이 종료될 때 (이 시점을 Response) 21번째가 첫번째 스레드를 사용하게 된다.
    - 이 기법을 Pooling 기법이라고 한다.

정리

  1. request 요청
  2. 최초 요청 시 서블릿 객체 생성 후 생성된 스레드가 필요한 메소드 호출
  3. response 후 스레드를 제거하지 않고 재사용을 위해 남겨둔다.
  4. 만약 클라이언트 20명이 동시접근을 한다면 스레드가 20개까지 생성된다.
  5. 만약 25명이 동시접근을 하게 된다면 5명은 대기하게 된다.(20개를 한계로 설정했으므로)
  6. 대기를 하다가 response가 끝난 스레드가 있다면 그 스레드를 재사용하게 된다 => pooling 기법


    즉, 서블릿객체도 재사용되고 스레드도 재사용된다. 서블릿 객체는 단 1개만 재사용되고 스레드는 20개, 100개 등 컴퓨터 성능에따라 달라진다.
    스레드 갯수를 늘리고싶다면 컴퓨터의 성능을 증가시키거나 분산처리를 한다. (Ex.100명을 다루는 컴퓨터를 10개 다룬다) 수평적으로 확장하므로 Scale-out이라고 한다.

3. 웹 배포서술자 (web.xml)

web.xml은 웹 서버에 진입하면서 최초로 실행된다.

<web.xml이 하는 일은?> 성을 지키는 문지기는 web.xml에 따라 일을 한다.

  1. ServletContext의 초기 파라미터 생성

    • 초기 파라미터는 한 번 설정해두면 어디서든 성 안에서 사용이 가능하다.

  2. Session의 유효시간 설정

    • 인증을 통해 들어오는 것이 Session. 이 Session이 얼마나 있을 수 있는지 설정한다.
    • Session의 유효시간을 늘리기 위해서는 문지기에게 말을 하여 유효시간을 늘릴 수 있다.
    • 몰래 잠입한 Session은 문지기에게 갈 수 없으므로 추방당한다.

  3. Servlet, JSP에 대한 정의 및 Servlet, JSP매핑

    • 요청한 자원(식별자, Requeset, Location 등..)이 어디인지 정확하게 알려주고 이동할 수 있도록 하는 것이 매핑

  4. Mime Type 매핑

    • Mime Type이란 들고올 데이터의 타입이 무엇인가. 즉, 성에 들어올 때 어떤 물건을 들고 들어왔냐라는 것.
    • 물건을 아무것도 안들고 올수도 있고(보통 조회Select의 목적. 따라서 HTTP의 GET 방식을 주로 사용. 무언가를 가져가기 위함 => GET 방식 요청은 데이터를 가져오지 않는다.) 들고온다면 들고온 데이터 타입을 알려준다.
    • Mime Type을 알아야 하는 이유는 성에서 받은 쌀을 우리 성에서 먹을 수 있는지 없는지 판단해야 하며 만약 먹을 수 있다면 가공이 필요하므로 해당 타입을 알아야 하고 다시 매핑을 해야한다.
    • 어떤 물건을 들고 들어오는지 알아야 해당 물건을 어디로 보낼지 알 수 있으므로 Mime Type을 알아야 한다.

  5. Welcome File list 설정

    • 데이터도 없고 목적도 없이 성에 방문할 때 안내 해야하는 위치가 있어야 한다. 그 위치가 바로 Welcome File. 이 리스트를 관리자가 설정한다.

  6. Error Pages 처리

    • 문지기가 모르는 주소 위치를 받는다면 Error Page로 전송한다.

  7. 리스너/필터 설정

    • 핕터란? a가 성에 들어온다면 a에 대한 신분을 확인하는 것이다. 만약 a가 다른 나라 사람이라면 반입을 금지한다. 또, a가 총을 들고와도 총을 뺏고 진입한다.
    • 리스너란? 리스너는 문지기가 아니다. 리스너는 오직 조건만을 확인하는 대상. 문지기가 일을 하면서 함께 리스너가 새로운 대상이 들어오는 것을 확인한다. 동시에 리스너의 조건을 만족하는 대상을 발견한다면 가져간다.

  8. 보안 설정

    • 이상한 사람이 들어온다면 반입을 금지하거나 현장수배범이 있다면 감옥으로 보내는 일을 한다.


4. FrontController 패턴

(문지기 web.xml이 하는 일이 너무 많아서 줄여주기 위함)

최초 앞단에서 Request 요청을 받아 필요한 클래스에 넘겨준다.

Why? web.xml에 다 정의하기 힘들기 때문이다.
web.xml에 너무 많은 Servlet/JSP 정의가 들어가있으면 매핑하는 내용이 너무 많아져서 web.xml에 어디로 이동해야 하는지 위치를 다 정의하지 않는다.
따라서 최초 앞단에서 request 요청을 받으면 FrontController로 넘긴다.

Request 요청을 받고 FrontController로 보내는 방법?

특정주소(.do)를 가진 request가 들어온다면 FrontController로 보내라는 약속을 web.xml에 작성한다.

최초의 요청이 왔을 때 URI 요청이든 자바 파일 요청이든 자원으로 접근을 못하므로 톰캣으로 접근하게 된다.

톰캣으로 접근하게 되면 톰캣이 자동으로 메모리에 request와 response라는 객체를 만들게 된다.
request 객체는 요청한 사람의 정보를 가진다. 어떤 데이터를 요청했는지, 어떤 데이터를 들고왔는지 등등.
response 객체는 요청한 사람의 정보를 토대로 만들어진다. response는 톰캣이응답해줘야 하는 객체이다.

Buffered 가변길이의 문자를 받아서 톰캣이 알아서 request, response 객체를 만들어주는 것이다. 또한, request.변수, request.함수를 사용할 수 있도록 해준다.

web.xml에서 자기 일을 하는데 JSP/Servlet 매핑이 너무 많이 들어있으면 복잡해지므로 특정 주소(.do)가 들어오면 frontcontroller가 특정 주소를 받아내도록 설정한다.
.do를 받은 FrontController는 다시 .do 주소를 자원을 찾아갈 수 있도록 다시 한 번 FrontController를 통해 .do에서 request를 한다. 내부에서는 자원 접근이 가능하므로 이 방식이 가능하다.
그러면 이때 원래 만들어져있던 request, response 객체가 바뀌게 된다!! (문제발생)

이때 새로운 요청이 생기기 때문에 request와 response가 새롭게 new 될 수 있다.
그래서 5번의 RequestDispatcher를 필요로 한다.

RequestDispatcher가 필요한 이유

a.do가 다시 내부에서 request를 하게 된다면 request, response가 새로 만들어지게 되므로 A의 요청을 잃기 때문에 이를 방지하기 위한 방법이 있다.
a.do의 reqeust를 새로만드는 것이 아니라 이전의 최초의 A가 요청했던 request, response 정보에 덮어씌워서 a.do의 request를 생성한다. 이렇게 하면 A의 request, response를 유지할 수 있게 된다.

=> 이것이 바로 RequestDispatcher를 이용하는 방법이다.

requestDispatcher를 이용한다면 이전의 request, response를 사라지지 않게 하고 재사용할 수 있다.


5. RequestDispatcher

필요한 클래스 요청이 도달했을 때 FrontController에 도착한 request와 response를 그대로 유지시켜준다.

이거를 많이 쓰는 경우?
만약 A라는 사람이 a.jsp를 요청했다.
응답으로는 a.html으로 응답이 올 것이다.
A는 웹브라우저를 요청했기 때문에 웹브라우저 화면에 a.html이 존재할 것이다.
a.jsp 요청에 대한 request와 response 객체가 만들어졌을 것이다.
웹 브라우저에 a.html이 있고 해당 화면에 버튼을 누르면 다음 페이지로 이동하도록 새로운 요청을 할 수 있다.
A에서 b.jsp로 이동한다는 요청을 한다면 b.html에 대한 응답이 올것이다.
그럼 a.html에서 b.html으로 이동이 될 것이다.

근데 a 화면에 있는 데이터(request)가 새로운 요청 b가 들어오므로 사라질 수 있다.
이를 방지하기 위해 requestDispatcher를 이용하여 a 화면에 있는 데이터(Request)를 b 화면에서도 그대로 사용할 수 있도록 할 수 있다.
즉, b화면에서 a화면의 데이터 aData를 그대로 사용할 수 있다. 왜냐하면, b에서 요청한 request는 새로운 request가 아니라 기존 a에 대한 requset를 재사용하기 때문이다.

=> 따라서 RequsetDispatcher를 이용해야 페이지간 데이터 이동이 가능하다. 데이터를 들고 페이지를 이동할 수 있는 기법.


6. DispatchServlet

스프링에서 지원하는 RequestDispatcher + FrontController 패턴, 주소분배 역할.

FrontController 패턴을 직접 짜거나 RequestDispatcher를 직접 구현 할 필요가 없다.
왜냐하면 스프링에는 DispatchServlet이 있기 때문이다.

DispatcherServlet이 자동 생성될 때 수 많은 객체가 생성(Ioc)된다. 보통은 필터들이다.
Ex) 필터, 컨트롤러, 객체, 서비스, 컴포넌트, 빈 등등..
해당 필터들은 내가 직접 등록할 수 도있고 기본적으로 필요한 객체들은 자동 등록된다.

7. 스프링 컨테이너

먼저 request가 온다. 스프링에서는 정적인 자원 요청이 들어오지 않으므로 apache는 생각하지 않는다.

다음으로 web.xml에 request가 도착하게 된다.

이 사이에 ContextLoaderListener가 존재하는데 얘는 request에 계속 new할 필요없이 공통적으로 존재하는 DB Connection이 있다면 or 모든 스레드가 공통적으로 사용해도 되는것이 있다면 미리 ContextLoaderListener를 통해 미리 띄워진다.
ContextLoaderListener는 web.xml의 root_applicationContext 파일을 읽는다.
이 파일을 읽은 후 스레드마다 딱 하나씩 공통적으로 사용해야 하는 것들을 메모리에 띄워준다. 그리고 IoC 컨테이너에서 관리한다. 모든 스레드마다 DB connection을 new해서 만들필요가 없잖아?

그 다음 DispatchServlet에서 컴포넌트 스캔을 통해 IoC로 객체들을 주소분배 해준다.
얘를 통해 Servlet이 만들어지면 각각의 독립적인 스레드가 만들어진다.

ContextLoaderListener가 DispatchServlet보다 먼저 실행되므로 DB에서 띄워진 객체는 DispatchServlet에서 메모리에 띄운 객체들한테는 접근을 하지 못한다.
단, DispatchServlet에서 메모리에 띄운 객체들은 DB에 접근이 가능하다.
따라서 DB 접근은 root_ApplicationContext 파일에서 메모리에 띄운 후 DispatchServlet에서 메모리에 띄운 객체들이 필요할 때 마다 DB에 접근해서 사용한다.


static은 main 메소드가 실행되기 전부터 메모리에 띄워져있는 것 = 태초에 존재
반대로, 자바파일은 객체로 메모리에 띄우는 것은 생성과 사라짐이 일어날 수 있는 존재. 특정한 시점에 메모리에 떴다가 특정한 시점에 메모리에서 사라지는 존재 => 객체

만약 자바파일에서 만든 클래스들이 static이라면 모든 사용자가 접근하고 이용하므로 자원이 하나라서 자원의 효율성이 있을수는 있으나 충돌이 발생할 수 있다.

따라서 자바파일에서 만든 대부분의 클래스들이 효율적으로 new로 메모리에 띄워져야 한다.

하지만, 스프링은 사용자가 new를 하지않고 IoC를 통해 스프링이 자동으로 new를 해준다. 이때 스프링에서 new를 해주는 것이 컴포넌트 스캔을 통한 DispatchServlet이다.

어디에서? src안에 있는 모든 파일들을 대상으로 한다.
스캔의 범위를 설정할 수 있으나 스프링부트 버전부터는 특정 패키지 이하는 all scan한다.
컴포넌트 스캔의 범위가 자동으로 패키지 이하로 잡히게 된다. DispatchServletdms 이 패키지 이하의 모든 자바파일을 스캔하여 필요한 것들을 메모리에 올리게 된다.

이때 필요하고 안필요한 객체의 기준은 무엇인가?
자바파일에서 어노테이션을 찾아서 기준으로 한다.

  • @Controller (컨트롤러의 역할이므로 DispatchServlet이 스캔할 때 메모리에 올려라)

  • @RestController

  • @Configration

  • @Repository

  • @Service

  • @component
    등등..
    즉, 어노테이션들의 역할을 배우고 이를 이용해서 IoC를 통해 메모리에 객체를 띄우고 싶을 때 띄울 수 있다.

  • @Hello
    이와 같은 Custom 어노테이션도 만들어서 메모리에 띄울 수도 있다.

6번 DispatchServlet의 목적: 컴포넌트 스캔을 하고 메모리에 뜬 객체들을 주소 분배시켜줄 수 있다.

그렇다면 DispatchServlet에 의해 생성되는 수 많은 객체들은 어디서 관리되는가?

첫째, ApplicationContext

수 많은 객체들이 ApplicationContext에 등록된다. 이것을 IoC라고 한다.
DispatchServlet이 컴포넌트 스캔할 때 등록된다.

IoC란 제어의 역전을 의미한다. 개발자가 직접 new를 통해 객체를 생성하게 된다면 해당 객체를 가르키는 레퍼런스 변수를 관리하기 어렵다. 그래서 스프링이 직접 해당 객체를 관리한다.
스프링이 직접 해당 객체를 관리하도록 하기 위해 우리가 알아야 하는것은 어떤 어노테이션을 붙여야 IoC가 관리하는지 알아야 하는 것이다.

이때 우리는 주소를 몰라도 된다. 왜냐하면 필요할 때 DI(Dependency Injection)하면 되기 때문이다.
DI란 의존성 주입이라고 한다. 필요한 곳에서 ApplicationContext에 접근하여 필요한 객체를 가져올 수 있다. ApplicationContext는 모든 정보가 담겨있으면서 싱글톤으로 관리되기 때문에 어디에서 접근하든 동일한 객체라는 것을 보장해준다.

ApplicationContext의 종류는 2가지가 있는데
root-applicationContext와 servlet-applicationContext가 있다.

root는 최상단 applicationContext. root는 스레드마다 관리하는 것이 아니라 하나만 만들어 관리하면 된다.

servlet은 servlet, web만 관리하는 applicationContext 따라서 모든 정보를 갖고있진 않다. 웹과 관련된 정보만 들고있다. 따라서 웹과 관련된 어노테이션들만 스캔하여 메모리에 띄워 주소분배한다.

  • servlet-applicationContext는 ViewResolver, Interceptor, MultipartResolver 객체를 생성하고 웹과 관련된 어노테이션(Controller, RestController)을 스캔하여 메모리에 띄우고 띄워진 객체는 DispatchServlet에 의해 실행된다.

  • root-applicationContext는 해당 어노테이션을 제외한 Service, Repository 어노테이션등을 스캔(=메모리에 로딩)하고 DB관련 객체를 생성한다.

  • 띄워진 객체는 ContextLoaderListener에 의해 실행된다.

둘째, Bean Factory

필요한 객체를 Bean Factory에 등록이 가능하다. 여기에 등록하면 초기에 메모리에 로드되지 않고 필요할 때! getBean() 메소드를 통해 호출하여 메모리에 로드할 수 있다.
이것 또한 IoC!!
그리고 필요할 때 DI하여 사용할 수도 있다.

ApplicationContext와 다른 점은 Bean Factory에 로드되는 객체들은 미리 로드되지 않고 필요할 때 호출하여 로드하기 때문에 lazy-loading되는 점이다.


8. 요청 주소에 따른 적절한 컨트롤러 요청(Handler Mapping)

Handler Mapping이 특정한 함수를 찾아준다.

Get요청 => http://localhost:8080/post/1
해당 주소 요청이 오면 적절한 컨트롤러의 함수를 찾아서 실행한다.


9. 응답

html 파일을 응답할지 Data를 응답할지 결정해야 한다.

html 파일(.jsp파일)을 응답한다면 ViewResolver가 관여한다.
ViewResolver가 관여하게 된다면 응답의 "패턴"을 만들어준다. 즉, 어떤 파일인지에 대한 파일 패턴을 만들어준다.
최종적으로는 톰캣이 받은 .jsp파일을 .html으로 변환하여 return하게 된다.

만약, 내가 응답할 데이터가 .jsp파일이라면 .jsp파일이 있는 경로가 prefix로 붙고 뒤에 suffix로 확장자가 붙어서 해당 리턴값(주소요청)에 대한 파일을 리턴하게 되는 패턴을 만들어준다. => ViewResolver의 역할

ViewResolver가 관여하지 않게하고 내가 직접 경로를 전부 다 적어서 설정해줄 수도 있다. 근데 굳이 이렇게 할 필요가 있나?

Data를 응답하게 된다면 MessageConverter가 작동하게 되는데 메시지를 컨버팅 할 때 기본전략은 json이다.
Data를 return하기 위해서는 메소드명 앞에 @ResponseBody를 붙여준다.
이렇게 되면 리턴값 hello를 파일로 안보고 데이터로 보게된다.

@ResponseBody 
String Hello(){
	return "hello";
}

만약 user라는 entity가 있고 필드로 int id, String name을 가질때
entity를 new하고 Hello메소드에 리턴할때 user를 리턴하게 해준다면 return하는 것이 String이 아니라 객체이므로 MessageConverter가 객체를 JSON으로 변경하여 수신자에게 응답해준다.
수신자는 {"id":1, "name":"홍길동} 으로 응답을 받게된다.

class A{
	@ResponseBody
	user Hello() {
    	return user;
    }
}

Json을 변환하는 컨버터에 Jackson를 바로 안 넣고 MessageConverter를 넣는이유는
만약 바로 Jackson을 바로 컨버터로 사용하게 된다면 Json보다 좋은 중간 데이터가 나왔을때 기존의 프로그램을 전부 Jackson에서 다른 것으로 변경해야 하기 때문에 번거로워진다.
그래서 MessageConverter를 두고 추상화로 Jackson을 두게 된다면 Jackson을 날리고 다른걸로 바꾸면 코드를 수정할 필요가 없이 바로 사용이 가능하다.


최종

<톰캣이 실행될 때>
사용자가 요청할 때가 아님!! 사용자가 요청하기 전에 서버를 켜야하므로!
서버가 켜질때 web.xml이 실행되고 성이 만들어지는 것임.

  1. (톰캣 실행시) web.xml이 문지기 역할로 loading
  2. ContextLoaderListener가 호출, create
  3. root-applicationContext(ApplicationContext.xml)가 loading => 보통 DB 관련된 객체들을 컴포넌트를 스캔하여 메모리에 올린다.
  4. 스캔하여 메모리에 올려지는 것들 running, create, crate => ServiceImpl, DAO, VO
  5. 이제 사용자로부터 request를 받는다.
  6. servlet-context.xml(presentation-layer.xml)에 의해 DispatcherServlet이 동작한다.
  7. DispatcherSErvlet가 웹과 관련된 것들(Controller 등)은 메모리에 띄우고 주소분배를 한다.
  8. 모든 요청이 끝나고 Response할때 data로 줄지 html파일로 줄지 결정
  9. 응답 결정이 끝나게 되면 정상적인 로직이 실행된다.

초반 세팅에 이런 것들이 중요하고
비즈니스로직을 짜는것이 중요하다.

최초입구에서 web.xml이 자기가 해야할 일을 기억하고
자기가 해야할 일을 dispatchServlet한테 분배하고
ContextLoaderListenr로 db에 관련된 객체들을 미리 메모리에 띄워두고정도를 알자.

0개의 댓글