스프링 학습을 위한 배경지식

김주언·2022년 10월 31일
1

1. 서버?

서버란 프로그램이라고 생각하면 됨
그런데 이 프로그램은 특정 포트의 소켓을 열어놓고 그 소켓에 클라이언트가 연결할때까지 무한 대기하는 것
그러다가 클라이언트가 연결하면 해당 클라이언트 소켓에서 요청을 받아와 수행하고 응답을 작성해서 전달한다.

예를 들어 서버에서 클라이언트의 요청을 읽을때나 클라이언트로의 응답을 보낼 때 FTP 프로토콜을 사용하면 FTP 서버가 되고 HTTP 프로토콜을 쓰면 HTTP 서버가 되는 것

아무튼 서버는 네트워크 기능 을 수행하는 프로그램!!

1.1 웹 서버

웹 브라우저 클라이언트로부터 HTTP 요청을 받아들이고 HTML 문서와 같은 웹 페이지를 반환하는 프로그램

1.1.1 정적 웹 서버

HTTP 서버 중에서도 리소스 파일을 리턴하는 서버

HTTP GET /file.html 요청을 받았을 때, 정적 웹서버는 지정된 디렉터리에서 file.html파일을 찾아서 이를 HTTP response 바디에 넣어서 클라이언트에게 전송한다. 이 때 해당 파일에 아무 작업도 하지 않아서 정적인 것

아파치나 Nginx가 정적 웹 서버이다. 아파치나 Nginx를 설치 후 지정된 경로에 원하는 리소스 파일을 두면 알아서 웹서버가 리소스 파일을 전송해줘서 클라이언트가 접근할 수 있게됨

1.1.2 동적 웹 서버

파일을 있는 그대로 전송하지 않고 어떠한 작업을 거친 후 전송한다. 클라이언트가 요청에 요청 매개변수를 보내면 이를 활용하거나 등 다양한 작업이 가능하다.

  1. 클라이언트는 요청에 매개변수와 값을 전송 (name = test)
  2. 서버가 요청과 매개변수에 맞는 작업 수행
  3. 작업이 완료된 html파일 클라이언트에게 전송

따라서 정적 웹 서버는 클라이언트가 어떤 요청을 해도 똑같은 결과를 반환하지만, 동적 웹 서버는 클라이언트에 따라, 또 매개변수나 요청의 종류에 따라 결과가 다르게 나타난다

그래서 각 요청별, 매개변수별 로직을 작성해주는 것이 백엔드 개발!

그런데, 사용자 요구에 따라 로직은 항상 변하기 때문에, 일정 규격이 정확한(??) 서버 (아파치나 Nginx)는 사용할 수가 없다. 그러면 서버 프로그램 작성해주기 위해서 처음부터 끝까지 소켓 프로그래밍, HTTP 파싱, 스레드 풀 관리 등 다 해야하나??

그러면 난 백엔드 공부 안햇음

암튼 그래서 동적 웹 서버 구현 도와주는 자바 프로그램이 있다. 그게 바로 서블릿 엔진
아파치 톰캣이 서블릿 엔진에 속함

1.2 서블릿 엔진 ( 컨테이너 )

웹 서버 단독으로는 처리할 수 없는 데이터베이스의 조회나 다양한 로직 처리가 필요한 동적 컨텐츠를 제공한다,

서블릿 엔진 또는 컨테이너는 서버 프로그램.
서블릿 엔진 설치 후, 서블릿 엔진에 클래스 파일과 해당 클래스 파일을 어떤 요청에서 실행하는지 설정해준다.

클래스 파일 작성 시 서블릿 엔진이 이해가능한 형태로 작성해야하는데, 이는 javax.servlet.http.HttpServlet을 상속하는 클래스들이다. HttpServlet을 상속받는 클래스를 작성해 특정 형식에 맞춰서 전달한다.
서블릿 엔진을 이용하면 서버를 처음부터 구현하지 않아도 비즈니스 로직 구현이 가능하다.

JSP, Servlet 구동환경을 제공한다.

1.3 WAS

웹 서버 + 웹 컨테이너 형태
인터넷 상에서 HTTP 프로토콜을 통해 사용자 컴퓨터나 장치에 애플리케이션을 수행해주는 미들웨어, 주로 동적 서버 컨텐츠를 수행하며 웹 서버와 구별이 된다.
주로 데이터베이스 서버와 같이 수행한다.

간단히 정리하자면 웹서버가 보낸 JSP, PHP 등의 파일을 수행한 결과를 다시 웹 서버로 보내주는 역할

⚙️ 클라이언트 → 웹 서버 → WAS → DB 구조 동작 과정

  1. 웹서버는 웹브라우저 클라이언트로부터 HTTP 요청을 받는다.

  2. 웹 서버는 클라이언트의 요청을 WAS에 보낸다.

  3. WAS는 관련된 서블릿을 메모리에 올린다.

  4. WAS는 web.xml을 참조하여 해당 서블릿에 대한 스레드를 생성

  5. HttpServletRequest와 HttpServletResponse 객체를 생성하여 Servlet에 전달
    5.1 스레드는 서블릿의 service() 메서드를 호출
    5.2 service() 메서드는 요청에 맞게 doGet() 또는 doPost()메서드를 호출

  6. 호출된 doGet(), doPost() 메서드는 인자에 맞게 생성된 적절한 동적 페이지를 Response 객체에 담아 WAS에 전달한다.

  7. WAS는 전달받은 Response 객체를 HttpResponse 형태로 바꾸어 Web Server에 전달

  8. 생성된 스레드를 종료하고, HttpServletRequest와 HttpServletResponse 객체를 제거한다




2. MVC 패턴

2.1 MVC 모델의 구성요소

2.1.1 모델

데이터, 정보들의 가공을 책임지는 컴포넌트

모델은 어플리케이션의 정보, 데이터를 나타낸다. 데이터베이스, 처음 정의하는 상수, 초기화 값, 변수 등을 뜻한다. 비즈니스 로직을 처리한 후 모델의 변경사항을 컨트롤러와 뷰에 전달한다.

모델 규칙

  • 사용자가 편집하길 원하는 모든 데이터를 가지고 있어야한다.
  • 뷰나 컨트롤러에 대해서 어떤 정보도 알지 않아야 한다.
  • 변경 발생 시 변경 통지에 대한 처리 방법을 구현한다.

2.1.2 뷰

사용자에게 보여지는 부분, 즉 유저 인터페이스(User interface)를 의미

MVC 패턴은 여러개의 뷰가 존재할 수 있으며, 모델에게 질의하여 데이터를 전달받는다. 뷰는 받은 데이터를 화면에 표시해주는 역할
모델에게 전달받은 데이터를 별도로 저장하지 않아야한다. 사용자가 화면에 표시된 내용을 변경하게 되면 모델에게 전달하여 모델을 변경해야 한다.

뷰 규칙

  • 모델이 가지고 있는 정보를 따로 저장해서는 안된다.
  • 모델이나 컨트롤러와 같이 다른 구성요소들을 몰라야한다.
  • 변경 발생 시 변경 통지에 대한 처리 방법을 구현한다.

2.1.3 컨트롤러

모델(Model)과 뷰(View) 사이를 이어주는 브릿지(Bridge) 역할

모델이나 뷰는 서로의 존재를 모르고 있다. 변경 사항을 외부로 알리고 수신하는 방법만 있음. 컨트롤러(Controller)는 이를 중재하기 위해 모델과 뷰에 대해 알고 있어야 한다. 모델이나 뷰로부터 변경 내용을 통지 받으면 이를 각 구성 요소에게 통지. 사용자가 어플리케이션을 조작하여 발생하는 변경 이벤트들을 처리하는 역할을 수행한다.

컨트롤러 규칙

  • 모델이나 뷰에 대해서 알고 있어야한다.
  • 모델이나 뷰의 변경을 모니터링 해야한다.


2.2 MVC 1

MVC1 패턴의 경우 View와 Controller를 모두 JSP가 담당하는 형태
즉 JSP 하나로 유저의 요청을 받고 응답을 처리한다.

이렇게 하면 프로젝트 구조가 커질수록 유지보수가 곤란해잉~

2.3 MVC 2

요청을 하나의 컨트롤러(Servlet)가 먼저 받는다. 즉 MVC1과는 다르게 Controller, View가 분리되어 있음. 따라서 역할이 분리되어 MVC1패턴에서의 단점을 보완할 수 있다. 그러므로 개발자는 M, V, C 중에서 수정해야 할 부분이 있다면, 그것만 꺼내어 수정하면 되어서 유지보수에 있어서도 큰 이점을 가집니다.

MV2는 MVC1 패턴보다 구조가 복잡해질 수 있지만, 개발자가 이러한 세부적인 구성까지 신경쓰지 않을 수 있도록 각종 프레임워크들이 지금까지 잘 발전되어 왔다. 그 중에서 대표적인 것이 바로 스프링 프레임워크.



3. MVC 패턴 적용 사례

3.1 MVC 패턴 적용 시나리오

  1. 브라우저 화면에서 서버로 데이터를 전달합니다.
  2. 컨트롤러에서 데이터를 전달받아 서비스에게 데이터를 전달합니다.
  3. 서비스는 JpaRepository를 이용하여 전달받은 데이터를 데이터베이스에 INSERT 합니다.
  4. INSERT 수행 후 컨트롤러는 서비스를 통해 데이터를 다시 조회합니다.
    5.조회한 데이터를 모델 객체를 통해 뷰에게 전달합니다.
  5. 화면에 변경이 발생하는지 확인합니다.





4. 스프링 프레임워크


4.1 스프링 MVC

스프링 MVC는 스프링의 서브 프로젝트
→ 스프링은 하나의 기능을 위해서만 만들어진 프레임워크가 아닌 코어 프레임워크 + 여러 서브 프로젝트의 결합

서브 프로젝트 ?
→ 별도의 설정이 존재할 수 있다는 것
즉, Spring Legacy Project로 생성한 프로젝트에도 servlet-contex, root-context로 설정파일이 분리되어 있음. 이렇게...

4.2 디스패처 서블릿

4.2.1 서블릿 기반 서버

웹 애플리케이션은 보통 자바 서블릿 기반으로 한다.
자바 서블릿 기반 서버의 동작은 아래와 같다.
(서브 클래스는 javax.servlet.http.HttpServlet을 상속받는 클래스들이다)

서버는 HTTP 요청을 받으면 해당 요청을 해석하여 지정된 서블릿 클래스를 실행한다.
HttpServlet 서브 클래스들은 아래와 같은 순서로 동작하게 된다.

  1. 요청 받아서 파라미터 해석 - doGet()
  2. 비즈니스 로직 수행 -process()
  3. 응답 작성 - response.setContentType(), etc ...

막상 중요한건 2번인데 1 / 3번을 반복해서 작성하게 된다. 시러잉

4.2.2 디스패처 서블릿

스프링은 어노테이션과 서브클래스 사용하여 반복작업을 줄여준다~~

스프링의 DispatcherServelt이라는 서브 클래스 덕분에 서블릿 클래스 작성할 필요가 없다
대신에 어노테이션과 인터페이스를 사용하여 스프링이 개발자가 작성한 비즈니스 로직 이해할 수 있도록 클래스를 구현해야한다.

아무튼 스프링 사용하면

  1. HttpServlet 상속받을 필요가 없다.
  2. doGet 오버라이딩 하지 않아도 된다
  3. HttpServletRequest를 직접 파싱하지 않아도 된다.
  4. HttpServletResponse 작성하지 않아도 된다.

4.2 스프링 MVC 구조

  • root-context.xml
    JAVA 영역 (POJO 영역)

  • servlet-context.xml
    Web 영역

  • WebApplicationContext
    기존 구조에 MVC 설정 포함하는 구조

4.3 로딩 구조

프로젝트 구동 시 관여하는 XML
우선, xml 파일은 모두 객체(Bean)를 정의한다.

  • Tomcat 구동 관련 설정

    • web.xml
      • 배포자 기술자 DD(Deployment Descriptor)
      • 설정을 위한 설정파일.
      • 최초로 WAS가 최초로 구동될 때, 각종 설정을 정의한다.
      • 여러 xml파일을 인식하도록 각 파일을 가리켜 준다.
  • 스프링 관련 설정

    • root-context.xml

      • view와 관련되지 않은 객체를 정의
      • Service, Repository(DAO), DB등 비즈니스 로직과 관련된 설정
    • servlet-context.xml

      • 요청과 관련된 객체를 정의
      • url과 관련된 controller나, @(어노테이션), ViewResolver, Interceptor, MultipartResolver 등의 설정

아래 내용은 web.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>
    root-context.xml의 경로 설정
  • <listener>
    스프링의 ContextLoaderListener 등록. 이는 웹 앱 구동시 함께 동작한다.

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을 로딩한다. 이 과정에서 등록된 객체들은 기존의 객체들과 연동된다.



참고 ) 디스패처 서블릿 동작 상세

  1. DispatcherServlet
    사용자의 모든 Request 처리

  2. HandlerMapping
    Request 처리를 담당하는 컨트롤러 찾기.
    @RequestMapping 설정된 컨트롤러를 찾는다.

  3. HandlerAdapter
    HandlerMapping 단계에서 찾은 컨트롤러를 동작시킨다.

  4. Controller
    실제로 요청을 처리하는 로직 작성
    다양한 타입의 결과 반환 가능하며 이를 ViewResolver가 처리한다.

  5. ViewResolver
    Controller가 반환한 결과를 어떤 view를 통해 처리할지 해석한다.
    servlet-context.xml에 정의된 InternalResourceViewResolver가 대표적

  6. View
    실제로 응답을 보내야하는 데이터를 JSP 이용하여 생성

  7. 만들어진 응답은 DispatcherServlet을 통해 전달된다.





6. 백엔드 서비스 아키텍처

6.1 레이어드 아키텍처 패턴

스프링 프로젝트 내부에서 코드를 분배하고 관리하는 방식. 애플리케이션 구성요소들을 수평적으로 분리한다


Model, Entity, DTO

보통 자바 클래스는 기능 수행 클래스와 데이터 저장 클래스로 구분할 수 있다.
기능 수행 클래스는 컨트롤러, 서비스, 퍼시스턴스처럼 로직을 수행한다.
기능 없이 DB에서 반환된 데이터를 담기 위한 클래스들은 기능에 따라 Entity, Model, DTO 등으로 부른다.

모델과 엔티티

이 프로젝트는 모델과 엔티티를 한 클래스에 구현한다.
즉 모델은 비즈니스 데이터 담는 역할 + DB의 테이블과 스키마 표현 역할

TodoEntity.java

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;
    }

DTO

서비스가 요청을 처리하고 클라이언트로 반환 시 Model 그 자체로 보내기보단 DTO로 변환해서 리턴한다. (Data Transfer Object, 데이터 전달에 사용하는 오브젝트)

DTO로 보내는 이유?

  • 비즈니스 로직 캡슐화
  • 클라이언트에게 필요한 정보가 모델에 모두 포함되어 있지 않은 경우가 잦기에
    (에러 메세지 등)

DTO 예제

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();
    }
}


Controller Layer

스프링 REST API Controller

스프링 부트 스타터 웹 디펜던시 설정을 해두었다. 이 패키지가 제공하는 어노테이션을 사용하면 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 {
}
  • REST API를 구현하므로 이 어노테이션을 통해 해당 컨트롤러가 RestController임을 명시한다. RestController 이용 시 HTTP 관련 코드 및 요청/응답 매핑을 스프링이 지원한다.

@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 등 조작 가능

profile
학생 점심을 좀 차리시길 바랍니다

0개의 댓글

관련 채용 정보