자바 백엔드 입문 14편. 빈 Spring Boot 프로젝트에 컨트롤러 한 개를 추가해 브라우저에 'Hello, Spring' 응답을 띄우는 가장 손에 잡히는 첫 코드. Bean이 컨테이너의 직원이라는 비유로 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 14편이에요. 12편에서 만든 빈 Spring Boot 프로젝트에 컨트롤러 클래스 한 개를 추가해 브라우저에 "Hello, Spring" 응답을 띄워볼게요. Phase 1의 마지막 글이고, Phase 2 IoC 컨테이너로 넘어가는 다리예요. 여기서 처음으로 Bean이라는 단어를 손에 잡히는 형태로 만납니다.
첫 Bean을 짤 때 어렵게 느껴지는 이유
7편에서 콘솔에 "Tomcat started" 띄우고 끝났는데, 브라우저에서 응답을 받으려면 "컨트롤러" 라는 게 필요해요. 처음 마주하면 두 가지가 어색합니다.
첫째, @RestController·@GetMapping 같은 어노테이션이 마법처럼 동작해요. "이게 어떻게 알아서 HTTP 요청을 받지?" 가 안 잡혀요.
둘째, Bean이라는 단어가 처음 등장합니다. 콩? 자바 빈? 헷갈리는 이름이에요. "Bean이랑 일반 객체가 어떻게 다른가" 가 안 보입니다.
이 글에서는 Bean = Spring 컨테이너의 직원 비유로 풀어 갑니다. 끝까지 따라하면 브라우저에서 첫 응답을 받게 되고, "Bean이 도대체 뭔지" 가 머리에 박혀요.
컨트롤러 클래스 한 개 추가하기
7편에서 만든 프로젝트 (my-shop) 의 메인 클래스 옆에 새 파일을 만들어요.
src/main/java/com/example/myshop/HelloController.java:
package com.example.myshop;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, Spring";
}
}
저장하면 — DevTools가 자동으로 재시작해줘요 (콘솔에 "Restarting..." 보임). 브라우저에서 http://localhost:8080/hello 접속하면 "Hello, Spring" 이 떠요. 첫 백엔드 API 완성.
10줄짜리 코드에 마법이 다 들어 있어요. 하나씩 풀어볼게요.
@RestController와 @GetMapping 풀이
@RestController 는 두 어노테이션을 합친 거예요.
@Controller— "이 클래스로 객체 한 개 만들어 컨테이너에 등록해 줘 + HTTP 요청 처리 컨트롤러로 인식해 줘"@ResponseBody— "이 메서드가 반환하는 값은 HTTP 응답 본문으로 그대로 보내 줘"
평범한 @Controller 만 쓰면 "뷰 템플릿(HTML)을 렌더해서 반환" 이 기본 동작이에요. REST API처럼 "JSON·문자열을 그대로 응답으로 보내고 싶다" 면 @RestController 가 정답. 현대 백엔드의 80%가 이 패턴이에요.
@GetMapping("/hello") 는 "HTTP GET 요청이 /hello 경로로 오면 이 메서드를 실행해 줘" 라는 메모예요. 비슷한 친구들이 있어요.
| 어노테이션 | HTTP 메서드 | 용도 |
|---|---|---|
@GetMapping | GET | 데이터 조회 |
@PostMapping | POST | 데이터 생성 |
@PutMapping | PUT | 데이터 전체 수정 |
@PatchMapping | PATCH | 데이터 부분 수정 |
@DeleteMapping | DELETE | 데이터 삭제 |
REST API의 기본 5종 세트. 19~23편 Web MVC에서 깊이 다뤄요.
Bean이 도대체 무엇인가 — 컨테이너의 직원
여기서 처음 등장하는 핵심 단어 Bean. 자바 빈(JavaBean)이라는 단어가 1990년대부터 있었고, Spring이 그 개념을 가져와 자기 식대로 재정의했어요.
Spring Bean = "Spring 컨테이너가 직접 만들고 관리하는 객체". 일반 객체와 결정적으로 다른 점이 하나 있어요 — 객체 생성을 우리가 안 하고 Spring이 해줘요. new HelloController() 같은 코드를 우리가 안 짭니다. @RestController 어노테이션이 박혀 있으면 Spring이 시작될 때 "아, 이거 Bean이구나" 하고 알아서 객체를 한 번 찍어내 컨테이너에 보관해요.
회사 비유로 풀어볼게요. Spring 컨테이너 = 회사 인사팀, Bean = 회사가 정식 채용해 인사팀이 관리하는 직원. 회사가 채용 절차를 통해 직원 한 명을 고용하면, 그 직원은 회사 어디서든 "필요할 때마다 호출되는" 자원이 돼요. 다른 부서에서 "개발팀의 김 과장 좀 빌려주세요" 하면 인사팀이 김 과장을 보내주는 그림이에요.
자바 코드에서도 똑같아요. @RestController 가 박힌 HelloController 클래스는 Spring 시작 시점에 객체 한 개로 찍혀 컨테이너에 보관돼요. 누군가 "GET /hello 요청 처리할 객체 좀 줘" 하면 컨테이너가 그 보관해둔 객체를 꺼내서 메서드를 호출해주는 거예요.
평범한 자바 객체 = 우리가 new로 만들어 우리가 관리. Spring Bean = Spring이 만들고 Spring이 관리. "누가 생명주기를 책임지나" 가 결정적 차이예요.
Spring이 시작될 때 일어나는 일
SpringApplication.run() 한 줄 호출에서 다음 5단계가 일어나요.
- Spring 컨테이너 생성 —
ApplicationContext라는 이름의 거대한 객체 보관소가 만들어짐 - 컴포넌트 스캔 —
@SpringBootApplication이 박힌 메인 클래스 패키지부터 시작해 모든 클래스 검사 - Bean 인식 —
@RestController·@Service·@Component·@Repository어노테이션 박힌 클래스 발견 - Bean 생성·등록 — 발견한 클래스로 객체 한 개씩 찍어내 컨테이너에 보관
- 내장 Tomcat 시동 — HTTP 서버 띄우고 8080 포트 열어 요청 대기
이 흐름에서 HelloController 는 3·4단계에서 만들어져 컨테이너에 보관돼요. 그 후 브라우저에서 /hello 요청이 들어오면 — Tomcat이 받음 → Spring DispatcherServlet이 처리 → @GetMapping("/hello") 박힌 메서드 찾음 → 컨테이너에서 HelloController Bean 꺼내 메서드 호출 → 반환값을 HTTP 응답으로 → 브라우저에 "Hello, Spring" 표시.
여기서 시험 함정이 하나 있어요. "우리가 직접 new HelloController() 를 해서 사용한다" 가 보기로 나오면 X. Spring Boot에서는 컨트롤러를 직접 new 하지 않습니다. Spring 컨테이너가 알아서 한 번 만들고 모든 요청에서 그 같은 객체를 재사용해요. 이 "한 번 만들어 계속 재사용" 이 Spring Bean의 기본 동작이고, Singleton Scope라고 불러요. 14편에서 다시 깊이 다뤄요.
DevTools가 도와주는 핫리로드
7편에서 추가한 Spring Boot DevTools 가 여기서 빛을 발해요. 코드를 한 줄 수정하고 저장하면, DevTools가 자동으로 다음을 처리해줍니다.
- 변경된
.class파일 감지 - Spring 컨테이너를 빠르게 재시작 (전체 JVM 재시작이 아니라 "애플리케이션 클래스 로더만" 교체 — 빠름)
- 콘솔에 "Restarting..." 출력
- 브라우저는 그대로, 다음 요청부터 새 코드 적용
서버를 직접 껐다 켤 필요가 없어요. 코드 짜고 바로 브라우저에서 결과 확인. 개발 속도 차원이 다릅니다.
한 줄 정리 — @RestController + @GetMapping 한 메서드 + 브라우저 접속 = 첫 백엔드 API. Bean = Spring 컨테이너가 직접 만들고 관리하는 객체.
시험 직전 한 번 더 — 첫 Bean 입문자가 매번 헷갈리는 것
- 첫 컨트롤러 =
@RestController+@GetMapping("/경로")박힌 메서드 @RestController=@Controller+@ResponseBody합친 어노테이션@Controller단독 = 뷰 템플릿(HTML) 렌더 기본,@RestController= 응답 본문 직접 반환- HTTP 메서드 매핑 =
@GetMapping·@PostMapping·@PutMapping·@PatchMapping·@DeleteMapping - Bean = Spring 컨테이너가 직접 만들고 관리하는 객체
- 일반 객체 = 우리가
new로 생성·관리, Bean = Spring이 생성·관리 @RestController·@Service·@Component·@Repository모두 "이 클래스로 Bean 한 개 만들어" 메모- Spring 시작 5단계 = 컨테이너 생성 → 컴포넌트 스캔 → Bean 인식 → Bean 등록 → Tomcat 시동
ApplicationContext= Spring 컨테이너의 정식 이름. 거대한 객체 보관소- 컨트롤러는 우리가
new안 함 — Spring이 자동 생성 - Spring Bean 기본 = Singleton Scope — 한 번 만들어 모든 요청에서 재사용
- Singleton이라 컨트롤러 안의 인스턴스 변수는 멀티스레드 환경에서 위험 (14편에서 깊이)
- DispatcherServlet = 모든 HTTP 요청의 진입점. 적절한 컨트롤러 찾아 호출
- 컴포넌트 스캔 시작점 =
@SpringBootApplication박힌 메인 클래스의 패키지 - 메인 클래스 패키지보다 "위" 또는 "옆" 패키지에 박힌 클래스는 스캔 X (자주 막히는 함정)
- DevTools = 코드 수정 시 자동 재시작 (전체 JVM 아니라 클래스 로더만 교체)
- 브라우저 새로고침만 누르면 새 코드 결과 확인 가능
- 기본 포트 8080 —
application.properties에서server.port=8090같이 변경 가능 application.properties또는application.yml= Spring Boot 설정 파일- "왜
@RestController클래스 안의 인스턴스 변수가 위험한가" 면접 단골 — Singleton 공유 + 멀티스레드
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 9편 — 자바 Stream API 람다
- 10편 — Maven·Gradle 의존성 관리
- 11편 — Spring Framework란
- 12편 — start.spring.io 첫 프로젝트
- 13편 — application.yml Spring Profiles
다음 글: