Spring Boot 완벽 가이드 | REST API·JPA·Security·Actuator·배포

Spring Boot 완벽 가이드 | REST API·JPA·Security·Actuator·배포

이 글의 핵심

Spring Boot로 엔터프라이즈 애플리케이션을 구축하는 완벽 가이드입니다. REST API, JPA, Spring Security, Actuator, 테스트, 배포까지 실전 예제로 정리했습니다.

실무 경험 공유: 레거시 Spring 프로젝트를 Spring Boot로 마이그레이션하면서, 설정 코드를 70% 줄이고 개발 속도를 2배 향상시킨 경험을 공유합니다.

들어가며: “Spring 설정이 복잡해요”

실무 문제 시나리오

시나리오 1: XML 설정이 수백 줄이에요
레거시 Spring은 XML이 복잡합니다. Spring Boot는 자동 설정합니다.

시나리오 2: 서버 설정이 번거로워요
Tomcat을 따로 설치해야 합니다. Spring Boot는 내장 서버를 제공합니다.

시나리오 3: 의존성 관리가 어려워요
버전 충돌이 자주 발생합니다. Spring Boot Starter가 해결합니다.


1. Spring Boot란?

핵심 특징

Spring Boot는 Spring 기반 애플리케이션을 빠르게 개발하는 프레임워크입니다.

주요 장점:

  • 자동 설정: Convention over Configuration
  • 내장 서버: Tomcat, Jetty 내장
  • Starter: 의존성 간편 관리
  • Actuator: 모니터링 내장
  • 프로덕션 준비: 즉시 배포 가능

2. 프로젝트 생성

Spring Initializr

# https://start.spring.io/
# 또는 CLI
curl https://start.spring.io/starter.zip \
  -d dependencies=web,data-jpa,postgresql,security \
  -d type=maven-project \
  -d language=java \
  -d bootVersion=3.2.0 \
  -d baseDir=myapp \
  -o myapp.zip

unzip myapp.zip
cd myapp
./mvnw spring-boot:run

3. REST API

Controller

// src/main/java/com/example/demo/controller/UserController.java
package com.example.demo.controller;

import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping
    public List<User> getAllUsers() {
        return userService.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return userService.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User created = userService.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
        return userService.findById(id)
                .map(existing -> {
                    user.setId(id);
                    return ResponseEntity.ok(userService.save(user));
                })
                .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteById(id);
        return ResponseEntity.noContent().build();
    }
}

4. JPA

Entity

// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
    }

    // Getters and Setters
}

Repository

// src/main/java/com/example/demo/repository/UserRepository.java
package com.example.demo.repository;

import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

Service

// src/main/java/com/example/demo/service/UserService.java
package com.example.demo.service;

import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public List<User> findAll() {
        return userRepository.findAll();
    }

    public Optional<User> findById(Long id) {
        return userRepository.findById(id);
    }

    public User save(User user) {
        return userRepository.save(user);
    }

    public void deleteById(Long id) {
        userRepository.deleteById(id);
    }
}

5. Spring Security

설정

// src/main/java/com/example/demo/config/SecurityConfig.java
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/users/**").authenticated()
                .anyRequest().authenticated()
            )
            .httpBasic();

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

6. Validation

// src/main/java/com/example/demo/dto/CreateUserDto.java
package com.example.demo.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public class CreateUserDto {

    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    private String name;

    @NotBlank(message = "Email is required")
    @Email(message = "Email must be valid")
    private String email;

    // Getters and Setters
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserDto dto) {
    // ...
}

7. 예외 처리

// src/main/java/com/example/demo/exception/GlobalExceptionHandler.java
package com.example.demo.exception;

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

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<Map<String, String>> handleNotFound(ResourceNotFoundException ex) {
        Map<String, String> error = new HashMap<>();
        error.put("error", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, String>> handleGeneral(Exception ex) {
        Map<String, String> error = new HashMap<>();
        error.put("error", "Internal server error");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

8. 테스트

// src/test/java/com/example/demo/service/UserServiceTest.java
package com.example.demo.service;

import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void findById_shouldReturnUser() {
        User user = new User();
        user.setId(1L);
        user.setName("John");

        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        Optional<User> result = userService.findById(1L);

        assertTrue(result.isPresent());
        assertEquals("John", result.get().getName());
    }
}

9. 배포

JAR 빌드

./mvnw clean package
java -jar target/myapp-0.0.1-SNAPSHOT.jar

Docker

FROM eclipse-temurin:21-jdk-alpine as builder
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

정리 및 체크리스트

핵심 요약

  • Spring Boot: 빠른 Spring 개발
  • 자동 설정: 최소한의 설정
  • REST API: @RestController
  • JPA: 데이터베이스 추상화
  • Spring Security: 인증/인가
  • Actuator: 모니터링

구현 체크리스트

  • Spring Boot 프로젝트 생성
  • REST API 구현
  • JPA Entity 정의
  • Spring Security 설정
  • Validation 구현
  • 테스트 작성
  • Docker 배포

같이 보면 좋은 글

  • NestJS 완벽 가이드
  • FastAPI 완벽 가이드
  • PostgreSQL 고급 가이드

이 글에서 다루는 키워드

Spring Boot, Java, Backend, REST API, JPA, Spring Security, Enterprise

자주 묻는 질문 (FAQ)

Q. Spring vs Spring Boot, 차이가 뭔가요?

A. Spring Boot는 Spring을 더 쉽게 사용하도록 만든 프레임워크입니다. 자동 설정과 내장 서버를 제공합니다.

Q. Kotlin으로도 사용할 수 있나요?

A. 네, Spring Boot는 Kotlin을 완벽하게 지원합니다.

Q. 마이크로서비스에 적합한가요?

A. 네, Spring Cloud와 함께 사용하면 마이크로서비스 아키텍처를 쉽게 구축할 수 있습니다.

Q. 프로덕션에서 사용해도 되나요?

A. 네, 전 세계 수많은 기업에서 엔터프라이즈 애플리케이션으로 사용합니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3