Spring Boot를 실무 흐름으로 이해하기
Java 생태계 표준 백엔드 프레임워크. IoC/DI, REST API, JPA, Spring Security, 테스트, Docker 배포까지 실무 중심으로 정리했습니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
Java 생태계 표준 백엔드 프레임워크. IoC/DI, REST API, JPA, Spring Security, 테스트, Docker 배포까지 실무 중심으로 정리했습니다.
Java 생태계 표준 백엔드 프레임워크. IoC/DI, REST API, JPA, Spring Security, 테스트, Docker 배포까지 실무 중심으로 정리했습니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
문법보다 요청이 들어와 검증, 처리, 저장, 응답으로 이어지는 경계를 먼저 잡습니다.
글로 읽은 내용을 머릿속에 오래 남기려면 먼저 흐름을 그림으로 잡는 편이 좋습니다. 아래 두 그림은 Spring Boot를 학습할 때 계속 되돌아볼 수 있는 기준 지도입니다.
Spring Boot를 처음 펼칠 때는 세부 명령보다 큰 그림이 먼저입니다. 이 섹션에서는 앞으로 배울 개념들이 어떤 문제를 풀기 위해 등장했는지부터 잡아봅니다.
| 특징 | 설명 |
|---|---|
| Auto-Configuration | 의존성만 추가하면 자동 설정 |
| 내장 서버 | Tomcat 내장 — JAR 하나로 실행 |
| Spring Initializr | start.spring.io에서 즉시 생성 |
| Actuator | 헬스체크·메트릭 엔드포인트 기본 제공 |
여기서는 프로젝트 생성을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# start.spring.io → Gradle Kotlin, Java 21, 의존성:
# Spring Web, Spring Data JPA, Spring Security
# H2 Database, Lombok, Validation
./gradlew bootRun # 실행
./gradlew bootJar # 빌드여기서는 첫 번째 REST API을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public Page<UserResponse> list(@RequestParam(defaultValue="0") int page) {
return userService.findAll(PageRequest.of(page, 20));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse create(@Valid @RequestBody CreateUserRequest req) {
return userService.create(req);
}
}여기서는 DI / IoC을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
// 권장: 생성자 주입 — final로 불변성 보장, 테스트 시 mock 주입이 쉬움
@Service
@RequiredArgsConstructor // Lombok이 final 필드를 받는 생성자를 자동 생성
public class OrderService {
private final PaymentClient paymentClient;
private final OrderRepository orderRepository;
public Order placeOrder(OrderRequest req) {
paymentClient.charge(req.amount());
return orderRepository.save(Order.from(req));
}
}
// 비권장: 필드 주입 — final 불가, 테스트 시 리플렉션 없이는 mock 주입 불가
@Service
public class LegacyOrderService {
@Autowired
private PaymentClient paymentClient;
}| 주입 방식 | 특징 | 권장 여부 |
|---|---|---|
| 생성자 주입 | final 필드 사용 가능, 필수 의존성을 명확히 드러냄, 순환 참조를 컴파일 시점에 발견 | 권장 |
| 세터 주입 | 선택적 의존성에 적합, 런타임에 재설정 가능 | 제한적으로 사용 |
| 필드 주입 | 코드는 짧지만 불변성 보장 불가, 순수 단위 테스트 작성이 어려움 | 지양 |
여기서는 REST API 완전 설계을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
| HTTP 상태 코드 | 사용 시점 |
|---|---|
| 200 OK | 조회·수정 성공, 응답 본문 있음 |
| 201 Created | 생성 성공 — Location 헤더에 새 리소스 URI 포함 권장 |
| 204 No Content | 삭제 성공 등 응답 본문이 없는 성공 |
| 400 Bad Request | 요청 값 검증 실패 (@Valid 실패 등) |
| 404 Not Found | 요청한 리소스가 존재하지 않음 |
| 409 Conflict | 중복 생성, 낙관적 락 충돌 등 상태 충돌 |
여기서는 Spring Data JPA을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
@Entity @Table(name="users")
@Getter @Builder @NoArgsConstructor @AllArgsConstructor
public class User {
@Id @GeneratedValue(strategy=IDENTITY) private Long id;
@Column(nullable=false) private String name;
@Column(nullable=false, unique=true) private String email;
@CreationTimestamp private LocalDateTime createdAt;
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}여기서는 예외 처리을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(err -> err.getField() + ": " + err.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(new ErrorResponse("VALIDATION_ERROR", message));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex) {
log.error("Unhandled exception", ex); // 원인 파악을 위해 반드시 로그에 스택트레이스 남기기
return ResponseEntity.internalServerError()
.body(new ErrorResponse("INTERNAL_ERROR", "일시적인 오류가 발생했습니다"));
}
}
record ErrorResponse(String code, String message) {}여기서는 Spring Security (JWT)을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // stateless API는 CSRF 토큰 불필요
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/login", "/api/signup").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}여기서는 테스트 (JUnit + MockMvc)을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired MockMvc mockMvc;
@MockBean OrderService orderService;
@Test
void 주문_생성_성공() throws Exception {
given(orderService.placeOrder(any())).willReturn(new Order(1L, 10000));
mockMvc.perform(post("/api/v1/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"productId": 1, "amount": 10000}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1));
}
@Test
void 검증_실패시_400() throws Exception {
mockMvc.perform(post("/api/v1/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isBadRequest());
}
}| 테스트 종류 | 범위 | 속도 | 대표 애너테이션 |
|---|---|---|---|
| 단위 테스트 | Service 로직 하나 (의존성은 Mockito로 대체) | 매우 빠름 | @ExtendWith(MockitoExtension.class) |
| 웹 계층 테스트 | Controller + 요청/응답 직렬화 | 빠름 | @WebMvcTest |
| 통합 테스트 | 전체 스프링 컨텍스트 + 실제 DB(H2 등) | 느림 | @SpringBootTest |
여기서는 Docker 배포을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradlew build.gradle.kts settings.gradle.kts ./
COPY gradle ./gradle
RUN ./gradlew dependencies --no-daemon
COPY src ./src
RUN ./gradlew bootJar --no-daemon
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
USER nobody
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]Spring Boot 실무 설계은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 결정 지점 | 확인 질문 | 실무 기준 |
|---|---|---|
| 경계 | Spring Boot 코드에서 바뀌기 쉬운 부분은 어디인가? | 입출력, 설정, 외부 연동, 핵심 규칙을 분리합니다. |
| 상태 | 상태가 어디서 생성되고 어디서 사라지는가? | 상태 소유자와 수명 주기를 코드로 드러냅니다. |
| 장애 | 실패했을 때 호출자는 무엇을 받는가? | timeout, fallback, error contract를 먼저 정합니다. |
이 섹션은 Spring Boot 운영 기준을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.
Spring Boot 검증 전략은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 품질 축 | 검증 방법 | 완료 기준 |
|---|---|---|
| 정확성 | 정상/실패 케이스를 자동화합니다. | 핵심 시나리오가 재현 가능하게 통과합니다. |
| 회귀 방지 | 버그 수정 시 동일 케이스를 테스트로 남깁니다. | 같은 장애가 다시 배포되지 않습니다. |
| 운영성 | 로그, 메트릭, 알림을 확인합니다. | 문제가 생겼을 때 원인 추적 경로가 있습니다. |