Spring Boot 완벽 가이드 | REST API·JPA·Security·Actuator·배포
이 글의 핵심
Spring Boot 완벽 가이드에 대해 정리한 개발 블로그 글입니다. Spring Boot로 엔터프라이즈 애플리케이션을 구축하는 완벽 가이드입니다. REST API, JPA, Spring Security, Actuator, 테스트, 배포까지 실전 예제로 정리했으며, 자동… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다. 관련 키워드: Spring…
이 글의 핵심
Spring Boot로 엔터프라이즈 애플리케이션을 구축하는 완벽 가이드입니다. REST API, JPA, Spring Security, Actuator, 테스트, 배포까지 실전 예제로 정리했으며, 자동 설정·ApplicationContext 생명주기·DI·AOP 프록시·프로덕션 패턴까지 내부 동작을 심화해서 다룹니다.
실무 경험 공유: 레거시 Spring 프로젝트를 Spring Boot로 마이그레이션하면서, 설정 코드를 70% 줄이고 개발 속도를 2배 향상시킨 경험을 공유합니다.
들어가며: “Spring 설정이 복잡해요”
실무 문제 시나리오
시나리오 1: XML 설정이 수백 줄이에요
레거시 Spring은 XML이 복잡합니다. Spring Boot는 자동 설정합니다. 시나리오 2: 서버 설정이 번거로워요
Tomcat을 따로 설치해야 합니다. Spring Boot는 내장 서버를 제공합니다. 시나리오 3: 의존성 관리가 어려워요
버전 충돌이 자주 발생합니다. Spring Boot Starter가 해결합니다.
1. Spring Boot란?
핵심 특징
Spring Boot는 Spring 기반 애플리케이션을 빠르게 개발하는 프레임워크입니다. 주요 장점:
- 자동 설정: Convention over Configuration
- 내장 서버: Tomcat, Jetty 내장
- Starter: 의존성 간편 관리
- Actuator: 모니터링 내장
- 프로덕션 준비: 즉시 배포 가능
2. 프로젝트 생성
Spring Initializr
# https://start.spring.io/
# 또는 CLI
curl https://start.spring.io/starter.zip \
-d dependencies=web,data-jpa,postgresql,security \
-d type=maven-project \
-d language=java \
-d bootVersion=3.2.0 \
-d baseDir=myapp \
-o myapp.zip
unzip myapp.zip
cd myapp
./mvnw spring-boot:run
3. REST API
Controller
// src/main/java/com/example/demo/controller/UserController.java
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User created = userService.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
return userService.findById(id)
.map(existing -> {
user.setId(id);
return ResponseEntity.ok(userService.save(user));
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteById(id);
return ResponseEntity.noContent().build();
}
}
4. JPA
Entity
// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
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;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
// Getters and Setters
}
Repository
// src/main/java/com/example/demo/repository/UserRepository.java
package com.example.demo.repository;
import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Service
// src/main/java/com/example/demo/service/UserService.java
package com.example.demo.service;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> findAll() {
return userRepository.findAll();
}
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
public User save(User user) {
return userRepository.save(user);
}
public void deleteById(Long id) {
userRepository.deleteById(id);
}
}
5. Spring Security
설정
// src/main/java/com/example/demo/config/SecurityConfig.java
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/users/**").authenticated()
.anyRequest().authenticated()
)
.httpBasic();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
6. Validation
// src/main/java/com/example/demo/dto/CreateUserDto.java
package com.example.demo.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class CreateUserDto {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
// Getters and Setters
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserDto dto) {
// ...
}
7. 예외 처리
// src/main/java/com/example/demo/exception/GlobalExceptionHandler.java
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Map<String, String>> handleNotFound(ResourceNotFoundException ex) {
Map<String, String> error = new HashMap<>();
error.put("error", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleGeneral(Exception ex) {
Map<String, String> error = new HashMap<>();
error.put("error", "Internal server error");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
8. 테스트
// src/test/java/com/example/demo/service/UserServiceTest.java
// 패키지 선언
package com.example.demo.service;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void findById_shouldReturnUser() {
User user = new User();
user.setId(1L);
user.setName("John");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
Optional<User> result = userService.findById(1L);
assertTrue(result.isPresent());
assertEquals("John", result.get().getName());
}
}
9. 배포
JAR 빌드
./mvnw clean package
java -jar target/myapp-0.0.1-SNAPSHOT.jar
Docker
FROM eclipse-temurin:21-jdk-alpine as builder
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
10. Spring Boot 내부 동작 심화 (엔지니어 관점)
실무에서 “왜 이렇게 동작하지?”를 설명하려면 자동 설정, 컨텍스트 생명주기, 빈과 DI, AOP 프록시가 한 줄로 이어진다고 보면 된다. 아래는 운영·장애 분석·성능 튜닝에 바로 연결되는 내부 모델이다.
10.1 자동 설정(Auto-configuration) 메커니즘
@SpringBootApplication은 사실 세 가지를 한 덩어리로 묶은 진입점 메타 애너테이션이다. @Configuration(구성 클래스), @ComponentScan(컴포넌트 스캔), @EnableAutoConfiguration(자동 설정 로딩)이 합쳐진 형태로 이해하면 이후 동작이 선명해진다.
자동 설정 클래스는 클래스패스와 프로퍼티를 읽어 조건부로 빈을 등록한다. Spring Boot 3.x에서는 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports에 등록된 클래스 목록이 로딩 대상이며, 과거 방식인 META-INF/spring.factories의 EnableAutoConfiguration 키는 레거시 호환 경로로만 기억하면 된다.
핵심은 조건 평가(Conditional) 이다. 예를 들어 @ConditionalOnClass는 특정 클래스가 클래스패스에 있을 때만 설정을 활성화하고, @ConditionalOnBean은 이미 특정 빈이 등록된 경우에만 추가 빈을 붙인다. @ConditionalOnProperty는 spring.* 같은 외부 설정 값으로 기능을 켜고 끈다. 이 조합 덕분에 “웹이면 서블릿 스택을, JPA면 Hibernate를” 같은 관례 기반 기본값이 자동으로 맞춰진다.
자동 설정끼리의 순서는 @AutoConfigureBefore / @AutoConfigureAfter / @AutoConfigureOrder로 조정한다. 충돌이나 중복 등록이 의심될 때는 spring.autoconfigure.exclude로 특정 자동 설정 클래스를 제외하거나, @SpringBootApplication(exclude = ...)로 좁혀서 검증하는 것이 일반적인 운영 패턴이다.
디버깅 팁: --debug 또는 logging.level.org.springframework.boot.autoconfigure=DEBUG로 어떤 자동 설정이 매칭/비매칭되었는지 로그로 확인할 수 있다. “로컬에서는 되는데 서버만 실패”류의 이슈는 대부분 클래스패스 차이 또는 프로파일·환경 변수 차이가 조건을 바꾼 경우가 많다.
10.2 ApplicationContext 생명주기
스프링 애플리케이션의 런타임 중심은 ApplicationContext이다. Spring Boot 웹 애플리케이션에서는 보통 AnnotationConfigServletWebServerApplicationContext(서블릿 기반) 계열이 사용되며, 컨텍스트 refresh 과정에서 빈 팩토리가 완성되고 이벤트가 발행된다.
refresh()의 큰 흐름을 엔지니어 수준에서만 짚으면 다음과 같다. (세부 단계명은 버전에 따라 세분화되지만 개념은 동일하다.)
- 환경 준비:
Environment(프로파일, 프로퍼티 소스)가 바인딩된다. - BeanFactory 후처리:
BeanFactoryPostProcessor가 빈 정의(Definition) 를 바꾼다.PropertySourcesPlaceholderConfigurer같은 것이 여기서 동작한다. - 빈 후처리기 등록:
BeanPostProcessor가 준비된다. 이후 단계에서 빈 인스턴스를 감싼다(AOP 포함). - 싱글톤 빈 초기화: 의존성 그래프를 따라 빈을 생성한다.
- 특화 단계: 웹이라면
onRefresh()에서 내장 웹 서버가 시작되는 흐름이 이어진다(Servlet 컨텍스트).
생명주기 이벤트로는 ContextRefreshedEvent, 부트의 경우 ApplicationReadyEvent 등이 대표적이다. @PostConstruct는 빈 생성 직후, ApplicationRunner / CommandLineRunner는 컨텍스트가 준비된 뒤 실행된다는 점이 다르다. “초기화 순서 버그” 를 볼 때는 이 단계를 기준으로 원인을 쪼개면 해결 속도가 빨라진다.
10.3 빈 생성과 의존성 주입(DI)의 실체
스프링 DI는 빈 정의 → 빈 팩토리 → 빈 인스턴스의 파이프라인으로 이해하는 것이 정확하다. @Component, @Bean, 자동 설정이 만드는 팩토리 메서드는 모두 결국 BeanDefinition으로 수렴한다.
주입 지점은 생성자, 필드, 세터가 있으며 Spring 권장은 불변성과 테스트 용이성 측면에서 가능한 한 필수 의존성은 생성자 주입이다. 생성자 주입은 의존성이 명시적으로 드러나고, 순환 의존을 조기에 드러내는 장점이 있다. 반면 순환 의존이 실제로 필요한 설계인지(대개는 아님)부터 재검토해야 한다.
@Autowired 해석은 타입을 기준으로 후보를 찾고, 복수 후보일 때 @Primary, @Qualifier, 컬렉션 주입 규칙으로 결정된다. 스코프가 다른 빈(예: 요청 스코프를 싱글톤에 주입)을 섞을 때는 프록시가 개입할 수 있어, 그때는 @Lazy나 ObjectProvider, Provider 같은 지연 조회 패턴을 고려한다.
내부적으로 BeanPostProcessor는 트랜잭션, 검증, AOP 같은 횡단 관심사를 빈 인스턴스에 덧입히는 지점이다. 그래서 “내가 만든 순수 객체”와 “컨테이너가 꺼내주는 객체”가 다를 수 있다는 점을 염두에 두어야 한다.
10.4 AOP 프록시 생성과 자기 호출(Self-invocation) 문제
스프링 AOP는 기본적으로 프록시 기반이다. 인터페이스 기반 빈에는 JDK 동적 프록시, 클래스 기반에는 CGLIB 서브클래스 프록시가 쓰이는 경우가 많다. Spring Boot 2.x 이후 설정에 따라 클래스 프록시를 우선하는 경향이 강해졌고(spring.aop.proxy-target-class 기본이 사실상 true에 가깝다), 이는 구체 클래스에 직접 @Transactional 등을 붙이는 코드 스타일과도 맞물린다.
프록시가 붙는 이유는 메서드 호출을 가로채 부가 기능(트랜잭션, 보안, 로깅, 재시도) 을 적용하기 위해서다. 따라서 같은 클래스 안에서 this로 메서드를 호출하면 프록시를 거치지 않는 자기 호출이 되어 @Transactional이 기대대로 동작하지 않을 수 있다. 해결은 트랜잭션 경계를 외부 컴포넌트로 분리하거나, 필요 시 TransactionTemplate/EntityManager 같은 명시 API로 경계를 옮기는 식으로 경계 설계를 바로잡는 것이 근본이다.
운영 관점에서는 “프록시 때문에 스택이 깊다”는 것이 관측(APM)에서 흔히 보인다. 이는 이상이 아니라 설계상 자연스러운 비용이며, 불필요한 AOP 포인트컷 과다 적용은 피하는 것이 좋다.
10.5 프로덕션 Spring Boot 패턴 (현장 체크리스트)
설정과 비밀: 12-Factor 관점에서 설정은 이미지에 박지 않고 환경 변수·시크릿 스토어로 주입한다. @ConfigurationProperties로 타입 안전 바인딩을 하고, @Validated로 제약을 걸면 시작 시점에 잘못된 설정을 조기 실패시킬 수 있다.
프로파일: spring.profiles.active로 dev / staging / prod를 분리하되, 프로덕션 전용 차이는 코드 분기보다 설정으로 해결하는 편이 안전하다.
관측성: Actuator 엔드포인트는 공개 범위를 최소화하고, Micrometer 메트릭과 분산 트레이싱(예: OpenTelemetry)을 붙여 지연의 원인을 프레임워크 경계에서부터 추적 가능하게 한다.
데이터 접근: 커넥션 풀(HikariCP)의 최대 풀 크기, 타임아웃, DB 드라이버의 네트워크 타임아웃을 함께 튜닝한다. 풀만 키우고 DB max_connections를 무시하면 오히려 장애가 커진다.
종료 처리: Kubernetes 환경에서는 유예 종료(graceful shutdown) 와 헬스 프로브 설정을 맞춰 진행 중 트래픽이 끊기지 않게 한다. Spring Boot는 서버 종료 시 요청을 배출하는 옵션을 제공하므로, 버전별 문서의 Graceful Shutdown 항목을 기준으로 조정한다.
아키텍처: 컨트롤러는 얇게, 도메인 규칙은 서비스/도메인 계층으로. 트랜잭션 경계는 일관성이 필요한 유스케이스 단위로 유지한다. “어디까지가 프록시 경계인지, 어디서 트랜잭션이 열리는지”를 팀 규칙으로 명시하면 장애 대응 비용이 줄어든다.
정리 및 체크리스트
핵심 요약
- Spring Boot: 빠른 Spring 개발
- 자동 설정: 최소한의 설정
- 내부 동작: 조건부 자동 설정, 컨텍스트 생명주기, DI 파이프라인, AOP 프록시 경계
- REST API: @RestController
- JPA: 데이터베이스 추상화
- Spring Security: 인증/인가
- Actuator: 모니터링
구현 체크리스트
- Spring Boot 프로젝트 생성
- REST API 구현
- JPA Entity 정의
- Spring Security 설정
- Validation 구현
- 테스트 작성
- Docker 배포
- 자동 설정 매칭 로그(
--debug)로 환경별 차이 점검 - 프로덕션: 프로파일·설정 바인딩·관측성·종료 처리 점검
같이 보면 좋은 글
- NestJS 완벽 가이드
- FastAPI 완벽 가이드
- PostgreSQL 고급 가이드
이 글에서 다루는 키워드
Spring Boot, Java, Backend, REST API, JPA, Spring Security, Enterprise, 자동 설정, ApplicationContext, DI, AOP
자주 묻는 질문 (FAQ)
Q. Spring vs Spring Boot, 차이가 뭔가요?
A. Spring Boot는 Spring을 더 쉽게 사용하도록 만든 프레임워크입니다. 자동 설정과 내장 서버를 제공합니다.
Q. Kotlin으로도 사용할 수 있나요?
A. 네, Spring Boot는 Kotlin을 완벽하게 지원합니다.
Q. 마이크로서비스에 적합한가요?
A. 네, Spring Cloud와 함께 사용하면 마이크로서비스 아키텍처를 쉽게 구축할 수 있습니다.
Q. 프로덕션에서 사용해도 되나요?
A. 네, 전 세계 수많은 기업에서 엔터프라이즈 애플리케이션으로 사용합니다.
Q. 자동 설정은 어떻게 켜지고 꺼지나요?
A. 클래스패스와 @Conditional* 조건, spring.* 프로퍼티를 조합해 빈 정의가 등록됩니다. 제외는 spring.autoconfigure.exclude 등으로 제어합니다.
Q. @Transactional이 같은 클래스 안 호출에서 안 먹는 이유는?
A. 프록시 밖에서 호출될 때만 부가 기능이 적용되는 경우가 많습니다. 자기 호출(this)은 프록시를 거치지 않아 경계 설계를 점검해야 합니다.
Q. ApplicationContext refresh는 왜 중요한가요?
A. 빈 정의 후처리·빈 생성·웹 서버 기동 등이 이 흐름에서 일어납니다. 초기화 순서 이슈는 이 단계를 기준으로 분해하면 원인 파악이 빨라집니다.
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「Spring Boot 완벽 가이드 | REST API·JPA·Security·Actuator·배포」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「Spring Boot 완벽 가이드 | REST API·JPA·Security·Actuator·배포」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.