백엔드 개발을 하다 보면 Spring Boot 코드만 작성해서는 전체 웹 서비스의 흐름을 이해하기 어렵다.
개발을 하면서 다음과 같은 용어들을 계속 만나게 된다.
각각 따로 보면 어려워 보이지만 결국 대부분 다음 흐름 안에서 연결된다.
사용자
↓
Frontend
↓
HTTP Request
↓
Backend API
↓
Database / Redis / 외부 API
↓
HTTP Response
↓
Frontend
↓
사용자 화면
이번 글에서는 이 전체 흐름을 기준으로 하나씩 정리해보자.
HTML은 웹페이지의 구조를 표현하는 마크업 언어다.
HTML은 HyperText Markup Language의 약자다.
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>안녕하세요</h1>
<p>웹 페이지입니다.</p>
<button>로그인</button>
</body>
</html>
브라우저는 HTML을 읽고 화면의 구조를 만든다.
HTML
↓
Browser
↓
화면
HTML 자체는 일반적인 의미의 프로그래밍 언어라기보다는 문서의 구조를 표현하는 언어다.
<h1>제목</h1>
<p>본문</p>
<button>버튼</button>
<img src="cat.png">
HTML이 웹페이지의 구조를 담당한다면 CSS는 디자인과 스타일을 담당한다.
<button class="login-button">
로그인
</button>
.login-button {
background-color: blue;
color: white;
padding: 10px;
}
정리하면 다음과 같다.
HTML
→ 구조
CSS
→ 디자인
JavaScript
→ 동작
JavaScript는 웹 브라우저에서 동작하는 대표적인 프로그래밍 언어다.
예를 들어 로그인 버튼을 눌렀을 때 백엔드 API를 호출할 수 있다.
async function login() {
const response = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email: "test@test.com",
password: "1234"
})
});
const data = await response.json();
console.log(data);
}
즉 JavaScript는 단순히 화면을 움직이는 것뿐만 아니라 백엔드 서버와 통신하는 역할도 한다.
XML은 데이터를 구조적으로 표현하기 위한 마크업 언어다.
XML은 Extensible Markup Language의 약자다.
예를 들어 사용자 데이터를 표현하면 다음과 같다.
<user>
<id>1</id>
<name>Jun</name>
<age>24</age>
</user>
HTML과 모양은 비슷하지만 목적이 다르다.
HTML
→ 웹 문서의 구조 표현
XML
→ 데이터의 구조 표현
XML에서는 태그를 직접 정의할 수 있다.
<company>
<name>ABC</name>
<employees>
<employee>
<name>Kim</name>
</employee>
<employee>
<name>Lee</name>
</employee>
</employees>
</company>
과거에는 서버 간 데이터 교환에 XML이 많이 사용되었고, 지금도 SOAP이나 일부 공공기관 API, 레거시 시스템 등에서 사용된다.
JSON은 현재 웹 API에서 가장 흔하게 볼 수 있는 데이터 표현 형식 중 하나다.
JSON은 JavaScript Object Notation의 약자다.
{
"id": 1,
"name": "Jun",
"age": 24
}
배열도 표현할 수 있다.
{
"users": [
{
"id": 1,
"name": "Kim"
},
{
"id": 2,
"name": "Lee"
}
]
}
같은 데이터를 XML과 JSON으로 표현해보자.
<user>
<id>1</id>
<name>Jun</name>
<age>24</age>
</user>
{
"id": 1,
"name": "Jun",
"age": 24
}
JSON은 비교적 간결하고 JavaScript 객체와도 자연스럽게 연결되기 때문에 현대적인 웹 API에서 매우 많이 사용된다.
하지만 XML이 사라진 것은 아니다.
JSON
→ REST API
→ 웹/모바일 서비스
→ 프론트엔드 ↔ 백엔드 통신
XML
→ SOAP
→ 일부 공공 API
→ 레거시 기업 시스템
→ 설정 파일 등
JSON이라는 데이터가 인터넷을 혼자 돌아다니는 것은 아니다.
대부분 HTTP Request 또는 HTTP Response 안에 담겨서 이동한다.
예를 들어:
POST /api/users HTTP/1.1
Content-Type: application/json
{
"name": "Jun",
"age": 24
}
여기서 JSON은 HTTP의 Body에 들어있다.
전체적인 HTTP Request를 단순화하면 다음과 같다.
HTTP Request
┌─────────────────────────────┐
│ Method + URL │
│ POST /api/users │
├─────────────────────────────┤
│ Header │
│ Content-Type: application/json
│ Authorization: Bearer ... │
├─────────────────────────────┤
│ Body │
│ │
│ { │
│ "name": "Jun", │
│ "age": 24 │
│ } │
└─────────────────────────────┘
Spring Boot에서는 다음과 같이 받을 수 있다.
@PostMapping("/api/users")
public void createUser(
@RequestBody CreateUserRequest request
) {
}
백엔드 개발자라면 반드시 알아야 한다.
대표적으로 데이터를 전달할 수 있는 위치는 다음과 같다.
HTTP Request
Method
URL
├── Path
├── Path Variable
└── Query Parameter
Headers
Cookies
Body
서버에 어떤 행동을 요청하는지를 표현한다.
대표적인 Method는 다음과 같다.
GET
→ 데이터 조회
POST
→ 데이터 생성
PUT
→ 리소스 전체 수정/교체
PATCH
→ 리소스 일부 수정
DELETE
→ 데이터 삭제
예를 들어:
GET /users/1
은 사용자를 조회하는 요청이고,
DELETE /users/1
은 사용자를 삭제하는 요청으로 설계할 수 있다.
HTTP Method 자체가 모든 의미를 강제하는 것은 아니지만 REST API에서는 이러한 의미를 살려 설계하는 것이 일반적이다.
특정 리소스를 식별할 때 많이 사용한다.
GET /users/123
여기서:
123
이 Path Variable이다.
Spring Boot에서는:
@GetMapping("/users/{id}")
public User getUser(
@PathVariable Long id
) {
...
}
처럼 받을 수 있다.
예를 들어:
/users/1
/users/2
/users/3
처럼 각각 다른 사용자를 표현할 수 있다.
검색, 필터링, 정렬, 페이지네이션 등에 많이 사용한다.
GET /users?page=2&size=20
여기서:
page=2
size=20
이 Query Parameter다.
Spring에서는 다음과 같이 받을 수 있다.
@GetMapping("/users")
public List<User> getUsers(
@RequestParam int page,
@RequestParam int size
) {
...
}
예를 들어 상품 검색이라면:
GET /products?category=computer&minPrice=100000&maxPrice=2000000
처럼 사용할 수 있다.
Header에는 요청이나 응답에 대한 부가 정보가 들어간다.
예를 들어:
Content-Type: application/json
Authorization: Bearer eyJ...
Accept: application/json
각각 다음과 같은 의미를 가질 수 있다.
Content-Type
→ 내가 보내는 Body가 어떤 형식인가?
Accept
→ 나는 어떤 형식의 Response를 받고 싶은가?
Authorization
→ 인증 정보
Spring에서는 다음과 같이 읽을 수도 있다.
@RequestHeader("Authorization")
String authorization
서버에 전달할 실제 데이터를 담는 공간이다.
예를 들어 회원가입 요청이라면:
POST /users
Content-Type: application/json
{
"email": "test@test.com",
"password": "1234",
"name": "Jun"
}
JSON 데이터가 Body에 들어간다.
Spring Boot에서는:
@PostMapping("/users")
public void createUser(
@RequestBody CreateUserRequest request
) {
}
처럼 받을 수 있다.
다음 요청이 있다고 하자.
POST /users/123/orders?coupon=SUMMER
Authorization: Bearer abcdef
Content-Type: application/json
{
"productId": 100,
"quantity": 2
}
각 부분을 나누면:
POST
→ HTTP Method
/users/123/orders
→ Path
123
→ Path Variable
coupon=SUMMER
→ Query Parameter
Authorization
Content-Type
→ Header
{
"productId": 100,
"quantity": 2
}
→ Body
이 구조는 백엔드 개발을 하면서 정말 자주 만나게 된다.
API는 Application Programming Interface의 약자다.
쉽게 말하면 프로그램과 프로그램이 서로 기능이나 데이터를 주고받기 위한 약속된 인터페이스라고 볼 수 있다.
예를 들어 프론트엔드가 백엔드에게:
"사용자 123의 정보를 줘"
라고 요청할 수 있도록 백엔드가 다음 API를 제공할 수 있다.
GET /api/users/123
Backend가 다음 JSON을 반환한다.
{
"id": 123,
"name": "Jun",
"email": "test@test.com"
}
즉:
Frontend
↓
API 호출
↓
Backend
↓
JSON Response
↓
Frontend
가 된다.
REST는 Representational State Transfer의 약자다.
REST는 특정 라이브러리나 프로그램이 아니라 웹 API를 설계하기 위한 아키텍처 스타일이다.
핵심적인 생각 중 하나는 서버의 데이터를 Resource로 보는 것이다.
예를 들어:
사용자
→ /users
상품
→ /products
주문
→ /orders
그리고 HTTP Method를 이용해서 행동을 표현한다.
GET /users/1
1번 사용자 조회
POST /users
사용자 생성
PATCH /users/1
1번 사용자 일부 수정
DELETE /users/1
1번 사용자 삭제
REST의 원칙을 고려하여 설계한 HTTP API를 일반적으로 REST API라고 부른다.
예를 들어 다음과 같이 설계할 수 있다.
GET /users
GET /users/{id}
POST /users
PATCH /users/{id}
DELETE /users/{id}
반대로 다음과 같이 모든 행동을 URL에 직접 넣는 방식은 REST스럽지 않은 설계의 예로 자주 언급된다.
/getUser
/createUser
/deleteUser
/updateUser
REST 스타일에서는:
Resource
+
HTTP Method
를 이용해서 의미를 표현하려고 한다.
아니다.
REST
→ API를 어떻게 설계할 것인가에 대한 스타일
JSON
→ 데이터를 어떤 형식으로 표현할 것인가
REST API가 JSON을 많이 사용하기 때문에 둘을 같이 보는 경우가 많을 뿐이다.
예를 들어:
GET /users/1
이라는 REST API가 다음 JSON을 반환할 수 있다.
{
"id": 1,
"name": "Jun"
}
외부 API를 사용하다 보면 다음과 같은 값을 받는 경우가 있다.
API_KEY=abc123xyz
API Key는 보통:
"이 API를 호출하는 클라이언트가 누구인지 식별하거나 사용 권한/사용량을 관리하기 위한 키"
로 사용된다.
예를 들어 날씨 API를 호출한다고 하자.
내 Backend
↓
Weather API
Weather API 회사는 아무나 무제한으로 API를 호출하게 하고 싶지 않을 수 있다.
그래서 사용자에게 API Key를 발급한다.
회원가입
↓
API Key 발급
↓
abc123xyz
그리고 요청할 때 API Key를 같이 보낸다.
API마다 다르다.
대표적으로 Header 또는 Query Parameter를 사용한다.
GET /weather
X-API-Key: abc123xyz
또는 서비스에 따라:
Authorization: Bearer abc123xyz
같은 형태를 사용할 수도 있다.
일부 API는 다음처럼 요구한다.
GET /weather?apiKey=abc123xyz
어디에 넣는지는 API 제공자의 문서를 따라야 한다.
보안상 API Key를 URL에 넣으면 로그나 브라우저 기록 등 여러 위치에 노출될 가능성이 있기 때문에 가능하다면 Header 방식이 선호되는 경우가 많다.
보통 비밀로 유지해야 하는 API Key는 Frontend에 넣으면 안 된다.
예를 들어:
const API_KEY = "SUPER_SECRET_KEY";
를 브라우저 JavaScript에 넣으면 사용자가 개발자 도구 등을 통해 확인할 수 있다.
따라서 일반적으로:
Frontend
↓
내 Backend
↓
Secret API Key 사용
↓
외부 API
구조를 사용한다.
Browser
→ Secret 저장 X
Backend Environment Variable
→ Secret 저장
예를 들어:
OPENAI_API_KEY=...
WEATHER_API_KEY=...
같은 값은 서버의 환경변수나 Secret 관리 시스템에서 관리한다.
다르다.
API Key는 주로:
"이 API를 호출하는 애플리케이션/클라이언트는 누구인가?"
를 식별하거나 접근 권한과 사용량을 관리하는 데 사용된다.
JWT는 웹 서비스 로그인에서 다음처럼 많이 사용된다.
사용자 로그인
↓
Backend 인증
↓
Access Token 발급
↓
Frontend가 이후 요청에 Token 포함
예:
Authorization: Bearer eyJhbGciOi...
즉 사용 사례에 따라 다르지만 단순화하면:
API Key
→ API 소비자/애플리케이션 인증에 자주 사용
JWT
→ 로그인한 사용자 인증/인가에 자주 사용
이라고 생각할 수 있다.
Frontend는 사용자가 직접 보는 화면과 사용자 상호작용을 담당하는 영역이다.
사용자
↓
Frontend
↓
Backend
↓
Database
예를 들어:
로그인 화면
회원가입 화면
상품 목록
검색창
버튼
그래프
대시보드
등을 구현한다.
웹 Frontend의 기본은 다음 세 가지다.
HTML
CSS
JavaScript
역할은 다음과 같다.
HTML
→ 구조
CSS
→ 디자인
JavaScript
→ 동작
그런데 실제 프로젝트에서는 JavaScript만 직접 사용하는 것보다 여러 프레임워크와 라이브러리를 사용한다.
대표적으로:
React
Vue
Angular
Svelte
등이 있다.
TypeScript는 JavaScript에 정적 타입 시스템을 추가한 언어라고 볼 수 있다.
JavaScript:
function add(a, b) {
return a + b;
}
TypeScript:
function add(a: number, b: number): number {
return a + b;
}
TypeScript에서는:
add("hello", 10);
처럼 타입이 맞지 않는 코드를 개발 단계에서 발견하기 쉬워진다.
큰 프로젝트에서는 TypeScript를 사용하는 경우가 많다.
React는 UI를 Component 단위로 구성할 수 있도록 도와주는 JavaScript 라이브러리다.
예를 들어:
function LoginButton() {
return (
<button>
로그인
</button>
);
}
다음과 같이 화면을 여러 Component로 나눌 수 있다.
App
├── Header
├── Sidebar
├── UserProfile
├── ProductList
│ ├── ProductCard
│ ├── ProductCard
│ └── ProductCard
└── Footer
컴포넌트를 조합해서 복잡한 UI를 구성한다.
Next.js는 React를 기반으로 웹 애플리케이션을 만들기 위한 프레임워크다.
React만 사용하면 프로젝트 구조, Routing, Rendering 등의 여러 부분을 직접 구성해야 할 수 있다.
Next.js는 이런 기능들을 통합적으로 제공한다.
대표적으로:
Routing
Server Rendering
Static Generation
API 관련 기능
이미지 최적화
빌드 최적화
React Server Components 관련 기능
등을 제공한다.
단순하게 보면:
JavaScript / TypeScript
↓
React
↓
Next.js
정도의 관계로 이해할 수 있다.
단, React는 라이브러리이고 Next.js는 React 기반 프레임워크라는 차이가 있다.
백엔드 개발자가 전문 프론트엔드 개발자 수준의 CSS나 UI 디자인 능력을 반드시 가질 필요는 없다.
하지만 Frontend와 Backend가 어떻게 통신하는지는 반드시 이해하는 것이 좋다.
최소한 다음 내용은 알아두는 것이 좋다.
HTML 기본 구조
JavaScript / TypeScript 기본
React Component 개념
HTTP Request / Response
fetch / axios
JSON
Cookie
Local Storage
CORS
JWT
Authorization Header
Form Data
파일 업로드
HTTP Status Code
CSR / SSR 개념
브라우저 개발자 도구 Network 탭
특히 백엔드 개발자에게 중요한 것은:
"Frontend가 내가 만든 API를 실제로 어떻게 호출하는가?"
를 이해하는 것이다.
예를 들어 Backend가 다음 API를 제공한다고 하자.
GET /api/users/1
Frontend에서는 fetch를 사용할 수 있다.
const response = await fetch(
"http://localhost:8080/api/users/1"
);
const user = await response.json();
console.log(user);
Backend:
@GetMapping("/api/users/{id}")
public UserResponse getUser(
@PathVariable Long id
) {
return userService.getUser(id);
}
흐름은:
React / Next.js
↓
fetch()
↓
HTTP Request
↓
Spring Boot
↓
JSON Response
↓
response.json()
↓
Frontend 화면에 표시
가 된다.
Frontend와 Backend를 분리하면 자주 만나는 것이 CORS다.
예를 들어:
Frontend
http://localhost:3000
Backend
http://localhost:8080
처럼 서로 다른 Origin에서 동작할 수 있다.
브라우저에는 보안상의 이유로 Cross-Origin 요청에 대한 정책이 존재한다.
서버가 적절한 CORS 응답 헤더를 제공하지 않으면 브라우저가 Frontend JavaScript에서 응답을 사용하지 못하게 할 수 있다.
Spring에서는 예를 들어 CORS 설정을 추가할 수 있다.
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(
CorsRegistry registry
) {
registry
.addMapping("/**")
.allowedOrigins(
"http://localhost:3000"
);
}
};
}
}
실제 서비스에서는 허용 Origin, Method, Header, Credential 정책 등을 보안 요구사항에 맞게 설정해야 한다.
Polling은 클라이언트가 서버에게 주기적으로 새로운 정보가 있는지 물어보는 방식이다.
예를 들어 주문 상태를 확인한다고 하자.
Frontend
↓
"주문 완료됐어?"
5초 후
↓
"주문 완료됐어?"
5초 후
↓
"주문 완료됐어?"
5초 후
↓
"주문 완료됐어?"
코드로 단순하게 표현하면:
setInterval(async () => {
const response = await fetch(
"/api/orders/123"
);
const order = await response.json();
console.log(order.status);
}, 5000);
이게 Polling이다.
Polling은 구현하기 쉽다.
장점
구현 간단
HTTP만 사용 가능
서버 구조 단순
하지만 단점도 있다.
단점
변화가 없어도 계속 Request 발생
서버 부하 증가 가능
실시간성이 Polling 주기에 의존
예를 들어 5초마다 Polling하면 서버에서 상태가 변경된 직후라도 클라이언트는 최대 약 5초 후에야 확인할 수 있다.
Webhook은 Polling과 방향이 반대다.
Polling:
내 서버
→ "바뀌었어?"
→ 외부 서버
내 서버
→ "바뀌었어?"
→ 외부 서버
Webhook:
내 서버
← "바뀌었어!"
← 외부 서버
즉 이벤트가 발생하면 상대 서버가 내 서버의 URL을 호출해주는 방식이다.
결제 시스템을 생각해보자.
사용자가 결제를 시작했다.
사용자
↓
내 Backend
↓
결제 서비스
결제가 바로 최종 확정되지 않을 수도 있다.
Polling을 사용하면:
내 Backend
→ 결제됐나요?
내 Backend
→ 결제됐나요?
내 Backend
→ 결제됐나요?
라고 계속 확인해야 한다.
Webhook을 사용하면 결제 서비스가:
결제 완료
↓
POST https://my-service.com/webhooks/payment
처럼 내 서버를 호출한다.
Payload 예:
{
"event": "PAYMENT_COMPLETED",
"paymentId": "pay_123",
"amount": 10000
}
내 Backend에서는:
@PostMapping("/webhooks/payment")
public ResponseEntity<Void> paymentWebhook(
@RequestBody PaymentWebhookRequest request
) {
paymentService.handle(request);
return ResponseEntity.ok().build();
}
처럼 처리할 수 있다.
Polling
Client
↓
Server
"변경됐어?"
"변경됐어?"
"변경됐어?"
반면:
Webhook
이벤트 발생
↓
Server
↓
Client가 등록한 URL 호출
비교하면 다음과 같다.
| 구분 | Polling | Webhook |
|---|---|---|
| 요청 시작 | 정보를 원하는 쪽 | 이벤트가 발생한 쪽 |
| 방식 | 주기적으로 확인 | 이벤트 발생 시 호출 |
| 구현 | 비교적 단순 | Endpoint 및 검증 필요 |
| 불필요한 요청 | 발생 가능 | 상대적으로 적음 |
| 대표 사례 | 상태 조회 | 결제 완료 알림 |
맞다.
Webhook도 결국 HTTP API를 이용하는 경우가 많다.
차이는 누가 먼저 요청하느냐에 있다.
일반 API:
내 서버
↓ Request
외부 서버
↓ Response
내 서버
Webhook:
외부 서버
↓ Request
내 서버의 Webhook Endpoint
즉 Webhook은 새로운 인터넷 프로토콜이 아니라 HTTP API를 이벤트 알림 방식으로 사용하는 패턴이라고 이해하면 쉽다.
아무나 다음 Endpoint를 호출할 수 있으면 위험하다.
POST /webhooks/payment
공격자가:
{
"event": "PAYMENT_COMPLETED",
"amount": 1000000
}
을 마음대로 보내면 안 된다.
그래서 Webhook 제공 서비스들은 흔히 Signature 검증 방식을 제공한다.
개념적으로:
Webhook Body
+
Secret
↓
Signature 생성
외부 서비스
↓
Webhook Request
Body
+
Signature Header
↓
내 Backend
↓
Signature 검증
이런 방식이다.
구체적인 검증 방법은 Webhook 제공자의 문서를 따라야 한다.
Polling과 Webhook을 공부하다 보면 WebSocket도 자주 등장한다.
일반 HTTP는 기본적으로:
Client
→ Request
→ Server
Server
→ Response
→ Client
형태다.
WebSocket은 연결을 유지하면서 양방향으로 데이터를 주고받을 수 있다.
Client
⇄
Server
그래서 다음과 같은 기능에 많이 사용된다.
실시간 채팅
실시간 게임
실시간 알림
실시간 가격
실시간 대시보드
Polling
→ 내가 계속 물어봄
Webhook
→ 이벤트가 발생한 서버가 내 서버를 호출
WebSocket
→ 연결을 유지하고 양쪽이 실시간 통신
예를 들어:
주문 상태를 10초마다 확인
→ Polling
결제 업체가 내 Backend에게 결제 완료 통보
→ Webhook
채팅 메시지를 실시간 송수신
→ WebSocket
Backend는 사용자에게 직접 보이지 않는 서버 측 로직을 담당한다.
대표적인 역할은 다음과 같다.
API 제공
회원가입 / 로그인
비즈니스 로직
Database 접근
Transaction 처리
인증 / 인가
외부 API 호출
파일 처리
메시지 큐
캐시
결제
AI 서비스 연결
대표적인 Backend 기술로는:
Java
→ Spring Boot
Python
→ FastAPI / Django / Flask
JavaScript / TypeScript
→ Node.js / NestJS / Express
C#
→ ASP.NET Core
Go
→ Gin / Echo 등
Kotlin
→ Spring Boot / Ktor
등이 있다.
Spring Boot는 Java 기반 Backend 애플리케이션을 개발할 때 많이 사용하는 프레임워크다.
예를 들어 REST API를 다음과 같이 만들 수 있다.
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public UserResponse getUser(
@PathVariable Long id
) {
return new UserResponse(
id,
"Jun"
);
}
}
요청:
GET /users/1
응답:
{
"id": 1,
"name": "Jun"
}
FastAPI는 Python으로 API 서버를 만들기 위한 웹 프레임워크다.
Spring Boot가 Java 생태계라면 FastAPI는 Python 생태계에서 API 서버를 빠르게 만들 때 많이 사용된다.
예를 들어:
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {
"id": user_id,
"name": "Jun"
}
요청:
GET /users/1
응답:
{
"id": 1,
"name": "Jun"
}
Spring Boot와 결국 같은 HTTP API를 제공할 수 있다.
둘 다 Backend API 서버를 만들 수 있다.
Frontend
↓
HTTP
↓
Spring Boot
또는
FastAPI
↓
Database / Redis / AI / 외부 API
차이는 주로 언어와 생태계다.
Spring Boot
→ Java / Kotlin
→ 대규모 Backend 서비스
→ Spring 생태계
→ JPA
→ Spring Security 등
FastAPI
→ Python
→ 간결한 API 개발
→ Python 생태계
→ AI / ML 라이브러리와 연결하기 편리
→ Pydantic 기반 데이터 검증
Python에는 AI/ML 관련 생태계가 매우 크다.
예를 들어:
PyTorch
Transformers
scikit-learn
pandas
NumPy
LangChain
LangGraph
등이 있다.
따라서 다음 구조를 사용할 수 있다.
Frontend
↓
Spring Boot
↓
FastAPI AI Service
↓
LLM / Embedding / ML Model
Spring Boot는:
사용자
인증
인가
결제
DB
비즈니스 로직
등을 담당하고,
FastAPI는:
LLM
RAG
Embedding
AI Agent
ML Model
데이터 분석
등을 담당하도록 분리할 수 있다.
물론 반드시 이렇게 나눠야 하는 것은 아니다. 프로젝트의 규모와 요구사항에 따라 하나의 Backend로 구성할 수도 있다.
결국 HTTP API를 사용하면 된다.
예를 들어 FastAPI:
@app.post("/analyze")
def analyze(request: AnalyzeRequest):
return {
"result": "analysis result"
}
Spring Boot:
POST http://ai-service:8000/analyze
Body:
{
"text": "분석할 데이터"
}
전체 흐름:
Frontend
↓
Spring Boot
↓ HTTP + JSON
FastAPI
↓
AI Model
↓
FastAPI
↓ JSON
Spring Boot
↓
Frontend
즉 Java 서버와 Python 서버라고 해서 특별한 방식이 필요한 것이 아니다.
HTTP라는 공통 규칙을 사용하면 서로 다른 언어로 만든 프로그램끼리도 통신할 수 있다.
예를 들어:
Frontend
TypeScript
Backend
Java
AI Service
Python
이라고 해보자.
언어가 전부 다르다.
하지만:
TypeScript
↓
HTTP + JSON
↓
Java
↓
HTTP + JSON
↓
Python
으로 통신한다.
각 프로그램은 상대방 내부 코드가 Java인지 Python인지 몰라도 된다.
중요한 것은 API 계약이다.
예를 들어:
POST /analyze
Content-Type: application/json
{
"text": "hello"
}
를 보내면:
{
"result": "..."
}
를 반환한다는 약속만 맞으면 된다.
이것이 API가 중요한 이유다.
그렇다면 Java 객체가 어떻게 JSON이 될까?
Spring Boot를 예로 들어보자.
public record UserResponse(
Long id,
String name
) {}
Controller가:
return new UserResponse(
1L,
"Jun"
);
을 반환하면 Spring이 JSON으로 변환할 수 있다.
{
"id": 1,
"name": "Jun"
}
이처럼:
Java Object
↓
JSON
으로 변환하는 것을 Serialization(직렬화)이라고 한다.
반대로:
JSON
↓
Java Object
으로 변환하는 것은 Deserialization(역직렬화)이라고 한다.
HTTP Body만 보면 서버는 이것이 JSON인지 XML인지 다른 형식인지 알아야 한다.
그래서 Header에:
Content-Type: application/json
을 넣는다.
즉:
Content-Type
"내가 보내는 Body는
이 형식으로 되어 있습니다."
라는 의미다.
XML이라면:
Content-Type: application/xml
같은 형태를 사용할 수 있다.
파일 업로드에서는:
Content-Type: multipart/form-data
를 자주 보게 된다.
이미지 파일을 업로드한다고 생각해보자.
JSON Body에 일반 문자열처럼 넣는 것보다 multipart/form-data를 사용하는 경우가 많다.
Frontend:
const formData = new FormData();
formData.append("image", file);
await fetch("/api/images", {
method: "POST",
body: formData
});
Spring Boot:
@PostMapping("/api/images")
public void upload(
@RequestParam MultipartFile image
) {
}
즉 HTTP Body라고 해서 항상 JSON만 들어가는 것은 아니다.
Body
JSON
XML
Form Data
Binary Data
Text
...
여러 형태가 가능하다.
Cookie는 브라우저가 저장하고 HTTP 요청에 함께 보낼 수 있는 작은 데이터다.
서버가:
Set-Cookie: sessionId=abc123
을 반환하면 브라우저가 저장할 수 있다.
이후 요청에서:
Cookie: sessionId=abc123
처럼 보낼 수 있다.
그래서 Session 기반 인증 등에 사용할 수 있다.
Local Storage는 브라우저 내부에 데이터를 저장할 수 있는 기능이다.
localStorage.setItem(
"theme",
"dark"
);
읽을 때:
const theme =
localStorage.getItem("theme");
Cookie와 Local Storage는 둘 다 브라우저 저장소와 관련 있지만 동작 방식과 보안 특성이 다르다.
특히 인증 토큰을 어디에 저장할지는 XSS, CSRF, SameSite, HttpOnly 등의 보안 요소를 함께 고려해야 한다.
Backend는 Response Body만 보내는 것이 아니라 Status Code도 반환한다.
대표적으로:
200 OK
→ 성공
201 Created
→ 생성 성공
204 No Content
→ 성공했지만 Response Body 없음
400 Bad Request
→ 잘못된 요청
401 Unauthorized
→ 인증 필요/인증 실패
403 Forbidden
→ 인증은 되었지만 권한 부족
404 Not Found
→ 리소스를 찾을 수 없음
409 Conflict
→ 현재 상태와 충돌
500 Internal Server Error
→ 서버 내부 오류
예를 들어:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"message": "User not found"
}
처럼 반환할 수 있다.
HTTP 요청을 그대로 네트워크에 보내면 통신 내용을 보호하기 어렵다.
HTTPS는 HTTP 통신을 TLS를 통해 보호한다.
HTTP
+
TLS
=
HTTPS
대략적인 구조는:
Browser
↓
TLS로 보호된 통신
↓
Server
가 된다.
그래서 로그인 정보, Token, API 데이터 등을 인터넷을 통해 전달할 때 HTTPS가 매우 중요하다.
다음 URL을 보자.
https://api.example.com:443/users/123?active=true
각 부분은:
https
→ Protocol / Scheme
api.example.com
→ Host
443
→ Port
/users/123
→ Path
active=true
→ Query Parameter
로 볼 수 있다.
개발하다 보면 다음 주소를 많이 본다.
http://localhost:8080
localhost는 현재 자신의 컴퓨터를 의미한다.
localhost
→ 내 컴퓨터
8080은 Port다.
예를 들어:
localhost:3000
→ Next.js
localhost:8080
→ Spring Boot
localhost:8000
→ FastAPI
localhost:5432
→ PostgreSQL
처럼 하나의 컴퓨터에서 여러 프로그램을 Port로 구분하여 실행할 수 있다.
백엔드 개발자 입장에서 가장 중요한 것은 다음 흐름을 이해하는 것이다.
사용자가 버튼 클릭
↓
React Event Handler
↓
fetch / axios
↓
HTTP Request
↓
Spring Controller
↓
Service
↓
Repository
↓
Database
↓
Repository
↓
Service
↓
Controller
↓
JSON Response
↓
Frontend
↓
State 변경
↓
화면 다시 Rendering
이 흐름을 이해하면 Frontend와 Backend 사이에서 발생하는 문제를 훨씬 쉽게 찾을 수 있다.
백엔드 개발자도 브라우저 개발자 도구의 Network 탭을 볼 줄 알아야 한다.
여기서 다음 정보를 확인할 수 있다.
Request URL
Request Method
Status Code
Request Headers
Response Headers
Query Parameters
Request Payload
Response Body
Cookie
요청 시간
예를 들어 Frontend에서:
"백엔드가 이상해요."
라는 문제가 발생했을 때 Network 탭을 보면:
POST /api/login
Status: 401
Response:
{
"message": "Invalid token"
}
처럼 실제 요청과 응답을 확인할 수 있다.
백엔드 개발에서도 매우 유용하다.
지금까지 배운 내용을 로그인 하나에 연결해보자.
email
password
fetch("/api/login", {
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
email,
password
})
});
POST /api/login
Content-Type: application/json
{
"email": "test@test.com",
"password": "1234"
}
@PostMapping("/login")
public LoginResponse login(
@RequestBody LoginRequest request
) {
return authService.login(request);
}
User Repository
↓
Database
↓
사용자 확인
사용자 인증 성공
↓
Access Token 생성
{
"accessToken": "eyJ..."
}
GET /api/me
Authorization: Bearer eyJ...
Backend가 Token을 검증하고 사용자 정보를 반환한다.
이번에는 Backend가 외부 API를 호출한다고 해보자.
사용자
↓
Next.js
↓
POST /api/report
↓
Spring Boot
↓
외부 AI API
Spring Boot가 외부 API를 호출할 때:
URL
Header
Authorization: Bearer API_KEY
Header
Content-Type: application/json
Body
{
"input": "분석해주세요"
}
처럼 보낼 수 있다.
외부 API가:
{
"result": "분석 결과..."
}
를 반환한다.
그러면:
외부 API
↓
Spring Boot
↓
필요한 데이터 가공/저장
↓
Frontend
가 된다.
여기서 API Key는 Backend가 관리한다.
Frontend에 Secret API Key 노출 ❌
Backend
Environment Variable / Secret
에서 관리 ⭕
결제는 Webhook을 이해하기 좋은 예다.
Frontend
↓
Backend
↓
결제 서비스
사용자가 결제를 진행한다.
결제 서비스에서 결제 상태가 확정되면:
결제 서비스
"결제 성공!"
↓
POST
https://my-service.com/webhooks/payment
↓
내 Backend
Payload:
{
"event": "PAYMENT_COMPLETED",
"paymentId": "pay_123"
}
Backend:
@PostMapping("/webhooks/payment")
public ResponseEntity<Void> webhook(
@RequestBody PaymentWebhook request
) {
paymentService.complete(
request.paymentId()
);
return ResponseEntity.ok().build();
}
실제 구현에서는 Webhook Signature 검증, 중복 이벤트 처리, 멱등성 등을 함께 고려해야 한다.
Webhook은 같은 이벤트가 여러 번 전달될 수 있다.
예를 들어:
PAYMENT_COMPLETED
paymentId = 123
이라는 이벤트가 네트워크 문제로 두 번 전달될 수 있다.
Webhook 1
→ 결제 처리
Webhook 2
→ 또 결제 처리?
이렇게 되면 안 된다.
따라서:
eventId 또는 paymentId 확인
↓
이미 처리했는가?
↓ ↓
YES NO
↓ ↓
무시 처리
같은 구조가 필요하다.
이런 성질을 Idempotency, 멱등성이라고 한다.
외부 API가 응답하지 않는다고 해서 영원히 기다릴 수는 없다.
내 Backend
↓
외부 API
...
...
응답 없음
그래서 Timeout을 설정한다.
Connection Timeout
→ 연결을 얼마나 기다릴 것인가?
Read Timeout
→ 응답 데이터를 얼마나 기다릴 것인가?
외부 API를 사용하는 Backend에서는 매우 중요한 설정이다.
외부 API 호출이 일시적으로 실패했을 때 다시 요청하는 것이다.
API 요청
↓
실패
1초 후
↓
Retry
또 실패
2초 후
↓
Retry
하지만 모든 요청을 무조건 Retry하면 안 된다.
예를 들어 결제 생성 요청을 아무 생각 없이 재시도하면 중복 처리 문제가 발생할 수 있다.
따라서:
Timeout
Retry
Idempotency
를 함께 고려해야 한다.
처음 보는 API 문서를 보면 최소한 다음을 확인해야 한다.
Base URL
Endpoint
HTTP Method
Path Parameter
Query Parameter
Request Header
Authentication 방식
Request Body
Response Body
Status Code
Rate Limit
Timeout 관련 정보
Webhook 지원 여부
예를 들어:
POST /v1/payments
Authorization:
Bearer API_KEY
Content-Type:
application/json
Body:
{
"amount": 10000
}
를 보고:
Method = POST
API Key = Header
Data = JSON Body
라고 바로 파악할 수 있어야 한다.
Backend API가 많아지면 API 문서를 관리해야 한다.
OpenAPI는 HTTP API의 구조를 표현하기 위한 명세 형식이다.
예를 들어:
/users/{id}:
get:
parameters:
- name: id
in: path
required: true
처럼 API 구조를 표현할 수 있다.
Swagger UI 같은 도구를 이용하면 이를 사람이 보기 편한 API 문서 형태로 제공할 수 있다.
Spring Boot 프로젝트에서도 자주 볼 수 있다.
Backend API
↓
OpenAPI Specification
↓
Swagger UI
↓
API 문서
웹 개발에서 수많은 기술이 등장하지만 결국 가장 중요한 흐름은 다음이다.
Frontend
HTML
CSS
JavaScript / TypeScript
React / Next.js
↓
HTTP
Method
URL
Path Variable
Query Parameter
Header
Body
Cookie
↓
Data Format
JSON
XML
Form Data
↓
Backend
Spring Boot
FastAPI
Node.js
...
↓
Infrastructure
Database
Redis
Kafka
Object Storage
외부 API
AI Service
...
그리고 서버와 서버 사이도 본질적으로 비슷하다.
Spring Boot
↓
HTTP + JSON
↓
FastAPI
Spring Boot
↓
HTTP + JSON + API Key
↓
외부 API
결제 서비스
↓
HTTP Webhook
↓
Spring Boot
웹페이지의 구조
웹페이지의 디자인
웹페이지의 동작과 서버 통신
JavaScript + 정적 타입 시스템
구조화된 데이터 표현 형식
웹 API에서 많이 사용하는 데이터 표현 형식
웹에서 Request와 Response를 주고받기 위한 프로토콜
Resource 중심으로 HTTP API를 설계하는 아키텍처 스타일
API 호출자를 식별하거나
API 접근 권한을 확인하기 위한 키
클라이언트가 서버에
주기적으로 변경 여부를 질문
이벤트가 발생하면
상대 서버가 내 서버를 호출
연결을 유지하면서
양방향 실시간 통신
사용자가 직접 보는 화면과 상호작용
API, 비즈니스 로직, DB,
인증, 외부 서비스 연동 등을 담당
Java/Kotlin 기반 Backend 프레임워크
Python 기반 API 프레임워크
AI/ML 서비스와 함께 사용하기 편리
┌──────────────────────────────────┐
│ 사용자 │
└────────────────┬─────────────────┘
↓
┌──────────────────────────────────┐
│ Frontend │
│ │
│ HTML │
│ CSS │
│ JavaScript / TypeScript │
│ React / Next.js │
└────────────────┬─────────────────┘
│
│ HTTP Request
│
│ Method
│ URL
│ Header
│ Cookie
│ Body(JSON)
↓
┌──────────────────────────────────┐
│ Backend │
│ │
│ Spring Boot │
│ FastAPI │
│ Node.js │
└───────┬────────────┬─────────────┘
│ │
↓ ↓
Database External API
│
│ API Key
↓
외부 서비스
외부 서비스에서 이벤트 발생
│
│ Webhook
↓
Backend
Frontend가 상태를 계속 확인
│
│ Polling
↓
Backend
실시간 양방향 연결
│
│ WebSocket
↕
Backend
결국 웹 개발의 핵심은 서로 다른 프로그램들이 정해진 규칙으로 데이터를 주고받는 것이다.
Frontend가 TypeScript이고 Backend가 Java이며 AI 서버가 Python이어도 상관없다.
TypeScript
↓
HTTP + JSON
↓
Java
↓
HTTP + JSON
↓
Python
각 프로그램이 서로의 내부 구현을 알 필요 없이 HTTP, JSON, API라는 약속을 통해 통신한다.
이 큰 그림을 이해하고 나면 REST API, API Key, Webhook, FastAPI 같은 개념들이 각각 독립적인 기술이 아니라 하나의 웹 시스템을 구성하는 서로 다른 요소라는 것을 이해할 수 있다.