TIL 1일차

HanEol~·6일 전
post-thumbnail

개요

초심을 찾기 위해 1일 TIL을 시작한다.

Spring MVC와 HTTP 요청 데이터 처리 정리

Spring Boot를 공부하면서 서버, HTTP, Spring MVC부터 요청 데이터 처리와 JSON 변환, 실시간 통신까지 정리한 내용입니다.


1. Gradle

Gradle이란?

Gradle은 빌드 자동화 도구이다.

Groovy 언어를 기반으로 작성할 수 있으며 프로젝트의 의존성과 빌드 설정 등을 관리한다.

주요 설정으로 다음과 같은 것들이 있다.

  • configurations
  • repositories
  • dependencies

특히 프로젝트에서 사용하는 라이브러리의 버전을 맞추는 것이 중요하다.


2. 서버란 무엇인가?

서버와 클라이언트가 통신하기 위해서는 서버의 위치를 알아야 한다.

기본적으로 다음과 같이 생각할 수 있다.

IP + Port
  • IP: 어떤 서버인지 식별
  • Port: 서버에서 어떤 프로그램과 통신할지 식별

API란?

API는 서버와 클라이언트가 데이터를 주고받기 위한 약속이다.

예를 들어

GET /users/1

이라는 API가 있다면,

"1번 사용자의 정보를 요청한다."

라는 규칙을 클라이언트와 서버가 공유하는 것이다.


3. Apache Tomcat과 WAS

Tomcat은 Servlet을 실행할 수 있는 WAS(Web Application Server)이다.

웹 애플리케이션에서는 크게 정적 페이지와 동적 페이지를 생각할 수 있다.

정적 페이지

이미 만들어져 있는 HTML, CSS, JS 등의 파일을 그대로 전달한다.

Client
  ↓
HTML 파일 요청
  ↓
Server
  ↓
HTML 파일 반환

동적 페이지

서버에서 로직을 실행한 후 결과를 만들어 반환한다.

Client
  ↓
Request
  ↓
Server
  ↓
비즈니스 로직 / DB 조회
  ↓
결과 생성
  ↓
Response

4. HTTP란?

HTTP는 데이터를 주고받는 양식을 정의한 통신 규약이다.

HTTP 메시지에는 여러 정보가 포함된다.

데이터를 어떻게 주고받을지에 대한 정보를 담는다.

Content-Type: application/json

Payload

실제로 전달하려는 데이터를 의미한다.

{
  "name": "Robbie",
  "age": 25
}

즉 간단하게 생각하면

Header   → 어떻게 보낼 것인가?
Payload  → 무엇을 보낼 것인가?

5. Spring MVC

Spring MVC는 웹 애플리케이션을 만들기 위한 구조이다.

기본적으로 다음과 같은 개념을 가진다.

Model
View
Controller

Spring MVC에서는 DispatcherServlet이 HTTP 요청을 받아 적절한 Controller로 전달하는 역할을 한다.

전체적인 흐름은 다음과 같다.

Client
  ↓
DispatcherServlet
  ↓
Controller
  ↓
Service ...
  ↓
Response

6. Controller

Controller는 HTTP 요청을 처리하는 진입점이다.

Spring에서는 메소드 단위로 API를 관리할 수 있다.

@GetMapping("/users")
public ...

Front Controller 패턴

Spring MVC에서는 DispatcherServlet이 모든 요청을 먼저 받아 적절한 Controller로 전달한다.

Client
   ↓
DispatcherServlet
   ↓
Controller

이러한 구조를 Front Controller 패턴이라고 한다.


7. @Controller와 @ResponseBody

@Controller

@Controller를 사용하면 기본적으로 반환값을 View 이름으로 해석한다.

@Controller
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

Thymeleaf가 설정되어 있다면 다음 파일을 찾는다.

templates/hello.html

@ResponseBody

View를 찾는 것이 아니라 반환값을 HTTP Response Body에 직접 전달하고 싶다면 @ResponseBody를 사용할 수 있다.

@ResponseBody
@GetMapping("/hello")
public String hello() {
    return "hello";
}

실무에서는 JSON API를 만들 때 @RestController를 많이 사용한다.


8. Thymeleaf와 Static

Thymeleaf를 사용하면 Controller에서 반환한 View 이름을 기준으로 templates 폴더의 HTML을 찾는다.

templates/
└── hello.html

Controller에서는

return "hello";

처럼 .html 확장자를 작성하지 않아도 된다.

반면 static 폴더에 있는 정적 리소스는 직접 접근할 수 있다.

또한 redirect:/를 사용하면 클라이언트에게 302 Redirect를 전달하여 다른 URL로 이동시킬 수 있다.


9. 실시간 방문자 수를 구현한다면?

페이지를 새로고침하지 않아도 방문자 수가 증가하는 것을 보여주고 싶다면 서버와 클라이언트가 데이터를 주고받는 방법을 고민해야 한다.

대표적으로

  1. Polling
  2. SSE / WebSocket

등의 방법이 있다.


10. Polling

Polling은 클라이언트가 일정한 주기마다 서버에 데이터를 요청하는 방식이다.

예를 들어 3초마다 방문자 수를 요청할 수 있다.

Client
   │
   │ GET /api/visitors/count
   ↓
Server
   │
   │ 현재 방문자 수
   ↓
Client

3초 후 다시 요청

JavaScript에서는 다음과 같이 구현할 수 있다.

function fetchVisitorCount() {
    fetch('/api/visitors/count')
        .then(response => response.json())
        .then(count => {
            document.getElementById('visitorCount').innerText = count;
        });
}

setInterval(fetchVisitorCount, 3000);

장점

  • 구현이 간단하다.
  • 소규모 서비스나 간단한 모니터링에 적합하다.

단점

데이터가 변경되지 않았더라도 일정 주기마다 요청을 보내게 된다.


11. SSE

SSE(Server-Sent Events)는 서버가 클라이언트에게 데이터를 Push하는 방식이다.

Polling과 비교하면 차이가 명확하다.

Polling

Client → Server
Client ← Server

Client → Server
Client ← Server

Client → Server
Client ← Server

SSE

Client → Server
          연결 유지

Server → Client
Server → Client
Server → Client

즉 클라이언트가 계속 물어보는 것이 아니라 서버가 데이터가 변경되었을 때 클라이언트에게 전달할 수 있다.

Spring에서는 SseEmitter를 사용할 수 있다.

@GetMapping(
    value = "/api/visitors/subscribe",
    produces = MediaType.TEXT_EVENT_STREAM_VALUE
)
public SseEmitter subscribe() {

    SseEmitter emitter = new SseEmitter();

    emitters.add(emitter);

    return emitter;
}

방문자 수가 변경되면 연결된 클라이언트에게 데이터를 전달한다.

for (SseEmitter emitter : emitters) {
    emitter.send(
        SseEmitter.event()
            .name("visitorCount")
            .data(count)
    );
}

클라이언트에서는 EventSource를 사용한다.

const eventSource =
    new EventSource('/api/visitors/subscribe');

eventSource.addEventListener(
    'visitorCount',
    function(event) {
        document.getElementById('visitorCount').innerText
            = event.data;
    }
);

12. SSE를 많은 사람이 사용하면?

SSE는 연결을 계속 유지하기 때문에 접속자가 많아지면 서버의 자원 사용량이 증가할 수 있다.

대표적으로 다음과 같은 문제가 있다.

1. 지속적인 연결 유지 비용

각 클라이언트와 연결을 유지해야 한다.

2. 서버 자원 사용

동시 접속자가 증가하면 메모리와 커넥션 관리에 부담이 생길 수 있다.

3. 여러 서버를 사용하는 경우

서버가 여러 대라면 데이터 공유 문제가 발생할 수 있다.

             Load Balancer
              /         \
             ↓           ↓
         Server A     Server B

A 서버에서 방문자 수가 변경되었는데 B 서버에 연결된 사용자에게 해당 이벤트가 전달되지 않을 수 있다.


13. 대규모 트래픽에서는?

원문에서는 다음과 같은 해결 방법을 생각했다.

Redis Pub/Sub

여러 서버 사이에서 이벤트를 전달할 수 있다.

             Redis
            /     \
           ↓       ↓
       Server A  Server B

WebFlux + Netty

Spring WebFlux와 Netty 기반의 Reactive Stack을 활용하는 방법도 있다.

Polling + Cache

실시간성이 아주 높지 않아도 된다면 데이터를 캐싱하고 Polling 주기를 조절하는 방법도 있다.


14. AJAX

AJAX는 페이지 전체를 새로고침하지 않고 서버와 비동기적으로 데이터를 주고받는 방식이다.

핵심은 다음과 같다.

전체 페이지 새로고침 ❌

필요한 데이터만 요청
        ↓
서버 응답
        ↓
필요한 부분만 변경

예를 들어 좋아요 버튼을 누른다고 생각해보자.

좋아요 클릭
   ↓
AJAX 요청
   ↓
Server
   ↓
좋아요 수 반환
   ↓
화면의 좋아요 수만 변경

현재는 XML보다는 JSON을 주고받는 경우가 많다.

대표적인 구현 방법은 다음과 같다.

  • XMLHttpRequest
  • Fetch API
  • Axios
  • jQuery

15. JSON과 Java 객체 변환

Spring에서 JSON API를 사용하다 보면 다음과 같은 변환이 필요하다.

Java 객체
   ↓
JSON

이것을 직렬화(Serialization)라고 한다.

반대로

JSON
   ↓
Java 객체

이것을 역직렬화(Deserialization)라고 한다.

Spring Boot에서는 Jackson이 이 역할을 담당한다.

핵심 클래스는 ObjectMapper이다.

ObjectMapper mapper = new ObjectMapper();

직렬화

String jsonString =
    mapper.writeValueAsString(user);

역직렬화

User user =
    mapper.readValue(jsonString, User.class);

16. Jackson의 3가지 처리 방식

Jackson에서는 크게 세 가지 방식으로 JSON을 처리할 수 있다.

방식설명사용 상황
Data BindingJSON과 Java 객체를 직접 매핑일반적인 API
Tree ModelJsonNode 트리 구조로 처리JSON 구조가 동적일 때
Streaming API토큰 단위로 순차 처리대용량 JSON

Data Binding

가장 일반적인 방법이다.

User user =
    mapper.readValue(json, User.class);

Tree Model

JSON을 JsonNode로 읽은 후 원하는 값을 꺼낼 수 있다.

JsonNode rootNode =
    mapper.readTree(json);

String name =
    rootNode.get("name").asText();

int age =
    rootNode.get("age").asInt();

JSON 구조가 고정된 Java 클래스보다 동적으로 처리해야 할 때 사용할 수 있다.


Streaming API

JSON을 전체 객체로 메모리에 올리지 않고 토큰 단위로 읽는다.

JsonParser parser =
    factory.createParser(json);

while (parser.nextToken() != JsonToken.END_OBJECT) {
    // 토큰 단위 처리
}

따라서 매우 큰 JSON을 처리할 때 메모리 사용량을 줄이는 데 유리하다.


17. Path Variable

Path Variable은 URL에 데이터를 포함시키는 방식이다.

/users/10

Spring에서는 다음과 같이 사용할 수 있다.

@GetMapping("/users/{id}")
public String getUser(
    @PathVariable Long id
) {
    ...
}

요청이

/users/10

이라면

id = 10

이 된다.


18. Request Param

Request Param은 주로 Query String을 통해 데이터를 전달할 때 사용한다.

/users?name=kim&age=96

Spring에서는 다음과 같이 사용할 수 있다.

@GetMapping("/users")
public String getUser(
    @RequestParam String name
) {
    ...
}

@RequestParam의 기본값은 required = true이므로 값이 없다면 400 Bad Request가 발생한다.

선택값

@RequestParam(required = false)
String name

값이 없으면 null이 될 수 있다.

기본값

@RequestParam(defaultValue = "0")
int age

값이 없다면 0을 사용한다.


19. Form Data와 JSON Body

여기서 RequestParam과 RequestBody를 구분해야 한다.

application/x-www-form-urlencoded

name=kim&age=96

Spring에서는 WebDataBinder가 데이터를 객체에 바인딩한다.

@PostMapping("/form-data")
public String handleFormData(UserDto userDto) {
    return userDto.getName();
}

application/json

{
    "name": "kim",
    "age": 96
}

이 경우 @RequestBody를 사용한다.

@PostMapping("/json-data")
public String handleJsonData(
    @RequestBody UserDto userDto
) {
    return userDto.getName();
}

처리 과정은 다음과 같다.

HTTP Request Body
        ↓
HttpMessageConverter
        ↓
Jackson ObjectMapper
        ↓
UserDto

즉,

@RequestBody + JSON → Jackson을 이용한 역직렬화

라고 이해하면 된다.


20. 지금까지 배운 내용을 하나의 흐름으로 보면

Spring에서 HTTP 요청이 들어왔을 때의 흐름을 연결해보면 다음과 같다.

Client
  │
  │ HTTP Request
  ↓
DispatcherServlet
  │
  ↓
Controller
  │
  ├── @PathVariable
  │
  ├── @RequestParam
  │
  └── @RequestBody
          │
          ↓
       Jackson
          │
          ↓
       Java 객체
          │
          ↓
     비즈니스 로직
          │
          ↓
       Response
          │
          ↓
         Client

그리고 JSON Response를 반환하는 경우에는 반대 방향으로 Jackson이 동작한다.

Java 객체
   ↓
Jackson
   ↓
JSON
   ↓
HTTP Response
   ↓
Client

마무리

이번 내용을 공부하면서 가장 중요하게 이해해야 할 부분은 각각의 annotation을 외우는 것보다 HTTP 요청이 Spring에 들어와서 어떻게 처리되는지를 연결해서 이해하는 것이라고 생각한다.

특히 다음 관계를 기억해두자.

HTTP
 ↓
DispatcherServlet
 ↓
Controller
 ↓
요청 데이터 바인딩
 ├─ PathVariable
 ├─ RequestParam
 └─ RequestBody → Jackson
 ↓
비즈니스 로직
 ↓
Response

그리고 실시간 데이터 전달에서는

Polling
→ Client가 주기적으로 요청

SSE
→ Server가 연결된 Client에게 Push

라는 차이가 핵심이다.

profile
기록을 통해 앞으로 나아가자!

0개의 댓글