1. Spring Boot란?
Spring Boot는 Spring Framework를 기반으로 자동 설정(Auto-Configuration)과 내장 서버를 제공하여 복잡한 XML 설정 없이 프로덕션 수준의 애플리케이션을 빠르게 개발할 수 있게 해주는 프레임워크입니다.
| 특징 | 설명 |
|---|---|
| Auto-Configuration | 의존성만 추가하면 설정을 자동으로 처리 |
| 내장 서버 | Tomcat/Netty 내장 — JAR 파일 하나로 실행 |
| Spring Initializr | start.spring.io에서 프로젝트 즉시 생성 |
| Actuator | 헬스체크, 메트릭, 모니터링 엔드포인트 기본 제공 |
2. 프로젝트 생성
start.spring.io에서 아래 설정으로 프로젝트를 생성하세요.
# 선택 항목
Project: Gradle - Kotlin (또는 Maven)
Language: Java 21
Spring Boot: 3.x (최신)
Dependencies:
- Spring Web
- Spring Data JPA
- Spring Security
- H2 Database (개발용) / PostgreSQL Driver (프로덕션)
- Lombok
- Validation
- Spring Boot DevToolstext
# build.gradle (Gradle Kotlin DSL)
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.projectlombok:lombok")
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
}kotlin
3. 첫 번째 REST API
// src/main/java/com/example/demo/DemoApplication.java
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
// src/main/java/com/example/demo/HelloController.java
@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
public Map<String, String> hello() {
return Map.of("message", "Hello, Spring Boot!");
}
}java
# 실행
./gradlew bootRun
# 테스트
curl http://localhost:8080/api/hello
# {"message":"Hello, Spring Boot!"}bash
4. DI / IoC (의존성 주입)
Spring의 핵심인 IoC 컨테이너는 객체(Bean)의 생성과 생명주기를 관리합니다. 생성자 주입 방식을 권장합니다.
// @Component 계열: @Service, @Repository, @Controller, @RestController
@Service
public class EmailService {
public void send(String to, String subject) {
// 실제 이메일 발송 로직
System.out.println("Sending to: " + to);
}
}
@Service
@RequiredArgsConstructor // Lombok: final 필드 생성자 자동 생성
public class UserService {
private final UserRepository userRepository; // 생성자 주입 (권장)
private final EmailService emailService;
public User create(String name, String email) {
User user = User.builder().name(name).email(email).build();
User saved = userRepository.save(user);
emailService.send(email, "회원가입 완료");
return saved;
}
}
// application.yml 설정
spring:
datasource:
url: jdbc:h2:mem:testdb
jpa:
show-sql: true
hibernate:
ddl-auto: create-dropjava
5. REST API 완전 설계
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// GET /api/v1/users?page=0&size=20
@GetMapping
public Page<UserResponse> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return userService.findAll(PageRequest.of(page, size));
}
// GET /api/v1/users/{id}
@GetMapping("/{id}")
public UserResponse getById(@PathVariable Long id) {
return userService.findById(id);
}
// POST /api/v1/users
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse create(@Valid @RequestBody CreateUserRequest req) {
return userService.create(req);
}
// PUT /api/v1/users/{id}
@PutMapping("/{id}")
public UserResponse update(@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest req) {
return userService.update(id, req);
}
// DELETE /api/v1/users/{id}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
userService.delete(id);
}
}
// DTO — record로 간결하게
public record CreateUserRequest(
@NotBlank String name,
@Email String email,
@Size(min = 8) String password
) {}
public record UserResponse(Long id, String name, String email, LocalDateTime createdAt) {}
java
6. 요청 검증 (Validation)
// 전역 예외 핸들러에서 검증 오류 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleValidation(MethodArgumentNotValidException e) {
Map<String, String> errors = new LinkedHashMap<>();
e.getBindingResult().getFieldErrors().forEach(err ->
errors.put(err.getField(), err.getDefaultMessage()));
return Map.of("status", 400, "errors", errors);
}
}
// 커스텀 검증 어노테이션
@Documented
@Constraint(validatedBy = UniqueEmailValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueEmail {
String message() default "이미 사용 중인 이메일입니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
@Autowired private UserRepository userRepository;
@Override
public boolean isValid(String email, ConstraintValidatorContext ctx) {
return email != null && !userRepository.existsByEmail(email);
}
}java
7. Spring Data JPA
// Entity
@Entity
@Table(name = "users")
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
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;
@Enumerated(EnumType.STRING)
private Role role = Role.USER;
@CreationTimestamp
private LocalDateTime createdAt;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Post> posts = new ArrayList<>();
}
// Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
// JPQL 쿼리
@Query("SELECT u FROM User u WHERE u.role = :role ORDER BY u.createdAt DESC")
List<User> findByRole(@Param("role") Role role);
// 페이징 + 검색
Page<User> findByNameContainingIgnoreCase(String name, Pageable pageable);
}
// QueryDSL로 동적 쿼리 (대규모 프로젝트 권장)
@Repository
@RequiredArgsConstructor
public class UserQueryRepository {
private final JPAQueryFactory queryFactory;
public List<User> search(String name, Role role) {
QUser u = QUser.user;
return queryFactory.selectFrom(u)
.where(
name != null ? u.name.containsIgnoreCase(name) : null,
role != null ? u.role.eq(role) : null
)
.orderBy(u.createdAt.desc())
.fetch();
}
}java
8. 예외 처리
// 커스텀 예외
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resource, Long id) {
super(resource + " not found with id: " + id);
}
}
// 전역 예외 핸들러
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException e) {
return ErrorResponse.of(404, e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneral(Exception e) {
return ErrorResponse.of(500, "서버 오류가 발생했습니다.");
}
}
record ErrorResponse(int status, String message, Instant timestamp) {
static ErrorResponse of(int status, String message) {
return new ErrorResponse(status, message, Instant.now());
}
}java
9. Spring Security (JWT)
// JWT 설정 (spring-boot-starter-security + jjwt 라이브러리)
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
@Value("${jwt.secret}") private String secret;
@Value("${jwt.expiration:3600000}") private long expiration;
public String generate(String email) {
return Jwts.builder()
.subject(email)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(Keys.hmacShaKeyFor(secret.getBytes()))
.compact();
}
public String extractEmail(String token) {
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
}
// Security 설정
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}java
10. 테스트 (JUnit 5 + MockMvc)
// 단위 테스트 (Service)
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository userRepository;
@Mock EmailService emailService;
@InjectMocks UserService userService;
@Test
void create_ShouldReturnUser_WhenValidInput() {
// given
var req = new CreateUserRequest("김철수", "cs@test.com", "password1!");
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
// when
var result = userService.create(req);
// then
assertThat(result.name()).isEqualTo("김철수");
verify(emailService).send(eq("cs@test.com"), anyString());
}
}
// 통합 테스트 (Controller)
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class UserControllerTest {
@Autowired MockMvc mockMvc;
@Autowired ObjectMapper objectMapper;
@Test
void createUser_ShouldReturn201() throws Exception {
var req = new CreateUserRequest("이영희", "yh@test.com", "password1!");
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("이영희"))
.andExpect(jsonPath("$.email").value("yh@test.com"));
}
@Test
void getUser_ShouldReturn404_WhenNotFound() throws Exception {
mockMvc.perform(get("/api/v1/users/99999"))
.andExpect(status().isNotFound());
}
}java
11. Docker 배포
# Dockerfile (멀티스테이지)
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
RUN addgroup -S spring && adduser -S spring -G spring
USER spring
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]dockerfile
# docker-compose.yml
services:
api:
build: .
ports: ["8080:8080"]
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/mydb
SPRING_DATASOURCE_USERNAME: user
SPRING_DATASOURCE_PASSWORD: ${DB_PASS}
JWT_SECRET: ${JWT_SECRET}
depends_on:
db: { condition: service_healthy }
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: mydb
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD", "pg_isready", "-U", "user"]
interval: 5s
retries: 5
volumes:
pgdata:yaml
12. 다음 단계
Spring Boot 심화 로드맵
• Spring WebFlux: 리액티브 비동기 처리 (R2DBC, Mono/Flux)
• Spring Cloud: 마이크로서비스 (Eureka, Gateway, Config Server)
• Spring Batch: 대용량 배치 처리
• QueryDSL: 타입 안전 동적 쿼리
• Redis 캐싱: @Cacheable + Spring Cache Abstraction
• Kafka: 이벤트 기반 마이크로서비스
연계 가이드: Java 가이드 · Docker 가이드 · Kubernetes 가이드
• Spring WebFlux: 리액티브 비동기 처리 (R2DBC, Mono/Flux)
• Spring Cloud: 마이크로서비스 (Eureka, Gateway, Config Server)
• Spring Batch: 대용량 배치 처리
• QueryDSL: 타입 안전 동적 쿼리
• Redis 캐싱: @Cacheable + Spring Cache Abstraction
• Kafka: 이벤트 기반 마이크로서비스
연계 가이드: Java 가이드 · Docker 가이드 · Kubernetes 가이드