Spring Boot 3 시작 가이드 — 프로젝트 생성부터 REST API까지

Spring Boot 3의 핵심 변화

Spring Boot 3은 Java 17 이상을 필수로 요구하며, Jakarta EE 10 네임스페이스(javax.*jakarta.*)로 전환되었습니다. GraalVM 네이티브 이미지를 공식 지원하고, 관찰 가능성(Observability)이 Micrometer 기반으로 통합되었습니다.

Spring Initializr를 통해 프로젝트를 생성하는 것이 가장 빠른 시작 방법입니다. 핵심 의존성은 spring-boot-starter-web이며, 이 하나만으로 내장 Tomcat, Jackson JSON 처리, Spring MVC가 모두 포함됩니다.

프로젝트 생성과 기본 구조

build.gradle 설정

// build.gradle — Spring Boot 3 의존성 설정
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.4'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '1.0.0'

java {
    sourceCompatibility = '17' // Java 17 이상 필수
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'       // REST API 핵심
    implementation 'org.springframework.boot:spring-boot-starter-validation' // 입력 검증
    testImplementation 'org.springframework.boot:spring-boot-starter-test'  // 테스트
}

메인 애플리케이션 클래스

// Application.java — Spring Boot 진입점
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // 자동 설정 + 컴포넌트 스캔 + 설정 클래스 역할
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        // 실행 결과:
        // Started Application in 1.234 seconds (process running for 1.567)
        // Tomcat started on port 8080
    }
}

@SpringBootApplication은 세 가지 어노테이션의 조합입니다. @EnableAutoConfiguration은 클래스패스의 라이브러리를 감지하여 자동 설정을 적용합니다. @ComponentScan은 현재 패키지와 하위 패키지의 빈을 자동 등록합니다. @Configuration은 이 클래스 자체를 설정 클래스로 표시합니다.

REST API 구현

도메인 모델과 컨트롤러

// TaskController.java — CRUD REST API 구현
package com.example.demo.controller;

import com.example.demo.dto.TaskRequest;
import com.example.demo.dto.TaskResponse;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

// DTO — record로 간결하게 정의
record TaskRequest(
    @jakarta.validation.constraints.NotBlank(message = "제목은 필수입니다")
    String title,
    String description
) {}

record TaskResponse(
    Long id,
    String title,
    String description,
    boolean completed,
    LocalDateTime createdAt
) {}

@RestController                    // JSON 응답 자동 직렬화
@RequestMapping("/api/tasks")      // 공통 경로 접두사
public class TaskController {

    // 인메모리 저장소 (실제로는 JPA Repository 사용)
    private final Map<Long, TaskResponse> store = new ConcurrentHashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    // GET /api/tasks — 전체 조회
    @GetMapping
    public List<TaskResponse> getAll() {
        return new ArrayList<>(store.values());
        // 응답 예시: [{"id":1,"title":"공부","completed":false,...}]
    }

    // GET /api/tasks/{id} — 단건 조회
    @GetMapping("/{id}")
    public ResponseEntity<TaskResponse> getById(@PathVariable Long id) {
        TaskResponse task = store.get(id);
        if (task == null) {
            return ResponseEntity.notFound().build(); // 404 반환
        }
        return ResponseEntity.ok(task); // 200 + JSON 본문
    }

    // POST /api/tasks — 생성
    @PostMapping
    public ResponseEntity<TaskResponse> create(@Valid @RequestBody TaskRequest req) {
        Long id = idGenerator.getAndIncrement();
        TaskResponse task = new TaskResponse(
            id, req.title(), req.description(), false, LocalDateTime.now()
        );
        store.put(id, task);
        return ResponseEntity.status(HttpStatus.CREATED).body(task);
        // 201 Created + 생성된 리소스 반환
    }

    // DELETE /api/tasks/{id} — 삭제
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        if (store.remove(id) == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.noContent().build(); // 204 No Content
    }
}

전역 예외 처리

// GlobalExceptionHandler.java — 통합 에러 응답
package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;
import java.util.List;

record ErrorResponse(int status, String message, LocalDateTime timestamp) {}

@RestControllerAdvice // 모든 컨트롤러에 적용되는 예외 처리기
public class GlobalExceptionHandler {

    // 검증 실패 시 처리
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex) {

        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .toList();

        ErrorResponse response = new ErrorResponse(
            400,
            String.join(", ", errors),
            LocalDateTime.now()
        );
        return ResponseEntity.badRequest().body(response);
        // 결과: {"status":400,"message":"title: 제목은 필수입니다","timestamp":"..."}
    }
}

자동 설정(Auto-Configuration) 이해

Spring Boot의 자동 설정은 “관례 우선(Convention over Configuration)” 원칙을 따릅니다. spring-boot-starter-web을 추가하면 다음이 자동으로 설정됩니다.

감지 조건자동 설정 내용
클래스패스에 Tomcat내장 웹 서버 구동
클래스패스에 JacksonJSON 직렬화/역직렬화
@RestController 존재Spring MVC 디스패처 서블릿
application.properties포트, 로깅 등 커스텀 설정

application.properties에서 주요 설정을 오버라이드할 수 있습니다.

// application.properties — 주요 설정 항목
// server.port=9090                    // 포트 변경 (기본 8080)
// spring.jackson.date-format=yyyy-MM-dd // JSON 날짜 형식
// logging.level.root=INFO             // 로그 레벨
// spring.profiles.active=dev          // 활성 프로파일

// 프로파일별 설정 파일
// application-dev.properties   → 개발 환경
// application-prod.properties  → 운영 환경

프로파일과 외부 설정

// AppConfig.java — @ConfigurationProperties 패턴
package com.example.demo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app") // app.* 설정을 바인딩
public record AppConfig(
    String name,          // app.name
    int maxRetries,       // app.max-retries (케밥 → 카멜 자동 변환)
    String apiUrl         // app.api-url
) {}

// application.properties에서:
// app.name=TaskManager
// app.max-retries=3
// app.api-url=https://api.example.com

// 사용 예시:
// @Service
// public class MyService {
//     private final AppConfig config;
//     public MyService(AppConfig config) {
//         this.config = config;
//         System.out.println(config.name()); // "TaskManager"
//     }
// }

정리

Spring Boot 3을 시작할 때 기억해야 할 핵심 사항입니다.

  • Java 17 이상이 필수이며, javax.*jakarta.* 마이그레이션이 필요합니다
  • @SpringBootApplication 하나로 자동 설정, 컴포넌트 스캔, 설정 클래스가 통합됩니다
  • REST API는 @RestController + @RequestMapping 조합으로 구현하고, record를 DTO로 활용하면 보일러플레이트를 줄일 수 있습니다
  • @Valid@RestControllerAdvice로 입력 검증과 예외 처리를 분리합니다
  • @ConfigurationProperties로 타입 안전한 설정 바인딩을 사용합니다
  • 프로파일(spring.profiles.active)로 환경별 설정을 관리합니다

Spring Boot의 강점은 “시작은 빠르게, 확장은 유연하게”입니다. 기본 설정으로 빠르게 시작하고, 필요할 때 자동 설정을 오버라이드하여 세밀하게 제어할 수 있습니다.

이 글이 도움이 되었나요?