Communitygithub.com

springboot-patterns

Spring Boot 아키텍처 패턴, REST API 설계, 계층형 서비스, 데이터 액세스, 캐싱, 비동기 처리 및 로깅. Java Spring Boot 백엔드 작업 시 사용합니다. REST 계층, 서비스, 데이터 액세스, 캐싱 또는 비동기 작업을 빌드하거나 검토할 때 활성화하세요.

springboot-patterns란 무엇인가요?

이 스킬은 Spring Boot 애플리케이션을 위한 모범 아키텍처 패턴과 API 설계 원칙을 제공합니다. 컨트롤러, 서비스, 리포지토리 계층을 구조화하고, Spring Data JPA, 캐싱, 비동기 처리, 예외 처리, 로깅, 필터 등 프로덕션 등급 서비스에 필요한 주요 패턴을 코드 예제와 함께 설명합니다. 특히 REST API 설계, 트랜잭션 관리, DTO 및 유효성 검증, 전역 예외 처리, 캐싱 전략, 비동기 메시징, 속도 제한(Rate Limiting)과 같은 고급 주제를 다룹니다. 이 스킬은 모든 Spring Boot 환경(Spring MVC, WebFlux)에서 사용할 수 있으며, 개발자가 일관되고 유지보수 가능하며 확장 가능한 백엔드 코드를 작성하도록 돕습니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/springboot-patterns

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Spring Boot 개발 패턴

확장 가능하고 프로덕션 등급의 서비스를 위한 Spring Boot 아키텍처 및 API 패턴입니다.

활성화 시점

  • Spring MVC 또는 WebFlux로 REST API 구축
  • 컨트롤러 → 서비스 → 리포지토리 계층 구조화
  • Spring Data JPA, 캐싱 또는 비동기 처리 구성
  • 유효성 검증, 예외 처리 또는 페이지네이션 추가
  • 개발/스테이징/프로덕션 환경을 위한 프로파일 설정
  • Spring Events 또는 Kafka를 이용한 이벤트 기반 패턴 구현

REST API 구조

@RestController
@RequestMapping("/api/markets")
@Validated
class MarketController {
  private final MarketService marketService;

  MarketController(MarketService marketService) {
    this.marketService = marketService;
  }

  @GetMapping
  ResponseEntity<Page<MarketResponse>> list(
      @RequestParam(defaultValue = "0") int page,
      @RequestParam(defaultValue = "20") int size) {
    Page<Market> markets = marketService.list(PageRequest.of(page, size));
    return ResponseEntity.ok(markets.map(MarketResponse::from));
  }

  @PostMapping
  ResponseEntity<MarketResponse> create(@Valid @RequestBody CreateMarketRequest request) {
    Market market = marketService.create(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(MarketResponse.from(market));
  }
}

리포지토리 패턴 (Spring Data JPA)

public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
  @Query("select m from MarketEntity m where m.status = :status order by m.volume desc")
  List<MarketEntity> findActive(@Param("status") MarketStatus status, Pageable pageable);
}

트랜잭션을 사용한 서비스 계층

@Service
public class MarketService {
  private final MarketRepository repo;

  public MarketService(MarketRepository repo) {
    this.repo = repo;
  }

  @Transactional
  public Market create(CreateMarketRequest request) {
    MarketEntity entity = MarketEntity.from(request);
    MarketEntity saved = repo.save(entity);
    return Market.from(saved);
  }
}

DTO 및 유효성 검증

public record CreateMarketRequest(
    @NotBlank @Size(max = 200) String name,
    @NotBlank @Size(max = 2000) String description,
    @NotNull @FutureOrPresent Instant endDate,
    @NotEmpty List<@NotBlank String> categories) {}

public record MarketResponse(Long id, String name, MarketStatus status) {
  static MarketResponse from(Market market) {
    return new MarketResponse(market.id(), market.name(), market.status());
  }
}

예외 처리

@ControllerAdvice
class GlobalExceptionHandler {
  @ExceptionHandler(MethodArgumentNotValidException.class)
  ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex) {
    String message = ex.getBindingResult().getFieldErrors().stream()
        .map(e -> e.getField() + ": " + e.getDefaultMessage())
        .collect(Collectors.joining(", "));
    return ResponseEntity.badRequest().body(ApiError.validation(message));
  }

  @ExceptionHandler(AccessDeniedException.class)
  ResponseEntity<ApiError> handleAccessDenied() {
    return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiError.of("Forbidden"));
  }

  @ExceptionHandler(Exception.class)
  ResponseEntity<ApiError> handleGeneric(Exception ex) {
    // 예상치 못한 오류를 스택 트레이스와 함께 로깅
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
        .body(ApiError.of("Internal server error"));
  }
}

캐싱

설정 클래스에 @EnableCaching이 필요합니다.

@Service
public class MarketCacheService {
  private final MarketRepository repo;

  public MarketCacheService(MarketRepository repo) {
    this.repo = repo;
  }

  @Cacheable(value = "market", key = "#id")
  public Market getById(Long id) {
    return repo.findById(id)
        .map(Market::from)
        .orElseThrow(() -> new EntityNotFoundException("Market not found"));
  }

  @CacheEvict(value = "market", key = "#id")
  public void evict(Long id) {}
}

비동기 처리

설정 클래스에 @EnableAsync가 필요합니다.

@Service
public class NotificationService {
  @Async
  public CompletableFuture<Void> sendAsync(Notification notification) {
    // 이메일/SMS 전송
    return CompletableFuture.completedFuture(null);
  }
}

로깅 (SLF4J)

@Service
public class ReportService {
  private static final Logger log = LoggerFactory.getLogger(ReportService.class);

  public Report generate(Long marketId) {
    log.info("generate_report marketId={}", marketId);
    try {
      // 로직
    } catch (Exception ex) {
      log.error("generate_report_failed marketId={}", marketId, ex);
      throw ex;
    }
    return new Report();
  }
}

미들웨어 / 필터

@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
  private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    long start = System.currentTimeMillis();
    try {
      filterChain.doFilter(request, response);
    } finally {
      long duration = System.currentTimeMillis() - start;
      log.info("req method={} uri={} status={} durationMs={}",
          request.getMethod(), request.getRequestURI(), response.getStatus(), duration);
    }
  }
}

페이지네이션 및 정렬

PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
Page<Market> results = marketService.list(page);

오류에 강한 외부 호출

public <T> T withRetry(Supplier<T> supplier, int maxRetries) {
  int attempts = 0;
  while (true) {
    try {
      return supplier.get();
    } catch (Exception ex) {
      attempts++;
      if (attempts >= maxRetries) {
        throw ex;
      }
      try {
        Thread.sleep((long) Math.pow(2, attempts) * 100L);
      } catch (InterruptedException ie) {
        Thread.currentThread().interrupt();
        throw ex;
      }
    }
  }
}

속도 제한 (필터 + Bucket4j)

보안 참고: X-Forwarded-For 헤더는 클라이언트가 위조할 수 있으므로 기본적으로 신뢰할 수 없습니다. 전달된 헤더는 다음 조건에서만 사용하세요:

  1. 애플리케이션이 신뢰할 수 있는 리버스 프록시(nginx, AWS ALB 등) 뒤에 있는 경우
  2. ForwardedHeaderFilter를 빈으로 등록한 경우
  3. application.properties에서 server.forward-headers-strategy=NATIVE 또는 FRAMEWORK를 구성한 경우
  4. 프록시가 X-Forwarded-For 헤더를 추가(append)하지 않고 덮어쓰도록(overwrite) 구성된 경우

ForwardedHeaderFilter가 올바르게 구성되면 request.getRemoteAddr()는 전달된 헤더에서 올바른 클라이언트 IP를 자동으로 반환합니다. 이 구성이 없으면 request.getRemoteAddr()를 직접 사용하세요. 이 메서드는 신뢰할 수 있는 유일한 값인 직접 연결 IP를 반환합니다.

@Component
public class RateLimitFilter extends OncePerRequestFilter {
  private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

  /*
   * 보안: 이 필터는 속도 제한을 위해 클라이언트를 식별하기 위해 request.getRemoteAddr()를 사용합니다.
   *
   * 애플리케이션이 리버스 프록시(nginx, AWS ALB 등) 뒤에 있는 경우, 정확한 클라이언트 IP 감지를 위해
   * Spring이 전달된 헤더를 올바르게 처리하도록 구성해야 합니다:
   *
   * 1. application.properties/yaml에 server.forward-headers-strategy=NATIVE (클라우드 플랫폼용)
   *    또는 FRAMEWORK를 설정합니다.
   * 2. FRAMEWORK 전략을 사용하는 경우 ForwardedHeaderFilter를 등록합니다:
   *
   *    @Bean
   *    ForwardedHeaderFilter forwardedHeaderFilter() {
   *        return new ForwardedHeaderFilter();
   *    }
   *
   * 3. 프록시가 X-Forwarded-For 헤더를 추가하지 않고 덮어쓰도록 구성하여 위조를 방지합니다.
   * 4. 컨테이너에 맞게 server.tomcat.remoteip.trusted-proxies 등을 구성합니다.
   *
   * 이 구성이 없으면 request.getRemoteAddr()는 클라이언트 IP 대신 프록시 IP를 반환합니다.
   * 신뢰할 수 있는 프록시 처리 없이 X-Forwarded-For를 직접 읽지 마세요. 손쉽게 위조될 수 있습니다.
   */
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    // ForwardedHeaderFilter가 구성된 경우 올바른 클라이언트 IP를 반환하고,
    // 그렇지 않으면 직접 연결 IP를 반환하는 getRemoteAddr()를 사용합니다.
    // 적절한 프록시 구성 없이 X-Forwarded-For 헤더를 직접 신뢰하지 마십시오.
    String clientIp = request.getRemoteAddr();

    Bucket bucket = buckets.computeIfAbsent(clientIp,
        k -> Bucket.builder()
            .addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))
            .build());

    if (bucket.tryConsume(1)) {
      filterChain.doFilter(request, response);
    } else {
      response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
    }
  }
}

백그라운드 작업

Spring의 @Scheduled를 사용하거나 큐(예: Kafka, SQS, RabbitMQ)와 통합하세요. 핸들러는 멱등성(idempotent)과 관찰 가능성(observable)을 유지하세요.

관찰 가능성

  • 구조화된 로깅(JSON): Logback 인코더 사용
  • 메트릭: Micrometer + Prometheus/OTel
  • 추적: OpenTelemetry 또는 Brave 백엔드와 함께 Micrometer Tracing 사용

프로덕션 기본값

  • 필드 주입보다는 생성자 주입 선호
  • Spring Boot 3+에서 RFC 7807 오류를 위해 spring.mvc.problemdetails.enabled=true 활성화
  • 워크로드에 맞게 HikariCP 풀 크기 구성, 타임아웃 설정
  • 쿼리에는 @Transactional(readOnly = true) 사용
  • 적절한 곳에서 @NonNull과 Optional을 통해 null 안전성 강제

기억하세요: 컨트롤러는 얇게, 서비스는 집중적으로, 리포지토리는 단순하게, 오류는 중앙에서 처리하세요. 유지보수성과 테스트 용이성을 최적화하세요.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬