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. 네, 전 세계 수많은 기업에서 엔터프라이즈 애플리케이션으로 사용합니다.