스프링 초급 (1) - First API: 브라우저에 응답 띄우기
프로젝트를 만들고 첫 API를 띄워 브라우저에서 응답을 확인하는 과정을 다룹니다.
스프링 초급 시리즈의 1편입니다. 전체 목차는 0편에 있습니다.
목표는 하나입니다. 브라우저 주소창에 localhost:8080/hello를 치면 글자가 뜨게 만드는 것.
별거 아닌 것 같지만, 이게 되면 서버가 떠 있고, 요청이 들어오고, 우리가 짠 코드가 실행돼서 응답이 나갔다는 뜻입니다. 웹 애플리케이션의 뼈대가 다 동작한 겁니다.
프로젝트 만들기
start.spring.io에 들어갑니다. 스프링이 공식으로 운영하는 프로젝트 생성기입니다. 폴더 구조와 빌드 설정을 대신 만들어 줍니다.
아래 화면처럼 고릅니다.
사진처럼 선택을 한 후, Dependencies에는 Spring Web 하나만 추가합니다.
GENERATE 버튼을 누르면 demo.zip이 받아집니다. 압축을 풀고 IDE에서 폴더를 엽니다. IntelliJ는 build.gradle을 보고 필요한 라이브러리를 알아서 내려받습니다. 처음에는 몇 분 걸립니다.
만들어진 것 열어보기
폴더가 이렇게 생겼습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
demo
├── build.gradle ← 어떤 라이브러리를 쓸지 적는 파일
├── settings.gradle
├── gradlew ← Gradle 실행 스크립트 (직접 열 일 없음)
├── gradle
│ └── wrapper
└── src
├── main
│ ├── java
│ │ └── com/example/demo
│ │ └── DemoApplication.java ← 여기서 시작한다
│ └── resources
│ ├── application.properties ← 설정 파일
│ ├── static
│ └── templates
└── test
└── java
└── com/example/demo
└── DemoApplicationTests.java
당장 볼 파일은 두 개입니다.
build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
plugins {
id 'java'
id 'org.springframework.boot' version '4.1.0'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
dependencies 블록에 우리가 고른 Spring Web이 spring-boot-starter-web이라는 이름으로 들어가 있습니다. starter가 붙은 건 “이거 하나 넣으면 필요한 것들이 딸려 오는 묶음”이라는 뜻입니다. 웹 서버(톰캣), JSON 변환기(Jackson), 스프링 MVC가 전부 이 한 줄에 들어 있습니다.
버전 번호를 안 적었는데도 동작합니다. io.spring.dependency-management 플러그인이 Boot 버전에 맞는 조합을 알고 있어서, 서로 안 맞는 버전이 섞이는 일을 막아줍니다.
DemoApplication.java
1
2
3
4
5
6
7
8
9
10
11
12
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
자바의 평범한 main 메서드입니다. 이 클래스를 실행하면 서버가 뜹니다.
@SpringBootApplication 하나가 여러 일을 합니다. 지금 알아야 할 건 두 가지입니다.
- 자동 설정 —
build.gradle에 뭐가 들어 있는지 보고 필요한 설정을 켭니다. 웹 스타터가 있으니 톰캣을 띄웁니다 - 컴포넌트 스캔 — 이 클래스가 있는 패키지와 그 하위 패키지를 훑어서 스프링이 관리할 클래스를 찾습니다
2번이 뒤에서 사고를 칩니다. 이번 편의 두 번째 실험에서 직접 확인합니다.
일단 실행해보기
DemoApplication의 main을 실행합니다. IDE에서 초록색 실행 버튼을 누르거나, 터미널에서 ./gradlew bootRun을 칩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.2)
INFO 12345 --- [demo] [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 21.0.3
INFO 12345 --- [demo] [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
INFO 12345 --- [demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
INFO 12345 --- [demo] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
INFO 12345 --- [demo] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
INFO 12345 --- [demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
INFO 12345 --- [demo] [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.284 seconds (process running for 1.512)
두 줄만 읽으면 됩니다.
Tomcat started on port 8080— 웹 서버가 8080번 포트에서 대기 중이다Started DemoApplication in 1.284 seconds— 정상적으로 다 떴다
그리고 로그가 여기서 멈춥니다. 프로그램이 끝난 게 아니라 요청을 기다리는 중입니다. 서버는 끄기 전까지 계속 떠 있습니다.
브라우저에서 localhost:8080을 열어봅니다.
Whitelabel Error Page. 404지만 서버는 살아 있습니다
에러 페이지지만 좋은 신호입니다. 서버가 살아 있으니까 404를 돌려준 것입니다. 죽어 있으면 연결 자체가 안 됩니다. 지금은 / 주소를 처리할 코드가 없어서 404가 났을 뿐입니다.
404는 “서버는 있는데 그 주소가 없다”입니다. 서버가 안 떠 있으면 브라우저는 “사이트에 연결할 수 없음”을 띄웁니다. 둘을 구분해야 어디를 고칠지 알 수 있습니다.
첫 엔드포인트 만들기
이제 주소 하나를 만듭니다. DemoApplication과 같은 패키지에 HelloController 클래스를 만듭니다.
위치 : src/main/java/com/example/demo/HelloController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "안녕하세요";
}
}
네 줄짜리 메서드입니다. 붙은 애노테이션 둘이 각각 이런 뜻입니다.
| 애노테이션 | 뜻 |
|---|---|
@RestController |
이 클래스는 웹 요청을 처리한다. 반환값은 그대로 응답 본문이 된다 |
@GetMapping("/hello") |
GET 방식으로 /hello에 요청이 오면 이 메서드를 실행한다 |
@RestController가 붙어 있으면 스프링이 시작할 때 이 클래스를 찾아서 주소와 메서드를 짝지어 표에 적어둡니다. 요청이 들어오면 그 표를 보고 실행할 메서드를 고릅니다. 우리가 하는 일은 표에 한 줄을 추가하는 것뿐입니다.
서버를 껐다 켭니다. 자바 코드를 고쳤으면 재시작해야 반영됩니다.
브라우저에서 localhost:8080/hello를 엽니다.
아까 404가 뜨던 자리에 우리가 만든 응답이 나옵니다
터미널에서 확인하려면 curl을 씁니다.
1
curl localhost:8080/hello
1
안녕하세요
응답 헤더까지 보려면 -i를 붙입니다.
1
curl -i localhost:8080/hello
1
2
3
4
5
6
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 15
Date: Thu, 30 Jul 2026 01:14:02 GMT
안녕하세요
200은 성공, Content-Type: text/plain은 그냥 문자열로 보냈다는 뜻입니다.
확인 1: 포트를 바꿔보기
8080이 이미 다른 프로그램에 쓰이고 있으면 서버가 안 뜹니다. 이럴 때 포트를 바꿉니다.
src/main/resources/application.properties를 엽니다. 비어 있을 겁니다. 한 줄 적습니다.
1
server.port=9000
재시작하면 로그가 달라집니다.
1
INFO 12345 --- [demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 9000 (http) with context path ''
이제 8080은 안 됩니다.
1
curl localhost:8080/hello
1
curl: (7) Failed to connect to localhost port 8080 after 3 ms: Couldn't connect to server
9000으로는 됩니다.
1
curl localhost:9000/hello
1
안녕하세요
자바 코드는 한 글자도 안 고쳤는데 동작이 바뀌었습니다. application.properties는 코드를 고치지 않고 애플리케이션의 동작을 바꾸는 자리입니다. 포트, DB 주소, 로그 레벨처럼 환경에 따라 달라지는 값이 여기 들어갑니다.
확인했으면 server.port 줄을 지우고 8080으로 돌아옵니다. 뒤에 나오는 예제는 전부 8080 기준입니다.
8080이 이미 쓰이고 있으면
Web server failed to start. Port 8080 was already in use.라는 메시지와 함께 뜨지 않습니다. 이때는 앞서 켜둔 서버가 꺼지지 않은 경우가 대부분입니다.
확인 2: 컨트롤러를 위 패키지로 옮기면
HelloController를 com.example.demo가 아니라 한 단계 위인 com.example로 옮겨봅니다.
1
src/main/java/com/example/HelloController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example; // demo가 빠졌다
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "안녕하세요";
}
}
재시작합니다. 여기가 함정입니다. 아무 에러도 안 납니다.
1
2
INFO 12345 --- [demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
INFO 12345 --- [demo] [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.192 seconds (process running for 1.427)
평소와 똑같이 떴습니다. 그런데 요청을 보내면
1
curl localhost:8080/hello
1
{"timestamp":"2026-07-30T01:16:33.408+00:00","status":404,"error":"Not Found","path":"/hello"}
404입니다. 코드는 그대로인데 주소를 못 찾습니다.
무슨 일이 일어났는지 보려면 로그 레벨을 올립니다. application.properties에 추가합니다.
1
logging.level.org.springframework.web=DEBUG
다시 요청을 보냅니다.
1
2
DEBUG o.s.web.servlet.DispatcherServlet : GET "/hello", parameters={}
DEBUG o.s.web.servlet.DispatcherServlet : Completed 404 NOT_FOUND
요청은 들어왔는데 어느 메서드로 연결됐는지가 안 찍혔습니다. 컨트롤러를 원래 자리(com.example.demo)로 되돌리고 다시 요청을 보내면 한 줄이 더 붙습니다.
1
2
3
4
DEBUG o.s.web.servlet.DispatcherServlet : GET "/hello", parameters={}
DEBUG s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.HelloController#hello()
DEBUG o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing ["안녕하세요"]
DEBUG o.s.web.servlet.DispatcherServlet : Completed 200 OK
Mapped to com.example.demo.HelloController#hello() — 이 줄이 있으면 매핑이 된 것이고, 없으면 스프링이 그 컨트롤러의 존재 자체를 모르는 것입니다.
이유는 앞에서 본 @SpringBootApplication의 컴포넌트 스캔입니다. 스캔 범위는 메인 클래스가 있는 패키지와 그 하위입니다.
1
2
3
com.example ← 스캔 안 함
└── demo ← 여기부터 스캔 (DemoApplication이 있는 곳)
├── HelloController ← 찾음
com.example은 com.example.demo보다 위라서 범위 밖입니다. @RestController를 붙여놨어도 스프링이 안 보니까 없는 것과 같습니다.
컨트롤러는 반드시 메인 클래스와 같은 패키지이거나 그 아래에 둡니다. 404가 났는데 주소는 분명히 맞다면 이걸 가장 먼저 의심합니다. 시작할 때 에러가 안 나기 때문에 찾기가 유난히 어렵습니다.
객체를 반환하면 JSON이 된다
지금까지는 문자열을 반환했습니다. 객체를 반환하면 어떻게 될까요.
com.example.demo 아래에 Member를 만듭니다.
1
2
3
4
package com.example.demo;
public record Member(Long id, String name, String email) {
}
record는 Java 16부터 들어온 문법입니다. 값을 담기만 하는 클래스를 한 줄로 만들어 줍니다. 생성자와 getId() 같은 조회 메서드가 자동으로 생깁니다.
컨트롤러를 생성하고, 아래와 같이 작성합니다.
위치 : com.example.demo.MemberController
1
2
3
4
5
6
7
8
@RestController
public class MemberController {
@GetMapping("/members/1")
public Member one(){
return new Member(1L, "민아", "abcd1234@example.com");
}
}
1
curl -i localhost:8080/members/1
1
2
3
4
5
HTTP/1.1 200
Content-Type: application/json
Content-Length: 58
{"id":1,"name":"민아","email":"abcd1234@example.com"}
객체가 JSON 문자열로 바뀌어 나갔습니다. Content-Type도 text/plain에서 application/json으로 알아서 바뀌었습니다.
이 일을 하는 건 Jackson이라는 라이브러리입니다. spring-boot-starter-web에 딸려 들어와 있어서 설정 없이 동작합니다. 스프링은 메서드가 반환한 타입을 보고, 문자열이면 그대로 쓰고 객체면 Jackson에게 넘겨 JSON으로 바꿉니다.
여기까지가 API 하나의 전부입니다. 주소를 받고, 자바 객체를 만들고, JSON으로 나갑니다. 앞으로 하는 일은 이 흐름에 살을 붙이는 것뿐입니다.
지금 컨트롤러는 /members/1 주소에 1번 회원만 고정으로 돌려줍니다. 2번 회원을 요청하려면 메서드를 하나 더 만들어야 합니다. 말이 안 되죠. 다음 편에서 이걸 고칩니다.
정리
start.spring.io에서 Java 21 / SNAPSHOT이 아닌 안정 버전 / Gradle / Spring Web 하나만 골라 시작한다.spring-boot-starter-web한 줄에 톰캣, 스프링 MVC, Jackson이 전부 들어 있다Tomcat started on port 8080로그가 보이면 서버는 살아 있는 것이다. 404는 “서버는 있는데 주소가 없다”는 뜻이다@RestController+@GetMapping("/주소")가 주소와 메서드를 짝지어 준다- 컴포넌트 스캔 범위는 메인 클래스가 있는 패키지와 그 하위다. 위 패키지에 두면 시작은 되는데 404가 난다
org.springframework.web로그를 DEBUG로 올리면Mapped to가 찍힌다. 매핑 문제를 여기서 잡는다- 문자열을 반환하면
text/plain, 객체를 반환하면 Jackson이 JSON으로 바꿔 준다
다음 2편에서는 고정값 대신 요청에 실려 온 값을 꺼내 쓰는 방법을 봅니다.
