🍃
Java 프레임워크 가이드

Spring Boot 완전 가이드

👥 방문자 수

Java 생태계에서 가장 널리 사용되는 백엔드 프레임워크. IoC/DI, REST API, JPA, Spring Security, 테스트, Docker 배포까지 실무 중심으로 정리했습니다.

Spring Boot 3.x REST API JPA / Hibernate Spring Security JUnit 5

1. Spring Boot란?

Spring Boot는 Spring Framework를 기반으로 자동 설정(Auto-Configuration)내장 서버를 제공하여 복잡한 XML 설정 없이 프로덕션 수준의 애플리케이션을 빠르게 개발할 수 있게 해주는 프레임워크입니다.

특징설명
Auto-Configuration의존성만 추가하면 설정을 자동으로 처리
내장 서버Tomcat/Netty 내장 — JAR 파일 하나로 실행
Spring Initializrstart.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 가이드