← 참고자료📚 전체 맵참고자료 →
Spring Framework

REST API 완전 정복

예외처리 · @Valid 유효성 검사 · application.yml 환경 설정

Part 1 — API · REST · Controller · Service
Part 2 — Repository · 전체 흐름 · DTO
Part 3 — 예외처리 · Valid · application.yml
예외 처리 — @ExceptionHandler · @ControllerAdvice

서버에서 에러가 났을 때 클라이언트에게 명확한 정보를 전달하는 방법. 에러 처리를 제대로 안 하면 스택트레이스가 그대로 노출된다.

① 예외 처리가 왜 필요한가
❌ 예외 처리 없을 때
GET /users/999 → 없는 사용자 조회
서버에서 NullPointerException 터짐
클라이언트한테 500 에러 + 스택트레이스 그대로 노출
{
  "status": 500,
  "error": "Internal Server Error",
  "trace": "java.lang.NullPointerException
    at UserService.java:25
    at UserController.java:14 ..."
  // ← 서버 내부 코드 노출 = 보안 위험!
}
✅ 예외 처리 있을 때
GET /users/999 → 없는 사용자
404 + 명확한 메시지로 클라이언트에 전달
서버 내부 코드 전혀 노출 안 됨
{
  "status": 404,
  "message": "없는 사용자입니다.",
  "code": "USER_NOT_FOUND",
  "timestamp": "2024-01-01T12:00:00"
}
② @ExceptionHandler — 컨트롤러 단위 예외 처리
@ExceptionHandler
@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> 조회(@PathVariable int id) {
        return ResponseEntity.ok(userService.사용자조회(id));
    }

    // 이 Controller 안에서 발생한 예외만 처리
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> 유저없음처리(UserNotFoundException e) {
        return ResponseEntity.status(404)
            .body(new ErrorResponse(404, e.getMessage(), "USER_NOT_FOUND"));
    }
}

// 문제점: Controller마다 @ExceptionHandler를 달아야 함
// UserController, OrderController, ProductController...
// 전부 같은 코드 반복 → @ControllerAdvice로 해결
③ @ControllerAdvice — 전역 예외 처리
@ControllerAdvice — 모든 Controller 예외를 한 곳에서
@ControllerAdvice  // 모든 Controller의 예외를 한 곳에서 처리
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> 유저없음(UserNotFoundException e) {
        return ResponseEntity.status(404)
            .body(new ErrorResponse(404, e.getMessage(), "USER_NOT_FOUND"));
    }

    @ExceptionHandler(DuplicateEmailException.class)
    public ResponseEntity<ErrorResponse> 이메일중복(DuplicateEmailException e) {
        return ResponseEntity.status(409)
            .body(new ErrorResponse(409, e.getMessage(), "DUPLICATE_EMAIL"));
    }

    // 예상 못한 모든 에러 처리
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> 서버에러(Exception e) {
        return ResponseEntity.status(500)
            .body(new ErrorResponse(500, "서버 오류가 발생했습니다.", "INTERNAL_ERROR"));
    }
}
④ @RestControllerAdvice vs @ControllerAdvice
@RestControllerAdvice ⭐ REST API에서 씀
= @ControllerAdvice + @ResponseBody
리턴값을 JSON으로 바로 응답
REST API 프로젝트에서 사용
@ControllerAdvice
HTML 뷰를 돌려줄 때
MVC 프로젝트에서 사용
⑤ 커스텀 예외 클래스 만들기
커스텀 예외 — 단순 버전 + 체계적 버전
// 단순 버전
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}

// 체계적인 버전 — 공통 부모 예외 만들기
public class BusinessException extends RuntimeException {
    private final String code;
    private final int status;

    public BusinessException(String message, String code, int status) {
        super(message);
        this.code = code;
        this.status = status;
    }
    public String getCode() { return code; }
    public int getStatus() { return status; }
}

// BusinessException 상속
public class UserNotFoundException extends BusinessException {
    public UserNotFoundException() {
        super("없는 사용자입니다.", "USER_NOT_FOUND", 404);
    }
}

public class DuplicateEmailException extends BusinessException {
    public DuplicateEmailException() {
        super("이미 있는 이메일입니다.", "DUPLICATE_EMAIL", 409);
    }
}
⑥ 에러 응답 DTO
ErrorResponse.java
@Getter @AllArgsConstructor
public class ErrorResponse {
    private int status;
    private String message;
    private String code;
    private LocalDateTime timestamp;

    public ErrorResponse(int status, String message, String code) {
        this.status = status;
        this.message = message;
        this.code = code;
        this.timestamp = LocalDateTime.now();
    }
}

// 응답 예시:
// {
//   "status": 404,
//   "message": "없는 사용자입니다.",
//   "code": "USER_NOT_FOUND",
//   "timestamp": "2024-01-01T12:00:00"
// }
⑦ HTTP 상태코드와 예외 연결
상태코드의미예외 상황
400Bad Request@Valid 유효성 검사 실패, 잘못된 요청 데이터
401Unauthorized로그인 안 됨, 토큰 없음 / 만료
403Forbidden로그인은 됐지만 권한 없음
404Not Found없는 사용자, 없는 게시글 등
409Conflict이미 있는 이메일, 중복 데이터
500Internal Server Error예상 못한 서버 내부 오류
⑧ 예외 계층 구조 설계
Exception
RuntimeException
BusinessException ← 공통 부모. 여기 하나만 잡으면 전부 처리됨
UserNotFoundException
(404)
DuplicateEmailException
(409)
OrderNotFoundException
(404)
UnauthorizedException
(401)
⑨ 예외 처리 실전 전체 코드
GlobalExceptionHandler.java — 실전 전체
@RestControllerAdvice
public class GlobalExceptionHandler {

    // BusinessException 상속한 것들 전부 처리
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> 비즈니스예외(BusinessException e) {
        return ResponseEntity.status(e.getStatus())
            .body(new ErrorResponse(
                e.getStatus(), e.getMessage(), e.getCode(),
                LocalDateTime.now()));
    }

    // @Valid 유효성 검사 실패
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> 유효성검사실패(
            MethodArgumentNotValidException e) {
        String message = e.getBindingResult()
                          .getFieldErrors()
                          .get(0)
                          .getDefaultMessage();
        return ResponseEntity.status(400)
            .body(new ErrorResponse(400, message, "VALIDATION_ERROR",
                LocalDateTime.now()));
    }

    // 예상 못한 모든 에러
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> 서버에러(Exception e) {
        return ResponseEntity.status(500)
            .body(new ErrorResponse(500, "서버 오류가 발생했습니다.",
                "INTERNAL_ERROR", LocalDateTime.now()));
    }
}
@Valid — 유효성 검사

클라이언트가 보내는 데이터를 믿으면 안 된다. @Valid 애너테이션 하나로 클린하게 검사할 수 있다.

① 유효성 검사가 왜 필요한가
❌ 검사 없을 때
{
  "name": "",
  "email": "이건이메일아님",
  "age": -999
}
// 이상한 데이터가 DB에 그대로 저장됨
✅ @Valid 있을 때
{
  "status": 400,
  "message": "이름은 필수입니다.",
  "code": "VALIDATION_ERROR"
}
// DB까지 가지도 않고 바로 400 응답
② 의존성 추가
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
③ 기본 제공 애너테이션 전체
유효성 검사 애너테이션 전체
public class UserCreateRequest {

    // ─── 문자열 ───────────────────────────────
    @NotNull              // null 안 됨
    @NotEmpty             // null, "" 안 됨
    @NotBlank             // null, "", " " 안 됨 (공백만 있어도 안 됨) ← 제일 많이 씀
    private String name;

    @Size(min=2, max=50)   // 길이 2~50
    private String name;

    @Email                // 이메일 형식이어야 함
    private String email;

    @Pattern(regexp = "^[a-zA-Z0-9]{6,12}$")  // 정규식 검사
    private String password;

    // ─── 숫자 ─────────────────────────────────
    @Min(1)               // 최솟값 1 이상
    @Max(150)             // 최댓값 150 이하
    private int age;

    @Positive             // 양수만 (0 안 됨)
    @PositiveOrZero       // 양수 또는 0
    @Negative             // 음수만
    @NegativeOrZero       // 음수 또는 0
    private int count;

    @DecimalMin("0.0")    // 소수 최솟값
    @DecimalMax("100.0") // 소수 최댓값
    private double score;

    // ─── 날짜 ─────────────────────────────────
    @Past                 // 과거 날짜만
    @PastOrPresent        // 과거 또는 현재
    @Future               // 미래 날짜만
    @FutureOrPresent      // 미래 또는 현재
    private LocalDate birthDate;

    // ─── 불린 ─────────────────────────────────
    @AssertTrue           // true여야 함
    @AssertFalse          // false여야 함
    private boolean agreeTerms;
}
④ @Valid vs @Validated
@Valid vs @Validated
// @Valid — javax 표준, 기본 유효성 검사
@PostMapping
public ResponseEntity<UserResponse> 회원가입(
        @Valid @RequestBody UserCreateRequest request) { }

// @Validated — 스프링 제공, 그룹 검사 가능
@PostMapping
public ResponseEntity<UserResponse> 회원가입(
        @Validated(CreateGroup.class) @RequestBody UserCreateRequest request) { }

// @Validated는 메서드 파라미터에도 사용 가능 (@PathVariable 등)
@Validated
@RestController
public class UserController {
    @GetMapping("/{id}")
    public User 조회(@PathVariable @Positive int id) { }
    // id가 양수인지도 검사 가능
}
@Valid@Validated
출처javax 표준스프링 제공
그룹 검사불가가능
메서드 파라미터 검사@RequestBody만@PathVariable 등도 가능
⑤ 커스텀 검사 애너테이션 만들기
왜 필요한가?

기본 제공 애너테이션으로 해결 안 되는 경우. 예: 전화번호 형식 검사, 특정 도메인 이메일만 허용 등.

커스텀 유효성 검사 — 전화번호 예시
// ① 애너테이션 정의
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface ValidPhone {
    String message() default "올바른 전화번호 형식이 아닙니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// ② 실제 검사 로직
public class PhoneValidator
        implements ConstraintValidator<ValidPhone, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext ctx) {
        if (value == null) return false;
        return value.matches("^01[016789]-?\\d{3,4}-?\\d{4}$");
    }
}

// ③ 사용
public class UserCreateRequest {
    @ValidPhone
    private String phone;
}
⑥ 중첩 객체 검사
중첩 객체에 @Valid 필요
public class OrderCreateRequest {
    @NotNull
    private String productName;

    @Valid    // ← 중첩 객체도 검사하려면 @Valid 붙여야 함!
    @NotNull
    private Address address;
}

public class Address {
    @NotBlank
    private String street;
    @NotBlank
    private String city;
}

// @Valid 없으면 Address 안의 @NotBlank 검사가 동작 안 함!
// 중첩 객체 검사할 때 @Valid 꼭 붙여야 함
⑦ 그룹 검사 — 생성할 때와 수정할 때 조건이 다른 경우
그룹 검사
// 그룹 인터페이스 정의
public interface CreateGroup {}
public interface UpdateGroup {}

// DTO에서 그룹 지정
public class UserRequest {

    @NotNull(groups = UpdateGroup.class)  // 수정할 때만 필수
    private Integer id;

    @NotBlank(groups = {CreateGroup.class, UpdateGroup.class})  // 생성/수정 둘 다
    private String name;

    @Email(groups = CreateGroup.class)   // 생성할 때만 검사
    private String email;
}

// Controller에서 그룹 지정
@PostMapping
public ResponseEntity<?> 생성(
        @Validated(CreateGroup.class) @RequestBody UserRequest request) { }

@PutMapping("/{id}")
public ResponseEntity<?> 수정(
        @Validated(UpdateGroup.class) @RequestBody UserRequest request) { }
⑧ 에러 메시지 커스터마이징
메시지 지정 + 전체 에러 꺼내기
// DTO에서 메시지 직접 지정
public class UserCreateRequest {
    @NotBlank(message = "이름은 필수입니다.")
    private String name;

    @Email(message = "이메일 형식이 올바르지 않습니다.")
    private String email;

    @Size(min=8, max=20, message = "비밀번호는 8~20자여야 합니다.")
    private String password;
}

// GlobalExceptionHandler에서 에러 메시지 꺼내기
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> 유효성검사실패(MethodArgumentNotValidException e) {

    // 첫 번째 에러 메시지만
    String message = e.getBindingResult()
                      .getFieldErrors()
                      .get(0)
                      .getDefaultMessage();

    // 모든 에러 메시지 한꺼번에
    List<String> messages = e.getBindingResult()
                              .getFieldErrors()
                              .stream()
                              .map(FieldError::getDefaultMessage)
                              .collect(Collectors.toList());

    return ResponseEntity.status(400)
        .body(new ErrorResponse(400, messages.toString(),
                                "VALIDATION_ERROR", LocalDateTime.now()));
}
⑨ @Valid 실전 전체 코드
@Valid 실전 전체
// DTO
@Getter
public class UserCreateRequest {

    @NotBlank(message = "이름은 필수입니다.")
    @Size(min=2, max=50, message = "이름은 2~50자여야 합니다.")
    private String name;

    @NotBlank(message = "이메일은 필수입니다.")
    @Email(message = "이메일 형식이 올바르지 않습니다.")
    private String email;

    @NotBlank(message = "비밀번호는 필수입니다.")
    @Size(min=8, max=20, message = "비밀번호는 8~20자여야 합니다.")
    @Pattern(regexp="^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$",
             message="비밀번호는 영문과 숫자를 포함해야 합니다.")
    private String password;

    @Min(value=1, message="나이는 1 이상이어야 합니다.")
    @Max(value=150, message="나이는 150 이하여야 합니다.")
    private int age;
}

// Controller
@PostMapping
public ResponseEntity<UserResponse> 회원가입(
        @Valid @RequestBody UserCreateRequest request) {
    // @Valid 실패하면 여기까지 오지도 않음
    // GlobalExceptionHandler에서 자동으로 400 처리
    return ResponseEntity.status(201)
        .body(userService.회원가입(request));
}
application.yml — 환경 설정

DB 주소, 포트번호, 비밀번호, JPA 설정 등 애플리케이션 동작에 필요한 모든 설정값을 코드 밖에서 관리하는 파일.

① application.yml이 왜 필요한가
이유 ① 보안
DB 비밀번호가 코드에 박혀있으면 Git에 올라가는 순간 유출됨
이유 ② 환경 분리
개발/운영 환경마다 설정이 다른데, 코드를 바꿔서 배포하면 위험
이유 ③ 유지보수
설정 바꿀 때마다 코드 수정 → 재배포 필요 없이 설정 파일만 바꾸면 됨
② .properties vs .yml 차이
application.properties
# 반복이 많음
spring.datasource.url=jdbc:mysql://localhost/db
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql...
application.yml ⭐ 더 많이 씀
# 계층 구조, 깔끔함
spring:
  datasource:
    url: jdbc:mysql://localhost/db
    username: root
    password: 1234
    driver-class-name: com.mysql...
⚠️ yml 작성 시 주의사항

들여쓰기는 반드시 스페이스(공백)만 사용. 탭(Tab) 쓰면 에러남.
보통 스페이스 2칸으로 들여쓰기.

③ 기본 구조
yml 기본 문법
# 문자열
name: 라희

# 숫자
port: 8080

# 불린
enabled: true

# 리스트
servers:
  - server1
  - server2

# 객체 (계층 구조)
database:
  host: localhost
  port: 3306
④ 서버 설정
server 설정
server:
  port: 8080                     # 포트번호 (기본값 8080)
  servlet:
    context-path: /api            # 모든 URL 앞에 /api 자동으로 붙음
                                  # /users → /api/users
  tomcat:
    max-threads: 200             # 최대 스레드 수
    connection-timeout: 5000     # 연결 타임아웃 (ms)
⑤ DB 설정
datasource 설정
spring:
  datasource:
    # MySQL
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Seoul
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver

    # H2 (테스트용 인메모리 DB)
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:

  # H2 콘솔 (브라우저에서 DB 직접 확인 가능)
  h2:
    console:
      enabled: true
      path: /h2-console            # localhost:8080/h2-console 로 접속
⑥ JPA 설정
jpa 설정
spring:
  jpa:
    hibernate:
      ddl-auto: update       # DB 테이블 자동 생성/수정 전략
    show-sql: true           # 실행되는 SQL을 콘솔에 출력
    properties:
      hibernate:
        format_sql: true     # SQL을 보기 좋게 포맷팅
        dialect: org.hibernate.dialect.MySQL8Dialect  # DB 방언
ddl-auto 옵션동작언제 쓰나
create시작할 때 테이블 새로 만듦데이터 전부 삭제됨
create-drop시작할 때 만들고 종료할 때 삭제테스트 환경
update변경된 것만 반영 (데이터 유지)개발 환경
validateEntity와 테이블 맞는지 검사만검증 목적
none아무것도 안 함운영 환경 (강력 권장)
⑦ 로깅 설정
logging 설정
logging:
  level:
    root: INFO                           # 전체 기본 로그 레벨
    com.myapp: DEBUG                     # 내 패키지는 DEBUG
    org.hibernate.SQL: DEBUG             # SQL 로그
    org.hibernate.type.descriptor: TRACE # SQL 파라미터 값도 출력
  file:
    name: logs/app.log                   # 로그 파일 저장 위치
로그 레벨 순서 — 아래로 갈수록 중요
TRACE DEBUG INFO WARN ERROR

INFO로 설정하면 INFO, WARN, ERROR만 출력됨
DEBUG로 설정하면 DEBUG, INFO, WARN, ERROR 전부 출력됨

⑧ 환경별 설정 분리 (dev, prod)
파일 구조
src/main/resources/
├── application.yml          # 공통 설정 + 활성 profile 지정
├── application-dev.yml      # 개발 환경 설정
└── application-prod.yml     # 운영 환경 설정
application.yml / dev / prod
# application.yml — 공통
spring:
  profiles:
    active: dev    # 운영 배포할 때는 prod로 바꿈
server:
  port: 8080

---
# application-dev.yml — 개발환경
spring:
  datasource:
    url: jdbc:h2:mem:testdb    # 개발은 H2 인메모리 DB
  jpa:
    hibernate:
      ddl-auto: create-drop    # 매번 새로 만들어도 됨
    show-sql: true
logging:
  level:
    com.myapp: DEBUG

---
# application-prod.yml — 운영환경
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/${DB_NAME}
    username: ${DB_USERNAME}   # 환경변수에서 읽어옴
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: none           # 운영은 절대 자동 변경 안 함!
    show-sql: false
logging:
  level:
    root: WARN
  file:
    name: logs/app.log
⑨ @Value — 설정값 코드에서 꺼내기
@Value 사용법
# application.yml
jwt:
  secret: mySecretKey123
  expiration: 3600000

// 코드에서 꺼내기
@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration;

    // 기본값 지정 — 설정이 없으면 "myapp" 사용
    @Value("${jwt.issuer:myapp}")
    private String issuer;
}
⑩ @ConfigurationProperties — 설정값 묶음으로 꺼내기
@Value vs @ConfigurationProperties

@Value → 설정값 하나씩 꺼낼 때
@ConfigurationProperties → 관련 설정값을 묶음으로 관리할 때 (필드 많을 때 권장)

@ConfigurationProperties 사용법
# application.yml
jwt:
  secret: mySecretKey123
  expiration: 3600000
  issuer: myapp
  refresh-expiration: 604800000

// 설정값 묶음 클래스
@ConfigurationProperties(prefix = "jwt")
@Component
@Getter @Setter
public class JwtProperties {
    private String secret;
    private long expiration;
    private String issuer;
    private long refreshExpiration;  // refresh-expiration → camelCase 자동 변환
}

// 사용
@RequiredArgsConstructor
public class JwtUtil {
    private final JwtProperties jwtProperties;

    public String generateToken() {
        return Jwts.builder()
            .setIssuer(jwtProperties.getIssuer())
            .compact();
    }
}
⑪ 민감한 정보 관리 — 환경변수
환경변수로 민감 정보 관리
# ❌ 절대 이렇게 하면 안 됨 — Git에 올라가면 큰일남
spring:
  datasource:
    password: 실제비밀번호1234

# ✅ 환경변수로 관리
spring:
  datasource:
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
jwt:
  secret: ${JWT_SECRET}

# 실행할 때 환경변수 주입
$ export DB_USERNAME=root
$ export DB_PASSWORD=1234
$ java -jar app.jar

# 또는 실행 시 직접 주입
$ java -jar app.jar \
  --spring.datasource.username=root \
  --spring.datasource.password=1234

# .gitignore에 추가해서 운영 설정 파일이 Git에 올라가지 않게
application-prod.yml
⑫ Jasypt — 설정값 암호화
Jasypt 설정값 암호화
// build.gradle
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5'

# application.yml — 암호화된 값은 ENC()로 감싸기
spring:
  datasource:
    password: ENC(암호화된값여기)
jasypt:
  encryptor:
    password: ${JASYPT_PASSWORD}   # 복호화 키는 환경변수로

// 암호화할 때 (직접 실행해서 암호화된 값 생성)
BasicTextEncryptor encryptor = new BasicTextEncryptor();
encryptor.setPassword("복호화키");
String encrypted = encryptor.encrypt("실제비밀번호");
// 이 encrypted 값을 ENC(암호화된값여기) 안에 넣으면 됨

// 환경변수 vs Jasypt
// 환경변수 → 서버 환경에서 외부로 빼서 관리
// Jasypt   → yml 파일 자체에서 암호화해서 관리
// 둘 다 알아야 — 상황에 따라 골라서 씀
⑬ @Profile — 특정 환경에서만 Bean 등록
@Profile 사용법
// dev 환경에서만 등록되는 Bean
@Profile("dev")
@Component
public class DevDataLoader implements CommandLineRunner {
    private final UserRepository userRepository;

    @Override
    public void run(String... args) {
        // 개발환경 시작할 때 테스트 데이터 자동 생성
        userRepository.save(new User("테스트유저", "[email protected]"));
    }
}

// prod 환경에서만 등록
@Profile("prod")
@Component
public class ProdConfig { }

// dev가 아닐 때
@Profile("!dev")
@Component
public class NotDevConfig { }
⑭ CommandLineRunner / ApplicationRunner — 서버 시작할 때 자동 실행
CommandLineRunner / ApplicationRunner
// CommandLineRunner — String[] args로 받음
@Component
public class DataLoader implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        // 서버 시작할 때 자동으로 여기 코드 실행됨
        System.out.println("서버 시작!");
    }
}

// ApplicationRunner — ApplicationArguments로 받음 (더 편리)
@Component
public class DataLoader implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // java -jar app.jar --mode=test 로 실행하면
        args.getOptionValues("mode");  // → ["test"]
    }
}

// @Profile("dev") + CommandLineRunner — 개발 시작할 때 테스트 데이터 넣기
@Profile("dev")
@Component
@RequiredArgsConstructor
public class DevDataLoader implements CommandLineRunner {
    private final UserRepository userRepository;

    @Override
    public void run(String... args) {
        userRepository.save(new User("테스트유저", "[email protected]"));
        System.out.println("개발 테스트 데이터 생성 완료");
    }
}
⑮ Actuator — 운영 서버 상태 모니터링
Actuator 설정
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-actuator'

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health, info, metrics   # 노출할 엔드포인트만 지정
  endpoint:
    health:
      show-details: always               # 상세 health 정보 표시

# 제공하는 엔드포인트
# GET /actuator/health  → 서버 살아있는지 확인
# GET /actuator/metrics → 메모리, CPU 등 지표
# GET /actuator/info    → 앱 정보

# ⚠️ 운영 환경에서 전부 노출하면 위험
# include에 필요한 것만 지정하거나 Security로 보호해야 함
⑯ application.yml 실전 전체 코드
실전 — dev / prod 전체
# application.yml — 공통
spring:
  profiles:
    active: dev

---
# application-dev.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true
      path: /h2-console
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        format_sql: true
server:
  port: 8080
jwt:
  secret: devSecretKey123456789012345678901234
  expiration: 3600000
  issuer: myapp
logging:
  level:
    com.myapp: DEBUG
    org.hibernate.SQL: DEBUG

---
# application-prod.yml
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/${DB_NAME}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: none
    show-sql: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
server:
  port: 8080
  servlet:
    context-path: /api
jwt:
  secret: ${JWT_SECRET}
  expiration: 3600000
  issuer: myapp
logging:
  level:
    root: WARN
  file:
    name: logs/app.log
management:
  endpoints:
    web:
      exposure:
        include: health
REST API 완전 정복 — 3부 완결
Part 1
API · REST · Controller · Service
Part 2
Repository · 전체 흐름 · DTO · MapStruct
Part 3
예외처리 · @Valid · application.yml
← 참고자료📚 전체 맵참고자료 →