[내배캠_단기 게임 서버 개발 부트캠프 5일차] Spring 입문 정리 - 백엔드 기초부터 SpringMVC까지

오수호·2026년 9월 4일

TIL

목록 보기
67/77

[내배캠_단기 게임 서버 개발 부트캠프] Spring 입문 정리 - 백엔드 기초부터 SpringMVC까지

Spring 입문 파트에서 배운 내용을 하나씩 노트로만 남겨두니까 흩어져서 잘 안 잡히길래, 백엔드 개발이 뭔지부터 시작해서 HTTP, 서블릿, MVC, DispatcherServlet까지 흐름대로 한 번에 정리해봤다.


1. 백엔드 개발과 Spring의 등장 배경

백엔드 개발을 레스토랑에 비유하면, 손님(클라이언트)은 주방(백엔드)을 직접 보지 못하고 웨이터(프론트엔드)를 통해 주문을 넣으면 주방에서 만든 요리를 전달받는 구조라고 한다. 이 비유가 꽤 와닿았다.

자바로 웹 서버 개발을 처음부터 다 하려면 신경 써야 할 게 너무 많다. Spring은 이 과정을 쉽게 만들어주는 프레임워크이고, Spring Boot는 그 Spring을 쓸 때마다 반복해야 하는 설정을 기본값으로 미리 잡아줘서 개발 속도를 더 끌어올린 것이다.

  • Spring: AOP, IoC, PSA 같은 강력한 기능을 제공하지만 XML 설정이 복잡했다.
  • Spring Boot(2014년 출시): 어노테이션 기반으로 XML 설정을 대체하고, 외부 라이브러리 버전 호환까지 자동으로 맞춰준다. 가장 강력한 기능 중 하나가 내장 Apache Tomcat — Boot가 없던 시절엔 개발자가 직접 Tomcat을 받아서 넣어줘야 했다.

여기서 나온 IoC(Inversion of Control, 제어의 역전)는 이후 프레임워크와 라이브러리를 구분 짓는 핵심 개념이라 다음 섹션에서 좀 더 짚어본다.


2. 개발 기본 용어 (네이밍 규칙 · JSON · XML)

자바에서 쓰는 명명 규칙은 두 가지로 나뉜다.

  • PascalCase : 클래스 이름
  • camelCase : 변수, 함수, 메서드 이름

JSON은 클라이언트와 서버가 통신할 때 쓰는 Key-Value 기반 데이터 형식이고, Value 자리에 또 다른 객체나 배열이 올 수 있다. 언어에 상관없이 같은 데이터를 주고받을 수 있게 해준다는 게 핵심이다.

과거엔 XML을 많이 썼는데, 태그 구조 때문에 가독성이 떨어지고 같은 데이터를 표현해도 용량이 더 크다. 지금은 JSON이 사실상 표준이다. 다만 JSON은 명세(RFC 8259)상 문자열·숫자·불리언·null·객체·배열 6가지 타입만 지원해서, 날짜(Date) 같은 타입은 표준 타입이 따로 없고 보통 문자열이나 숫자(타임스탬프)로 변환해서 주고받는다는 점은 알아두면 좋다.


3. 라이브러리와 프레임워크

구분라이브러리프레임워크
의미필요한 클래스/함수 모음개발환경 전체의 틀 (Frame + Work)
장점생산성↑, 검증된 안정성일관된 구조, 협업 용이, 보안·테스트 도구 기본 제공
단점버전 호환성 문제, 기존 코드와 충돌학습 곡선 높음, 구조를 강제당함

노트에는 "프레임워크는 라이브러리의 모음"이라고 되어 있었는데, 이 둘을 실질적으로 가르는 기준은 누가 흐름을 제어하느냐다. 라이브러리는 내 코드가 라이브러리를 호출하지만, 프레임워크는 프레임워크가 전체 흐름을 쥐고 내가 작성한 코드를 필요할 때 불러준다. 이게 앞서 나온 IoC(제어의 역전)이고, "Don't call us, we'll call you"라는 문장으로도 자주 설명된다. Unity에서 개발자가 직접 Update()를 호출하는 게 아니라 엔진이 매 프레임 알아서 호출해주는 구조도 같은 원리다.


4. 네트워크와 서버, 웹 서버 vs WAS

네트워크는 여러 장비가 IP주소·서브넷마스크·게이트웨이 등을 설정하고 프로토콜을 통해 서로 통신하는 기술이다. 서버는 이 네트워크를 이용해서 사용자의 요청을 처리하고 다시 전달해주는 역할을 한다.

택배에 비유하면 이해가 쉬웠다.

  • 주소(IP) : xxx.xxx.x.xxx
  • 받는 사람, 즉 포트(Port) : :8080

웹 서버 vs WAS(Web Application Server)는 계속 헷갈리기 쉬운 개념인데 이렇게 정리했다.

  • 웹 서버: 정적인 콘텐츠(HTML, CSS, 이미지)를 그대로 클라이언트에 전달한다. 동적인 요청이 들어오면 WAS로 넘긴다. 대표적으로 Apache, Nginx.
  • WAS: HTTP 기반으로 동적인 처리(로직 실행, DB 조회 등)를 담당한다. Tomcat, JBoss 등이 있다.

Apache HTTP Server와 Apache Tomcat은 둘 다 아파치 소프트웨어 재단 산하 프로젝트이다. 다만 두 프로젝트는 하나로 합쳐진 제품이 된 적은 없고 지금도 서로 독립된 별개의 소프트웨어다. 다만 실무에서 Apache(또는 Nginx)를 Tomcat 앞단에 리버스 프록시로 세워서 정적 콘텐츠나 SSL 종료, 로드밸런싱을 처리하고 동적 요청만 Tomcat으로 넘기는 구성을 흔히 쓰다 보니, 이 둘이 "짝지어 다닌다"는 인상에서 "합쳐졌다"는 오해가 생긴 것 같다. Tomcat 혼자서도 정적 파일 서빙 + 서블릿 컨테이너 역할을 둘 다 할 수 있어서 더 헷갈리기 쉬운 부분이기도 하다.


5. HTTP 이해하기

HTTP(HyperText Transfer Protocol)는 웹에서 데이터를 주고받기 위한 통신 규약이다. 클라이언트가 요청을 보내고 응답을 기다리는 동기적 방식으로 동작하며, 1.1/2.0/3.0 등 여러 버전이 있다.

무상태(Stateless): 서버는 클라이언트의 이전 상태를 기억하지 않는다. 마치 "저장 기능 없는 게임에 접속하는 것"과 같다. 매 요청마다 서버 입장에선 처음 보는 손님인 셈이다. 덕분에 서버를 수평으로 늘리기(Scale Out)는 쉽지만, 클라이언트가 매번 필요한 정보를 다 실어 보내야 한다는 단점이 있다. 실제로는 로그인 상태 유지를 위해 쿠키/세션으로 이 무상태성을 보완하는 방식을 많이 쓴다.

HTTP 메시지 구조

  • Request: StartLine(Method, Path, Version) → Header(Key/Value) → Empty Line → Message Body
  • Response: StartLine(Status Code, Status Text) → Header → Empty Line → Message Body

HTTP Method

Method역할비고
GET리소스 조회
POST리소스 생성다른 메서드로 애매할 때도 종종 사용
PUT리소스 전체 수정
PATCH리소스 부분 수정
DELETE리소스 삭제
HEADGET과 동일하지만 Body 제외실무에서 거의 못 봄
OPTIONS지원 가능한 Method 목록 확인

Method 속성 3가지

  • 안전성(Safe): GET은 서버 데이터를 변경하지 않으므로 안전하다. 나머지는 데이터를 바꾸므로 안전하지 않다.
  • 멱등성(Idempotent): 한 번 호출하든 여러 번 호출하든 서버 상태가 똑같이 유지되는 성질. GET·PUT·DELETE는 스펙상 멱등이 보장되고, POST는 멱등하지 않다. 멱등성이 있으면 요청 실패 시 재시도해도 안전하다는 뜻이라 장애 복구에 유용하다.
  • 캐시가능성(Cacheable): GET, HEAD, POST 응답은 이론상 캐시 가능하지만 실무에선 거의 GET/HEAD만 캐싱한다. 변경 가능성이 적은 정적 자원(HTML, CSS, 이미지, JS)이 주 대상이다.

HTTP 상태 코드

코드대의미
1xx요청 수신, 처리 계속 중 (거의 안 씀)
2xx성공
3xx리다이렉션, 추가 조치 필요
4xx클라이언트 에러
5xx서버 에러 (재시도하면 성공할 수도 있음)

6. API와 RESTful 설계

API(Application Programming Interface)는 프로그램끼리 대화하는 방법, 즉 인터페이스다. 다른 소프트웨어와 통신할 때 지켜야 할 규칙을 정의한다.

REST는 서버 구현을 모르는 클라이언트도 API를 잘 사용할 수 있게 하려는 고민의 결과물이고, RESTful은 그 REST 지침을 잘 따르는 설계를 말한다. 반드시 지켜야 하는 강제 규칙이라기보단 권고사항에 가깝다.

RESTful API 디자인 원칙

  • 동사보단 명사, 단수보단 복수 (/users)
  • 마지막에 슬래시(/) 넣지 않기
  • _ 대신 - 사용
  • 대문자 사용하지 않기
  • 확장자 포함하지 않기
  • 계층 구조로 표현하기 (/users/1/orders)
  • 메서드로 행위를 구분: 조회는 GET, 저장은 POST, 수정은 PUT, 삭제는 DELETE

REST를 원래 정의한 논문(Roy Fielding)에는 사실 URI 설계 규칙보다 더 중요한 제약조건이 있는데, 그중 실무에서 거의 지켜지지 않는 게 HATEOAS(응답에 다음에 할 수 있는 행동의 링크까지 포함시키는 것)다. 위 디자인 원칙들은 REST를 "얼마나 잘 지켰는지" 단계로 나눈 리처드슨 성숙도 모델(Richardson Maturity Model) 기준으로 보면 아직 앞 단계에 해당하는 관행들이고, 그럼에도 실무에서는 이 정도만 지켜도 충분히 RESTful하다고 인정받는 편이다.


7. Gradle과 의존성 관리

Spring Initializr는 스프링 부트 프로젝트를 빠르게 생성해주는 웹 도구다. 프로젝트 구조, 빌드 설정 파일, 의존성 버전 충돌 방지까지 자동으로 처리해준다.

의존성이란 프로젝트가 동작하는 데 필요한 외부 라이브러리를 뜻한다. 예전엔 개발자가 직접 .jar 파일을 다운받아 넣었지만, 요즘은 Maven이나 Gradle 같은 빌드 도구가 이 역할을 대신한다.

  • Maven: pom.xml을 XML로 작성. 프로젝트가 복잡해질수록 설정 파일이 기하급수적으로 늘어난다.
  • Gradle: build.gradle을 Groovy나 Kotlin으로 작성. 프로그래밍 코드처럼 유연하고 동적인 설정이 가능하고 텍스트 양도 적다. 인텔리제이 우측 Gradle 탭에서 바로 빌드/실행할 수 있고, 자바 코드를 실행 가능한 .jar 파일로 빌드해주는 것도 이 도구의 역할이다.
  • Maven Repository: 자바 라이브러리를 모아두는 온라인 저장소. Maven이 표준이던 시절 만들어졌지만 Gradle과도 호환돼서 지금도 라이브러리 검색 시 여기서 Gradle 의존성 코드를 복사해 dependencies에 추가하는 식으로 쓴다.

Gradle이 Maven보다 대규모 프로젝트에서 빌드 속도가 빠른 이유 중 하나는, 태스크 간 의존관계를 그래프(DAG)로 구성해서 변경되지 않은 부분은 다시 빌드하지 않는 증분 빌드(incremental build)를 지원하기 때문이다. 그리고 팀 프로젝트에서는 gradlew(Gradle Wrapper)를 커밋해두면, 로컬에 Gradle이 따로 설치돼 있지 않거나 버전이 달라도 프로젝트에 지정된 버전 그대로 빌드할 수 있어서 CI 환경에서도 자주 쓰인다.


8. Annotation과 Lombok

Annotation은 자바 코드에 메타데이터를 추가해서 컴파일러나 런타임에 특정 동작을 트리거하는 용도로 쓰인다 (@Override가 대표적).

Lombok은 반복적으로 작성되는 보일러플레이트 코드를 어노테이션 하나로 자동 생성해주는 라이브러리다.

주요 Lombok 어노테이션

  • @Getter / @Setter
  • @ToString
  • @NoArgsConstructor / @AllArgsConstructor / @RequiredArgsConstructor (final 필드와 @NonNull이 붙은 필드만 매개변수로 받는 생성자를 생성한다)
  • @Slf4j: 로그용 Logger 객체 자동 생성

설정 방법: Ctrl+Alt+S → 어노테이션 프로세서 검색 → "Enable annotation processing" 체크. 혹은 Shift 두 번 눌러 플러그인 검색 후 Lombok 설치 여부 확인.

application.properties는 Spring Boot의 기본 설정을 바꾸는 파일이다. 예를 들어 server.port=8081로 바꾸면 시작 포트가 바뀐다.

Lombok은 컴파일 시점에 어노테이션 프로세서(APT)가 소스코드의 AST를 직접 조작해서 메서드를 끼워 넣는 방식으로 동작한다. 반면 @Autowired처럼 스프링이 처리하는 어노테이션은 런타임에 리플렉션으로 읽어서 동작하는 방식이라, 같은 "어노테이션"이어도 동작 시점이 다르다는 걸 알아두면 좋다. 실무 팁 하나 더: JPA 엔티티에 @Data(내부적으로 @EqualsAndHashCode, @ToString 포함)를 습관적으로 붙이면, 양방향 연관관계가 걸린 필드까지 전부 포함해서 equals/hashCode/toString을 만들다가 서로를 무한 참조해서 StackOverflowError가 나는 경우가 꽤 흔하다. 엔티티에는 연관관계 필드를 제외하고 직접 어노테이션을 조합해서 쓰는 걸 권장한다.


9. 테스트 코드 작성하기

버그 수정을 위한 테스트 방법은 크게 두 가지다.

  • 블랙박스 테스팅: 사용자 입장에서 테스트. 누구나 할 수 있지만 기능이 늘어날수록 범위가 커지고 테스트 품질이 사람마다 달라진다 (QA 직군이 필요한 이유).
  • 개발자 테스트: 개발자가 직접 테스트 코드를 작성. 기능 추가/확장 시 테스트하기 쉽지만, 테스트 코드 유지보수에 리소스가 들고 개발 기간이 늘어날 수 있다.

Spring Boot는 build.gradle의 테스트 관련 의존성을 기본 제공해서 개발자 테스트를 쉽게 만들 수 있다. 자바 클래스 우클릭 → 테스트 생성을 하면 test 폴더에 동일한 패키지 경로로 테스트 클래스가 생기고, main 메서드 없이도 @Test 어노테이션이 붙은 메서드 단위로 개별 실행하거나 클래스 단위로 한 번에 실행할 수 있다. Assertion으로 기대값과 다른 결과가 나오면 에러를 발생시켜 실패를 감지한다.

테스트 코드를 짤 때 흔히 쓰는 구조가 AAA 패턴이다 — Arrange(준비: 테스트 대상과 입력값 세팅) → Act(실행: 실제 메서드 호출) → Assert(검증: 결과 확인). 그리고 테스트 범위가 커지면 보통 단위 테스트(Unit, 메서드 하나) → 통합 테스트(Integration, 여러 컴포넌트 연동) → E2E 테스트(전체 시나리오) 순으로 계층을 나누는데, 아래로 갈수록 실행은 느리고 비싸지지만 실제 사용 환경에 가까워진다. 그래서 단위 테스트를 가장 많이, E2E는 최소한으로 두는 게 일반적이다.


10. Servlet의 개념과 한계

초기 웹페이지는 모든 사용자에게 동일한 정적 HTML만 보여줄 수 있었다. 이 한계를 넘어 동적으로 페이지를 만들기 위해 등장한 게 Servlet이다.

Servlet: 서버에서 실행되어 클라이언트 요청을 처리하고 결과를 동적으로 생성해 응답하는 자바 프로그램 (Server + -let = "작은 서버"). jakarta.servlet.http.HttpServlet을 상속받아 구현한다.

서블릿 컨테이너: 서블릿은 스스로 실행될 수 없고, 서블릿을 관리하며 웹 서버와 통신하게 해주는 환경이 필요하다. 대표적으로 Tomcat. 생명주기 관리, 요청 전달, 응답 전송, 동시 요청 처리를 담당한다. 스프링 부트에선 서블릿이 표준 컴포넌트가 아니라 자동으로 탐지되지 않아서, @ServletComponentScan 어노테이션을 붙여줘야 한다.

서블릿 동작 과정

  1. 클라이언트가 서버에 요청
  2. HttpServletRequest / HttpServletResponse 객체 생성
  3. 요청을 처리할 서블릿을 찾음 (과거엔 web.xml에서 찾음)
  4. 찾은 서블릿의 service() 메서드 실행
  5. 요청이 GET인지 POST인지 등에 따라 doGet(), doPost() 등 알맞은 메서드 실행
  6. 결과를 HttpServletResponse에 담아 클라이언트로 응답

서블릿의 한계

  • 낮은 응집도: 요청 처리, 비즈니스 로직, 데이터 접근, 응답 생성이 한 클래스에 다 몰려 있다.
  • 강한 결합도: 비즈니스 로직이 HttpServletRequest/HttpServletResponse에 직접 의존해서, 웹 환경 밖에서 단위 테스트하기 어렵다.
  • 반복적인 상용구 코드: 파라미터 파싱, 타입 변환, 응답 타입 설정 같은 코드가 매 요청마다 반복된다.

이 문제를 관심사의 분리(SoC) 원칙으로 풀기 위해 등장한 게 SpringMVC다 (다음 섹션에서 이어진다).

여기서 실무에서 자주 걸려 넘어지는 포인트 하나: 서블릿 컨테이너는 서블릿 인스턴스를 요청마다 새로 만드는 게 아니라 싱글톤으로 하나만 만들어서 재사용하고, 들어오는 요청마다 스레드만 새로 배정해서 service()를 실행한다. 그래서 서블릿(그리고 스프링 빈도 기본 스코프가 싱글톤이라 마찬가지)에 요청별로 달라져야 할 값을 멤버 변수로 저장해버리면, 동시에 들어온 다른 요청이 그 값을 덮어써버리는 동시성 버그가 생긴다. 상태는 항상 메서드 로컬 변수나 파라미터로 다뤄야 한다.


11. MVC 패턴과 SpringMVC

MVC는 애플리케이션 코드를 세 가지 역할로 나누는 설계 방식이다.

역할담당
Model데이터와 비즈니스 로직, DB 연동
View사용자에게 보여지는 화면(UI)
Controller요청을 받아 Model과 View를 연결하는 중간 다리

Controller: HTTP 요청을 받아 URL 매핑으로 실행할 메서드를 결정하고, 비즈니스 로직을 호출한 뒤 데이터를 Model에 담아 View 이름을 반환한다. @Controller 어노테이션이 붙은 클래스가 이 역할을 한다.

@Controller
public class HelloController {
    @GetMapping("/api/hello")
    @ResponseBody
    public String hello(){
        return "Hello World!";
    }

    @GetMapping("/api/get")
    @ResponseBody
    public String get(){
        return "Get Method 요청";
    }
}

메서드 경로는 중복될 수 있지만(같은 경로에 GET/POST를 다르게 매핑하는 식), 같은 메서드 시그니처(메서드명)는 중복이 불가능하다. @RequestMapping("/api")를 클래스 레벨에 붙이면 각 메서드의 @GetMapping에서 공통 경로를 생략할 수 있다 — @RequestMapping이 그 경로를 클래스의 모든 메서드에 적용해주기 때문이다.

Model / View: Model은 Controller가 View로 데이터를 전달할 때 쓰는 Key-Value 객체로, Spring이 기본 제공하므로 따로 만들 필요가 없다. Controller가 반환한 View 이름을 ViewResolver가 실제 View 파일과 매핑해서 렌더링한다. JSP나 Thymeleaf가 대표적인 View 템플릿 라이브러리다.

SSR vs CSR

  • SSR(Server Side Rendering): 서버가 완성된 HTML을 만들어 브라우저에 전달. 초기 로딩이 빠르고 검색엔진(SEO)에 유리하다. Thymeleaf가 이 방식.
  • CSR(Client Side Rendering): 서버는 거의 빈 HTML과 JS만 보내고, 브라우저가 JS로 데이터를 요청해 화면을 직접 그린다. React/Vue가 대표적. 초기 로딩은 느리지만 이후 화면 전환이 앱처럼 부드럽다. 참고로 구글 크롤러는 SSR 페이지는 데이터가 이미 채워진 채로 읽지만, CSR은 빈 껍데기만 읽고 데이터가 없다고 판단할 수 있어서 SEO에 상대적으로 불리하다.

최근엔 이 둘을 섞은 절충안(빌드 시점에 미리 HTML을 만들어두는 SSG, 요청 시점에 필요한 부분만 서버에서 다시 그려주는 하이브리드 방식 등)도 많이 쓰이는데, 그건 Next.js 같은 프론트엔드 프레임워크 영역이라 여기선 개념만 짚고 넘어간다.


12. 프론트 컨트롤러 패턴과 DispatcherServlet

API가 100개면 서블릿 방식으로는 클래스도 100개, 메서드도 그만큼 늘어나서 유지보수·확장성 면에서 최악이다. 이 문제를 푸는 게 프론트 컨트롤러 패턴이다.

프론트 컨트롤러 패턴: 모든 클라이언트 요청을 단일 진입점에서 받아, 공통 처리는 중앙에서 하고 개별 처리는 알맞은 핸들러에 위임하는 디자인 패턴. 공통 로직 유지보수가 쉽고, 새 컨트롤러 추가도 쉬워진다.

어댑터 패턴: 서로 다른 인터페이스를 가진 클래스들을 연결해주는 패턴. Spring은 @Controller 기반, HttpRequestHandler 기반 등 다양한 형태의 컨트롤러를 지원하는데, 프론트 컨트롤러 패턴만으로는 이 여러 형태를 통일해서 다루기 어렵다. 그래서 HandlerAdapter라는 어댑터를 하나 더 두고, DispatcherServlet은 항상 이 어댑터 인터페이스로만 컨트롤러를 호출한다. 컨트롤러 종류가 늘어나도 DispatcherServlet 코드는 안 바뀌고 어댑터만 추가되는 구조다.

DispatcherServlet의 흐름

  1. 클라이언트 요청 → DispatcherServlet 도달
  2. HandlerMapping에게 요청을 처리할 Handler(Controller) 조회 요청 (HandlerMapping은 URL 경로와 Controller 메서드를 매칭해둔 정보)
  3. 찾은 Handler에게 HandlerAdapter를 통해 요청 처리 위임
  4. Controller가 비즈니스 로직 수행 후 ModelAndView(Model 데이터 + View 논리적 이름) 반환
  5. ViewResolver에게 View 이름 전달 → 실제 View 객체 조회
  6. View에 Model 데이터를 전달해 렌더링 요청
  7. 렌더링 결과를 클라이언트에 응답

이 전체 흐름을 프론트 컨트롤러 패턴이 없던 시절엔 개발자가 if문 등으로 직접 라우팅을 구현해야 했다는 게 핵심이다.


13. 정적 페이지와 동적 페이지 반환하기

static 폴더의 HTML은 주소창에 파일명으로 직접 접근할 수 있다(Controller를 거치지 않는 정적 리소스 서빙). 반면 Thymeleaf 의존성이 있는 상태에서 @Controller가 문자열을 반환하면, ViewResolver는 templates 폴더에서 그 이름의 뷰를 찾는다. 그래서 Controller에서 static 폴더의 파일명을 그냥 반환해도 찾지 못한다.

Controller를 거쳐서 static 폴더의 HTML에 접근하고 싶다면 redirect:를 쓰면 된다.

@GetMapping("/html/redirect")
public String htmlStatic(){
    return "redirect:/hello.html";
}

redirect:는 브라우저에게 "이 주소로 다시 요청해라"라고 응답하는 것이어서, 결국 브라우저가 /hello.html을 다시 직접 요청하게 되고 그건 정적 리소스 경로이므로 정상적으로 찾아진다.

templates 폴더의 HTML은 확장자 없이 이름만 반환하면 된다.

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

동적 HTML 생성 과정: 반환할 데이터를 Model에 담는다 → Model이 데이터를 사용할 View 이름과 함께 전달된다 → ViewResolver가 View에 Model 데이터를 적용해서 완성된 페이지를 클라이언트에 반환한다.

정적 리소스는 어차피 자주 안 바뀌는 파일들이라, 실무에서는 여기에 Cache-Control이나 ETag 같은 HTTP 캐시 헤더를 길게 걸어두고 브라우저가 재요청 없이 캐시에서 바로 쓰게 만드는 경우가 많다. 이 부분은 5번에서 다룬 HTTP 캐시가능성(Cacheable) 개념과 그대로 이어진다.


14. 데이터를 Client에 반환하는 방법 (JSON)

Spring 서버의 역할은 HTML/CSS/JS를 반환하는 것만이 아니라, DB에서 조회한 결과값을 사용자에게 돌려주는 것도 포함한다. 프론트엔드와 백엔드가 각자 발전하면서 서버가 HTML을 통째로 만들어 주기보다 필요한 데이터만 JSON으로 반환하는 느슨한 결합을 선호하는 흐름이 됐다. 보통 초기 페이지는 HTML로 받고, 이후 AJAX로 비동기 요청을 보내면 그 응답은 JSON으로 받는 구조다.

문제는 서버(자바) 입장에선 JSON이라는 타입이 따로 없다는 점이다.

@Controller
@RequestMapping("/response")
public class ResponseController {
    // Content-Type: text/html
    @GetMapping("/json/string")
    @ResponseBody
    public String helloStringJson() {
        return "{\"name\":\"Robbie\",\"age\":95}";
    }

    // Content-Type: application/json
    @GetMapping("/json/class")
    @ResponseBody
    public Star helloClassJson() {
        return new Star("Robbie", 95);
    }
}
  • String을 반환하면 겉보기엔 JSON처럼 생긴 문자열일 뿐이고, Content-Type은 text/html로 잡힌다.
  • 객체를 반환하면 Spring이 내부적으로 자바 객체를 JSON으로 변환해서 Content-Type: application/json으로 응답한다.

@ResponseBody를 안 붙이면 반환값을 뷰 이름으로 해석해서 그 이름의 HTML을 찾으려 하기 때문에, "이건 뷰가 아니라 데이터"라는 걸 알려주기 위해 붙여야 한다. @RestController는 @Controller + @ResponseBody를 합쳐놓은 것이라, 모든 메서드에 매번 @ResponseBody를 붙일 필요가 없다.

이 객체 → JSON 변환은 마법처럼 일어나는 게 아니라, Spring이 내부적으로 HttpMessageConverter(기본 구현체는 Jackson 라이브러리 기반의 MappingJackson2HttpMessageConverter)를 거쳐서 처리하는 것이다. 반환 타입이 String이면 문자열 컨버터가, 객체면 Jackson 컨버터가 선택되는 식이라 Content-Type이 달라지는 것도 이 때문이다. 그리고 응답 상태 코드나 헤더까지 직접 제어하고 싶으면 객체를 그냥 반환하는 대신 ResponseEntity<T>로 감싸서 반환하면 ResponseEntity.status(201).body(star)처럼 상태 코드와 본문을 한 번에 지정할 수 있다.


15. API 명세서와 실습

API 명세서는 API를 만든 사람이 사용할 사람에게 알려주는 일종의 사용설명서다. 프론트/백엔드 동시 개발을 가능하게 하고, 유지보수도 쉽게 만들어준다.

핵심 요소

  • API 개요: 만든 목적
  • Endpoint: 기능별 URL
  • Method: 어떤 작업을 할지
  • Parameter: 요청 시 필요한 추가 정보
  • Request Body: 생성/수정할 데이터 본문
  • Response: 처리 후 서버가 돌려주는 응답
  • Status Codes: 성공/실패를 나타내는 코드

이 개념으로 영화 추천 API를 만들어보는 실습을 했다. 명세서를 먼저 작성하고 코드를 작성하는 순서였고, ObjectMapper로 Map ↔ JSON 데이터 형태를 오갔다. 응답 시 response.setContentType("application/json; charset=UTF-8")로 응답 속성을 JSON으로 지정했고, PostMan으로 직접 GET 요청을 보내서 결과를 확인했다.

요즘은 이런 API 명세서를 손으로 문서화하는 대신, 코드에 어노테이션을 달아두면 명세서를 자동 생성해주는 Swagger(OpenAPI) 같은 도구를 많이 쓴다. 코드와 문서가 따로 노는 걸 막을 수 있어서, 나중에 API가 많아지면 한 번쯤 도입해볼 만하다.


정리하면, 백엔드는 결국 "클라이언트의 HTTP 요청을 받아서, 서버가 알맞은 처리를 하고, 그 결과를 어떤 형태(HTML이든 JSON이든)로 응답하느냐"의 문제였다. Servlet이 그 처리를 직접 다 떠안다가 한계에 부딪혔고, MVC로 역할을 쪼갠 다음, 프론트 컨트롤러 패턴 + 어댑터 패턴으로 다양한 컨트롤러를 하나의 진입점(DispatcherServlet)에서 통일해서 다루게 된 흐름으로 이해하니 훨씬 정리가 잘 됐다.

profile
게임개발자 취준생입니다

0개의 댓글