서버란 프로그램이라고 생각하면 됨
그런데 이 프로그램은 특정 포트의 소켓을 열어놓고 그 소켓에 클라이언트가 연결할때까지 무한 대기하는 것
그러다가 클라이언트가 연결하면 해당 클라이언트 소켓에서 요청을 받아와 수행하고 응답을 작성해서 전달한다.
예를 들어 서버에서 클라이언트의 요청을 읽을때나 클라이언트로의 응답을 보낼 때 FTP 프로토콜을 사용하면 FTP 서버가 되고 HTTP 프로토콜을 쓰면 HTTP 서버가 되는 것
아무튼 서버는 네트워크 기능 을 수행하는 프로그램!!
웹 브라우저 클라이언트로부터 HTTP 요청을 받아들이고 HTML 문서와 같은 웹 페이지를 반환하는 프로그램
HTTP 서버 중에서도 리소스 파일을 리턴하는 서버
HTTP GET /file.html 요청을 받았을 때, 정적 웹서버는 지정된 디렉터리에서 file.html파일을 찾아서 이를 HTTP response 바디에 넣어서 클라이언트에게 전송한다. 이 때 해당 파일에 아무 작업도 하지 않아서 정적인 것
아파치나 Nginx가 정적 웹 서버이다. 아파치나 Nginx를 설치 후 지정된 경로에 원하는 리소스 파일을 두면 알아서 웹서버가 리소스 파일을 전송해줘서 클라이언트가 접근할 수 있게됨
파일을 있는 그대로 전송하지 않고 어떠한 작업을 거친 후 전송한다. 클라이언트가 요청에 요청 매개변수를 보내면 이를 활용하거나 등 다양한 작업이 가능하다.
따라서 정적 웹 서버는 클라이언트가 어떤 요청을 해도 똑같은 결과를 반환하지만, 동적 웹 서버는 클라이언트에 따라, 또 매개변수나 요청의 종류에 따라 결과가 다르게 나타난다
그래서 각 요청별, 매개변수별 로직을 작성해주는 것이 백엔드 개발!
그런데, 사용자 요구에 따라 로직은 항상 변하기 때문에, 일정 규격이 정확한(??) 서버 (아파치나 Nginx)는 사용할 수가 없다. 그러면 서버 프로그램 작성해주기 위해서 처음부터 끝까지 소켓 프로그래밍, HTTP 파싱, 스레드 풀 관리 등 다 해야하나??
그러면 난 백엔드 공부 안햇음
암튼 그래서 동적 웹 서버 구현 도와주는 자바 프로그램이 있다. 그게 바로 서블릿 엔진
아파치 톰캣이 서블릿 엔진에 속함
웹 서버 단독으로는 처리할 수 없는 데이터베이스의 조회나 다양한 로직 처리가 필요한 동적 컨텐츠를 제공한다,
서블릿 엔진 또는 컨테이너는 서버 프로그램.
서블릿 엔진 설치 후, 서블릿 엔진에 클래스 파일과 해당 클래스 파일을 어떤 요청에서 실행하는지 설정해준다.
클래스 파일 작성 시 서블릿 엔진이 이해가능한 형태로 작성해야하는데, 이는 javax.servlet.http.HttpServlet을 상속하는 클래스들이다. HttpServlet을 상속받는 클래스를 작성해 특정 형식에 맞춰서 전달한다.
서블릿 엔진을 이용하면 서버를 처음부터 구현하지 않아도 비즈니스 로직 구현이 가능하다.
JSP, Servlet 구동환경을 제공한다.
웹 서버 + 웹 컨테이너 형태
인터넷 상에서 HTTP 프로토콜을 통해 사용자 컴퓨터나 장치에 애플리케이션을 수행해주는 미들웨어, 주로 동적 서버 컨텐츠를 수행하며 웹 서버와 구별이 된다.
주로 데이터베이스 서버와 같이 수행한다.
간단히 정리하자면 웹서버가 보낸 JSP, PHP 등의 파일을 수행한 결과를 다시 웹 서버로 보내주는 역할
⚙️ 클라이언트 → 웹 서버 → WAS → DB 구조 동작 과정
웹서버는 웹브라우저 클라이언트로부터 HTTP 요청을 받는다.
웹 서버는 클라이언트의 요청을 WAS에 보낸다.
WAS는 관련된 서블릿을 메모리에 올린다.
WAS는 web.xml을 참조하여 해당 서블릿에 대한 스레드를 생성
HttpServletRequest와 HttpServletResponse 객체를 생성하여 Servlet에 전달
5.1 스레드는 서블릿의 service()
메서드를 호출
5.2 service()
메서드는 요청에 맞게 doGet()
또는 doPost()
메서드를 호출
호출된 doGet()
, doPost()
메서드는 인자에 맞게 생성된 적절한 동적 페이지를 Response 객체에 담아 WAS에 전달한다.
WAS는 전달받은 Response 객체를 HttpResponse 형태로 바꾸어 Web Server에 전달
생성된 스레드를 종료하고, HttpServletRequest와 HttpServletResponse 객체를 제거한다
데이터, 정보들의 가공을 책임지는 컴포넌트
모델은 어플리케이션의 정보, 데이터를 나타낸다. 데이터베이스, 처음 정의하는 상수, 초기화 값, 변수 등을 뜻한다. 비즈니스 로직을 처리한 후 모델의 변경사항을 컨트롤러와 뷰에 전달한다.
모델 규칙
사용자에게 보여지는 부분, 즉 유저 인터페이스(User interface)를 의미
MVC 패턴은 여러개의 뷰가 존재할 수 있으며, 모델에게 질의하여 데이터를 전달받는다. 뷰는 받은 데이터를 화면에 표시해주는 역할
모델에게 전달받은 데이터를 별도로 저장하지 않아야한다. 사용자가 화면에 표시된 내용을 변경하게 되면 모델에게 전달하여 모델을 변경해야 한다.
뷰 규칙
모델(Model)과 뷰(View) 사이를 이어주는 브릿지(Bridge) 역할
모델이나 뷰는 서로의 존재를 모르고 있다. 변경 사항을 외부로 알리고 수신하는 방법만 있음. 컨트롤러(Controller)는 이를 중재하기 위해 모델과 뷰에 대해 알고 있어야 한다. 모델이나 뷰로부터 변경 내용을 통지 받으면 이를 각 구성 요소에게 통지. 사용자가 어플리케이션을 조작하여 발생하는 변경 이벤트들을 처리하는 역할을 수행한다.
컨트롤러 규칙
MVC1 패턴의 경우 View와 Controller를 모두 JSP가 담당하는 형태
즉 JSP 하나로 유저의 요청을 받고 응답을 처리한다.
이렇게 하면 프로젝트 구조가 커질수록 유지보수가 곤란해잉~
요청을 하나의 컨트롤러(Servlet)가 먼저 받는다. 즉 MVC1과는 다르게 Controller, View가 분리되어 있음. 따라서 역할이 분리되어 MVC1패턴에서의 단점을 보완할 수 있다. 그러므로 개발자는 M, V, C 중에서 수정해야 할 부분이 있다면, 그것만 꺼내어 수정하면 되어서 유지보수에 있어서도 큰 이점을 가집니다.
MV2는 MVC1 패턴보다 구조가 복잡해질 수 있지만, 개발자가 이러한 세부적인 구성까지 신경쓰지 않을 수 있도록 각종 프레임워크들이 지금까지 잘 발전되어 왔다. 그 중에서 대표적인 것이 바로 스프링 프레임워크.
스프링 MVC는 스프링의 서브 프로젝트
→ 스프링은 하나의 기능을 위해서만 만들어진 프레임워크가 아닌 코어 프레임워크 + 여러 서브 프로젝트의 결합
서브 프로젝트 ?
→ 별도의 설정이 존재할 수 있다는 것
즉, Spring Legacy Project로 생성한 프로젝트에도 servlet-contex, root-context로 설정파일이 분리되어 있음. 이렇게...
웹 애플리케이션은 보통 자바 서블릿 기반으로 한다.
자바 서블릿 기반 서버의 동작은 아래와 같다.
(서브 클래스는 javax.servlet.http.HttpServlet을 상속받는 클래스들이다)
서버는 HTTP 요청을 받으면 해당 요청을 해석하여 지정된 서블릿 클래스를 실행한다.
HttpServlet 서브 클래스들은 아래와 같은 순서로 동작하게 된다.
doGet()
process()
response.setContentType(), etc ...
막상 중요한건 2번인데 1 / 3번을 반복해서 작성하게 된다. 시러잉
스프링은 어노테이션과 서브클래스 사용하여 반복작업을 줄여준다~~
스프링의 DispatcherServelt이라는 서브 클래스 덕분에 서블릿 클래스 작성할 필요가 없다
대신에 어노테이션과 인터페이스를 사용하여 스프링이 개발자가 작성한 비즈니스 로직 이해할 수 있도록 클래스를 구현해야한다.
아무튼 스프링 사용하면
- HttpServlet 상속받을 필요가 없다.
- doGet 오버라이딩 하지 않아도 된다
- HttpServletRequest를 직접 파싱하지 않아도 된다.
- HttpServletResponse 작성하지 않아도 된다.
root-context.xml
JAVA 영역 (POJO 영역)
servlet-context.xml
Web 영역
프로젝트 구동 시 관여하는 XML
우선, xml 파일은 모두 객체(Bean)를 정의한다.
Tomcat 구동 관련 설정
스프링 관련 설정
root-context.xml
servlet-context.xml
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<listener>
root-context.xml이 처리되면 파일에 있는 빈 설정들이 동작한다. 이 파일에 정의된 빈들(객체들)은 스프링의 context영역 내부에 생성되고 객체들 간의 의존성이 처리됨
root-context.xml이 처리된 후 스프링 MVC에서 사용하는 DispatcherServlet이라는 서블릿 관련 설정이 동작한다.
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
내부적으로 웹 관련 처리 준비 작업을 진행할 때 사용하는 파일이 servlet-context.xml
DispatcherServlet에서 XmlWebApplicationContext를 이용하여 servlet-contex.xml을 로딩한다. 이 과정에서 등록된 객체들은 기존의 객체들과 연동된다.
DispatcherServlet
사용자의 모든 Request 처리
HandlerMapping
Request 처리를 담당하는 컨트롤러 찾기.
@RequestMapping
설정된 컨트롤러를 찾는다.
HandlerAdapter
HandlerMapping 단계에서 찾은 컨트롤러를 동작시킨다.
Controller
실제로 요청을 처리하는 로직 작성
다양한 타입의 결과 반환 가능하며 이를 ViewResolver가 처리한다.
ViewResolver
Controller가 반환한 결과를 어떤 view를 통해 처리할지 해석한다.
servlet-context.xml에 정의된 InternalResourceViewResolver가 대표적
View
실제로 응답을 보내야하는 데이터를 JSP 이용하여 생성
만들어진 응답은 DispatcherServlet을 통해 전달된다.
스프링 프로젝트 내부에서 코드를 분배하고 관리하는 방식. 애플리케이션 구성요소들을 수평적으로 분리한다
보통 자바 클래스는 기능 수행 클래스와 데이터 저장 클래스로 구분할 수 있다.
기능 수행 클래스는 컨트롤러, 서비스, 퍼시스턴스처럼 로직을 수행한다.
기능 없이 DB에서 반환된 데이터를 담기 위한 클래스들은 기능에 따라 Entity, Model, DTO 등으로 부른다.
이 프로젝트는 모델과 엔티티를 한 클래스에 구현한다.
즉 모델은 비즈니스 데이터 담는 역할 + DB의 테이블과 스키마 표현 역할
package com.example.demo.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TodoEntity {
private String id;
private String userId;
private String title;
private boolean done;
}
@Builder
디자인 패턴 중 빌더 패턴 (생성 패턴), 롬복 사용시 빌더 클래스를 따로 개발하지 않아도 된다. 빌더 패턴 사용은 생성자를 이용해 오브젝트를 생성하는 것과 비슷하다.
// 롬복이 이러한 코드를 자동 생성해준다
ToodEntity todo = TodoEntity.builder()
.id("id값")
.userId("userID값")
.title("제목")
.build()
@NoArgsConstructor
매개변수가 없는 생성자를 구현해준다.
public TodoEntity() {
}
@AllArgsConstructor
클래스의 모든 멤버 변수를 매개변수로 받는 생성자 구현
public TodoEntity(String id, String userId, String title, boolean done){
super();
this.id = id;
this.userId = userId;
this.title = title;
this.done = done;
}
@Data
클래스 멤버 변수의 Getter / Setter 메서드 구현
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
// 생략
public void setDone(boolean done) {
this.done = done;
}
서비스가 요청을 처리하고 클라이언트로 반환 시 Model 그 자체로 보내기보단 DTO로 변환해서 리턴한다. (Data Transfer Object, 데이터 전달에 사용하는 오브젝트)
package com.example.demo.dto;
import com.example.demo.model.TodoEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TodoDTO {
private String id;
private String title;
private boolean done;
public TodoDTO(final TodoEntity entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.done = entity.isDone();
}
}
스프링 부트 스타터 웹 디펜던시 설정을 해두었다. 이 패키지가 제공하는 어노테이션을 사용하면 REST 구현이 쉽다.
@RestController
/ @RequestMapping
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("test") // 리소스
public class TestController {
}
@GetMapping
메서드를 추가하고 @GetMapping
설정해준다
public class TestController {
@GetMapping
public String testController() {
return "hello world";
}
}
@GetMapping
은 HTTP에 매핑@RequestMapping(/test)
는 URI에 매핑됨앱 실행 후 http://localhost:8080/test
로 접근하면 Hello World 출력됨
@GetMapping에 HTTP 메서드 지정 말고도 경로를 지정하는 것 또한 가능하다!
@GetMapping("/testGetMapping")
public String testControllerWithPath() {
return "hello world & testGetMapping";
}
위의 코드는 아래와 같이 매핑된다.
@PathVariable
/test/{id}
와 같은 URI경로로 넘어오는 파라미터를 변수로 받을 수 있다.
@GetMapping("/{id}")
public String testControllerPathVar(@PathVariable(required = false) int id) {
return "Hello world " + id;
}
@RequestParams
/test?id=123
과 같은 요청 파라미터를 받을 수 있다. ?
뒤의 파라미터에 매핑된다.
@RequestBody
반환하고자 하는 리소스가 복잡할 때 사용한다.
DTO 추가 후 DTO를 요청 바디로 받는 메서드 추가하여 확인해보기
package com.example.demo.dto;
import lombok.Data;
// DTO
@Data
public class TestRequestBodyDTO {
private int id;
private String message;
}
// 컨트롤러 클래스에 메서드 추가
// /test경로는 이미 존재하므로 /test/testRequestBody로 지정
@GetMapping("/testRequestBody")
public String testControllerRequestBody(@RequestBody TestRequestBodyDTO testRequestBodyDTO) {
return "Hello World! ID " + testRequestBodyDTO.getId() + " Message : " + testRequestBodyDTO.getMessage();
}
@RequestBody TestRequestBodyDTO testRequestBodyDTO
는 Request Body로 보내오는 JSON을 TestRequestBodyDTO
오브젝트로 변환하여 가져온다. 따라서 클라이언트가 전송하는 요청 바디의 JSON 문자열 내부는 TestRequestBodyDTO
와 의미적으로 동일해야 한다.
@ResponseBody
와 @ResponseEntity
문자열 리턴말고 복잡한 오브젝트 리턴하기
@RestController
는 @ResponseBody
와 @Controller
의 조합으로 이루어져 있다. @Controller는 @Component로서 스프링이 이 클래스의 오브젝트를 자동으로 생성, 의존성 연결을 수행한다. @ResponseBody는 이 클래스의 반환값이 웹의 ResponseBody라는 의미이다. 즉, 스프링은 메서드가 리턴할 때 리턴된 오브젝트를 JSON 형태로 변환 후 HttpResponse에 담아 반환한다는 것이다.
@GetMapping("/testResponseBody")
public ResponseDTO<String> testControllerResponseBody() {
List<String> list = new ArrayList<>();
list.add("Hello World! ResponseDTO");
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return response;
}
@GetMapping("/testResponseEntity")
public ResponseEntity<?> testControllerResponseEntity() {
List<String> list = new ArrayList<>();
list.add("Hello World! ResponseEntity. Error 400!");
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
// http status를 400로 설정.
return ResponseEntity.badRequest().body(response);
}
ResponseEntity는 HTTP 응답의 바디 외에도 status나 header 등 조작 가능