스레드끼리는 Code·Data·Heap을 공유하고 Stack·PC·레지스터는 각자 가집니다. 프로세스끼리는 메모리를 공유하지 않아 IPC(파이프, 소켓, 공유 메모리)가 필요합니다.
같은 프로세스의 스레드가 공유하는 영역과 각자의 스택
보완스레드 전환이 싼 이유는 같은 주소 공간을 쓰기 때문입니다. 프로세스 전환은 페이지 테이블 교체와 TLB 무효화가 따라와 비용이 큽니다. 반대로 프로세스는 하나가 죽어도 다른 프로세스에 영향이 없다는 격리 장점이 있습니다.
비유
집과 가족
프로세스는 집, 스레드는 그 집에 사는 가족이에요.
가족은 냉장고와 거실(Heap)을 같이 쓰지만, 각자 자기 일기장(Stack)이 있어요. 옆집 냉장고는 열 수 없어서 전화(IPC)로 물어봐야 해요.
가족 한 명이 이사 오는 건 새 집을 짓는 것보다 훨씬 쉽고 빨라요. 서버가 요청마다 집 대신 가족을 늘리는 이유예요.
집→프로세스
가족→스레드
같이 쓰는 냉장고→Heap
각자 일기장→Stack
옆집에 전화→IPC
예시
// 한 JVM 프로세스 안에서 두 스레드가 같은 Heap 객체(list)를 공유
List<String> list = Collections.synchronizedList(new ArrayList<>());
Thread a = new Thread(() -> list.add("A"));
Thread b = new Thread(() -> list.add("B"));
a.start(); b.start();
a.join(); b.join();
System.out.println(list); // [A, B] 또는 [B, A]
기술 도메인
Tomcat: 한 프로세스 + 요청당 스레드(thread-per-request)
Nginx: master 프로세스 + CPU 코어 수만큼의 worker 프로세스, 각 worker는 이벤트 루프
PostgreSQL: 연결마다 프로세스를 띄움 → 커넥션 풀(PgBouncer)이 중요
Chrome: 탭마다 프로세스를 분리해 한 탭이 죽어도 나머지는 살아 있음
테이블
비교·정리 표
용어
정의
프로그램
디스크에 저장된 실행 파일
프로세스
메모리에 올라가 실행 중인 프로그램. 자원 할당 단위
스레드
프로세스 안의 실행 흐름. CPU 스케줄링(실행) 단위
동시성 문제
이론
경쟁 상태(Race Condition): 실행 순서에 따라 결과가 달라지는 상황
임계 구역(Critical Section): 공유 자원에 접근하는 코드 영역
동기화: 임계 구역에 한 번에 한 스레드만 들어가게 통제 (synchronized, ReentrantLock)
보완동시성 문제는 원자성만이 아닙니다. 한 스레드가 바꾼 값이 다른 스레드에 보이지 않는 가시성 문제도 있고, 이는 volatile로 해결합니다. 단, volatile은 count++ 같은 복합 연산의 원자성은 보장하지 않습니다.
비유
칠판에 쿠키 개수 적기
쿠키 통 옆 칠판에 "100개"라고 적혀 있어요. 두 친구가 동시에 칠판을 보고 쿠키를 하나씩 넣은 뒤 둘 다 "101"이라고 고쳐 적었어요. 실제로는 102개인데 칠판에는 101개예요.
그래서 규칙을 정해요. "칠판 앞에는 열쇠를 가진 한 명만 설 수 있어요." 이 열쇠가 락이에요.
칠판→공유 변수
칠판 앞 자리→임계 구역
동시에 고쳐 쓰기→경쟁 상태
한 명만 쓰는 열쇠→synchronized, Lock
예시
count++는 읽기 → 더하기 → 쓰기 3단계입니다. 두 스레드가 동시에 100을 읽으면 둘 다 101을 써서 결과는 102가 아니라 101이 됩니다.
// ❌ 경쟁 상태
private int count = 0;
public void increase() { count++; }
// ✅ 방법 1: 락
public synchronized void increase() { count++; }
// ✅ 방법 2: CAS 기반 원자 연산 (락 없이)
private final AtomicInteger count = new AtomicInteger();
public void increase() { count.incrementAndGet(); }
기술 도메인
재고 차감·좌석 예매: DB 비관적 락(SELECT … FOR UPDATE), JPA 낙관적 락(@Version), Redis 분산 락(Redisson)
Spring 빈은 기본이 싱글톤이라 모든 요청 스레드가 같은 객체를 씁니다. 빈에 변경 가능한 필드를 두면 바로 경쟁 상태가 됩니다.
동기·비동기 / 블로킹·논블로킹
이론
서로 다른 두 축입니다.
동기/비동기: 작업 완료를 누가 신경 쓰는가. 호출한 쪽이 결과를 받아 이어서 처리하면 동기, 완료 처리를 콜백 등에 넘기면 비동기.
블로킹/논블로킹: 호출된 함수가 제어권을 언제 돌려주는가. 끝날 때까지 붙잡으면 블로킹, 바로 돌려주면 논블로킹.
동기 블로킹 서버에서 DB 조회가 200ms 걸리면, 그동안 스레드는 CPU를 거의 안 쓰면서도 점유됩니다. 동시 요청이 스레드 수를 넘으면 큐에서 대기하고, 큐도 차면 연결 거부나 타임아웃이 납니다.
보완Spring Boot 내장 Tomcat 기본값은 최대 스레드 200(server.tomcat.threads.max), 대기 큐 100(server.tomcat.accept-count)입니다. 이벤트 루프 방식은 OS의 I/O 멀티플렉싱(Linux epoll, macOS kqueue) 위에서 동작합니다.
비유
라면 끓이기
동기 + 블로킹: 물이 끓을 때까지 냄비만 쳐다봐요.
동기 + 논블로킹: 그림을 그리다가 계속 와서 "끓었나?" 확인해요.
비동기 + 논블로킹: 주전자 알람을 켜 두고 그림을 그려요. "삐~" 하면 그때 가요.
비동기 + 블로킹: 알람을 켜 놓고도 냄비 앞에 서서 기다려요.
선생님 200명이 모두 냄비만 쳐다보고 있으면, 새로 온 친구를 봐줄 선생님이 없어요. 이게 스레드 풀 고갈이에요.
선생님→스레드
물 끓이기→DB 조회, 외부 API
알람→콜백
예시
// 비동기 + 논블로킹: 두 외부 호출을 동시에 보내고, 둘 다 끝나면 합침
CompletableFuture<User> user = CompletableFuture.supplyAsync(() -> userApi.get(id));
CompletableFuture<Point> point = CompletableFuture.supplyAsync(() -> pointApi.get(id));
user.thenCombine(point, MyPage::new)
.thenAccept(page -> log.info("done: {}", page)); // 완료 시 콜백
// 비동기 + 블로킹 (안티패턴): 맡겨 놓고 바로 기다림
User u = user.get();
기술 도메인
동기가 충분: 일반 CRUD API (Spring MVC + JPA/JDBC). 코드가 읽기 쉽고 디버깅이 쉬움
비동기가 유리: 채팅·알림, API 게이트웨이(Spring Cloud Gateway는 Netty 기반), 외부 API를 많이 호출하는 서버 (WebFlux + R2DBC, Node.js)
비동기는 상위 호환이 아니라, I/O 대기가 길고 동시 연결이 많을 때 고르는 도구입니다.
테이블
비교·정리 표
조합
동작
예
동기 + 블로킹
끝날 때까지 기다림
JDBC 쿼리
동기 + 논블로킹
멈추지 않지만 완료 여부를 직접 반복 확인
논블로킹 소켓 + polling
비동기 + 블로킹
맡겨 놓고 결국 기다림 (대개 안티패턴)
Future.get()
비동기 + 논블로킹
맡기고 다른 일, 완료 시 콜백
Netty, WebFlux, Node.js
3. Java JVM · GC · THREAD
Java는 안정적인 멀티스레드, Spring 생태계, JVM 덕분에 서버 개발의 주력 언어입니다. 여기서는 문법보다 서버가 도는 환경으로서 봅니다.
JVM 실행 구조
이론
javac가 소스를 JVM 바이트코드(.class)로 컴파일
클래스 로더가 .class를 메모리에 적재
실행 엔진이 인터프리터로 실행하다가, 자주 쓰이는(hot) 코드는 JIT 컴파일러가 네이티브 코드로 바꿔 캐싱
그래서 Java 프로그램은 플랫폼 독립적이고, JVM은 플랫폼 종속적입니다. ("Write Once, Run Anywhere")
JVM: 바이트코드 실행
JRE: JVM + 표준 라이브러리 (실행 환경)
JDK: JRE + 개발 도구 (javac, jar, javap, jlink 등)
JDK · JRE · JVM의 관계
보완Oracle은 Java 11부터 JRE를 따로 배포하지 않습니다. 지금은 JDK를 설치하거나, jlink로 필요한 모듈만 담은 런타임을 만들어 씁니다.
비유
통역사와 그림책
Java 코드는 어느 나라에서나 읽을 수 있는 공통 그림책(바이트코드)으로 만들어요. 나라마다 있는 통역사(JVM)가 그 나라 말로 읽어 줘요.
통역사는 자주 읽는 페이지를 아예 외워 버려서 다음엔 더 빨리 읽어요(JIT).
공통 그림책→바이트코드(.class)
나라별 통역사→OS별 JVM
외워 버리기→JIT 컴파일
통역사 + 사전→JRE
그림책 만드는 도구 상자까지→JDK
예시
$ javac Main.java # Main.class (바이트코드) 생성
$ java Main # JVM이 Main.class 실행
$ javap -c Main # 바이트코드 확인: aload_0, invokevirtual ...
같은 Main.class를 macOS, Linux, Windows의 JVM에서 그대로 실행할 수 있습니다.
기술 도메인
Kotlin, Scala, Groovy도 JVM 바이트코드로 컴파일되어 Java 라이브러리를 그대로 씀
Spring Boot 앱은 실행 가능한 .jar 하나로 배포하고, Docker 이미지에 JDK/JRE 런타임을 함께 담음
GraalVM Native Image는 미리 네이티브로 컴파일해 시작 시간과 메모리를 줄임 (서버리스에 유리)
메모리와 GC
이론
JVM 런타임 데이터 영역:
JVM 메모리 영역
Heap 세대 구조: Young(Eden + Survivor 0/1)과 Old. "대부분의 객체는 금방 죽는다"는 약한 세대 가설에 따라 Young만 자주 청소합니다.
GC 대상 = GC Root(스택의 지역변수, static 변수, JNI 참조 등)에서 참조를 따라가 닿지 않는 객체
Minor GC: Eden이 차면 Young 영역 정리. 살아남은 객체는 Survivor로, 여러 번 살아남으면 Old로 승격(Promotion)
Major GC: Old 영역 정리 / Full GC: Heap 전체(+Metaspace) 정리
GC 중에는 애플리케이션 스레드가 멈추는 Stop-The-World가 발생 → 응답 지연의 원인
보완Java 8부터 클래스 메타데이터 영역이 PermGen에서 네이티브 메모리의 Metaspace로 바뀌었습니다. 승격 기준 나이는 -XX:MaxTenuringThreshold(기본·최대 15)로 정합니다. "Major GC"는 공식 용어가 아니어서 GC 구현마다 의미가 조금씩 다릅니다.
비유
장난감 방 정리
새 장난감은 입구 바구니(Eden)에 넣어요. 바구니가 가득 차면 선생님이 정리해요. 아무도 끈을 잡고 있지 않은 장난감은 치우고, 계속 갖고 노는 장난감은 큰 선반(Old)으로 옮겨요.
정리하는 동안에는 모두 잠깐 "얼음!"이에요(Stop-The-World). 그리고 "절대 버리지 마" 상자(static)에 계속 넣기만 하면 방이 꽉 차 버려요.
입구 바구니→Eden
큰 선반→Old Generation
장난감을 잡은 끈→참조
얼음!→Stop-The-World
방이 꽉 참→OutOfMemoryError
예시
Member m = new Member(); // Eden에 생성, 스택 변수 m이 참조 → 도달 가능
m = null; // 참조 끊김 → 도달 불가 → 다음 Minor GC에서 회수
static List<Member> cache = new ArrayList<>();
cache.add(new Member()); // static이 계속 참조 → 회수 안 됨 → 쌓이면 메모리 누수
기술 도메인
운영 옵션 예: java -Xms2g -Xmx2g -XX:+UseG1GC -jar app.jar. OutOfMemoryError가 나면 힙 덤프를 떠서 누수 객체를 추적합니다.
테이블
비교·정리 표
영역
공유
내용
Method Area (Metaspace)
모든 스레드
클래스 정보, static 변수, 상수 풀
Heap
모든 스레드
new로 만든 객체, 배열
JVM Stack
스레드별
메서드 호출 프레임(지역변수, 매개변수)
PC Register
스레드별
현재 실행 중인 명령 위치
Native Method Stack
스레드별
C/C++ 네이티브 메서드 호출
비교·정리 표
GC
특징
Serial / Parallel
단순, 처리량 위주. 소규모·배치
G1
Java 9부터 기본. Heap을 리전으로 나눠 멈춤 시간 목표 관리
ZGC
밀리초 이하 멈춤. Java 21에서 세대별(Generational) 모드 도입
스레드와 스레드 풀
이론
JVM은 프로세스 하나에 스레드 여럿으로 동작합니다.
Java의 기본 스레드(플랫폼 스레드)는 OS 스레드와 1:1로 매핑되고, 스케줄링은 OS가 합니다. 스레드마다 스택 메모리(64비트 Linux 기본 약 1MB)를 잡으므로 생성 비용이 큽니다.
너무 많으면 메모리와 컨텍스트 스위칭 비용이 커져 오히려 느려집니다.
스레드 풀: 스레드를 미리 만들어 두고 작업마다 재사용합니다.
보완Java 21의 가상 스레드는 JVM이 관리하는 경량 스레드로, 소수의 플랫폼 스레드(캐리어) 위에 M:N으로 올라갑니다. 블로킹 I/O를 만나면 캐리어에서 내려오기 때문에, 동기 코드 그대로 높은 동시성을 얻습니다. Java 21에서는 synchronized 안에서 블로킹하면 캐리어에 고정(pinning)되는 문제가 있었고 Java 24(JEP 491)에서 해결되었습니다.
비유
놀이터 그네
친구가 올 때마다 그네를 새로 만들면 너무 오래 걸려요. 그래서 그네 10개를 미리 만들어 두고 차례대로 타요. 이게 스레드 풀이에요.
가상 스레드는 이래요. 그네는 몇 개 없지만, 타던 친구가 신발 끈을 묶느라 멈추면 그 자리에 다른 친구가 바로 타요. 그래서 친구 수만 명도 함께 놀 수 있어요.
그네→플랫폼 스레드
미리 만든 그네들→스레드 풀
신발 끈 묶기→블로킹 I/O
잠깐 비켜 주기→가상 스레드 언마운트
예시
// 고정 크기 스레드 풀: 스레드 10개가 작업 100개를 나눠 처리
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
int n = i;
pool.submit(() -> handle(n));
}
pool.shutdown();
// Java 21 가상 스레드: 작업마다 스레드 하나, 수만 개도 가능
try (var ex = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int n = i;
ex.submit(() -> handle(n));
}
}
기술 도메인
Tomcat의 요청 처리 스레드 풀 (기본 최대 200)
같은 원리의 커넥션 풀: HikariCP(Spring Boot 기본, 기본 최대 10개)가 DB 연결을 재사용
Spring Boot 3.2+는 spring.threads.virtual.enabled=true 한 줄로 Tomcat 요청 처리를 가상 스레드로 전환
4. 객체지향 OOP · SOLID
서비스는 반드시 바뀝니다. 캡슐화·상속·다형성·추상화는 OOP의 필수 요소이고, SOLID는 설계 품질을 높이는 선택적 원칙입니다.
절차지향 vs 객체지향
이론
비유
혼자 요리 vs 역할 나눠 요리
절차지향은 한 사람이 레시피 1번부터 끝까지 순서대로 해요. 냉장고 재료를 누구나 꺼내 쓰니까 뭐가 없어졌는지 알기 어려워요.
객체지향은 반죽 친구, 오븐 친구, 장식 친구가 각자 자기 일을 맡고 "반죽 다 됐어?" 하고 서로 부탁해요.
레시피 순서→절차, 함수
역할 맡은 친구→객체
서로 부탁하기→메시지(메서드 호출)
예시
// 절차지향: 데이터(balance)를 여러 함수가 직접 수정
int balance = 10000;
balance = withdraw(balance, 3000);
// 객체지향: 데이터와 규칙을 객체가 함께 가짐
Account account = new Account(10000);
account.withdraw(3000);
기술 도메인
절차지향: C로 짠 임베디드·OS 커널, 짧은 스크립트·배치
객체지향: Java/Spring 엔터프라이즈 백엔드처럼 요구사항이 자주 바뀌고 여러 명이 오래 유지보수하는 코드
테이블
비교·정리 표
구분
절차지향
객체지향
핵심 질문
어떤 작업을 어떤 순서로?
어떤 객체가 어떤 책임을 지고 협력?
구성
함수 중심, 데이터와 기능 분리
객체 = 상태 + 행동, 메시지로 협력
장점
흐름이 단순, 작은 프로그램에 빠름
변경 범위가 좁고 확장에 강함
한계
공유 데이터 변경 추적이 어렵고, 규모가 커지면 복잡도 급증
설계 비용, 과설계 위험
캡슐화 (Encapsulation)
이론
데이터와 그 데이터를 다루는 기능을 묶고, 내부 구현을 숨기는 것. 외부는 객체가 공개한 메서드로만 상호작용하므로 내부가 바뀌어도 영향이 적습니다.
모든 private 필드에 getter/setter를 다는 것은 캡슐화가 아닙니다. 객체가 스스로 책임을 수행하게 만드는 것이 핵심입니다. ("Tell, Don't Ask")
비유
자판기
자판기 문을 열고 음료를 직접 꺼내면 안 돼요. 버튼만 눌러요. 돈이 모자라면 자판기가 알아서 음료를 안 줘요.
안에서 음료를 어떻게 꺼내는지는 몰라도 되고, 자판기 속이 바뀌어도 버튼은 그대로예요.
잠긴 자판기 속→private 필드
버튼→public 메서드
돈이 모자라면 안 줌→규칙(검증)은 객체 안에
예시
public class Account {
private long balance;
public void withdraw(long amount) {
if (amount <= 0) throw new IllegalArgumentException("금액은 0보다 커야 합니다");
if (balance < amount) throw new IllegalStateException("잔액이 부족합니다");
balance -= amount;
}
}
// ❌ account.setBalance(account.getBalance() - 1000); 규칙이 호출부로 새어 나감
// ✅ account.withdraw(1000); 규칙은 Account 안에
기술 도메인
JPA 엔티티에 setter를 열지 않고 order.cancel() 같은 도메인 메서드로 상태를 바꾸는 관례
API 응답은 엔티티 대신 DTO로 내보내 내부 구조를 숨김
테이블
비교·정리 표
접근 제어자
접근 범위
private
같은 클래스
(default)
같은 패키지
protected
같은 패키지 + 다른 패키지의 자식 클래스
public
어디서나
상속 (Inheritance)
이론
기존 클래스의 상태와 행동을 물려받아 새 클래스를 정의하는 것. "is-a" 관계를 표현하고 다형성의 수단이 됩니다.
부모 구현에 자식이 강하게 결합되어, 부모 변경이 자식을 깨뜨릴 수 있음
Java는 클래스 다중 상속을 지원하지 않음 (인터페이스는 여러 개 구현 가능)
코드 재사용이 목적이면 합성(Composition)이 대개 더 적절 ("has-a")
비유
닮은 가족, 그리고 로봇 팔
펭귄은 새예요. 그래서 "새"가 가진 것(알을 낳아요)을 물려받아도 이상하지 않아요.
하지만 "줄 서기"가 "아무 데나 끼어 앉는 의자 줄"을 물려받으면 새치기가 가능해져요(Stack과 Vector). 그럴 땐 물려받지 말고 가지고 있어요. 로봇이 팔을 "가지고" 있는 것처럼요(합성).
펭귄은 새다→is-a, 상속
로봇이 팔을 가졌다→has-a, 합성
예시
class Member {
protected String name;
public void login() { ... }
}
class AdminMember extends Member { // 관리자는 회원이다 (is-a) → 상속 OK
public void banUser(Member target) { ... }
}
// 합성: 재사용만 필요하면 필드로 가짐
class OrderService {
private final DiscountPolicy discountPolicy; // has-a
}
기술 도메인
실패 사례: JDK의 java.util.Stack은 Vector를 상속해서, 스택인데도 중간 삽입(add(index, e))이 가능해짐. 그래서 지금은 ArrayDeque 사용을 권장
성공 사례: 예외 계층 RuntimeException → IllegalArgumentException처럼 개념적으로 같은 종류일 때
다형성 (Polymorphism)
이론
같은 타입으로 다뤄도 실제 객체에 따라 다른 동작을 하는 것. 핵심은 "구현체를 갈아끼울 수 있다"이며, OCP·DIP로 이어집니다.
비유
"노래해!"
"노래해!"라고 똑같이 말해도 참새는 짹짹, 고양이는 야옹, 강아지는 멍멍 해요. 누가 듣느냐에 따라 다르게 해요. 이게 다형성이에요.
"그려 줘!"에 크레파스를 주면 크레파스 그림, 물감을 주면 물감 그림이 나와요. 무엇을 주는지 보고 미리 정해져요(오버로딩).
"노래해!"→같은 메서드 호출
참새·고양이→서로 다른 구현체
누가 듣는지 보고 결정→런타임 동적 디스패치
예시
interface Payment { void pay(int amount); }
class CardPayment implements Payment { public void pay(int a) { /* 카드 */ } }
class KakaoPayment implements Payment { public void pay(int a) { /* 카카오 */ } }
class OrderService {
private final Payment payment; // 구체 타입을 모름
OrderService(Payment payment) { this.payment = payment; }
void order(int amount) { payment.pay(amount); } // 실제 객체의 pay가 실행
}
기술 도메인
Spring은 같은 인터페이스의 빈들을 Map<String, Payment>로 주입해 줌 → 전략 패턴으로 결제 수단 선택
JDBC: 코드는 Connection 인터페이스만 쓰고, 실제 동작은 MySQL/PostgreSQL 드라이버 구현체가 결정
테이블
비교·정리 표
구분
오버라이딩
오버로딩
의미
부모 메서드를 자식이 재정의
같은 이름, 다른 매개변수
결정 시점
런타임 (동적 디스패치)
컴파일 타임
다형성 종류
서브타입 다형성
애드혹(ad-hoc) 다형성
추상화 (Abstraction)
이론
복잡한 세부를 숨기고 필요한 핵심만 드러내는 것. "무엇을 하는가"와 "어떻게 하는가"를 분리합니다.
비유
리모컨
TV 리모컨은 "켜기", "채널 올리기" 버튼만 알면 돼요. 안에 전선이 어떻게 연결됐는지는 몰라도 돼요.
인터페이스는 "이 버튼들이 꼭 있어야 해" 하는 약속 카드예요. 추상 클래스는 몸통은 다 만들어져 있고 얼굴만 직접 그리는 반쯤 만든 인형 키트예요.
버튼→무엇을 하는가(메서드 이름)
안의 전선→어떻게 하는가(구현)
약속 카드→interface
반쯤 만든 키트→abstract class
예시
abstract class Animal {
protected final String name; // 공통 상태
protected Animal(String name) { this.name = name; }
abstract void sound(); // 자식마다 다름
void sleep() { System.out.println(name + " 잠을 잔다"); } // 공통 구현
}
class Dog extends Animal {
Dog(String name) { super(name); }
@Override void sound() { System.out.println("멍멍"); }
}
Spring의 JpaRepository: 인터페이스만 선언하면 구현체는 프레임워크가 만들어 줌
테이블
비교·정리 표
구분
abstract class
interface
목적
공통 상태·구현 공유
역할·규약 정의
개수
하나만 상속
여러 개 구현
필드
인스턴스 필드 가능
상수(public static final)만
생성자
있음
없음
구현 제공
일반 메서드
default·static(Java 8+), private(Java 9+)
SOLID 원칙
이론
DIP의 "역전": 원래 OrderService → MySqlOrderRepository로 향하던 의존이, 구현체가 상위 쪽 인터페이스 OrderRepository를 구현하면서 MySqlOrderRepository → OrderRepository로 방향이 뒤집힙니다.
비유
우리 반 약속 다섯 가지
S: 급식 당번은 급식만, 칠판 당번은 칠판만 해요.
O: 레고 성에 탑을 더 올리고 싶으면 블록을 끼우면 돼요. 성을 부수지 않아요.
L: 술래 대신 들어온 친구가 "난 술래 안 할래" 하면 놀이가 망가져요. 대신 들어오면 같은 약속을 지켜요.
I: 축구하러 갈 땐 축구공만 든 작은 가방을 들어요. 필요 없는 짐까지 다 든 큰 가방은 안 돼요.
D: 선풍기는 벽 속 발전소가 아니라 콘센트 모양에 맞춰요. 그래서 어느 집에 가도 꽂을 수 있어요.
콘센트 모양→인터페이스(추상화)
어느 집 전기든→어떤 구현체든
예시
// SRP: 가입·메일·로그를 각각 분리
class UserService { void register(User u) { ... } }
class EmailSender { void sendWelcome(User u) { ... } }
// OCP: if-else 대신 구현체 추가
// if ("CARD".equals(type)) ... else if ("KAKAO".equals(type)) ... ❌
class TossPayment implements Payment { ... } // ✅
// LSP 위반: 계약(send)을 지키지 못하는 구현체
class KakaoNotification implements Notification {
public void send(String msg) { throw new UnsupportedOperationException(); }
}
// ISP: 거대한 Worker 대신 역할별 분리
interface Workable { void work(); }
interface Approvable { void approve(); }
// DIP: 인터페이스에 의존 + 생성자 주입
class OrderService {
private final OrderRepository repository;
OrderService(OrderRepository repository) { this.repository = repository; }
}
기술 도메인
SRP: Controller / Service / Repository 계층 분리
OCP: Spring Security 필터 체인에 필터를 추가해 인증 방식을 확장
LSP: List.of(), Collections.unmodifiableList()의 add()는 예외를 던짐. JDK는 이를 "선택적 연산"으로 문서화했지만, 호출부가 수정 가능하다고 가정하면 깨지는 대표 사례
ISP: Spring Data의 Repository → CrudRepository → ListCrudRepository처럼 필요한 만큼만 상속
DIP: Spring IoC 컨테이너가 구현체를 생성자로 주입 (테스트에서는 가짜 구현으로 교체)
테이블
비교·정리 표
원칙
정의
SRP 단일 책임
클래스가 바뀌는 이유는 하나여야 한다
OCP 개방-폐쇄
확장에는 열려 있고, 수정에는 닫혀 있어야 한다
LSP 리스코프 치환
자식 타입은 부모 타입의 계약을 깨지 않고 대체 가능해야 한다
ISP 인터페이스 분리
클라이언트는 쓰지 않는 메서드에 의존하지 않아야 한다
DIP 의존성 역전
상위 정책과 하위 구현 모두 추상화에 의존해야 한다
5. 아키텍처 패턴 ARCHITECTURE
아키텍처는 관심사를 어떻게 나눌지, 의존성을 어느 방향으로 흐르게 할지를 정하는 일입니다.
MVC
이론
Model: 데이터와 비즈니스 로직. View와 Controller를 몰라야 함
View: 사용자에게 보여주는 표현. 비즈니스 로직이 없어야 함
Controller: 요청을 받아 Model을 호출하고 응답(View)을 결정. 로직이 들어가면 비대해짐
한계: 큰 틀만 제공하므로, 규모가 커지면 Model 안에 비즈니스 로직과 데이터 접근이 뒤섞입니다. → Layered Architecture
MVC 구조
비유
인형극
이야기와 인형들(Model), 무대와 조명(View), 그리고 "이제 공주가 나올 차례예요!" 하고 둘을 이어 주는 진행자(Controller)가 있어요.
인형은 무대가 어떻게 생겼는지 몰라도 되고, 무대는 이야기를 직접 바꾸지 않아요.
이야기와 인형→Model
무대→View
진행자→Controller
예시
요청 → DispatcherServlet → HandlerMapping → Controller → Model 생성 → ViewResolver → View(Thymeleaf) 렌더링 → 응답
@Controller
public class PostController {
@GetMapping("/posts/{id}")
public String detail(@PathVariable Long id, Model model) {
model.addAttribute("post", postService.find(id));
return "post/detail"; // templates/post/detail.html
}
}
기술 도메인
Spring MVC의 @Controller + Thymeleaf (서버 사이드 렌더링)
REST API에서는 @RestController가 View 대신 JSON을 반환. 이때 View 역할은 프론트엔드(React 등)가 맡음
Layered Architecture
이론
역할별 계층으로 나누고, 상위는 하위를 쓸 수 있지만 하위는 상위를 몰라야 합니다.
한계: 의존성이 결국 DB 기술을 향합니다. MySQL → MongoDB로 바꾸면 Repository가 제공하던 메서드가 달라져 Service까지 영향을 받을 수 있습니다.
Layered Architecture
비유
3층짜리 식당
1층 홀 직원(Controller)이 주문을 받고, 2층 셰프(Service)가 무엇을 어떻게 요리할지 정하고, 3층 창고지기(Repository)는 재료만 꺼내 줘요.
위층이 아래층에 부탁하지, 창고지기가 손님에게 직접 가지는 않아요. 그런데 창고를 냉장고에서 냉동고로 바꾸면 셰프 요리법까지 바뀔 수 있어요. 이게 Layered의 한계예요.
1차 과제에서 많이 고민했던 세 가지 — 타입 선택, 예외 처리 위치, 로직의 책임 — 를 이번 세미나 내용과 연결해 다시 봅니다.
0-1Primitive vs Wrapper
핵심두 타입의 가장 큰 차이는 null을 허용하느냐입니다. 값이 없을 수도 있으면 Wrapper, 반드시 있으면 Primitive를 고릅니다.
이론
null을 표현할 수 있는가
기본형(int, long, boolean …)은 항상 값이 있어야 하고, 래퍼 타입(Integer, Long, Boolean …)은 “값 없음”을 null로 표현할 수 있습니다. 두 타입 사이에는 자동 박싱·언박싱이 일어납니다.
값이 없다는 것과 0은 다르다
int의 0은 실제 숫자이고 Integer의 null은 “입력하지 않음”입니다. 나이 0세와 나이 미입력을 같은 값으로 처리하면 검색·검증의 의미가 바뀝니다.
주의할 함정 두 가지
래퍼 → 기본형 언박싱 시 값이 null이면 NullPointerException이 발생합니다.
래퍼끼리 ==로 비교하면 값이 아니라 참조를 비교할 수 있으므로 equals 또는 Objects.equals를 씁니다.
Controller 파라미터에서 생기는 문제
required = false이고 defaultValue도 없는 파라미터(= null이 들어올 수 있음)를 기본형으로 받으면 런타임 에러가 납니다.
반대로 필수이거나 기본값이 있어 null이 들어올 일이 없는데 래퍼를 쓰면 의미 없는 null 가능성이 생깁니다.
둘 다 사소해 보이지만, 파라미터마다 “null이 올 수 있나?”를 한 번 짚는 습관이 중요합니다.
비유
신청서의 “나이” 칸에 0을 적는 것과 빈칸으로 두는 것은 다른 의미입니다. 빈칸을 허용하는 신청서라면 “미입력”을 표현할 방법(= null)이 필요합니다.
예시
필수 / 기본값 / 선택 파라미터
@RequestParam("page") int page // 필수: 없으면 400
@RequestParam(name = "page", defaultValue = "1") int page // 기본값: 없으면 1
@RequestParam(name = "category", required = false) Integer category // 선택: 없으면 null
언박싱 NPE와 값 비교
Integer count = null;
// int value = count; // 언박싱 시 NullPointerException
boolean same = Objects.equals(Integer.valueOf(1000), Integer.valueOf(1000)); // true
기술 도메인
요청 파라미터 바인딩, 요청·응답 DTO 필드, DB의 nullable 컬럼 매핑에서 같은 고민이 반복됩니다. “이 값은 비어 있을 수 있는가?”를 먼저 정하고 타입을 고릅니다.
테이블
선택·필수·기본값의 실제 차이
선언
누락 시
의미
@RequestParam("page") int page
보통 400
요청 파라미터가 필수
@RequestParam(name="page", defaultValue="1") int page
// Service: 문제가 생긴 곳에서는 던지기만
Post post = repository.findById(id)
.orElseThrow(() -> new PostNotFoundException(id));
// Web 계층: 한 곳에서 HTTP 응답으로 변환
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(PostNotFoundException.class)
ResponseEntity<Map<String, String>> notFound(PostNotFoundException e) {
return ResponseEntity.status(404).body(Map.of(
"code", "POST_NOT_FOUND", "message", "게시글을 찾을 수 없습니다."));
}
}
PostNotFoundException은 프로젝트에서 정의한 RuntimeException 하위 타입입니다. 클라이언트에는 내부 스택 트레이스 대신 합의된 오류 코드를 보냅니다.
기술 도메인
Spring에서는 @RestControllerAdvice + @ExceptionHandler로 “예외 → HTTP 응답” 변환을 한 곳에 모읍니다. 검증 로직과 오류 응답 형식을 분리할 수 있습니다.
보완try-catch를 밖으로 옮기는 것 자체가 AOP는 아닙니다. @ExceptionHandler도 엄밀히는 Spring MVC의 예외 처리 메커니즘입니다. 다만 “반복되는 관심사를 분리한다”는 해결 방향이 비슷합니다.
0-3도메인 로직 vs 비즈니스 로직
핵심도메인 로직 = 객체가 스스로 지키는 규칙(예: post.update()). 비즈니스 로직 = 그 도메인 로직들을 조합해 문제를 해결하는 흐름. 다만 조직마다 두 용어를 섞어 쓰기도 합니다.
이론
왜 헷갈릴까?
MVC에서는 Model이 핵심 로직을, Controller가 흐름을 담당한다고 했습니다. 그런데 Controller가 커지면서 레이어드 아키텍처의 business(Service) 계층이 생겼고, 그 계층이 “비즈니스 로직”을 맡는다고 말합니다. 그래서 “핵심 로직 = 비즈니스 로직 아닌가?”라는 의문이 생깁니다.
세미나의 구분: 핵심(도메인) 로직 ≠ 비즈니스 로직
도메인 로직: 도메인 객체가 가지는 역할과 책임입니다. 게시글의 update, updateTitle, updateContent 등.
비즈니스 로직: 비즈니스 문제를 해결하기 위한 도메인 로직의 조합과 흐름입니다. 도메인 로직은 이 흐름의 적절한 위치에서 호출됩니다.
정답은 “해석에 따라 다르다”
두 용어는 실무에서 자주 혼용됩니다. 도메인 객체가 값만 담고 로직이 없는 단순한 프로젝트라면 구분 자체가 의미 없을 수도 있습니다. 공부하는 입장에서는 엄밀하게 나눠 생각해 보는 것이 설계 감각에 도움이 됩니다.
비유
요리법의 규칙(“계란은 반드시 익힌다”)은 도메인 로직, 주문 하나를 처리하는 순서(주문 확인 → 재료 확인 → 조리 → 포장)는 비즈니스 로직입니다.
예시
“게시글 수정”이라는 비즈니스 로직의 흐름
게시글 조회수정 요청이 온 게시글이 실제로 존재하는지 확인
권한 확인요청한 사용자가 수정 권한이 있는 사람인지 확인
내용 수정 — 도메인 로직post.update(title, content) 호출, 제목 검증 등 규칙은 Post가 지킴
저장수정된 게시글을 저장소에 반영
코드로 나누면
// Service: 비즈니스 흐름 조율
public void updatePost(long id, long memberId, String title, String content) {
Post post = repository.findById(id).orElseThrow();
if (!post.isWrittenBy(memberId)) throw new ForbiddenException();
post.update(title, content); // ← 도메인 로직 호출
repository.save(post);
}
// Post: 도메인 규칙
public void update(String title, String content) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("제목은 비어 있을 수 없습니다.");
}
this.title = title;
this.content = content;
}
기술 도메인
Controller는 HTTP 연결, Service는 유스케이스 흐름 조율, Domain은 규칙, Repository는 저장을 맡습니다. 제목 검증을 생성·수정에 각각 중복하지 않도록 도메인의 공통 메서드로 묶을 수 있고, 권한 정책의 위치는 프로젝트 설계에 따라 달라집니다.
테이블
Controller가 커질 때 분리할 기준
계층
주요 책임
게시글 수정 예시
Controller
HTTP 요청·응답 연결
경로 ID, JSON DTO를 받고 Service 호출
Service
유스케이스 수행 순서
조회, 정책 확인, 변경, 저장
Domain
객체 상태와 규칙
제목 검증 및 내용 변경
Repository
저장과 조회
ID로 조회하고 변경 결과 저장
보완“비즈니스 로직”은 도메인 규칙까지 포함하는 넓은 의미로도 널리 쓰입니다. “도메인 로직 ≠ 비즈니스 로직”은 보편적 정의가 아니라 책임을 나눠 생각하기 위한 관점으로 받아들이면 됩니다.
01HTTP
1차 세미나의 클라이언트-서버 구조에서 “요청과 응답”에도 규칙이 있다고 했습니다. 그중 우리가 가장 많이 쓰는 규칙이 HTTP입니다.
1-1HTTP란? 4가지 특징
핵심HTTP는 클라이언트와 서버가 데이터를 주고받는 애플리케이션 계층의 통신 규약입니다. 요청은 항상 클라이언트가 시작하고, 서버는 이전 요청을 기억하지 않습니다(무상태).
이론
HTTP(HyperText Transfer Protocol)
처음에는 HTML 같은 하이퍼텍스트 문서를 보내려고 만들어졌지만, 바이트 데이터라면 무엇이든 보낼 수 있어서 지금은 이미지·영상·JSON 등 거의 모든 데이터를 HTTP로 주고받습니다.
① 클라이언트-서버 구조 (Request-Response)
역할에 따라 클라이언트(브라우저, 앱, 임베디드 프로그램 등)와 서버로 나뉩니다. 클라이언트가 요청을 보내고 서버가 응답합니다. 대화의 시작은 언제나 클라이언트입니다.
② 무상태 (Stateless)
서버는 이전 요청을 기억하지 않고 모든 요청을 독립적으로 처리합니다.
장점 어떤 서버가 응답해도 되므로 서버를 수평 확장(Scale-out)하기 쉽습니다.
단점 요청마다 필요한 정보를 모두 담아야 하므로 보내는 데이터가 커집니다.
무상태는 “DB에 아무것도 저장하지 않는다”는 뜻이 아닙니다. 게시글·회원 같은 자원은 저장합니다. 요청 해석이 이전 요청의 맥락에 의존하지 않는다는 점이 핵심입니다.
③ 비연결성 (Connectionless)
기본적으로 응답 후 연결을 끊어, 수많은 클라이언트와 연결을 유지하느라 서버 자원을 낭비하지 않습니다. 다만 매번 TCP 연결(3-way handshake)을 새로 맺는 비용이 크기 때문에 HTTP/1.1부터는 Keep-Alive(지속 연결)로 연결을 재사용합니다.
Q. 연결을 유지하는데 왜 비연결성인가요? 유지되는 것은 전송 계층의 TCP 연결이지 HTTP 통신 단위가 아니기 때문입니다. HTTP는 요청-응답이 끝나면 그 대화가 끝나고, 연결의 생성·해제 과정도 여전히 존재합니다.
④ 텍스트 기반 · 확장 가능
HTTP/1.1 메시지는 사람이 읽을 수 있는 텍스트이고, 헤더를 추가해 기능을 확장합니다. 리다이렉션의 Location, 캐시 정책의 Cache-Control처럼 데이터 전달 외의 기능을 붙일 수 있습니다.
비유
배달 앱에 주문할 때마다 메뉴와 배송 주소를 매번 적는 것과 같습니다. 가게는 “지난번 그 손님”을 기억하지 않으니, 어느 지점(서버)이 주문을 받아도 똑같이 처리할 수 있습니다.
예시
무상태가 확장에 유리한 이유
서버 A가 직전 요청의 로그인 상태를 자기 메모리에만 보관하면, 다음 요청을 서버 B가 받았을 때 같은 사용자인지 판단할 수 없습니다. 각 요청에 인증 정보(세션 ID, 토큰)를 함께 보내고 공통 검증 규칙을 쓰면 A와 B가 똑같이 처리할 수 있습니다.
기술 도메인
로드밸런서 뒤에 여러 서버를 두는 구조는 무상태 요청을 전제로 할 때 가장 단순해집니다. 대신 로그인 같은 “상태”는 토큰이나 공유 세션 저장소처럼 애플리케이션 차원에서 따로 설계해야 합니다.
테이블
HTTP 버전과 연결을 구분하기
구분
연결·메시지 관점
주의점
HTTP/1.0
전형적으로 요청마다 새 연결
지속 연결 확장이 사용되기도 함
HTTP/1.1
지속 연결이 기본
응답 종료 ≠ TCP 종료
HTTP/2
하나의 연결에서 여러 스트림 다중화
바이너리 프레임으로 메시지 전송
HTTP/3
QUIC 기반 스트림
TCP 3-way handshake 설명을 그대로 적용하면 안 됨
보완HTTP/1.1은 지속 연결이 기본값이므로 “요청마다 연결을 끊는다”는 설명은 HTTP/1.0 기준입니다. 또 HTTP/2는 바이너리 프레이밍, HTTP/3은 QUIC(UDP 기반)을 쓰므로 “HTTP = 텍스트 · TCP · 비연결”로 일반화하지 않습니다.
1-2URL 읽는 법
핵심URL은 자원이 어디에 있는지를 알려 주는 주소입니다. scheme://host:port/path?query 다섯 조각으로 나눠 읽습니다.
이론
URL(Uniform Resource Locator)
자원을 위치로 식별하는 방법으로, URI의 한 종류입니다. 클라이언트가 서버의 자원을 요청하려면 그 자원이 어디 있는지 알려 줘야 합니다.
경로(path)와 쿼리(query)는 역할이 다르다
/posts/42는 특정 게시글을 식별하고, /posts?page=2&keyword=spring은 목록에 적용할 조건을 표현합니다.
알아 두면 좋은 디테일
#section 같은 fragment는 브라우저 안에서만 쓰이고 서버로 보내는 요청에는 포함되지 않습니다.
https://api.sopt.org:443/posts/42?preview=true#comments
GET /posts/42?preview=true HTTP/1.1
Host: api.sopt.org
// #comments는 요청에 포함되지 않습니다.
기술 도메인
http의 기본 포트는 80, https는 443이라 생략할 수 있습니다. 서버는 어떤 경로를 같은 자원으로 볼지, 어떤 쿼리를 필터로 받을지를 API 계약에 정해야 합니다.
테이블
URL 구성요소
구성요소
의미
scheme
사용할 프로토콜 (http, https)
host
서버의 도메인 또는 IP
port
서버 프로그램의 포트 (http 80, https 443은 생략 가능)
path
서버 안에서 자원의 경로
query
key=value 형태의 추가 정보, &로 연결
1-3HTTP 메시지 구조
핵심요청과 응답은 같은 뼈대를 가집니다. 시작 줄 → 헤더 → 빈 줄 → 바디 순서입니다.
이론
HTTP 메시지
HTTP로 주고받는 데이터 덩어리를 HTTP 메시지라고 합니다.
시작 줄: 요청이면 메서드 + 경로 + 버전, 응답이면 버전 + 상태 코드 + 설명
헤더: 메시지에 대한 부가 정보 (Key: Value)
빈 줄: 헤더가 끝났다는 표시
바디: 실제 데이터 (없어도 됨)
읽는 순서
시작 줄에서 무엇을 어디에 하려는지 보고, 헤더에서 인증·형식을 확인한 뒤, 빈 줄 이후 바디를 해석합니다. 응답도 상태 코드로 큰 결과를 먼저 알리고 헤더·바디로 상세 정보를 전달합니다. 204 응답처럼 바디가 없는 응답도 유효합니다.
HTTP 메시지 구조
비유
편지 봉투(시작 줄 + 헤더)에는 받는 곳과 배송 정보가, 봉투 안(바디)에는 실제 내용이 있습니다.
예시
요청 메시지 (Request)
POST /posts HTTP/1.1 ← 요청 줄
Host: api.sopt.org ← 헤더
Content-Type: application/json
Authorization: Bearer eyJhbGci...
← 빈 줄
{ ← 바디
"title": "2차 세미나 후기",
"content": "REST가 뭔지 알게 됐다!"
}
응답 메시지 (Response)
HTTP/1.1 201 Created ← 상태 줄
Content-Type: application/json ← 헤더
Location: /posts/42
← 빈 줄
{ ← 바디
"id": 42,
"title": "2차 세미나 후기",
"content": "REST가 뭔지 알게 됐다!"
}
기술 도메인
Spring MVC는 요청 줄·헤더·바디를 읽어 Controller 파라미터로 바꿔 주고(→ 6-2), Controller의 반환 값을 상태 줄·헤더·바디로 다시 써 줍니다(→ 6-3).
1-4메서드 · 안전 · 멱등
핵심메서드는 자원에 대해 무엇을 하고 싶은지를 나타냅니다. 멱등한 요청(GET·PUT·DELETE)은 여러 번 보내도 결과가 같아서 안심하고 재시도할 수 있습니다.
이론
HTTP 메서드
요청 줄의 첫 단어로, GET 조회 · POST 생성/처리 · PUT 전체 대체 · PATCH 일부 수정 · DELETE 삭제를 뜻합니다. 요청이 기대하는 행위를 표현하므로 알맞은 메서드를 고르는 것이 중요합니다. 이외에도 OPTIONS, HEAD, TRACE, CONNECT가 표준 메서드입니다.
안전(Safe)
호출해도 자원이 변경되지 않는 메서드입니다. GET, HEAD, OPTIONS(, TRACE)가 해당합니다.
멱등(Idempotent)
한 번 호출하든 여러 번 호출하든 서버 상태가 같은 메서드입니다. 안전한 메서드 + PUT, DELETE가 해당합니다.
DELETE /posts/1을 10번 보내도 1번 게시글이 삭제된 상태는 같습니다 → 멱등
POST /posts를 10번 보내면 게시글이 10개 생깁니다 → 멱등 아님
PATCH는 구현에 따라 다릅니다. “좋아요 1 증가” 같은 요청은 멱등이 아닙니다.
왜 중요할까?
네트워크 오류로 응답을 못 받았을 때 멱등한 요청은 안심하고 재시도할 수 있습니다. 결제 요청(POST)을 무심코 재시도하면 중복 결제가 일어날 수 있습니다.
비유
에어컨을 “20도로 설정”(PUT)하는 버튼은 몇 번 눌러도 20도지만, “1도 올리기”(POST/PATCH) 버튼은 누를수록 온도가 달라집니다.
예시
멱등성을 서버 상태로 비교하기
✓ 멱등: 반복해도 최종 상태 동일
PUT /posts/42
{"title":"새 제목","content":"새 본문"}
// 1회든 3회든 같은 상태
✗ 멱등 아님: 반복할수록 달라짐
PATCH /posts/42
{"increment": 1}
// 증가 연산이라면 매번 상태가 바뀜
멱등성은 응답이 매번 같다는 뜻이 아닙니다. DELETE를 두 번 보내면 첫 응답은 204, 두 번째는 404일 수 있지만 “삭제된 상태”라는 서버 효과는 같으므로 멱등입니다.
기술 도메인
재시도 설계
응답이 유실되면 호출자는 서버가 작업을 수행했는지 확신할 수 없습니다. 결제·주문 같은 POST에는 요청 식별자(Idempotency Key)를 함께 보내고 서버에서 중복 처리를 막는 정책을 둘 수 있습니다.
테이블
HTTP 메서드 한눈에 보기
메서드
의미
바디
예시
GET
자원 조회
보통 없음 (쿼리 파라미터 사용)
게시글 목록 조회
POST
자원 생성, 처리 요청
있음
게시글 작성
PUT
자원 전체 대체 (없으면 생성)
있음
게시글 전체 수정
PATCH
자원 일부 수정
있음
게시글 제목만 수정
DELETE
자원 삭제
보통 없음
게시글 삭제
CRUD와 메서드의 의미
메서드
의도
본문
대표 경로
GET
표현 조회
일반적으로 사용하지 않음
/posts?page=1
POST
자원별 의미에 따른 처리; 생성에 자주 사용
처리할 데이터
/posts
PUT
대상 자원 상태를 요청 표현으로 대체
대체할 표현
/posts/42
PATCH
부분 변경 적용
변경 명세
/posts/42
DELETE
대상 URI와 자원의 연결 제거
일반적으로 사용하지 않음
/posts/42
메서드의 속성 — 안전과 멱등
속성
의미
해당 메서드
안전(Safe)
호출해도 자원이 변경되지 않음
GET, HEAD, OPTIONS, TRACE
멱등(Idempotent)
한 번 호출하든 여러 번 호출하든 결과(서버 상태)가 같음
GET, PUT, DELETE (+ HEAD, OPTIONS, TRACE)
보장 안 됨
반복 시 상태가 달라질 수 있음
POST, PATCH (구현에 따라)
그 밖의 표준 메서드
메서드
역할
HEAD
GET과 같은 대상의 메타데이터를 조회하되 응답 본문은 없음
OPTIONS
대상에서 가능한 통신 옵션 확인; CORS preflight에도 사용
TRACE
요청 경로 진단용 루프백; 보안상 비활성화되는 경우가 많음
CONNECT
프록시를 통해 터널 설정
보완원문의 {"likes": +1}은 유효한 JSON이 아닙니다. 증가 의도를 표현하려면 {"increment": 1} 같은 계약이 필요합니다. 또 PUT이 “없으면 생성”까지 허용할지는 API 계약으로 정하고, 로그 기록 같은 부수 효과는 안전성 정의와 별개입니다.
1-5상태 코드
핵심응답의 세 자리 숫자로 결과를 알립니다. 2xx 성공 · 3xx 이동 · 4xx 클라이언트 잘못 · 5xx 서버 잘못. 오류를 200으로 감추지 않습니다.
이론
첫 자리만 봐도 결과를 안다
상태 코드는 클라이언트·프록시·모니터링 도구가 바디를 열어 보지 않고도 결과를 판단하게 해 주는 약속입니다.
4xx와 5xx의 차이
4xx는 클라이언트의 요청이 잘못된 것이라 같은 요청을 다시 보내도 대개 실패합니다. 5xx는 서버 쪽 문제라 잠시 후 재시도하면 성공할 수도 있습니다.
3xx와 Location
301·302 응답은 보통 헤더 Location에 이동한 자원의 위치를 함께 보내고, 브라우저가 자동으로 그곳으로 이동합니다.
⚠️ 모든 응답을 200 + "success": false로 보내지 않기
모니터링·캐시·클라이언트의 공통 오류 처리가 실제 실패를 인식하지 못하게 됩니다.
비유
택배 조회의 배송 완료(2xx) · 주소 이전(3xx) · 주소 오류(4xx) · 물류센터 장애(5xx)와 같습니다. 주소 오류는 다시 보내도 실패하지만, 물류센터 장애는 기다리면 해결될 수 있습니다.
예시
게시판에서 어떤 코드를 쓸까?
POST /posts → 201 Created + Location: /posts/42
GET /posts/42 → 200 OK
DELETE /posts/42 → 204 No Content (바디 없음)
GET /posts/999 → 404 Not Found
POST /posts {제목 없음} → 400 Bad Request
로그인 안 하고 작성 → 401 Unauthorized
남의 글 삭제 시도 → 403 Forbidden
PATCH /posts (미지원) → 405 Method Not Allowed
처리 못 한 예외 → 500 Internal Server Error
기술 도메인
코드에 따라 클라이언트의 후속 처리가 달라진다
401이면 재로그인, 403이면 권한 안내, 404면 “없는 게시글” 화면, 409면 충돌 해결을 안내합니다. 503에는 Retry-After를 줄 수 있습니다. 재시도는 메서드의 멱등성과 간격까지 함께 고려합니다.
301·302는 일부 클라이언트가 POST를 GET으로 바꿀 수 있으므로, 메서드를 유지해야 하면 307·308을 검토합니다.
테이블
상태 코드 범위
범위
분류
의미
1xx
Informational
요청을 받았고 처리 중 (거의 안 씀)
2xx
Success
요청 성공
3xx
Redirection
다른 곳으로 가서 요청을 완료하라
4xx
Client Error
클라이언트가 잘못함 — 같은 요청을 다시 보내도 실패
5xx
Server Error
서버가 잘못함 — 재시도하면 성공할 수도 있음
자주 쓰는 상태 코드
코드
이름
게시판 또는 인프라 예시
200
OK
목록·단건 조회 또는 본문 있는 수정 성공
201
Created
새 게시글 생성; Location으로 URI 전달 가능
204
No Content
삭제 성공; 응답 본문 없음
301
Moved Permanently
영구 이동 위치를 Location으로 전달
302
Found
일시적인 다른 위치로 이동
400
Bad Request
잘못된 JSON, 타입 변환 실패, 입력 검증 실패
401
Unauthorized
유효한 인증 자격 증명 없음; 인증 챌린지와 관련
403
Forbidden
요청은 이해했지만 수행을 거부
404
Not Found
대상 자원이 없거나 존재를 공개하지 않음
405
Method Not Allowed
해당 자원에서 메서드 미지원; Allow 헤더 제공
409
Conflict
현재 자원 상태와 충돌; 중복 또는 버전 충돌
500
Internal Server Error
처리되지 않은 예외 등 서버 내부 문제
502
Bad Gateway
중간 서버가 상위 서버에서 유효하지 않은 응답을 받음
503
Service Unavailable
과부하·점검 등으로 일시적 제공 불가
보완4xx라고 모두 재시도가 무의미한 것은 아닙니다. 408(시간 초과), 429(요청 과다)는 상황이 바뀌면 성공할 수 있습니다. 502는 “서버가 프록시와 연결되지 않은 상태”라기보다 게이트웨이·프록시가 상위 서버로부터 유효하지 않은 응답을 받은 경우입니다.
1-6HTTP 헤더
핵심헤더는 메시지에 대한 부가 정보(메타데이터)입니다. 특히 Content-Type(내가 보내는 형식)과 Accept(내가 받고 싶은 형식)를 구분합니다.
이론
Key: Value
헤더는 Key: Value 형태로 바디의 형식·크기, 인증 정보, 캐시 정책, 쿠키 등 데이터 외의 정보를 담습니다.
방향을 함께 기억하기
Host·Accept·Authorization은 요청에, Location·Set-Cookie는 응답에, Content-Type·Cache-Control은 양쪽에 쓰입니다.
비유
택배 송장의 “취급 주의 · 냉장 · 받는 사람 · 무게” 표시와 같습니다. 상자 안 물건(바디)을 열어 보지 않아도 어떻게 다뤄야 할지 알 수 있습니다.
예시
Content-Type과 Accept
POST /posts HTTP/1.1
Host: api.sopt.org
Content-Type: application/json // 내가 보내는 바디는 JSON
Accept: application/json // 응답도 JSON으로 받고 싶음
Authorization: Bearer access-token
{"title":"2차 세미나 후기","content":"HTTP 학습"}
서버가 지원하지 않는 요청 본문 형식이면 415, 제공할 수 없는 응답 형식을 요구하면 406이 될 수 있습니다.
기술 도메인
Spring의 @RequestHeader로 헤더 값을 받을 수 있고, @RequestBody 변환은 Content-Type을 보고 알맞은 HttpMessageConverter를 고릅니다. Content-Length는 바이트 길이이며 항상 필요한 것은 아닙니다.
테이블
자주 쓰는 헤더
헤더
방향
의미
Host
요청
요청하는 서버의 도메인 (필수)
Content-Type
양쪽
바디의 데이터 형식 (application/json, multipart/form-data)
Content-Length
양쪽
바디의 크기
Accept
요청
클라이언트가 받고 싶은 응답 형식
Authorization
요청
인증 정보 (Bearer {토큰})
Cookie / Set-Cookie
요청 / 응답
쿠키 전송 / 쿠키 저장 지시
Location
응답
새로 생성된 자원 또는 리다이렉트할 위치
Cache-Control
양쪽
캐시 정책
자주 사용하는 헤더 전체 비교
헤더
방향
읽는 방법
Host
요청
HTTP/1.1에서 대상 호스트 식별
Content-Type
요청·응답
현재 본문의 미디어 타입
Content-Length
요청·응답
본문 길이(바이트); 항상 필요한 것은 아님
Accept
요청
클라이언트가 수용할 응답 미디어 타입
Authorization
요청
Bearer 토큰 등 인증 자격 증명
Cookie / Set-Cookie
요청 / 응답
저장된 쿠키 전송 / 브라우저에 저장 지시
Location
응답
생성 자원의 URI 또는 이동 위치
Cache-Control
요청·응답
저장·재사용과 검증 정책
1-7무상태 HTTP에서 사용자를 기억하는 방법
핵심서버가 기억하지 못하니 매 요청마다 “내가 누구인지” 증명할 정보를 함께 보냅니다. 쿠키·세션·토큰(JWT)이 그 방법입니다.
이론
로그인 상태는 어떻게 유지될까?
HTTP는 무상태라 서버는 방금 로그인한 사용자를 기억하지 못합니다. 그래도 한 번 로그인하면 계속 로그인 상태로 쓸 수 있는 이유는, 클라이언트가 매 요청마다 증명 정보를 같이 보내기 때문입니다.
세 가지 방식
쿠키: 서버가 Set-Cookie로 저장시키면 브라우저가 이후 요청마다 자동으로 보냅니다.
쿠키는 저장·전송 수단, 세션은 서버의 상태 관리 방식, JWT는 토큰 형식입니다. 그래서 “세션 ID를 쿠키에 담는” 식으로 함께 쓰입니다.
비유
쿠키는 지갑, 세션 ID는 사물함 번호표(물건은 가게에 보관), JWT는 위조 방지 도장이 찍힌 출입증(정보가 출입증에 적혀 있음)입니다.
예시
세션 ID의 왕복과 JWT 요청
// 세션 방식: 로그인 응답
HTTP/1.1 200 OK
Set-Cookie: SID=random-session-id; Path=/; HttpOnly; Secure; SameSite=Lax
// 이후 요청: 브라우저가 자동으로 쿠키 전송
GET /members/me HTTP/1.1
Cookie: SID=random-session-id
// JWT 방식: 클라이언트가 직접 헤더 구성
GET /members/me HTTP/1.1
Authorization: Bearer signed-access-token
모바일 앱과 협업하는 서버는 주로 토큰 방식을 씁니다. 세션은 여러 서버 간 세션 공유, JWT는 짧은 유효 기간과 재발급·폐기 설계를 함께 고민해야 합니다. 자세한 내용은 이후 인증/인가 세미나에서 다룹니다.
테이블
쿠키 · 세션 · 토큰 비교
방식
동작
상태 저장 위치
특징
쿠키
서버가 Set-Cookie로 저장시키면, 브라우저가 이후 요청마다 자동으로 전송
클라이언트
탈취·위변조 위험, 민감 정보 저장 금지
세션
서버가 사용자 정보를 저장하고, 클라이언트에는 세션 ID만 쿠키로 전달
서버
안전하지만 서버가 상태를 가짐 → 확장 시 세션 공유 문제
토큰 (JWT)
서버가 서명한 토큰을 발급, 클라이언트가 Authorization 헤더로 전송
클라이언트
서버가 상태를 갖지 않아 확장에 유리, 발급 후 강제 만료가 어려움
로그인 요청 다음에는 무엇을 보내나요?
방식
로그인 때
다음 요청 때
서버가 확인할 것
세션 + 쿠키
서버 세션 생성 후 세션 ID를 Set-Cookie로 전달
브라우저가 Cookie에 세션 ID 전송
세션 저장소에서 사용자·만료 조회
JWT Bearer
서명된 토큰 발급
클라이언트가 Authorization 헤더 구성
서명, 만료, 발급자·대상 등 검증
보완JWT는 일반적으로 암호화가 아니라 서명됩니다. 누구나 내용을 읽을 수 있으므로 비밀 정보를 넣지 않습니다. JWT도 쿠키에 담을 수 있고, 서버가 폐기 목록을 유지하는 설계도 가능합니다.
02API
HTTP는 “어디로 무엇을 요청하는지”의 규칙일 뿐, 구체적으로 어떤 데이터를 주고받을지는 정해 주지 않습니다. 그 약속이 API입니다.
2-1인터페이스와 API
핵심인터페이스는 “어떻게 쓰는지”만 알면 “어떻게 동작하는지”는 몰라도 되게 하는 약속입니다. API는 프로그램끼리 맺는 그 약속이고, 서버 개발자가 만드는 것은 그중 Web API입니다.
이론
인터페이스(Interface)
1차 세미나의 Payment 인터페이스를 떠올려 봅시다. OrderService는 내부가 카드 결제인지 카카오 결제인지 몰라도 pay()를 호출하는 방법만 알면 결제할 수 있었습니다. 서로 다른 두 대상이 소통하기 위한 약속이 인터페이스입니다.
API(Application Programming Interface)
프로그램과 프로그램이 서로 기능과 데이터를 주고받기 위해 정해 둔 약속(규격)입니다. 생각보다 넓은 의미로 쓰여서 Java의 List.add()도, 운영체제의 파일 읽기도 API입니다.
서버는 약속 하나로 모든 클라이언트를 상대한다
서버는 Web API 하나만 잘 만들어 두면 iOS·Android·Web 어떤 클라이언트든 그 약속만 지키면 사용할 수 있습니다. HTTP가 메서드·상태 코드의 공통 의미를 정해도, title 필드가 필수인지, 최대 길이가 얼마인지, 누락 시 무엇을 반환할지는 API에서 정해야 합니다.
비유
식당 메뉴판: 메뉴를 고르면 음식이 나온다는 것만 알면 되고, 주방의 레시피와 조리 과정은 몰라도 됩니다. 메뉴판이 API, 주방이 서버 내부 구현입니다.
예시
interface Payment {
void pay(int amount); // 쓰는 쪽은 이 약속만 안다
}
게시글 생성 API 계약
아래 표(테이블)처럼 메서드·경로·요청·검증·성공·실패를 문서로 합의해 두면 Web·iOS·Android가 같은 API를 사용할 수 있습니다. 필드 이름이나 타입을 바꿀 때도 기존 호출자에 미칠 영향을 계약 단위로 검토합니다.
기술 도메인
API 문서에는 경로 · 메서드 · 헤더 · 요청 필드 · 상태 코드 · 응답 필드 · 오류 형식을 합의합니다. 실무에서는 Swagger(OpenAPI), Postman 컬렉션 등으로 공유합니다.
테이블
일상 속 인터페이스
인터페이스
사용자가 아는 것
사용자가 몰라도 되는 것
자판기 버튼
버튼을 누르면 음료가 나온다
내부 기계 장치, 재고 관리 방식
콘센트
플러그를 꽂으면 전기가 들어온다
발전소, 송전망
식당 메뉴판
메뉴를 고르면 음식이 나온다
주방의 레시피, 조리 과정
API의 종류
종류
설명
예시
라이브러리/언어 API
프로그래밍 언어나 라이브러리가 제공하는 기능
List.add(), String.length() (Java API)
OS API
운영체제가 프로그램에게 제공하는 기능
파일 읽기/쓰기, 스레드 생성 (시스템 콜)
Web API
네트워크(HTTP)를 통해 다른 서버의 기능을 사용
카카오 로그인, 토스 결제, 공공데이터 API
게시글 생성 API 계약 예시
항목
합의 내용
메서드·경로
POST /api/v1/posts
인증
Authorization: Bearer 토큰 (인증 도입 시)
요청
application/json; title, content 문자열
검증
title은 공백 불가; 길이 제한은 프로젝트 규칙으로 명시
성공
201 Created + Location + 생성된 게시글 표현
실패
400 입력 오류, 401 인증 실패 등의 코드·응답 형식
2-2Web API의 통신 방식
핵심Web API를 설계하는 방식에는 REST · SOAP · GraphQL · gRPC가 있고, 대부분의 웹/앱 서비스는 REST를 씁니다. 우리가 앞으로 구현할 것도 REST입니다.
이론
네 가지 방식은 종류가 조금씩 다르다
REST: HTTP를 그대로 활용하는 자원 중심의 아키텍처 스타일
SOAP: XML 기반의 엄격한 메시징 규약 (WSDL 명세)
GraphQL: 클라이언트가 필요한 필드를 고르는 쿼리 언어와 실행 모델
gRPC: HTTP/2 + Protocol Buffers 기반의 RPC 프레임워크
REST를 이해하려면 HTTP부터
REST는 “HTTP를 잘 쓰자”는 약속이기 때문에 앞에서 HTTP를 먼저 배웠습니다.
비유
REST는 정해진 메뉴 주문, SOAP는 도장까지 찍는 엄격한 주문서, GraphQL은 필요한 재료만 골라 담는 샐러드 바, gRPC는 주방 직원끼리의 빠른 내선 전화입니다.
예시
같은 게시글을 요청하는 서로 다른 방식
REST: URI로 대상을 표현
GET /posts/42
GraphQL: 필요한 필드를 쿼리로 선택
query {
post(id: "42") {
id
title
}
}
어느 방식이든 인증·검증·오류 처리 계약은 따로 필요합니다.
기술 도메인
gRPC는 보통 HTTP/2와 Protocol Buffers(바이너리)를 써서 서버 ↔ 서버(마이크로서비스) 통신에 많이 쓰이고, SOAP는 금융권·레거시 기업 시스템에서 WSDL 계약과 함께 쓰입니다.
테이블
Web API 방식 한눈에 보기
방식
특징
주 사용처
REST
HTTP를 그대로 활용, 자원 중심, 주로 JSON
대부분의 웹/앱 서비스
SOAP
XML 기반, 엄격한 규격과 명세(WSDL)
금융권, 레거시 기업 시스템
GraphQL
클라이언트가 필요한 데이터를 쿼리로 골라서 요청
화면마다 필요한 데이터가 다양한 서비스
gRPC
HTTP/2 + Protocol Buffers(바이너리), 매우 빠름
서버 ↔ 서버(마이크로서비스) 간 통신
REST·SOAP·GraphQL·gRPC를 구체적으로 비교하기
방식
계약과 메시지
장점이 드러나는 상황
고려할 점
REST 스타일 HTTP API
URI·메서드·미디어 타입
표준 HTTP 도구·캐시 활용
화면에 따라 여러 호출이 필요할 수 있음
SOAP
XML 메시지, WSDL 계약과 관련 표준
엄격한 계약과 표준 기능이 필요한 연동
메시지와 도구 구성이 복잡할 수 있음
GraphQL
스키마·쿼리·필드 선택
화면마다 필요한 데이터가 다른 경우
쿼리 비용·권한·캐시 설계 필요
gRPC
서비스·메서드 정의, 보통 protobuf
타입 기반 코드 생성과 서버 간 통신
브라우저 연동과 운영 도구 제약 검토
보완“gRPC는 매우 빠르다”처럼 성능을 단정하기보다 데이터 크기·네트워크·구현에 따라 달라진다고 보는 것이 정확합니다. REST도 JSON만 써야 하는 것은 아닙니다.
03REST API
HTTP만으로는 “어떻게 설계하라”까지 알 수 없습니다. HTTP의 장점을 살려 일관되게 설계하자는 약속이 REST입니다.
GET /getAllPosts, POST /deletePost?id=1 같은 API도 HTTP로 동작은 합니다. 하지만 개발자마다 규칙이 제각각이라 문서 없이는 무엇을 하는 API인지 알기 어렵습니다.
REST(REpresentational State Transfer)
자원을 이름(URI)으로 구분하고, 그 자원의 상태를 표현(Representation)으로 주고받는 소프트웨어 아키텍처 스타일입니다. 2000년 HTTP 명세의 주요 저자인 로이 필딩이 박사 논문에서 제안했고, 이 원칙을 잘 지킨 API를 RESTful API라고 부릅니다.
REST = 세 가지 구성 요소
자원(Resource) — 무엇을 → URI (/posts/1)
행위(Verb) — 어떻게 → HTTP 메서드 (GET, POST, DELETE)
표현(Representation) — 어떤 형태로 → 바디 + Content-Type (JSON, XML)
POST /deletePost처럼 URI에 동사를 넣으면 메서드와 URI가 하는 일이 겹치게 됩니다.
비유
도서관에서 책 번호(URI)로 대상을 고르고, 대출·반납(메서드)으로 할 일을 정합니다. 책 번호에 “대출하기”를 적어 넣지 않습니다.
예시
행위 중심 URI → 자원 중심 URI
✗ URI에 동사가 들어감
GET /getAllPosts
POST /createPost
POST /deletePost?id=1
GET /post_update?id=1&title=hi
✓ 자원 + 메서드
GET /posts
POST /posts
DELETE /posts/1
PATCH /posts/1 {"title":"hi"}
DELETE /posts/1
└─ 행위 ─┘ └─ 자원 ─┘ → "1번 post를 삭제한다"
기술 도메인
자원은 DB 테이블과 반드시 1:1이 아닙니다. 사용자에게 의미 있는 대상을 모델링합니다. 응답 JSON은 자원 그 자체가 아니라 그 시점의 자원 상태를 나타낸 표현입니다. URI는 복수형 명사로 일관되게 짓습니다.
테이블
REST의 구성 요소
구성 요소
의미
표현 방법
예시
자원(Resource)
무엇을
URI
/posts/1
행위(Verb)
어떻게
HTTP 메서드
GET, POST, DELETE
표현(Representation)
어떤 형태로
바디 + Content-Type
JSON, XML
행위 중심 경로를 자원 중심으로 바꾸기
기존 설계
개선 예시
이유
GET /getAllPosts
GET /posts
목록이라는 자원과 조회 의미 분리
POST /createPost
POST /posts
컬렉션에 생성 요청
POST /deletePost?id=1
DELETE /posts/1
삭제 의도를 메서드에 표현
GET /post_update?id=1&title=hi
PATCH /posts/1 + 변경 본문
읽기 요청 GET으로 서버 상태를 변경하지 않음
보완REST는 원래 HTTP·JSON에 한정된 정의가 아니라 분산 하이퍼미디어 시스템의 아키텍처 스타일입니다. 다만 필딩이 HTTP 표준화에 핵심적으로 참여했기 때문에 HTTP와 잘 맞고, “자원·행위·표현”은 HTTP API를 설명하는 유용한 요약입니다.
3-2REST의 6가지 제약
핵심필딩이 제시한 6가지 조건 중 Code on Demand만 선택입니다. 개발하면서 가장 신경 쓸 것은 Uniform Interface(→ 3-3)입니다.
이론
❶ Client-Server
클라이언트(UI·사용자 경험)와 서버(API·비즈니스 로직)의 관심사를 분리해 서로 독립적으로 발전할 수 있게 합니다.
❷ Stateless
서버는 클라이언트의 상태(컨텍스트)를 저장하지 않고, 요청에 처리에 필요한 정보가 모두 담겨야 합니다. HTTP의 무상태성과 같은 이야기라 서버를 늘리기 쉽습니다.
Q. 세션을 쓰면 무상태가 아닌 거 아닌가요? REST 관점에서는 엄밀히 제약을 어기는 것이 맞습니다. 하지만 HTTP 프로토콜 자체는 여전히 상태를 저장하지 않으므로 HTTP의 무상태 특성이 깨지는 것은 아닙니다. 그리고 우리는 REST API를 만드는 게 목적이 아니라 서비스를 만드는 것이 목적이므로, 요구사항에 따라 제약을 완화할 수 있습니다.
❸ Cacheable
응답은 캐시 가능 여부를 명시해야 합니다. 여기서 캐시는 서버 내부의 Redis가 아니라 브라우저·프록시·CDN 등 응답을 받는 쪽의 캐시로, 요청이 서버에 도달하기 전에 응답해 트래픽 자체를 줄이는 것이 목적입니다. 예: Cache-Control: max-age=3600
❹ Uniform Interface
모든 자원을 같은 방식으로 다룬다는 REST의 가장 핵심적인 제약입니다. → 3-3에서 자세히
❺ Layered System
클라이언트는 상대가 실제 서버인지 중간의 프록시·로드밸런서인지 알 필요가 없습니다. 1차 세미나의 Web Server(Nginx) → WAS 구조가 가능한 이유입니다.
❻ Code on Demand (선택)
서버가 클라이언트에 실행 가능한 코드(JavaScript 등)를 보낼 수 있습니다. 유일한 선택 조건입니다.
비유
창구와 손님의 역할을 나누고(Client-Server), 매번 완결된 주문서를 쓰고(Stateless), 자주 묻는 안내문은 미리 붙여 두고(Cacheable), 모든 창구가 같은 양식을 쓰며(Uniform), 손님은 접수 데스크 뒤 구조를 몰라도 되는(Layered) 민원실입니다.
예시
캐시: 재사용과 재검증은 다르다
HTTP/1.1 200 OK
Cache-Control: private, max-age=60 // 60초간 그대로 재사용 가능
ETag: "post-42-v3"
// 60초 후: 바뀌었는지만 물어보기(재검증)
GET /posts/42 HTTP/1.1
If-None-Match: "post-42-v3"
// 안 바뀌었으면 본문 없이 응답
HTTP/1.1 304 Not Modified
ETag: "post-42-v3"
no-cache는 “저장 금지”가 아니라 “재사용 전에 검증하라”이고, 저장 자체를 막는 것은 no-store입니다.
기술 도메인
Layered System ≠ 레이어드 아키텍처
REST의 계층은 클라이언트 → CDN → 리버스 프록시 → 애플리케이션처럼 네트워크 구성요소 사이의 경계입니다. Controller → Service → Repository는 프로그램 내부의 코드 구조입니다. 이름은 비슷하지만 관점이 다릅니다.
테이블
6가지 제약이 해결하는 문제
제약
정확히 나누는 것
얻는 효과·비용
Client-Server
사용자 인터페이스와 서버 데이터·처리 책임
계약을 유지하면서 독립적으로 진화
Stateless
요청 처리에 필요한 세션 문맥을 요청에 포함
분산·복구에 유리; 요청 정보 반복 비용
Cacheable
응답이 재사용 가능한지 판단할 정보 제공
지연·트래픽 감소; 오래된 데이터 제어 필요
Uniform Interface
공통된 자원·메시지·전이 규칙
구성요소 결합 완화; 일반화에 따른 비용
Layered System
직접 연결한 계층 밖의 내부 구조를 숨김
프록시·보안·로드밸런싱 도입
Code on Demand
필요할 때 실행 코드를 내려 기능 확장
클라이언트 확장; 가시성 감소, 선택 제약
보완실제로 모든 제약(특히 HATEOAS)을 엄밀히 지키는 API는 많지 않습니다. 일반적으로 “REST API”라고 하면 자원 중심 URI + HTTP 메서드 + 적절한 상태 코드 + JSON 정도를 의미합니다. 제약을 완화한 API를 엄밀한 의미의 REST라고 부르기는 어렵다는 점만 구분해 두면 됩니다.
3-3Uniform Interface와 일관된 응답 구조
핵심모든 자원을 같은 방식으로 다룹니다. 실무에서는 여기에 공통 응답 구조와 일관된 네이밍까지 맞추면 클라이언트 협업이 훨씬 쉬워집니다.
이론
네 가지 세부 조건
URI로 자원을 식별한다.
표현(JSON 등)을 통해 자원을 조작한다.
메시지만 보고도 어떻게 처리할지 알 수 있다 (Self-descriptive).
응답에 다음 행동으로 갈 수 있는 링크를 포함한다 (HATEOAS).
한 발 더: 일관된 응답 구조
공통된 응답 구조와 일관된 데이터 형태(네이밍 등)를 유지하는 것도 균일한 인터페이스에 포함된다고 보는 시각이 있습니다. 클라이언트는 서버 통신 코드를 공통 응답 구조로 한 번 추상화할 수 있어서 협업 관점에서 정말 중요합니다.
비유
지하철 안내판이 역마다 같은 양식으로 현재 위치와 갈 수 있는 출구(링크)를 보여 주면, 처음 가는 역에서도 헤매지 않습니다.
예시
✗ 성공/실패마다 모양이 다름
// 성공
{
"id": 42,
"title": "2차 세미나 후기",
"createdAt": "2026-10-04T14:00:00"
}
// 실패 (400)
{
"code": "POST_TITLE_BLANK",
"message": "게시글 제목은 비어 있을 수 없습니다."
}
✓ 모든 응답이 같은 봉투
// 201 Created
{
"code": "POST_CREATED",
"message": "게시글이 생성되었습니다.",
"data": {"id": 42, "title": "세미나 후기"}
}
// 400 Bad Request
{
"code": "POST_TITLE_BLANK",
"message": "게시글 제목은 비어 있을 수 없습니다.",
"data": null
}
공통 봉투를 쓰더라도 상태 코드는 실제 결과를 반영해야 합니다. 오류일 때 data를 null로 둘지 생략할지도 계약에 명시하고, 204를 선택했다면 봉투도 보내지 않습니다.
테이블
균일한 인터페이스의 네 가지 세부 조건
조건
의미
공부할 예시
자원 식별
URI 등으로 대상을 식별
/posts/42
표현을 통한 조작
표현과 메타데이터를 통해 상태 변경
JSON 표현을 PUT으로 전달
자기 서술적 메시지
표준 의미와 미디어 타입 등으로 처리 의미 파악
메서드·상태·Content-Type·캐시 정보
HATEOAS
표현의 하이퍼미디어를 따라 가능한 상태 전이 선택
다음 페이지, 취소 가능 작업 등의 링크
보완공통 JSON 봉투는 협업을 위한 좋은 관례이지 REST 제약 자체는 아닙니다. 성공·실패 구조가 다르다고 REST 위반인 것도, 링크를 넣는다고 HATEOAS가 완성되는 것도 아닙니다.
04Spring
API를 어떻게 설계하는지 알았으니, 이제 어떻게 구현할지 봅니다. Spring의 네 가지 특징 IoC · DI · AOP · POJO와 Spring Boot를 차례로 정리합니다.
4-1Spring이란? 라이브러리 vs 프레임워크
핵심Spring은 객체를 대신 만들고 연결해 주는 컨테이너를 중심으로, 개발자가 비즈니스 로직에 집중하게 해 주는 프레임워크입니다. 라이브러리는 내가 호출하고, 프레임워크는 내 코드를 호출합니다.
이론
Spring
자바 엔터프라이즈 애플리케이션 개발을 편하게 해 주는 오픈소스 경량 프레임워크입니다. 단순히 기능을 모아 둔 라이브러리가 아니라 애플리케이션의 객체와 실행 흐름을 관리합니다.
제어 흐름의 주인이 다르다
라이브러리: 내가 필요할 때 가져다 호출합니다. (Jackson, Lombok, Collections.sort)
프레임워크: 정해진 틀 안에 내 코드를 끼워 두면, 알맞은 때에 프레임워크가 호출합니다. (Spring MVC, Spring Security)
이 “호출의 방향이 뒤집히는 것”이 바로 다음 카드의 제어의 역전(IoC)입니다.
비유
라이브러리는 공구함(내가 꺼내 쓴다), 프레임워크는 공연 무대(무대 진행표에 따라 내 차례가 오면 불려 나간다)입니다.
예시
라이브러리: 내가 호출
Collections.sort(list);
프레임워크: Spring이 내 메서드를 호출
@RestController
public class PostController {
@GetMapping("/posts/{postId}")
public PostResponse getPost(
@PathVariable("postId") Long postId) {
return postService.findPost(postId);
}
}
getPost를 호출하는 코드는 어디에도 없습니다. GET /posts/10 요청이 오면 Spring MVC가 알아서 이 메서드를 호출합니다.
기술 도메인
개발자는 매핑(@GetMapping)과 구성(Bean 등록)으로 규칙을 선언하고, Spring은 그 규칙대로 실행합니다.
테이블
라이브러리 vs 프레임워크
라이브러리
프레임워크
제어 흐름
내가 라이브러리를 호출
프레임워크가 내 코드를 호출
예시
Jackson, Lombok
Spring MVC, Spring Security
보완원문 예시는 경로가 "/posts"인데 @PathVariable postId를 받고 있어 실제로는 동작하지 않습니다. 경로 변수를 받으려면 "/posts/{postId}"처럼 경로에 변수가 있어야 하고, 반환 타입과 Service 호출 인자도 맞춰야 합니다.
4-2IoC · 제어의 역전
핵심“어떤 객체를 만들어 누구에게 넣을지”, “어떤 메서드를 언제 호출할지”를 개발자가 아니라 Spring이 결정합니다.
이론
IoC(Inversion of Control)
애플리케이션의 제어를 개발자가 아닌 외부에서 관리하는 것입니다. 1차 과제처럼 일반 프로그램에서는 개발자가 직접 객체를 new하고 의존성을 연결하고 메서드를 호출합니다.
요청 실행: HTTP 요청에 따라 알맞은 Controller 메서드를 호출합니다. (→ Spring MVC)
비유
직접 장 보고 요리하던 사람(개발자)이 밀키트 서비스(Spring)를 쓰면, 재료 준비와 배달은 서비스가 하고 나는 조리법(비즈니스 로직)에만 집중합니다.
예시
✗ 개발자가 직접 제어
class PostService {
// 내가 생성하고 내가 선택
private final PostRepository postRepository
= new PostRepository();
}
✓ 제어의 역전
class PostService {
private final PostRepository postRepository;
// 누가 만들어 넣어 줄지 PostService는 모른다
public PostService(PostRepository postRepository) {
this.postRepository = postRepository;
}
}
조립을 누가 하는가
// 순수 Java: main에서 직접 조립
PostRepository repository = new MemoryPostRepository();
PostService service = new PostService(repository);
PostController controller = new PostController(service);
// Spring: 어노테이션만 붙이면 컨테이너가 조립
@Repository class MemoryPostRepository implements PostRepository { }
@Service class PostService { PostService(PostRepository r) { ... } }
기술 도메인
개발자는 여전히 의존성 타입, Bean 등록, URL 매핑을 정의합니다. Spring이 비즈니스 흐름을 임의로 정하는 것이 아니라 선언된 규칙을 실행하는 것이고, Service 안에서 도메인 메서드를 호출하는 순서는 여전히 애플리케이션 코드가 정합니다.
4-3DI · 의존성 주입
핵심필요한 객체를 안에서 만들지 않고 밖에서 받습니다. 그래야 인터페이스(추상화)에만 의존할 수 있어 구현 교체와 테스트가 쉬워집니다.
이론
DI(Dependency Injection)
객체가 필요한 의존성을 직접 생성하지 않고 외부에서 주입받는 것입니다. Spring에서는 IoC 컨테이너가 이 주입을 대신해 줍니다.
DI와 DIP는 왜 같이 다닐까?
1차 세미나의 의존성 역전 원칙(DIP)은 “구현체가 아닌 추상화에 의존하라”였습니다. 그런데 인터페이스는 new로 인스턴스를 만들 수 없으므로, 객체 안에서 직접 생성하면 결국 new PostMysqlRepository()처럼 구현체를 알게 됩니다. 밖에서 주입받아야 추상화에만 의존할 수 있습니다.
세 개념 정리
DIP — 추상화에 의존하라 (설계 원칙)
DI — 의존 객체를 외부에서 넣어 준다 (구현 기법)
IoC — 제어권이 프레임워크로 넘어간다 (더 큰 개념)
의존성 주입(DI)
비유
스탠드가 전구를 직접 만들어 붙박이로 넣으면 전구가 나갔을 때 스탠드째 바꿔야 합니다. 규격 소켓(인터페이스)을 두고 전구를 밖에서 끼우면(주입) LED든 백열등이든 갈아 끼울 수 있습니다.
예시
✗ 주입이 아니면 결국 구현체에 의존
class PostService {
private final PostRepository postRepository
= new PostMysqlRepository();
}
✓ 주입으로 인터페이스에만 의존
class PostService {
private final PostRepository postRepository;
public PostService(PostRepository postRepository) {
this.postRepository = postRepository;
}
}
저장소 구현과 테스트를 바꾸는 지점
interface PostRepository { void save(Post post); }
class MemoryPostRepository implements PostRepository { ... } // 실습·테스트용
class MysqlPostRepository implements PostRepository { ... } // DB 연동
PostService forPractice = new PostService(new MemoryPostRepository());
PostService forDatabase = new PostService(new MysqlPostRepository());
기술 도메인
DI는 Spring 없이도 쓸 수 있는 기법이고, Spring은 이를 자동화합니다. 같은 인터페이스의 구현체가 여러 개 Bean으로 등록되면 어떤 것을 넣을지 모호해지므로 @Primary나 @Qualifier로 선택 기준을 명시합니다.
테이블
DI·DIP·IoC를 혼동하지 않는 기준
개념
질문
예시
IoC
생성·실행의 제어를 누가 갖는가?
컨테이너가 객체를 만들고 MVC가 Controller 호출
DI
필요한 객체를 어떻게 얻는가?
생성자 인자로 Repository를 전달받음
DIP
소스 코드의 의존 방향이 어디로 향하는가?
Service가 저장 기술 대신 Repository 추상화에 의존
보완“DI와 DIP는 항상 같이 적용된다”는 엄밀히는 아닙니다. 구체 클래스도 주입할 수 있고(DI ○, DIP ✗), DIP는 추상화의 소유권과 의존 방향까지 다룹니다. 다만 DIP를 실현하려면 DI가 사실상 필요하다는 방향으로 이해하면 정확합니다.
4-4AOP · 관점 지향 프로그래밍
핵심트랜잭션·로깅처럼 여러 곳에서 반복되는 공통 관심사를 핵심 로직에서 떼어 냅니다. Spring은 프록시 + 어노테이션(@Transactional)으로 이를 제공합니다.
이론
AOP(Aspect-Oriented Programming)
여러 곳에서 반복되는 공통 관심사(aspect)를 핵심 비즈니스 로직과 분리하는 방법입니다. 직접 구현하면 같은 코드가 메서드마다 반복됩니다.
프록시가 감싼다
Spring은 원본 객체를 프록시 객체로 감싸서, 외부 호출이 프록시를 지날 때 부가 동작(트랜잭션 시작·커밋·롤백)을 실행합니다. 덕분에 구현체나 라이브러리가 바뀌어도 업무 코드는 영향을 덜 받습니다.
⚠️ self-invocation 주의
객체 내부에서 자기 메서드를 this.method()로 호출하면 프록시를 거치지 않아서 그 메서드에 붙인 AOP가 동작하지 않습니다.
비유
건물 입구 보안 게이트(프록시)는 밖에서 들어올 때만 검사합니다. 이미 안에 들어온 사람이 방에서 방으로 이동할 때(self-invocation)는 게이트를 지나지 않습니다.
예시
✗ AOP 없이: 트랜잭션 코드가 업무 코드를 덮음
public void createPost(CreatePostRequest request) {
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false); // 트랜잭션 시작
postRepository.save(connection, request); // ← 진짜 하고 싶은 일은 이 한 줄
connection.commit(); // 커밋
} catch (Exception e) {
if (connection != null) connection.rollback(); // 롤백
throw e;
} finally {
if (connection != null) connection.close();
}
}
✓ AOP: 선언만 하면 프록시가 감싸 줌
@Service
class PostService {
private final PostRepository postRepository;
@Transactional
public void createPost(CreatePostRequest request) {
postRepository.save(...);
}
}
self-invocation
외부 → [프록시: 트랜잭션 시작] → outer() ✓ advice 적용
└ this.inner() ✗ 프록시를 다시 지나지 않음
기술 도메인
inner()에 붙인 새로운 트랜잭션 전파나 로깅은 적용되지 않지만, 바깥 메서드가 이미 시작한 트랜잭션 안에서는 실행될 수 있습니다. 해결은 책임을 나눠 다른 Bean을 통해 호출하도록 바꾸는 것부터 검토합니다. 기본 롤백 대상은 RuntimeException과 Error이고, checked exception은 rollbackFor 등 별도 설정이 필요합니다.
테이블
AOP에서 자주 만나는 용어
용어
뜻
트랜잭션 예시
Aspect
분리한 횡단 관심사
트랜잭션 관리
Advice
언제 실행할 부가 동작
실행 전 시작, 성공 시 커밋, 실패 시 롤백
Pointcut
부가 동작을 적용할 호출 조건
특정 서비스 메서드에 적용
Target
원래 업무를 수행하는 객체
PostService
Proxy
호출을 가로채 부가 동작을 수행하는 객체
외부 호출이 거치는 서비스 대리 객체
보완AOP가 모든 기술 교체를 자동으로 추상화해 주지는 않습니다. @Transactional도 트랜잭션 매니저 설정과 실제 DB 연동이 있어야 의미가 있고, 실습의 인메모리 Map은 @Transactional만으로 롤백되지 않습니다.
4-5POJO · 평범한 자바 객체
핵심Spring은 특정 클래스를 상속하거나 특정 인터페이스를 구현하라고 강요하지 않습니다. 평범한 Java 객체로 개발하므로 1차 세미나의 객체지향 원칙을 그대로 지킬 수 있습니다.
이론
POJO(Plain Old Java Object)
특정 기술이나 프레임워크에 종속되지 않은 일반 Java 객체입니다. Spring 기능을 쓰면서도 Java의 객체지향적인 장점을 유지할 수 있습니다.
왜 중요할까?
도메인 객체를 일반 클래스로 두면 프레임워크 없이도 규칙을 테스트할 수 있고, 프레임워크를 바꿔도 핵심 코드가 덜 흔들립니다. 그래서 Spring은 객체지향 원칙을 지키면서 WAS를 개발할 수 있는 프레임워크입니다.
비유
특정 브랜드 전용 부품이 아니라 표준 규격 부품으로 조립하는 것과 같습니다. 작업대(프레임워크)를 바꿔도 부품은 그대로 쓸 수 있습니다.
예시
public class Post { // extends, implements 강요 없음
private Long id;
private String title;
...
}
기술 도메인
Spring 이전의 EJB 같은 기술은 특정 기반 클래스·인터페이스를 반드시 구현해야 했습니다. 어노테이션을 일부 붙인다고 해서 객체지향 설계가 사라지는 것은 아닙니다.
4-6Spring Boot
핵심Spring Boot는 Spring을 대체하는 것이 아니라 쉽게 쓰게 해 주는 도구입니다. 내장 Tomcat · Starter · 자동 구성 덕분에 설정 없이 바로 서버를 띄울 수 있습니다.
이론
Spring의 불편함
빈, DB 연결, 트랜잭션, MVC 등을 XML 또는 Java 설정으로 전부 직접 설정
라이브러리 간 버전 호환성을 직접 맞춤
.war로 빌드해 외부 Tomcat에 직접 배포
“Hello World”를 띄우기까지가 너무 오래 걸렸습니다.
Spring Boot
Spring 애플리케이션을 최소한의 설정으로 빠르게 만들고 실행하게 해 주는 도구입니다. @SpringBootApplication 한 줄로 컴포넌트 스캔과 자동 구성이 켜집니다.
비유
Spring이 재료와 주방 도구라면, Spring Boot는 재료 손질과 도구 배치가 끝난 밀키트입니다. 원하면 간을 바꿀 수 있지만(설정 변경), 기본값만으로도 바로 요리할 수 있습니다.
예시
@SpringBootApplication // 이 한 줄로 컴포넌트 스캔 + 자동 구성
public class SeminarApplication {
public static void main(String[] args) {
SpringApplication.run(SeminarApplication.class, args);
}
}
외부 설정과 실행
# src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: seminar-board
# 터미널
./gradlew bootRun # 개발 중 실행
./gradlew bootJar # 실행 가능한 jar 빌드
java -jar build/libs/생성된-파일명.jar
기술 도메인
자동 구성은 조건부로 동작합니다. 클래스패스의 라이브러리, 설정 값, 이미 등록된 Bean을 보고 필요한 Bean을 등록하고, 개발자가 같은 Bean을 직접 정의하면 자동 구성은 물러납니다. JPA를 추가해도 DataSource·드라이버·접속 설정이 맞아야 DB 연결이 완성됩니다.
테이블
Spring Boot의 주요 기능
기능
설명
내장 서버
Tomcat을 내장해, java -jar app.jar 하나로 서버 실행
Starter 의존성
spring-boot-starter-web 하나만 추가하면 웹 개발에 필요한 라이브러리를 호환되는 버전으로 한 번에
자동 구성 (Auto Configuration)
클래스패스에 있는 라이브러리를 보고 필요한 빈을 자동 등록 (예: JPA가 있으면 EntityManager 등록)
외부 설정
application.yml로 포트, DB 주소 등을 코드 밖에서 관리
운영 기능 (Actuator)
헬스 체크, 메트릭 등 모니터링 기능 제공
Spring vs Spring Boot
Spring
Spring Boot
설정
개발자가 직접
자동 구성 + 필요한 것만 변경
서버
외부 WAS에 배포
내장 Tomcat
빌드 결과
.war
실행 가능한 .jar
의존성 관리
버전 직접 관리
Starter + 버전 자동 관리
POJO의 의미와 Boot가 해결하는 설정 부담
Boot 기능
직접 준비하던 작업
Boot에서의 변화
Starter
필요한 웹 라이브러리 묶음 선정
용도별 의존성 묶음 제공
의존성 관리
라이브러리 버전 조합 관리
관리되는 버전 집합 제공
자동 구성
MVC·DataSource 등 Bean 구성
클래스패스·속성·기존 Bean 등의 조건에 따라 구성
내장 서버
별도 서버 설치와 배포 준비
실행 가능한 애플리케이션으로 구동
외부 설정
환경별 값을 코드에 하드코딩
프로퍼티·YAML·환경변수 등으로 분리
Actuator
운영 상태 확인 기능 직접 구현
추가 의존성·설정을 통해 헬스·메트릭 제공
보완“Spring = war, Boot = jar”는 절대적인 구분이 아닙니다. Spring도 Java 설정과 다양한 배포 방식을 쓸 수 있고, Boot도 war 배포를 지원합니다. 대표적인 경향으로 이해하면 됩니다.
05Spring의 내부 구조
Spring을 본격적으로 쓰기 전에, 객체(Bean)가 어떻게 만들어지고 관리되는지 봅니다. Bean → Container → Singleton → 주입 방식 순서로 이어집니다.
5-1Bean과 등록 방법
핵심Bean은 Spring Container가 생성하고 관리하는 객체입니다. 직접 만든 클래스는 @Service 같은 어노테이션(Component Scan)으로, 외부 라이브러리 객체는 @Bean으로 등록합니다.
이론
Bean이란?
일반 객체는 new로 직접 만들어 쓰지만, @Service를 붙인 클래스는 Spring Bean으로 등록되어 Container가 생명주기를 관리하고 필요한 곳에 자동으로 주입합니다.
① Component Scan — 가장 일반적인 방법
@Controller, @RestController, @Service, @Repository가 붙은 클래스를 Spring이 찾아 Bean으로 등록합니다. 애플리케이션이 시작될 때 한 번 일어나며, 이 어노테이션들은 모두 내부적으로 @Component를 기반으로 합니다.
② @Bean — 직접 등록
@Configuration 클래스 안의 @Bean 메서드가 반환하는 객체를 Bean으로 관리합니다. 단순 생성이 아니라 생성 방법을 제어해야 할 때(옵션 설정, 외부 라이브러리 객체) 주로 씁니다.
결정적인 차이
Bean으로 쓸 객체를 만들 때 별도의 설정이나 제어가 필요하냐입니다. (XML 등록 방식도 있지만 거의 쓰이지 않아 생략합니다.)
비유
Component Scan은 사원증을 단 직원을 자동으로 명단에 올리는 것, @Bean은 계약 조건을 직접 정해 외부 전문가를 초빙하는 것입니다.
예시
일반 객체
private final PostService postService
= new PostService();
postService.createPost(request);
Spring Bean
@Service
public class PostService {
...
}
@Bean: 생성 방법을 직접 제어
@Configuration
public class AppConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.findAndRegisterModules(); // 내가 원하는 설정
return mapper;
}
}
외부 객체를 Bean으로 등록해 주입받기
@Configuration
class TimeConfig {
@Bean
Clock clock() { return Clock.systemUTC(); } // Clock 소스에 @Component를 붙일 수 없음
}
@Service
class PostService {
private final Clock clock;
PostService(Clock clock) { this.clock = clock; } // 테스트에서는 고정 시계로 교체 가능
Instant now() { return Instant.now(clock); }
}
기술 도메인
@SpringBootApplication의 기본 스캔 범위는 그 클래스의 패키지와 하위 패키지입니다. 범위 밖의 클래스는 @Service를 붙여도 등록되지 않습니다. 또 new로 직접 만든 객체는 Bean이 아니며, Post 같은 도메인 객체나 요청 DTO는 Bean으로 만들 필요가 없습니다.
테이블
객체가 존재하는 것과 Bean인 것은 다릅니다
방식
등록 대상
대표 사용
Component Scan
스캔 대상의 stereotype 클래스
직접 작성한 Controller·Service·Repository
@Bean
팩토리 메서드가 반환하는 객체
외부 라이브러리 객체, 생성 옵션 제어
@Import / 명시적 설정 등록
설정 클래스 등을 로드
스캔 외 방식으로 구성 연결
보완원문은 “@Configuration도 @Component 기반이므로 @Bean 등록도 결국 Component Scan에 포함된다”고 설명합니다. 설정 클래스가 스캔으로 발견되는 경우가 많은 것은 맞지만, @Bean 등록 자체는 스캔과 별개의 메커니즘이고 설정 클래스는 @Import 등으로도 로드할 수 있습니다.
5-2Spring Container
핵심Bean = Spring이 관리하는 객체, Container = Bean을 관리하는 관리자. 코드에서 Container 역할을 하는 것이 ApplicationContext이고, 앱이 시작될 때 필요한 Bean을 미리 만들어 연결해 둡니다.
이론
Spring Container
Bean을 생성·관리하고, Bean 사이의 의존관계를 연결하고, 생명주기를 관리합니다. IoC와 DI의 주체이므로 IoC Container, DI Container라고도 부릅니다.
BeanFactory
Bean을 생성하고 관리하는 핵심 컨테이너 기능입니다. Bean의 생성, 저장, 의존성 주입을 담당합니다.
ApplicationContext
Spring Container를 대표하는 인터페이스, 즉 Container라는 개념을 코드로 동작하게 만든 객체입니다. BeanFactory를 상속받아 그 기능을 모두 제공하면서 이벤트 발행, application.yml 같은 설정(Environment) 관리, 생명주기 관리 등 부가 기능까지 제공합니다.
그래서 new를 안 써도 된다
Controller나 Service를 직접 new하지 않아도 되는 이유는, 애플리케이션이 시작될 때 Spring이 필요한 객체를 미리 준비하기 때문입니다.
Spring IoC Container의 동작IoC Container: BeanFactory와 ApplicationContext
비유
BeanFactory는 물건을 만들고 보관하는 창고, ApplicationContext는 그 창고에 안내 방송·설정 관리·출입 기록까지 더한 물류 운영 센터입니다.
예시
컨테이너가 동작하는 과정
SpringApplication.run()애플리케이션 시작
ApplicationContext 생성웹 애플리케이션에 맞는 컨테이너 구현체가 만들어짐
Bean 정보 수집Component Scan, @Bean 메서드 탐색 → “어떻게 만들지”(Bean 정의) 등록
Bean 생성 + 의존관계 연결생성자 주입이면 의존 대상이 먼저 필요: Repository → Service → Controller 순
Bean 초기화 · 후처리초기화 콜백, AOP 프록시 적용 등
애플리케이션 실행HTTP 요청을 처리할 준비 완료 (종료 시에는 종료 콜백 수행)
기술 도메인
Bean 정의(클래스·생성자 인자·scope 등 “설계도”)와 Bean 인스턴스(실제 객체)를 구분해 두면 흐름이 명확해집니다. @Transactional의 프록시는 BeanPostProcessor 같은 후처리 단계에서 적용됩니다.
보완Component Scan은 BeanFactory 인터페이스 자체의 기능이라기보다 ApplicationContext의 스캐너·설정 처리 인프라가 수행합니다. 또 “IoC Container = DI Container”는 엄밀히 같은 개념은 아니며, 모든 Bean이 시작 시점에 생성되는 것도 아닙니다(lazy·scope에 따라 다름).
5-3Singleton과 동시성
핵심Bean은 기본적으로 하나만 만들어 모든 요청이 공유합니다(Singleton). 그래서 Bean에 요청별 데이터를 필드로 저장하면 안 됩니다. 지역 변수·파라미터로 처리하는 Stateless 구조로 설계합니다.
이론
Singleton Scope
하나의 Spring Container 안에서 Bean 인스턴스를 하나만 만들어 공유합니다. 요청이 초당 1,000번 오는데 매번 PostService를 새로 만들면 메모리 사용량과 객체 생성 비용, GC 부담이 커지기 때문입니다.
문제: 스레드가 객체를 공유한다
Spring MVC는 기본적으로 요청마다 다른 스레드(Thread per Request)로 처리하고, 스레드들은 Heap을 공유합니다. 즉 모든 스레드가 같은 Bean 객체를 동시에 씁니다. 1차 세미나의 동시성 문제가 그대로 생깁니다.
해결: Stateless하게
요청별 데이터는 지역 변수나 파라미터로 다룹니다. 지역 변수는 스레드마다 따로 생기는 Stack에 있으므로 공유되지 않습니다. final Repository 필드처럼 바뀌지 않는 의존 객체 참조는 공유해도 괜찮습니다.
비유
사무실에 공용 화이트보드 하나(싱글톤 필드)만 두고 모두가 자기 이름을 덮어 쓰면 다른 사람 작업과 섞입니다. 각자 개인 메모지(지역 변수)에 쓰면 문제가 없습니다.
예시
✗ 요청별 값을 필드에 저장
@Service
public class MemberService {
private String currentMember;
public void process(String member) {
currentMember = member;
// 작업 수행
}
}
✓ 지역 변수·파라미터로 처리
@Service
public class MemberService {
public void process(String member) {
String message = "안녕하세요, " + member;
sendResult(message);
}
}
어떻게 꼬이는가
Thread A: currentMember = "Alice";
Thread B: currentMember = "Bob";
Thread A: sendResult(currentMember); // Alice가 아니라 Bob으로 처리될 수 있음!
기술 도메인
저장소처럼 의도적으로 상태를 공유해야 하는 Bean은 동시성 정책을 명시해야 합니다. ConcurrentHashMap도 “조회 후 수정”, “ID 발급 후 저장” 같은 복합 동작까지 자동으로 원자화하지는 않습니다.
테이블
Scope와 thread-safe를 구분하기
Scope
공유 범위
학습 포인트
singleton
Bean 정의당 컨테이너에서 하나
기본값; 공유 필드의 동시성 주의
prototype
요청해서 얻을 때 새 객체
singleton에 한 번 주입하면 자동으로 매번 새로 바뀌지는 않음
request
웹 요청 하나
웹 환경 전용; 요청 데이터 수명과 연결
session
HTTP 세션 하나
사용자 세션별 수명; 여러 동시 요청은 가능
보완Spring이 singleton을 쓴다고 Bean이 자동으로 thread-safe해지지는 않습니다. 또 지역 변수에 공유 가변 객체의 참조를 담았다면 그 객체까지 안전해지는 것은 아닙니다. (WebFlux·비동기 처리는 Servlet MVC와 스레드 모델이 다릅니다.)
5-4의존성 주입 방식 3가지
핵심필드 · Setter · 생성자 주입 중 생성자 주입을 씁니다. final로 불변을 보장하고, 의존성 누락과 순환 참조를 일찍 발견하며, Spring 없이도 테스트할 수 있습니다.
이론
필드 주입 비권장
Spring이 객체를 만든 뒤 필드에 넣어 줍니다. 가장 간단하지만 final을 쓸 수 없고, Spring 없이 new로 만들면 의존성이 비어 있어 테스트가 어렵습니다. 거의 쓰이지 않습니다.
수정자(Setter) 주입 제한적
객체 생성 후 Setter를 호출해 넣습니다. 역시 final을 쓸 수 없지만, 선택적이거나 바뀔 수 있는 의존성을 표현할 때 제한적으로 씁니다.
생성자 주입 권장
객체가 만들어지는 순간 생성자로 의존성을 받습니다. 생성 시점에 의존성이 결정되므로 final로 불변을 보장하고, 순환 의존을 시작 시점에 발견할 수 있습니다. 가장 많이 쓰이는 방법입니다.
비유
생성자 주입은 필수 부품이 다 있어야 조립이 끝나는 공장입니다. 부품이 빠지면 출고(애플리케이션 시작) 전에 바로 걸립니다. 필드 주입은 일단 출고한 뒤 부품을 끼우는 방식이라, 빠진 것을 늦게 발견합니다.
예시
✗ 필드 주입
@Service
public class PostService {
@Autowired
private PostRepository postRepository;
}
△ Setter 주입
@Service
public class PostService {
private PostRepository postRepository;
@Autowired
public void setPostRepository(PostRepository postRepository) {
this.postRepository = postRepository;
}
}
✓ 생성자 주입
@Service
public class PostService {
private final PostRepository postRepository;
public PostService(PostRepository postRepository) { // 생성자가 하나면 @Autowired 생략 가능
this.postRepository = postRepository;
}
}
// 테스트: Spring 없이 가짜 저장소로 바로 생성
PostService service = new PostService(new FakePostRepository());
기술 도메인
순환 의존
A의 생성자에 B가, B의 생성자에 A가 필요하면 생성 순서를 만족할 수 없어 컨테이너 생성이 실패합니다. Setter로 우회하기보다 책임이 과하게 얽혀 있는지 점검하고 공통 책임을 별도 객체로 분리하는 편이 좋습니다.
테이블
생성자 주입을 사용해야 하는 이유
이유
설명
불변성
final로 선언해 주입 이후 변경을 막을 수 있음
누락 방지
의존 객체 없이는 객체 생성 자체가 불가능 → 컴파일 시점에 발견
테스트 용이
Spring 없이도 new PostService(new FakeRepository())로 테스트 가능
순환 참조 조기 발견
애플리케이션 시작 시점에 바로 오류
보완“컴파일 시점에 발견”은 new로 직접 호출할 때의 이야기입니다. Spring이 주입할 Bean이 없거나 후보가 여러 개인 문제는 보통 컴파일이 아니라 애플리케이션 시작 시점에 드러납니다. 또 final은 참조 재할당을 막을 뿐, 참조 대상 객체의 내부 상태까지 불변으로 만들지는 않습니다.
06Spring MVC
API를 구현하기 전 마지막 단계입니다. 클라이언트의 HTTP 요청이 어떤 과정을 거쳐 Controller에 도착하고, 다시 응답이 되는지 따라가 봅니다.
6-1Tomcat과 Spring MVC
핵심Tomcat은 Servlet이 실행될 수 있는 환경(WAS)을 제공하고, Spring MVC는 그 위에서 HTTP 요청을 처리하는 웹 프레임워크입니다.
이론
Spring MVC
Spring에서 웹 애플리케이션의 HTTP 요청과 응답을 처리하는 프레임워크입니다. GET /posts/10 요청을 알맞은 Controller와 연결하고, Controller의 결과를 다시 HTTP 응답으로 만들어 줍니다.
역할 구분
Tomcat: 네트워크로 들어온 HTTP 요청을 Servlet 요청·응답 객체로 만들어 Servlet에 전달합니다.
Spring MVC: 그 Servlet 환경 위에서 DispatcherServlet을 중심으로 요청을 처리합니다.
Spring Boot와 내장 Tomcat
Spring Boot가 내장 Tomcat을 포함한 실행 환경을 구성해 주므로 Tomcat을 따로 설치하지 않아도 앱을 실행할 수 있습니다.
비유
Tomcat은 건물과 전기·수도 설비, Spring MVC는 그 건물 1층에 차린 민원 접수 창구 시스템입니다. 건물이 있어야 창구를 열 수 있고, 창구가 손님을 담당 부서로 안내합니다.
모든 요청을 하나의 진입점으로 모은 뒤 알맞은 Controller에 전달하는 Front Controller 패턴의 구현체입니다. 직접 모든 일을 하지 않고, 각 구성요소에 작업을 위임하면서 전체 흐름을 조율합니다.
❷ HandlerMapping — “이 요청은 누가 처리하지?”
GET /posts 요청이 오면 @GetMapping("/posts")가 붙은 Handler를 찾아 줍니다. 여기서 Handler는 Controller 클래스만이 아니라 실행할 메서드까지 포함하는 개념입니다.
❸ HandlerAdapter — “찾은 Handler를 실행”
Handler의 종류마다 실행 방법이 다를 수 있는데, HandlerAdapter가 이를 추상화해서 DispatcherServlet이 모든 실행 방식을 알 필요가 없게 합니다.
❹ Argument Resolver — “파라미터에 무슨 값을 넣지?”
Controller 메서드의 파라미터 어노테이션과 타입을 보고, HTTP 요청에서 값을 꺼내 넣어 줍니다. 덕분에 개발자는 요청을 직접 파싱하지 않습니다. @RequestBody는 Argument Resolver가 맡되, 실제 JSON ↔ Java 객체 변환은 HttpMessageConverter가 담당합니다.
❺ Controller
HTTP 요청을 받아 애플리케이션의 처리 로직(Service)으로 연결하는 진입점입니다. 메서드가 실행될 때 파라미터에는 이미 Argument Resolver가 추출한 값이 들어 있습니다.
Spring MVC 구조
비유
병원에 비유하면 DispatcherServlet = 접수처, HandlerMapping = “정형외과 3번 진료실로 가세요” 안내, HandlerAdapter = 진료실 연결, Argument Resolver = 진료에 필요한 서류(차트·검사 결과) 준비, Controller = 의사입니다.
예시
GET /posts/10?keyword=spring 요청을 따라가 보기
DispatcherServlet요청을 받고 처리 흐름 시작
HandlerMapping@GetMapping("/posts/{postId}")가 붙은 PostController.getPost를 찾음
HandlerAdapter찾은 Handler 메서드를 실행할 준비
Argument Resolver@PathVariable postId ← 10, @RequestParam keyword ← "spring" (문자열 "10" → Long 10 변환 포함)
Controller 실행getPost(10L, "spring") 호출 → Service로 연결
@GetMapping("/posts/{postId}")
public PostResponse getPost(
@PathVariable("postId") Long postId,
@RequestParam("keyword") String keyword
) {
return postService.findPost(postId, keyword);
}
JSON 본문이 객체로 바뀌는 위치
POST /posts
Content-Type: application/json
{"title":"MVC 학습","content":"요청 흐름 확인"}
@PostMapping("/posts")
public PostResponse create(@RequestBody CreatePostRequest request) { ... }
// Body bytes → HttpMessageConverter → CreatePostRequest → Controller 호출
기술 도메인
HandlerMapping은 경로뿐 아니라 HTTP 메서드·consumes·produces 같은 조건도 봅니다. 어노테이션 Controller는 보통 RequestMappingHandlerMapping / RequestMappingHandlerAdapter가 담당합니다. 타입 변환이나 필수 값 확인이 실패하면 Controller 본문이 실행되기 전에 400 오류가 날 수 있습니다(예: /posts/abc).
테이블
요청 데이터와 매핑되는 Controller 파라미터
Controller 파라미터
HTTP 요청의 어디에서 가져오는가?
예시
@PathVariable
URL Path
/posts/10 → 10
@RequestParam
Query Parameter
?keyword=spring → "spring"
@RequestHeader
HTTP Header
Authorization: Bearer ...
@CookieValue
Cookie
Cookie: sessionId=abc
@RequestBody
HTTP Request Body
JSON → Java 객체
바인딩 어노테이션의 입력 위치와 실패 지점
어노테이션
입력 위치
주의할 점
@PathVariable
경로 템플릿의 변수
/posts/abc를 Long으로 변환하면 오류
@RequestParam
쿼리·form 파라미터
필수 여부·기본값·빈 문자열 정책
@RequestHeader
요청 헤더
헤더 누락과 인증 실패의 책임 구분
@CookieValue
요청 쿠키
쿠키의 존재와 세션 유효성은 다름
@RequestBody
요청 본문
메시지 컨버터로 역직렬화
@ModelAttribute
주로 쿼리·form 값을 객체에 바인딩
JSON 본문 변환과 다름
@RequestPart
multipart의 특정 part
파일 또는 part별 본문 변환
보완모든 네트워크 요청이 무조건 DispatcherServlet을 지나는 것은 아닙니다. Servlet 매핑과 Filter·Security의 선행 처리에 따라 그 전에 응답이 끝날 수 있습니다. 또 @RequestBody(required = true)는 “본문이 반드시 있어야 한다”는 뜻이지 DTO의 모든 필드가 필수가 된다는 뜻은 아닙니다.
6-3응답이 만들어지는 두 갈래
핵심JSON 응답은 HttpMessageConverter가 객체를 JSON으로 바꿔 바로 씁니다. HTML 응답(SSR)은 Controller가 view 이름을 돌려주면 ViewResolver가 View를 찾아 렌더링합니다. REST API 서버는 주로 JSON 갈래를 씁니다.
Controller는 HTML의 이름(view name)을 반환합니다. DispatcherServlet은 ViewResolver에게 view name에 맞는 View를 찾게 하고, 찾은 View에 Model 데이터를 넘겨 렌더링한 HTML을 응답에 씁니다.
@RestController = @Controller + @ResponseBody
@Controller에서 반환한 String은 보통 뷰 이름, @RestController에서는 응답 본문으로 처리됩니다.
비유
JSON 응답은 재료(데이터)를 포장해 그대로 배달하는 것, HTML 응답은 주방에서 완성된 요리(페이지)로 만들어 내보내는 것입니다. 앱·프론트엔드는 재료를 받아 직접 화면을 그립니다.
예시
JSON 응답 (REST API)
Controller객체 반환
HttpMessageConverter객체 → JSON
HttpServletResponse응답 본문에 기록
Tomcat → ClientHTTP 응답 전송
HTML 응답 (SSR)
Controllerview name + Model 반환
DispatcherServlet → ViewResolver이름으로 View 탐색
View.render(model)HTML 렌더링
Tomcat → ClientHTTP 응답 전송
// JSON/API 응답
@RestController
class PostApiController {
@GetMapping("/api/posts/{id}")
PostResponse get(@PathVariable("id") Long id) {
return service.findById(id); // → JSON 본문
}
}
// 서버에서 템플릿 렌더링 (템플릿 엔진 설정 필요)
@Controller
class PostPageController {
@GetMapping("/posts/{id}")
String page(@PathVariable("id") Long id, Model model) {
model.addAttribute("post", service.findById(id));
return "posts/detail"; // → 뷰 이름
}
}
기술 도메인
반환 값은 HandlerMethodReturnValueHandler가 해석합니다. ResponseEntity를 쓰면 본문뿐 아니라 상태 코드와 헤더도 정할 수 있습니다. 처리 중 예외는 HandlerExceptionResolver 체계로 넘어가며, @ExceptionHandler·@RestControllerAdvice도 이 흐름과 연결됩니다(→ 0-2).
보완HttpMessageConverter가 “새 응답 객체를 만들어 Tomcat에 반환”한다기보다, Servlet API의 HttpServletResponse응답 스트림에 데이터를 쓰는 것입니다. 또 ViewResolver는 정적 HTML 파일만 찾는 것이 아니라 템플릿 엔진(Thymeleaf 등)의 View를 찾습니다.
07실습 · Spring으로 마이그레이션
세미나 내용을 바탕으로 1차 과제를 Spring으로 옮깁니다. 역할과 책임에 따라 객체를 잘 나눠 두었다면 네 단계로 끝나는 쉬운 과정입니다.
STEP 1빌드 설정과 진입점
핵심build.gradle에 Boot 플러그인 2개 + webmvc 의존성을 추가하고, Main에 @SpringBootApplication과 SpringApplication.run()을 넣으면 Spring 서버가 뜹니다.
이론
바뀌는 것은 두 파일뿐
build.gradle: Spring Boot 플러그인, 의존성(버전) 관리 플러그인, Spring WebMVC 의존성 추가
Main: 설정 자동화 어노테이션(@SpringBootApplication)과 실행 코드(SpringApplication.run) 추가
이렇게 간단한 변경만으로 Spring을 실행할 수 있게 해 주는 것이 바로 Spring Boot입니다.
순서
build.gradle 수정 → Gradle 동기화 (실행 전에 한 번 필요)
Main 수정 → 실행
브라우저에서 http://localhost:8080 접속 → 아래 화면이 보이면 성공
비유
기존 작업장(도메인·서비스 코드)은 그대로 두고, 건물에 접수 시스템과 전기 설비(Spring Boot + 내장 Tomcat)만 새로 설치하는 공사입니다.
예시
build.gradle — 초록색 줄이 추가된 부분
plugins {
id 'java'
id 'org.springframework.boot' version '4.0.8' id 'io.spring.dependency-management' version '1.1.7'}
group = 'org.sopt'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' testImplementation platform('org.junit:junit-bom:6.0.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}
Main.java
package org.sopt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplicationpublic class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args); }
}
패키지 배치
src/main/java/org/sopt/
Main.java ← 최상위 패키지에 둔다
controller/PostController.java
service/PostService.java
repository/PostRepository.java
domain/Post.java
dto/CreatePostRequest.java, UpdatePostRequest.java, PostResponse.java
src/main/resources/
application.yml
static/index.html ← 필요할 때 추가
localhost:8080 접속 결과 — 오류처럼 보이지만 성공입니다
기술 도메인
위 화면은 오류처럼 생겼지만 서버는 정상 실행되었고, 우리 게시판이 루트(/)에 정적 페이지를 제공하지 않아 404가 난 것입니다. 마음에 안 들면 src/main/resources/static/index.html을 만들어 꾸미면 됩니다. 서버가 아예 안 떴다면 브라우저는 “연결할 수 없음”을 보여 줍니다.
@SpringBootApplication의 스캔 범위는 org.sopt와 하위 패키지이므로, 다른 최상위 패키지에 둔 클래스는 @Service를 붙여도 발견되지 않을 수 있습니다.
보완Boot 4.0.8은 세미나 자료의 실습 기준 버전입니다. 사용하는 JDK·Gradle이 선택한 Boot 버전과 호환되는지 공식 요구 사항을 확인하세요. 테스트 의존성도 Boot가 관리하는 버전을 따를지, 원문처럼 JUnit BOM을 명시할지 한 가지로 정하는 것이 좋습니다.
STEP 2Bean 등록과 생성자 주입
핵심각 계층 클래스에 @RestController · @Service · @Repository를 붙이고, 직접 new하던 연결을 생성자 주입으로 바꿉니다.
이론
계층별 어노테이션
Controller → @RestController, Service → @Service, Repository → @Repository. 그 밖에 Bean으로 등록할 클래스는 @Component만 붙이면 Component Scan으로 등록됩니다.
new 제거 → 생성자로 받기
Controller 안의 new PostService(), Service 안의 new PostRepository()를 지우고 각각 생성자 인자로 받습니다. Repository가 인터페이스라면 @Repository는 구현 클래스에 붙입니다.
콘솔 UI 걷어 내기
Scanner·System.out 기반 입출력은 Controller에서 제거하고, 요청 DTO와 응답 DTO로 바꿉니다. Post와 DTO는 요청마다 만들어지는 데이터 객체이므로 Bean으로 만들지 않습니다.
비유
직원(Bean)을 인사 명단에 등록하면, 회사(Container)가 각자에게 필요한 동료를 배정(주입)해 줍니다.
예시
// Controller
@RestController
public class PostController {
private final PostService postService; // 생성자 주입
public PostController(PostService postService) {
this.postService = postService;
}
...
}
// Service
@Service
public class PostService {
private final PostRepository postRepository; // 생성자 주입
public PostService(PostRepository postRepository) {
this.postRepository = postRepository;
}
...
}
// Repository
@Repository
public class PostRepository {
// 원래 Bean은 상태를 가지면 안 되지만, 아직 DB가 없으니 여러 스레드가 공유하는 저장소로 활용
private final Map<Long, Post> posts = new HashMap<>();
...
}
요청·응답 DTO
public record CreatePostRequest(String title, String content) {}
public record UpdatePostRequest(String title, String content) {}
public record PostResponse(Long id, String title, String content) {}
// Controller가 기대하는 Service 동작 (구현은 1차 과제에서 이관)// PostResponse create(CreatePostRequest request)// List<PostResponse> findAll(int page)// PostResponse findById(Long id)// PostResponse update(Long id, UpdatePostRequest request)// void delete(Long id)
기술 도메인
인메모리 Map은 서버를 재시작하면 사라지고, 여러 요청이 동시에 접근하므로 동시성 문제도 남아 있습니다(→ 5-3). DTO 선언만으로 공백·길이 검증이 적용되지는 않습니다. record는 JDK 16 이상에서 사용할 수 있습니다.
보완“상태 있는 Bean은 무조건 금지”는 아닙니다. 저장소의 Map은 의도된 공유 상태이고, 대신 별도의 동시성 설계가 필요합니다. 예제의 HashMap을 운영 환경에서 안전한 저장소로 간주하지 않습니다. (원문은 Map<Integer, Post>이지만 Controller의 Long postId와 맞추려면 키 타입을 통일하세요.)
클래스의 @RequestMapping("/api/v1/posts")와 메서드의 @GetMapping("/{postId}")가 합쳐져 최종 경로 /api/v1/posts/{postId}가 됩니다.
전용 어노테이션이 없는 메서드
Spring MVC에는 HEAD·OPTIONS·TRACE 전용 어노테이션이 없지만 @RequestMapping(method = RequestMethod.HEAD, path = "/posts")처럼 쓸 수 있습니다.
@Controller가 아니라 @RestController인 이유
과제로 직접 알아봅시다! (힌트: 6-3)
비유
하나의 “게시글 창구”(/api/v1/posts)에 조회 · 작성 · 수정 · 삭제 담당자를 각각 배치하고(매핑), 손님이 낸 서류에서 필요한 칸을 담당자 책상에 옮겨 주는 것(바인딩)입니다.
예시
완성된 PostController
@RestController
@RequestMapping("/api/v1/posts")
public class PostController {
private final PostService service;
public PostController(PostService service) {
this.service = service;
}
@PostMapping // POST /api/v1/posts
public ResponseEntity<PostResponse> create(
@RequestBody CreatePostRequest request) {
PostResponse post = service.create(request);
return ResponseEntity.created(
URI.create("/api/v1/posts/" + post.id())).body(post); // 201 + Location
}
@GetMapping // GET /api/v1/posts?page=1
public List<PostResponse> list(
@RequestParam(name = "page", defaultValue = "1") int page) {
return service.findAll(page);
}
@GetMapping("/{postId}") // GET /api/v1/posts/1
public PostResponse get(@PathVariable("postId") Long id) {
return service.findById(id);
}
@PutMapping("/{postId}") // PUT /api/v1/posts/1
public PostResponse update(@PathVariable("postId") Long id,
@RequestBody UpdatePostRequest request) {
return service.update(id, request);
}
@DeleteMapping("/{postId}") // DELETE /api/v1/posts/1
public ResponseEntity<Void> delete(@PathVariable("postId") Long id) {
service.delete(id);
return ResponseEntity.noContent().build(); // 204
}
}
기술 도메인
생성은 201 + Location, 삭제는 204를 반환해 1-5의 상태 코드 규칙을 지킵니다. ResponseEntity는 org.springframework.http, URI는 java.net, List는 java.util, 매핑 어노테이션은 org.springframework.web.bind.annotation에서 import합니다. 경로 변수 이름을 명시하면(@PathVariable("postId")) 컴파일 옵션에 덜 의존합니다. page=0이나 음수는 int로 바인딩돼도 의미상 유효하지 않으므로 범위 검증이 필요합니다.
테이블
매핑 어노테이션
HTTP 메서드
전용 매핑 어노테이션
GET
@GetMapping
POST
@PostMapping
PUT
@PutMapping
PATCH
@PatchMapping
DELETE
@DeleteMapping
메서드 파라미터 바인딩 어노테이션
어노테이션
가져오는 데이터
예시
@RequestBody
HTTP Body
JSON
@PathVariable
URL 경로 변수
/posts/{id}
@RequestParam
Query Parameter
/posts?page=1
@RequestHeader
HTTP Header
Authorization
@CookieValue
Cookie
sessionId
@ModelAttribute
Query Parameter/Form 데이터
폼 객체
@RequestPart
Multipart 요청의 특정 Part
파일 업로드
매핑과 바인딩은 서로 다른 설정입니다
기능
매핑
파라미터·응답
생성
@PostMapping
@RequestBody CreatePostRequest → 201 + Location
목록
@GetMapping
@RequestParam(page, 기본값 1) → 목록
단건
@GetMapping("/{postId}")
@PathVariable postId → 단건 또는 404
전체 수정
@PutMapping("/{postId}")
경로 ID + @RequestBody UpdatePostRequest
일부 수정
@PatchMapping("/{postId}")
부분 변경 DTO·명세 필요
삭제
@DeleteMapping("/{postId}")
경로 ID → 204, 본문 없음
보완원문 예시를 그대로 붙여 넣기 전에 확인할 점: ① public public 오타, ② 수정(PUT) 메서드에 수정할 값을 받는 @RequestBody가 빠져 있음, ③ ApiResponse·DTO·Service 메서드는 프로젝트에서 직접 정의해야 하는 타입. 또 Spring의 RequestMethod에는 CONNECT가 없습니다.
STEP 4실행 확인과 다음 과제
핵심이제 기본적인 CRUD를 제공하는 서버를 만들 수 있습니다! 🥳 생성 → 조회 → 수정 → 삭제를 실제로 호출해 상태 코드와 본문을 확인하고, 남은 문제를 다음 과제로 가져갑니다.
이론
확인 순서
시작 로그에서 애플리케이션과 Tomcat이 정상 기동했는지 확인
POST로 게시글을 생성하고 응답의 id 또는 Location 기록
그 ID로 단건 조회 → 수정 → 재조회하여 값이 바뀌었는지 확인
삭제 응답이 204이고 본문이 없는지 확인
없는 ID · 잘못된 JSON · 숫자가 아닌 ID 요청의 응답도 확인
아직 남은 문제들
서버를 껐다 켜면 모든 데이터가 사라지는데 어떡하지? 영속성
저장소 Map에 생성·수정·삭제 요청이 동시에 오면 어떡하지? 동시성
예외가 발생하면 어떻게 응답을 반환하지? 예외 응답
검색 기능은 어떻게 만들지? 검색
이 문제들은 과제와 다음 세미나에서 다룹니다.
비유
가게 문이 열렸는지(서버 기동)만 보지 말고, 실제로 주문을 넣고 받아 보는(API 호출) 단계까지 확인합니다.
예시
curl로 CRUD 호출하기
# 생성 → 201 Created + Location
curl -i -X POST http://localhost:8080/api/v1/posts \
-H 'Content-Type: application/json' \
-d '{"title":"세미나 후기","content":"Spring 시작"}'
# 목록 / 단건 → 200 OK
curl -i 'http://localhost:8080/api/v1/posts?page=1'
curl -i http://localhost:8080/api/v1/posts/1
# 수정 → 200 OK
curl -i -X PUT http://localhost:8080/api/v1/posts/1 \
-H 'Content-Type: application/json' \
-d '{"title":"수정한 제목","content":"수정한 본문"}'
# 삭제 → 204 No Content
curl -i -X DELETE http://localhost:8080/api/v1/posts/1
Postman을 쓴다면 같은 요청을 컬렉션으로 저장해 두면 이후 과제에서도 재사용할 수 있습니다.
검색: keyword, page, size 등의 계약과 빈 결과·범위 검증을 정한 뒤 저장소 조회를 확장합니다.
스스로 설명해 보기: “같은 Service가 여러 요청을 처리해도 괜찮은 이유”, “@RequestBody의 JSON은 누가 변환하는가”, “세션을 쓰는 HTTP와 REST 무상태 제약의 차이”, “생성자 주입과 DIP의 차이”를 코드와 함께 말할 수 있는지 확인합니다.
테이블
단순 호출을 넘어서 결과를 검증하기
검증 요청
기대 결과
확인할 이유
POST 유효한 제목·본문
201 + 생성 ID·Location
실제로 자원이 생성되었는지
생성 ID로 GET
200 + 입력한 값
저장과 응답 변환 연결
같은 ID로 PUT 후 GET
200 + 수정한 값
도메인 변경·저장 확인
DELETE 후 GET
204 이후 404
삭제와 없음 응답 확인
GET /posts/abc (Long ID)
보통 400
Controller 전 타입 변환 실패
지원하지 않는 메서드
405
매핑 계약 확인
서버 재시작 후 조회
인메모리라면 데이터 소실
영속 저장 필요성 확인
보완위 표는 설계 목표입니다. Bean·매핑만 추가한 지금 단계에서는 “없는 게시글” 같은 예외가 아직 404로 변환되지 않고 500이 날 수 있습니다. 결과와 목표의 차이가 곧 다음 과제입니다. (원문의 피드백 이미지는 제공된 파일이 없어 포함하지 않았습니다.)