Spring Boot 요약정리

Posted by Albert 10Day 9Hour 11Min 8Sec ago [2025-12-01]

Spring Boot는 Spring 프레임워크 기반의 애플리케이션을 빠르고 쉽게 개발할 수 있게 해주는 프레임워크입니다. 복잡한 설정 없이도 바로 실행 가능한 독립형 애플리케이션을 만들 수 있습니다.

1. Spring Boot 기본 개념

Spring Boot는 "관례 우선 설정(Convention over Configuration)" 원칙을 따릅니다. 기본 설정을 자동으로 제공하여 개발자가 비즈니스 로직에 집중할 수 있게 합니다.

기본 애플리케이션 구조

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

@SpringBootApplication 어노테이션은 세 가지 어노테이션을 합친 것입니다:

  • @Configuration: 스프링 설정 클래스
  • @EnableAutoConfiguration: 자동 설정 활성화
  • @ComponentScan: 컴포넌트 스캔

2. 의존성 관리 (Dependencies)

Maven 예시 (pom.xml)

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
</parent>

<dependencies>
    <!-- 웹 애플리케이션 개발 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- JPA/Hibernate -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- 개발 도구 (자동 재시작) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

3. REST API 개발

기본 컨트롤러

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    // GET: 모든 사용자 조회
    @GetMapping
    public List<User> getAllUsers() {
        return userService.findAll();
    }
    
    // GET: 특정 사용자 조회
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    // POST: 사용자 생성
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody @Valid User user) {
        User savedUser = userService.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
    }
    
    // PUT: 사용자 수정
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id, 
            @RequestBody @Valid User user) {
        return userService.update(id, user)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    // DELETE: 사용자 삭제
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

4. 데이터베이스 연동 (JPA)

Entity 클래스

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    @Email
    private String email;
    
    @Column(nullable = false)
    @Size(min = 2, max = 50)
    private String name;
    
    @Column(nullable = false)
    private String password;
    
    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
}

Repository 인터페이스

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // 메서드 이름으로 쿼리 자동 생성
    Optional<User> findByEmail(String email);
    
    List<User> findByNameContaining(String name);
    
    // JPQL 쿼리
    @Query("SELECT u FROM User u WHERE u.createdAt > :date")
    List<User> findRecentUsers(@Param("date") LocalDateTime date);
    
    // Native SQL 쿼리
    @Query(value = "SELECT * FROM users WHERE email LIKE %:domain", 
           nativeQuery = true)
    List<User> findByEmailDomain(@Param("domain") String domain);
}

Service 계층

@Service
@Transactional
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    public List<User> findAll() {
        return userRepository.findAll();
    }
    
    public Optional<User> findById(Long id) {
        return userRepository.findById(id);
    }
    
    public User save(User user) {
        // 비밀번호 암호화
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        return userRepository.save(user);
    }
    
    public Optional<User> update(Long id, User userDetails) {
        return userRepository.findById(id)
            .map(user -> {
                user.setName(userDetails.getName());
                user.setEmail(userDetails.getEmail());
                return userRepository.save(user);
            });
    }
    
    public void delete(Long id) {
        userRepository.deleteById(id);
    }
}

5. 설정 파일 (application.properties / application.yml)

application.yml 예시

spring:
  application:
    name: my-application
  
  ' 데이터베이스 설정
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
  
  ' JPA 설정
  jpa:
    hibernate:
      ddl-auto: update  ' none, validate, update, create, create-drop
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect
  
  ' 서버 설정
server:
  port: 8080
  servlet:
    context-path: /api

' 로깅 설정
logging:
  level:
    root: INFO
    com.myapp: DEBUG
    org.hibernate.SQL: DEBUG

6. 예외 처리 (Exception Handling)

커스텀 예외

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

전역 예외 핸들러

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(
            ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage())
        );
        return ResponseEntity.badRequest().body(errors);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(
            Exception ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "Internal server error",
            LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                             .body(error);
    }
}

@Data
@AllArgsConstructor
class ErrorResponse {
    private int status;
    private String message;
    private LocalDateTime timestamp;
}

7. 유효성 검증 (Validation)

@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @PostMapping
    public ResponseEntity<Product> createProduct(
            @RequestBody @Valid ProductRequest request) {
        // 검증 통과 시 실행
        return ResponseEntity.ok(productService.create(request));
    }
}

@Data
public class ProductRequest {
    
    @NotBlank(message = "제품명은 필수입니다")
    @Size(min = 2, max = 100, message = "제품명은 2-100자여야 합니다")
    private String name;
    
    @NotNull(message = "가격은 필수입니다")
    @Min(value = 0, message = "가격은 0 이상이어야 합니다")
    private BigDecimal price;
    
    @Email(message = "올바른 이메일 형식이 아닙니다")
    private String contactEmail;
    
    @Pattern(regexp = "^\\d{3}-\\d{4}-\\d{4}$", 
             message = "전화번호 형식: 010-1234-5678")
    private String phone;
}

8. Security (보안)

기본 Security 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .httpBasic();
        
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

JWT 인증 필터

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenProvider jwtTokenProvider;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        
        String token = getTokenFromRequest(request);
        
        if (token != null && jwtTokenProvider.validateToken(token)) {
            String username = jwtTokenProvider.getUsernameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

9. 테스팅 (Testing)

단위 테스트

@SpringBootTest
class UserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @MockBean
    private UserRepository userRepository;
    
    @Test
    void testFindUserById() {
        // Given
        User user = new User(1L, "test@example.com", "Test User", "password", null, null);
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        
        // When
        Optional<User> result = userService.findById(1L);
        
        // Then
        assertTrue(result.isPresent());
        assertEquals("test@example.com", result.get().getEmail());
    }
}

통합 테스트

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    void testCreateUser() throws Exception {
        User user = new User(null, "new@example.com", "New User", "password", null, null);
        
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(user)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.email").value("new@example.com"));
    }
}

10. 고급 기능

스케줄링

@Configuration
@EnableScheduling
public class SchedulingConfig {
}

@Component
public class ScheduledTasks {
    
    private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class);
    
    // 5초마다 실행
    @Scheduled(fixedRate = 5000)
    public void reportCurrentTime() {
        logger.info("현재 시간: {}", LocalDateTime.now());
    }
    
    // Cron 표현식: 매일 오전 2시 실행
    @Scheduled(cron = "0 0 2 * * ?")
    public void performDailyTask() {
        logger.info("일일 작업 실행");
    }
}

비동기 처리

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(5);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}

@Service
public class EmailService {
    
    @Async
    public CompletableFuture<String> sendEmail(String to, String subject) {
        // 이메일 발송 로직
        try {
            Thread.sleep(3000); // 시뮬레이션
            return CompletableFuture.completedFuture("이메일 발송 완료: " + to);
        } catch (InterruptedException e) {
            return CompletableFuture.failedFuture(e);
        }
    }
}

캐싱

@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("users", "products");
    }
}

@Service
public class ProductService {
    
    @Cacheable(value = "products", key = "'id")
    public Product findById(Long id) {
        // 데이터베이스 조회
        return productRepository.findById(id).orElse(null);
    }
    
    @CachePut(value = "products", key = "'product.id")
    public Product update(Product product) {
        return productRepository.save(product);
    }
    
    @CacheEvict(value = "products", key = "'id")
    public void delete(Long id) {
        productRepository.deleteById(id);
    }
}

이 가이드는 Spring Boot의 핵심 개념부터 실무에서 자주 사용되는 고급 기능까지 포괄합니다. 각 코드 예시는 실제 프로젝트에서 바로 활용할 수 있으며, 초보자는 기본 개념부터 순차적으로 학습하고, 숙련자는 필요한 부분을 참고하여 프로젝트에 적용할 수 있습니다.




LIST

Copyright © 2014 visionboy.me All Right Reserved.