인터페이스는 현지화되어 있으며, Skill 소개와 설명 문서는 선택한 언어로 AI가 자동 번역합니다. 코드·명령어·경로·전문 용어는 원문을 유지하며 원작자 문서가 우선합니다.
Skills 둘러보기
디자인 및 창작 · SKILL.md

spring-boot-patterns

spring-boot-patterns

Use this skill when implementing Spring Boot patterns — dependency injection, AOP, repository patterns, security configuration, testing patterns, transaction management. This skill enforces: constructor injection (no @Autowired on fields), @Transactional at service layer, AOP for cross-cutting concerns, repository abstraction with Spring Data, slice tests w…

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill 문서

GitHub

name: spring-boot-patterns description: > Use this skill when implementing Spring Boot patterns — dependency injection, AOP, repository patterns, security configuration, testing patterns, transaction management. This skill enforces: constructor injection (no @Autowired on fields), @Transactional at service layer, AOP for cross-cutting concerns, repository abstraction with Spring Data, slice tests with TestContainers. Do NOT use for: Spring Boot project structure (see architecture skill), non-Spring Java, frontend. version: "1.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [backend, spring-boot, patterns, phase-10]

Spring Boot Patterns

Purpose

Implement production-grade Spring Boot patterns — dependency injection, aspect-oriented programming, data access, security, transaction management, and testing.

Architecture Decision Trees

Injection Strategy

CriterionConstructorField (@Autowired)Setter
ImmutabilityYes (final fields)NoNo
TestabilityInstantiate directlyReflection requiredEasy override
Null safetyCompiler-enforcedRuntime NPE if missingRuntime NPE
Circular dependencyDetected at startupHiddenHidden
Constructor paramsAll in one callZero-arg constructor neededMutable
Framework couplingNone (pure Java)Spring annotationSpring annotation

Decision: Constructor injection always. Field injection only in tests (via @InjectMocks). Setter injection never.

Transaction Propagation

PropagationBehaviorUse Case
REQUIRED (default)Join existing or create newMost service methods
REQUIRES_NEWSuspend existing, create newAudit logging, independent actions
NESTEDSavepoint within existingPartial rollback within batch
MANDATORYFail if no existing txInternal service calls that need caller tx
NEVERFail if existing txLong-running reads, avoid connection hold
SUPPORTSOptional txRead-only queries, optional transactional
NOT_SUPPORTEDSuspend existingBackground tasks, after-commit actions

Decision: REQUIRED for write operations. REQUIRES_NEW for audit/events. Read-only SUPPORTS.

Testing Slice Strategy

AnnotationScopeWhat's MockedWhat's Real
@SpringBootTestFull appNothing (full context)Everything
@WebMvcTestWeb layer onlyService layerControllers, filters
@DataJpaTestJPA onlyNothingRepositories, entities
@JsonTestJSON onlyNothingJSON serialization
@RestClientTestREST clientServerRestTemplate/WebClient
@DataRedisTestRedisNothingRedis repos

Decision: @WebMvcTest for controller tests. @DataJpaTest for repository tests. @SpringBootTest for integration/E2E only.

Agent Protocol

Trigger

User request includes: Spring Boot patterns, Spring design patterns, dependency injection Spring, AOP, Spring Data, Spring Security, transaction management, Spring Testing, @Transactional, @Autowired, TestContainers, WireMock, @DataJpaTest, @WebMvcTest.

Input Context

  • Spring Boot version (3.x preferred)
  • Data access (Spring Data JPA, JDBC, R2DBC)
  • Security (Spring Security, OAuth2, JWT)
  • Testing framework (JUnit 5, Mockito, TestContainers)
  • Architecture (package-by-feature, hexagonal)

Output Artifact

Pattern implementations with code examples, configuration snippets, testing strategies.

Response Format

Produce artifact directly. No preamble, no postamble, no explanations. No filler, no hedging, no transitions. Strip articles a/an/the where unambiguous. Compress output — why use many token when few do trick.

Completion Criteria

  • DI pattern with constructor injection and qualifiers
  • AOP aspect for logging/performance/timing
  • Repository pattern with Spring Data
  • Security chain with JWT or OAuth2
  • Transaction propagation and isolation configured
  • Test slices for unit, integration, and E2E

Max Response Length

4096 tokens

Workflow

Step 1: Constructor Injection Pattern

// CORRECT: Constructor injection with required dependencies
@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final InventoryClient inventoryClient;
    private final EventPublisher eventPublisher;

    public OrderService(
        OrderRepository orderRepository,
        @Qualifier("cachingClient") InventoryClient inventoryClient,
        EventPublisher eventPublisher
    ) {
        this.orderRepository = orderRepository;
        this.inventoryClient = inventoryClient;
        this.eventPublisher = eventPublisher;
    }
}

// WRONG: Field injection — never do this
@Service
public class OrderService {
    @Autowired private OrderRepository orderRepository;  // NO
    @Autowired private InventoryClient inventoryClient;  // NO
}

// WRONG: Setter injection — avoid
@Service
public class OrderService {
    private OrderRepository orderRepository;

    @Autowired  // NO — use constructor
    public void setOrderRepository(OrderRepository repo) {
        this.orderRepository = repo;
    }
}

Step 2: Interface Segregation for Dependencies

// Define narrow interfaces — one responsibility each
public interface OrderRepository {
    Optional<Order> findById(OrderId id);
    Page<Order> findByCustomerId(String customerId, Pageable pageable);
    Order save(Order order);
    void deleteById(OrderId id);
}

public interface InventoryClient {
    boolean checkAvailability(String sku, int quantity);
    int reserveStock(String sku, int quantity);
    void releaseStock(String sku, int quantity);
}

public interface EventPublisher {
    void publish(DomainEvent event);
}

// Implementation with qualifier
@Component
@Qualifier("cachingClient")
public class CachingInventoryClient implements InventoryClient {
    private final InventoryClient delegate;
    private final CacheManager cache;

    public CachingInventoryClient(InventoryClient delegate, CacheManager cache) {
        this.delegate = delegate;
        this.cache = cache;
    }

    @Override
    public boolean checkAvailability(String sku, int quantity) {
        return cache.get(sku, () -> delegate.checkAvailability(sku, quantity));
    }
}

Step 3: AOP for Cross-Cutting Concerns

@Aspect
@Component
public class LoggingAspect {
    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    @Around("execution(* com.project..*(..)) && @annotation(LogExecution)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.nanoTime();
        Object result = joinPoint.proceed();
        long duration = System.nanoTime() - start;
        log.info("{} executed in {}ms",
            joinPoint.getSignature().toShortString(),
            TimeUnit.NANOSECONDS.toMillis(duration));
        return result;
    }

    @AfterThrowing(
        pointcut = "execution(* com.project.*.application.*.*(..))",
        throwing = "ex"
    )
    public void logApplicationException(JoinPoint joinPoint, RuntimeException ex) {
        log.error("Application exception in {}: {}",
            joinPoint.getSignature().toShortString(), ex.getMessage());
    }
}

// Custom annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecution {}

Step 4: Repository Pattern

// Domain port (in domain package — zero Spring imports)
public interface OrderRepository {
    Optional<Order> findById(OrderId id);
    Order save(Order order);
    void delete(OrderId id);
}

// Spring Data JPA adapter (in infrastructure)
@Component
public class SpringDataOrderRepository implements OrderRepository {
    private final JpaOrderRepository jpaRepo;

    public SpringDataOrderRepository(JpaOrderRepository jpaRepo) {
        this.jpaRepo = jpaRepo;
    }

    @Override
    public Optional<Order> findById(OrderId id) {
        return jpaRepo.findById(id.getValue())
            .map(OrderJpaEntity::toDomain);
    }

    @Override
    @Transactional
    public Order save(Order order) {
        OrderJpaEntity entity = OrderJpaEntity.fromDomain(order);
        return jpaRepo.save(entity).toDomain();
    }
}

// Spring Data interface (internal to infrastructure)
interface JpaOrderRepository extends JpaRepository<OrderJpaEntity, String> {
    Page<OrderJpaEntity> findByCustomerId(String customerId, Pageable pageable);
    Optional<OrderJpaEntity> findByOrderNumber(String orderNumber);
}

Step 5: Transaction Management

@Service
public class PaymentService {
    private final PaymentGateway gateway;
    private final PaymentRepository paymentRepo;
    private final OrderRepository orderRepo;

    public PaymentService(
        PaymentGateway gateway,
        PaymentRepository paymentRepo,
        OrderRepository orderRepo
    ) {
        this.gateway = gateway;
        this.paymentRepo = paymentRepo;
        this.orderRepo = orderRepo;
    }

    @Transactional(rollbackFor = PaymentException.class)
    public PaymentResult processPayment(ProcessPaymentCommand command) {
        // 1. Validate order
        Order order = orderRepo.findById(command.orderId())
            .orElseThrow(() -> new OrderNotFoundException(command.orderId()));

        // 2. Charge payment gateway
        PaymentCharge charge = gateway.charge(
            command.amount(),
            command.paymentToken()
        );

        // 3. Save payment record
        Payment payment = Payment.create(command.orderId(), charge.transactionId(), command.amount());
        paymentRepo.save(payment);

        // 4. Update order status
        order.markAsPaid();
        orderRepo.save(order);

        return new PaymentResult(payment.getId(), charge.transactionId());
    }
}

Step 6: Security Configuration

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    private final JwtTokenProvider tokenProvider;
    private final UserDetailsService userDetailsService;

    public SecurityConfig(JwtTokenProvider tokenProvider, UserDetailsService userDetailsService) {
        this.tokenProvider = tokenProvider;
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**", "/actuator/health").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .authenticationProvider(authenticationProvider())
            .addFilterBefore(
                new JwtAuthenticationFilter(tokenProvider, userDetailsService),
                UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

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

Step 7: Event-Driven Pattern

// Domain event
public record OrderSubmittedEvent(OrderId orderId, String customerId, Money total) implements DomainEvent {}

// Publisher
@Component
public class SpringEventPublisher implements EventPublisher {
    private final ApplicationEventPublisher publisher;

    public SpringEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    @Override
    public void publish(DomainEvent event) {
        publisher.publishEvent(event);
    }
}

// Listener
@Component
public class OrderSubmittedHandler {
    private final EmailService emailService;
    private final InventoryService inventoryService;

    public OrderSubmittedHandler(EmailService emailService, InventoryService inventoryService) {
        this.emailService = emailService;
        this.inventoryService = inventoryService;
    }

    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handleOrderSubmitted(OrderSubmittedEvent event) {
        emailService.sendOrderConfirmation(event.orderId(), event.customerId());
        inventoryService.reserveItems(event.orderId());
    }
}

// Async event processing
@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}

@Component
public class AsyncOrderHandler {
    @EventListener
    @Async
    public void handle(OrderSubmittedEvent event) {
        // Long-running task runs on thread pool
    }
}

Production Considerations

Performance

  • Connection pool: spring.datasource.hikari.maximum-pool-size=20 (adjust per concurrent users)
  • JPA: spring.jpa.open-in-view=false (prevents lazy init exceptions in views, forces explicit fetch planning)
  • Caching: Spring Cache with Caffeine for local, Redis for distributed; annotate methods with @Cacheable
  • Async: @Async with custom ThreadPoolTaskExecutor (corePoolSize=10, maxPoolSize=50)
  • Logging: logback-spring.xml with structured JSON appender; async appender for high-throughput
  • Graceful shutdown: server.shutdown=graceful + spring.lifecycle.timeout-per-shutdown-phase=30s

Observability

  • Actuator: expose /actuator/health, /actuator/metrics, /actuator/prometheus (secured behind management port)
  • Metrics: Micrometer with MicrometerRegistry; custom metrics via MeterRegistry bean
  • Tracing: Micrometer Tracing with Brave + Zipkin for distributed tracing
  • Health indicators: custom HealthIndicator for external services (DB, Redis, Kafka, downstream APIs)

Common Pitfalls

  • N+1 query: use @EntityGraph or JOIN FETCH in JPQL; enable hibernate.query.fail_on_pagination_over_collection_fetch
  • Lazy loading exceptions: disable open-in-view, plan fetches explicitly
  • @Transactional on private methods: Spring proxies can't intercept private calls — move to public or use self-injection

Anti-Patterns

Anti-PatternWhyFix
@Autowired on fieldsCan't test without Spring; hidden depsConstructor injection
@Transactional in controllersTransaction spans HTTP response; connection heldService-layer only
@DataJpaTest with full contextSlow test startupUse slice annotations
@SpringBootTest for every testSlow CI; tests depend on full contextSlice tests per concern
Catch-all @ComponentScanSlow startup; unexpected beans picked upExplicit configuration with @Enable...
Giant application.propertiesHard to maintain; no type safetyType-safe @ConfigurationProperties per module
Using @Query with string concatenationSQL injection riskUse @Param + named parameters or Specification
@Async without custom executorDefault SimpleAsyncTaskExecutor creates unlimited threadsDefine ThreadPoolTaskExecutor bean
Service calling service directly (no interface)Tight coupling; can't proxy for tx/AOPDefine interface, proxy will apply cross-cutting

Rules

  • references/spring-boot-data-access.md — Spring Boot Data Access Reference
  • references/spring-boot-deployment.md — Spring Boot Deployment
  • references/spring-boot-observability.md — Spring Boot Observability
  • references/spring-boot-security.md — Spring Boot Security Reference
  • references/spring-patterns.md — Spring Patterns
  • references/spring-testing.md — Spring Testing

Handoff

Hand off to backend/spring-boot/architecture/SKILL.md for project structure or backend/universal/testing/SKILL.md for broader testing patterns.

Implementation Patterns

Factory Pattern for Module Creation

function createModule<T>(config: ModuleConfig): T { const dependencies = initializeDependencies(config); const module = new Module(dependencies); module.hooks.onInit(); return module as T; }

Builder Pattern for Complex Configuration

class ConfigBuilder { private config: AppConfig = new AppConfig(); withDatabase(url: string): ConfigBuilder { ... } withCache(ttl: number): ConfigBuilder { ... } withLogging(level: string): ConfigBuilder { ... } build(): AppConfig { return this.config; } }

Production Considerations

Deployment Checklist

  • Production build with optimizations enabled
  • Environment variables configured per environment
  • Health check endpoint responds correctly
  • Error tracking and monitoring integrated
  • Logging level configured (not debug in production)
  • Resource limits configured
  • Database migrations applied
  • Static assets built and served from CDN or cache
  • Feature flags toggled appropriately
  • Rollback plan documented and tested

Monitoring and Alerting

MetricThresholdSeverityAction
Error rate> 1%CriticalRollback or fix
p95 latency> 500msWarningProfile and optimize
Uptime< 99.9%CriticalInvestigate infrastructure
Memory usage> 80%WarningCheck for leaks
CPU usage> 80%WarningScale up or optimize

Rules

  • Prefer composition over inheritance
  • Favor immutable data structures
  • Use dependency injection for testability
  • Keep functions pure when possible — no side effects
  • Fail fast with clear error messages
  • Don't repeat yourself (DRY) — extract shared logic
  • Keep it simple (KISS) — avoid unnecessary complexity
  • You aren't gonna need it (YAGNI) — build what's required
  • Separate concerns — single responsibility per module
  • Code to interfaces, not implementations
  • Write self-documenting code — clear names over comments
  • Prefer standard library over third-party dependencies
  • Handle errors explicitly — no silent failures
  • Validate inputs at boundaries
  • Log at appropriate levels (debug, info, warn, error)
저장한 Skills