[SKALA] Spring Boot 설정 관리와 MVC·Actuator 정리

2026. 7. 29. 08:49·개발/SKALA 4기
반응형

Configuration, Profile부터 Spring MVC 요청 처리와 운영 모니터링까지

이전 글에서는 객체지향 프로그래밍과 REST API, Spring Boot의 기본 구조, IoC와 의존성 주입을 정리했다.

이번에는 Spring Boot 애플리케이션을 실제로 구성하고 운영하는 방법을 중심으로 정리해보았다.

 

전체 흐름은 다음과 같다.

외부 설정 관리
→ 환경별 Profile 분리
→ Spring MVC 요청 처리
→ Controller·Service·Repository 역할 분리
→ View 또는 JSON 응답
→ Actuator를 이용한 운영 상태 확인

 


1. Configuration과 외부 설정 관리

Spring Boot 애플리케이션에는 코드 외에도 다양한 설정이 필요하다.

예를 들면 다음과 같다.

서버 포트
데이터베이스 주소와 계정
로그 레벨
외부 API 주소
API Key
파일 업로드 제한
서비스 동작 모드

이러한 값을 Java 코드 안에 직접 작성하는 것을 하드코딩이라고 한다.

public class DatabaseConnection {

    private final String url =
            "jdbc:mysql://localhost:3306/shop";

    private final String username = "root";
    private final String password = "password";
}

하드코딩 방식은 간단해 보이지만 실행 환경이 바뀔 때 문제가 생긴다.

  • 개발 DB와 운영 DB가 다르면 코드를 수정해야 한다.
  • 설정이 바뀔 때마다 다시 컴파일하고 배포해야 한다.
  • 비밀번호와 API Key가 소스 코드에 노출될 수 있다.
  • 테스트 환경과 운영 환경의 설정 차이로 오류가 발생할 수 있다.

Spring Boot에서는 이러한 설정을 코드 밖으로 분리한다.

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/shop
    username: root
    password: ${DB_PASSWORD}

logging:
  level:
    org.springframework: INFO

Configuration은 애플리케이션 실행에 필요한 환경 정보를 코드가 아닌 설정 파일이나 환경변수로 분리하여 관리하는 방식이다. 이를 통해 코드를 변경하지 않고도 실행 환경을 전환할 수 있다.

설정을 외부로 분리하는 이유

외부 설정의 장점은 단순히 파일을 보기 좋게 만드는 것이 아니다.

코드
→ 애플리케이션의 동작과 규칙

설정
→ 실행 환경에 따라 달라지는 값

두 영역을 분리하면 다음 효과를 얻을 수 있다.

효과 설명
유지보수성 코드 수정 없이 포트나 DB 주소 변경
보안성 비밀번호와 Key를 코드 밖에서 관리
환경 전환 개발·테스트·운영 환경을 쉽게 전환
CI/CD 연계 환경변수를 이용해 자동 배포
가시성 설정 파일에서 시스템 구성을 확인

자료에서도 설정 변경 시 코드 수정이 필요 없고, 민감한 값을 외부에 분리하며, 환경변수 기반 CI/CD 구성이 가능하다는 점을 설명한다.


Environment와 PropertySource

Spring Boot는 여러 위치에서 설정을 읽는다.

명령줄 인자
JVM 시스템 속성
운영체제 환경변수
application.yml
코드에서 지정한 기본값

이처럼 설정의 출처 하나하나를 PropertySource라고 한다.

Spring의 Environment는 여러 PropertySource를 모아 관리하며, 애플리케이션이 요청한 설정값을 최종적으로 결정해 반환한다.

PropertySource 여러 개
→ Environment가 통합 관리
→ 우선순위가 높은 값 선택
→ Bean과 자동 설정에 제공

Environment는 외부 설정값과 활성 Profile을 관리하는 중앙 객체이며, @Value, @ConfigurationProperties, Auto-Configuration이 설정을 읽는 기반이 된다.


같은 Key가 여러 곳에 있다면

동일한 설정이 여러 위치에 정의되면 우선순위가 높은 값이 적용된다.

핵심적인 순서는 다음과 같다.

명령줄 인자
> JVM 옵션
> OS 환경변수
> application.yml
> 코드 기본값

예를 들어 application.yml에는 다음 설정이 있다고 하자.

server:
  port: 8080

실행할 때 다음 옵션을 전달하면 서버는 9090 포트로 실행된다.

java -jar shop.jar --server.port=9090

명령줄 인자가 설정 파일보다 우선하기 때문이다.

java -Dserver.port=8085 -jar shop.jar

이 경우에는 JVM 옵션으로 8085를 지정할 수도 있다.

PropertySource는 우선순위에 따라 위에서부터 값을 탐색하고, Environment가 최종 하나의 값으로 정리해 반환한다.


Environment로 값을 직접 조회하기

server:
  port: 8080

app:
  name: My Demo App
  version: 1.0.0
  mode: local
@Component
public class EnvPrinter implements CommandLineRunner {

    private final Environment environment;

    public EnvPrinter(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void run(String... args) {
        String port =
                environment.getProperty("server.port");

        String mode =
                environment.getProperty("app.mode");

        System.out.println("server.port = " + port);
        System.out.println("app.mode = " + mode);
    }
}

CommandLineRunner는 Spring Boot 애플리케이션이 실행되고 Bean 구성이 완료된 후 한 번 실행되는 콜백 인터페이스다.

설정값이나 초기화 결과를 확인하는 간단한 실습에 사용할 수 있다.


@Value로 단일 값 주입하기

설정 하나나 두 개를 빠르게 읽을 때는 @Value를 사용할 수 있다.

spring:
  application:
    name: shop-api

server:
  port: 8080
@Component
public class AppInfo {

    @Value("${spring.application.name}")
    private String applicationName;

    @Value("${server.port:8081}")
    private int port;
}

${server.port:8081}에서 8081은 해당 설정이 없을 때 사용할 기본값이다.

${설정 Key}
→ 값이 반드시 있어야 함

${설정 Key:기본값}
→ 값이 없으면 기본값 사용

@Value의 장점은 사용법이 간단하다는 것이다.

그러나 설정이 많아지면 문제가 생긴다.

@Value("${mail.host}")
private String host;

@Value("${mail.port}")
private int port;

@Value("${mail.username}")
private String username;

@Value("${mail.password}")
private String password;

설정이 여러 클래스에 흩어지고 구조를 파악하기 어려워질 수 있다. 또한 설정값을 하나의 객체로 묶어 검증하거나 재사용하기가 어렵다.


@ConfigurationProperties로 설정을 객체에 묶기

관련 설정이 여러 개라면 @ConfigurationProperties를 사용하는 것이 좋다.

app:
  name: shop-api

  cors:
    allowed-origins:
      - https://shop.example.com
      - https://admin.example.com

  timeout: 30s
  max-upload: 25MB
  mode: PRO
@ConfigurationProperties(prefix = "app")
public record AppProperties(
        String name,
        Cors cors,
        Duration timeout,
        DataSize maxUpload,
        Mode mode
) {

    public record Cors(
            List<String> allowedOrigins
    ) {
    }

    public enum Mode {
        BASIC,
        PRO,
        ENTERPRISE
    }
}

애플리케이션에서 Configuration Properties Scan을 활성화한다.

@SpringBootApplication
@ConfigurationPropertiesScan
public class ShopApplication {

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

이 방식에서는 YAML의 계층 구조가 Java 객체의 구조로 자연스럽게 연결된다.

app.name
→ AppProperties.name

app.cors.allowed-origins
→ AppProperties.cors.allowedOrigins

app.timeout
→ Duration

app.max-upload
→ DataSize

app.mode
→ Enum

@ConfigurationProperties는 구조적인 설정 바인딩, 타입 변환, 검증과 IDE 자동완성에 유리하며 Spring Boot에서 여러 관련 설정을 관리할 때 권장되는 방식이다.


@Value와 @ConfigurationProperties 비교

구분 @Value @ConfigurationProperties
적합한 대상 한두 개의 단일 값 관련된 여러 설정
구조 표현 어려움 계층형 설정 가능
타입 변환 제한적 Duration, DataSize, Enum 등 지원
검증 설정 클래스 단위 검증 어려움 @Validated 사용 가능
재사용 낮음 설정 객체 주입 가능
유지보수 설정이 많으면 어려움 대규모 설정에 유리

@Value는 단일 값에 적합하고, @ConfigurationProperties는 여러 값을 객체 단위로 묶어 타입 안전하게 관리하는 방식이다.


설정값 검증

잘못된 설정으로 애플리케이션이 실행되면 실제 요청을 처리하는 과정에서 뒤늦게 오류가 발생할 수 있다.

Spring Boot에서는 설정값을 애플리케이션 시작 시점에 검증할 수 있다.

db:
  url: jdbc:mysql://localhost:3306/shop
  username: root
  password: password
  pool-size: 10
@Validated
@ConfigurationProperties(prefix = "db")
public record DatabaseProperties(

        @NotBlank
        String url,

        @NotBlank
        String username,

        @NotBlank
        String password,

        @Min(1)
        @Max(10)
        int poolSize
) {
}

사용되는 대표적인 검증 어노테이션은 다음과 같다.

어노테이션의미

@NotNull null 금지
@NotBlank null·빈 문자열·공백 문자열 금지
@Min, @Max 숫자 범위 제한
@Size 문자열이나 컬렉션 크기 제한
@Email 이메일 형식 검증
@Pattern 정규표현식 검증

예를 들어 비밀번호가 비어 있거나 pool-size가 15로 설정되어 있다면 애플리케이션은 시작 과정에서 실패한다.

설정 파일 로드
→ 설정 객체 바인딩
→ Validation 수행
→ 잘못된 설정 발견
→ ApplicationContext 시작 중단

자료의 117페이지 실행 결과에서도 db.password가 공백이거나 poolSize가 최대값을 넘으면 APPLICATION FAILED TO START가 발생하고, 잘못된 Property와 원인이 출력된다.

이 방식은 운영 중 오류가 발생하는 것보다 훨씬 안전하다.


2. Profile을 이용한 환경 분리

하나의 애플리케이션이라도 실행 환경에 따라 설정은 달라진다.

개발 환경 dev
→ 개발용 DB
→ 상세한 DEBUG 로그
→ 테스트용 외부 API

테스트 환경 test
→ 테스트 DB
→ Mock 서비스

운영 환경 prod
→ 운영 DB
→ INFO 또는 WARN 로그
→ 실제 외부 API

Profile은 현재 활성화된 환경에 따라 다른 설정이나 Bean을 적용하는 기능이다.

자료에서는 Profile을 dev·test·prod 환경별로 DB, 포트, 로그 설정을 나누고 실행 시점에 원하는 환경을 선택하는 기능으로 설명한다.


환경별 설정 파일

일반적으로 다음과 같이 파일을 구성한다.

application.yml
→ 모든 환경의 공통 설정

application-dev.yml
→ 개발 환경 설정

application-test.yml
→ 테스트 환경 설정

application-prod.yml
→ 운영 환경 설정
# application.yml
spring:
  application:
    name: shop-api

server:
  port: 8080
# application-dev.yml
spring:
  config:
    activate:
      on-profile: dev

server:
  port: 8081
# application-prod.yml
spring:
  config:
    activate:
      on-profile: prod

server:
  port: 9001

dev Profile을 활성화하면 공통 설정과 application-dev.yml이 병합된다.

application.yml
+
application-dev.yml
=
개발 환경의 최종 설정

동일한 Key가 있다면 Profile 전용 파일의 값이 공통 설정보다 우선 적용된다.


Profile 활성화

JAR 실행 시 다음과 같이 지정할 수 있다.

java -jar shop-api.jar \
  --spring.profiles.active=prod

Gradle로 실행할 때는 다음처럼 전달할 수 있다.

./gradlew bootRun \
  --args='--spring.profiles.active=dev'

실행 중인 Profile은 Environment를 통해 확인할 수 있다.

@Component
public class ProfilePrinter implements CommandLineRunner {

    private final Environment environment;

    public ProfilePrinter(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void run(String... args) {
        String[] profiles =
                environment.getActiveProfiles();

        System.out.println(
                String.join(", ", profiles)
        );
    }
}

@Profile로 Bean 자체를 분리하기

설정값뿐 아니라 환경에 따라 서로 다른 Bean을 등록할 수도 있다.

public interface MessageSender {
    void send(String message);
}

개발 환경에서는 실제 메시지를 보내지 않는 구현체를 사용할 수 있다.

@Component
@Profile("dev")
public class ConsoleMessageSender
        implements MessageSender {

    @Override
    public void send(String message) {
        System.out.println(
                "[DEV] " + message
        );
    }
}

운영 환경에서는 실제 외부 시스템을 사용하는 구현체를 등록한다.

@Component
@Profile("prod")
public class ExternalMessageSender
        implements MessageSender {

    @Override
    public void send(String message) {
        // 실제 메시지 서비스 호출
    }
}

Service는 어떤 Profile이 활성화되었는지 알 필요가 없다.

@Service
public class NotificationService {

    private final MessageSender messageSender;

    public NotificationService(
            MessageSender messageSender
    ) {
        this.messageSender = messageSender;
    }
}

Spring 컨테이너가 현재 Profile에 맞는 구현체만 Bean으로 등록하기 때문이다.

dev 활성화
→ ConsoleMessageSender 등록

prod 활성화
→ ExternalMessageSender 등록

Profile은 설정 파일을 분리하는 기능뿐 아니라 @Profile을 이용해 환경별 Bean 구성도 다르게 만들 수 있다.


3. Spring MVC 구조와 HTTP 요청 처리

MVC 구조와 관련해서는 다음 글에 더 자세히 정리를 해두었으니 참고하길 바란다.

https://daniellee09.tistory.com/55

 

[Spring] MVC 패턴과 Spring MVC 동작 원리 정리

Controller, Service, RepositorySpring Boot로 REST API를 만들다 보면 자연스럽게 다음과 같은 구조를 사용하게 된다.Controller→ Service→ Repository→ Database처음에는 단순히 “Spring에서는 이렇게 폴더를 나누는

daniellee09.tistory.com

 

Spring MVC는 웹 애플리케이션을 역할에 따라 나누는 구조다.

MVC는 다음 세 요소의 약자다.

Model
View
Controller

 

구성 요소 역할
Model 데이터와 비즈니스 상태 처리
View 사용자에게 결과 표현
Controller 요청을 받고 처리 흐름 제어

Spring MVC에서 Model은 넓은 의미로 Service, Repository, 도메인 데이터와 비즈니스 로직을 포함한다.

View는 HTML뿐 아니라 JSON, XML, 파일과 같은 응답 표현도 포함할 수 있다. Controller는 요청을 분석해 적절한 비즈니스 로직을 호출한다.


Spring MVC의 핵심 특징

Spring MVC는 다음과 같은 특징을 가진다.

  • 어노테이션 기반 요청 매핑
  • DispatcherServlet을 이용한 Front Controller 구조
  • POJO 기반 Controller
  • IoC와 DI 통합
  • 다양한 View 기술 지원
  • REST API와 JSON 응답 지원
  • Controller 단위 및 전역 예외 처리
  • Spring Boot 자동 구성

spring-boot-starter-web을 추가하면 Spring MVC, Jackson, 내장 Tomcat과 기본 로깅 등이 함께 구성된다.

dependencies {
    implementation(
        'org.springframework.boot:spring-boot-starter-web'
    )
}

DispatcherServlet

Spring MVC의 중심에는 DispatcherServlet이 있다.

DispatcherServlet은 모든 요청이 처음 통과하는 Front Controller다.

Client
→ DispatcherServlet
→ 적절한 Controller 탐색
→ Controller 실행
→ View 또는 JSON 응답

DispatcherServlet이 모든 비즈니스 로직을 직접 처리하는 것은 아니다.

요청과 응답의 전체 흐름을 조정하고, 실제 작업은 다른 컴포넌트에 위임한다.

DispatcherServlet
→ 전체 흐름 제어

HandlerMapping
→ 요청을 처리할 Controller 탐색

Controller
→ 요청을 받고 Service 호출

Service
→ 비즈니스 로직

Repository
→ 데이터 접근

사용자 요청이 DispatcherServlet으로 들어오고, HandlerMapping을 통해 Controller를 찾은 뒤 Service와 Repository를 거쳐 DB에 접근하는 과정이다.


Spring MVC의 전체 요청 흐름

HTML 화면을 반환하는 전통적인 MVC 흐름은 다음과 같다.

1. 클라이언트가 HTTP 요청 전송
2. DispatcherServlet이 요청 수신
3. HandlerMapping이 Controller 탐색
4. Controller가 Service 호출
5. Service가 비즈니스 로직 실행
6. Repository가 DB 조회 또는 저장
7. Controller가 결과를 Model에 저장
8. Controller가 논리적 View 이름 반환
9. ViewResolver가 실제 View 탐색
10. View가 Model 데이터를 이용해 HTML 생성
11. DispatcherServlet이 응답 반환

REST API에서는 마지막 부분이 달라진다.

Controller가 Java 객체 반환
→ HttpMessageConverter
→ Jackson이 JSON으로 직렬화
→ HTTP Response Body 반환

Controller와 요청 매핑

Controller는 사용자 요청을 받고 Service를 호출한 뒤 응답을 반환한다.

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(
            ProductService productService
    ) {
        this.productService = productService;
    }
}

@RequestMapping은 공통 경로나 HTTP Method를 지정할 수 있다.

@RequestMapping("/api/products")

메서드별로는 HTTP Method 전용 어노테이션을 사용하는 것이 일반적이다.

@GetMapping
@PostMapping
@PutMapping
@PatchMapping
@DeleteMapping
@GetMapping("/{id}")
public ProductResponse findById(
        @PathVariable Long id
) {
    return productService.findById(id);
}

@RequestParam

@RequestParam은 Query String이나 Form Parameter를 받는다.

GET /products?keyword=mouse&page=1
@GetMapping
public List<ProductResponse> search(
        @RequestParam String keyword,
        @RequestParam(defaultValue = "1") int page
) {
    return productService.search(
            keyword,
            page
    );
}

기본적으로 required=true이므로 값이 없으면 400 Bad Request가 발생한다.

선택값으로 만들 때는 다음과 같이 사용한다.

@RequestParam(required = false)
String category

여러 파라미터를 모두 Map으로 받을 수도 있다.

@GetMapping("/params")
public Map<String, String> params(
        @RequestParam Map<String, String> params
) {
    return params;
}

@PathVariable

@PathVariable은 URL 경로에 포함된 값을 받는다.

GET /products/10
@GetMapping("/{id}")
public ProductResponse findById(
        @PathVariable Long id
) {
    return productService.findById(id);
}

@RequestParam과 @PathVariable의 차이는 다음과 같다.

구분 @RequestParam @PathVariable
위치 Query String URL 경로
예시 /products?id=10 /products/10
주 용도 검색·필터·정렬·선택 옵션 특정 자원 식별
필수성 선택값으로 사용 가능 일반적으로 자원 식별에 필수
REST 스타일 조건 전달 자원 경로 표현

자료에서도 @RequestParam은 선택적인 파라미터 전달에, @PathVariable은 REST API에서 자원 식별에 주로 사용한다고 비교한다.


Model

Spring MVC의 Model 객체는 Controller에서 View로 데이터를 전달하는 저장소다.

@Controller
public class ProductPageController {

    @GetMapping("/products/{id}")
    public String detail(
            @PathVariable Long id,
            Model model
    ) {
        Product product =
                productService.findEntityById(id);

        model.addAttribute(
                "product",
                product
        );

        return "product-detail";
    }
}

Model에는 Key와 Value 형태로 값을 저장한다.

key: product
value: 조회된 Product 객체

이 데이터는 현재 HTTP 요청 범위에서만 유효하다.

Controller
→ model.addAttribute()
→ View

ModelAndView를 이용해 View 이름과 데이터를 한 객체에 담을 수도 있다.

@GetMapping("/info")
public ModelAndView info() {
    ModelAndView modelAndView =
            new ModelAndView("info");

    modelAndView.addObject(
            "title",
            "Spring MVC"
    );

    return modelAndView;
}

하지만 일반적인 Controller 코드에서는 Model을 메서드 인자로 받고 View 이름을 문자열로 반환하는 방식이 더 단순하다.


4. Controller·Service·Repository와 View의 역할

MVC를 실제 Spring Boot 프로젝트에 적용할 때는 내부 구조를 다음과 같이 나눈다.

Controller
→ Service
→ Repository
→ Database

이 구조는 MVC와 완전히 같은 개념은 아니다.

MVC는 요청·데이터·표현을 분리하는 패턴이고, Controller–Service–Repository는 애플리케이션 내부 책임을 계층별로 나누는 Layered Architecture다.

Spring Boot에서는 두 구조가 함께 사용된다.


Controller

Controller의 책임은 HTTP 요청과 응답이다.

  • URL과 HTTP Method 매핑
  • 요청 파라미터 수신
  • 요청 데이터 형식 검증
  • Service 호출
  • 상태 코드와 응답 반환
  • View 이름 또는 JSON 데이터 반환

Controller에는 핵심 비즈니스 로직을 넣지 않는 것이 좋다.

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService service;

    public ProductController(
            ProductService service
    ) {
        this.service = service;
    }

    @GetMapping("/{id}")
    public ProductResponse findById(
            @PathVariable Long id
    ) {
        return service.findById(id);
    }
}

Service

Service는 실제 업무 규칙을 담당한다.

  • 가격 계산
  • 주문 가능 여부 검사
  • 재고 검증
  • 데이터 변환
  • 여러 Repository 작업 조합
  • 트랜잭션 처리
@Service
public class ProductService {

    private final ProductRepository repository;

    public ProductService(
            ProductRepository repository
    ) {
        this.repository = repository;
    }

    public ProductResponse findById(Long id) {
        Product product = repository.findById(id)
                .orElseThrow(
                        () -> new NoSuchElementException(
                                "상품이 없습니다."
                        )
                );

        return ProductResponse.from(product);
    }

    @Transactional
    public void addProduct(Product product) {
        if (product.getPrice() < 0) {
            throw new IllegalArgumentException(
                    "가격은 0 이상이어야 합니다."
            );
        }

        repository.save(product);
    }
}

@Transactional은 데이터 변경 작업을 하나의 단위로 묶는다.

모든 작업 성공
→ Commit

작업 중 예외 발생
→ Rollback

Service는 비즈니스 로직, 검증, 계산과 트랜잭션을 담당하고 Repository에 데이터 접근을 위임한다.


Repository

Repository는 데이터 저장소에 접근하는 계층이다.

public interface ProductRepository {

    Product save(Product product);

    Optional<Product> findById(Long id);

    List<Product> findAll();

    void deleteById(Long id);
}

실제 구현 방식은 다양할 수 있다.

Memory
JDBC
MyBatis
JPA
MongoDB
외부 API

메모리 구현체를 만들면 다음과 같다.

@Repository
public class MemoryProductRepository
        implements ProductRepository {

    private final Map<Long, Product> store =
            new HashMap<>();

    private final AtomicLong sequence =
            new AtomicLong();

    @Override
    public Product save(Product product) {
        long id = sequence.incrementAndGet();

        Product saved = new Product(
                id,
                product.getName(),
                product.getPrice()
        );

        store.put(id, saved);

        return saved;
    }

    @Override
    public Optional<Product> findById(Long id) {
        return Optional.ofNullable(
                store.get(id)
        );
    }

    @Override
    public List<Product> findAll() {
        return new ArrayList<>(
                store.values()
        );
    }

    @Override
    public void deleteById(Long id) {
        store.remove(id);
    }
}

Repository 인터페이스를 사용하면 Service는 구체적인 저장 기술을 알 필요가 없다.

Service
→ ProductRepository 인터페이스에 의존

MemoryProductRepository
JdbcProductRepository
JpaProductRepository
→ 인터페이스 구현

따라서 저장 기술이 바뀌어도 Service의 변경을 줄일 수 있다.

자료에서도 Repository 분리의 장점으로 관심사 분리, 유지보수성, Mock 기반 테스트와 저장 기술 교체의 유연성을 설명한다.


계층별 차이

계층 핵심 책임 대표 어노테이션
Controller 요청과 응답 @Controller, @RestController
Service 비즈니스 로직과 트랜잭션 @Service, @Transactional
Repository 데이터 조회와 저장 @Repository

 

Controller
→ HTTP를 안다

Service
→ 업무 규칙을 안다

Repository
→ 데이터를 어디서 가져오는지 안다

View와 ViewResolver

View는 사용자에게 최종 결과를 표현하는 계층이다.

Spring MVC의 View는 HTML만 의미하지 않는다.

  • HTML View
  • JSON View
  • PDF·Excel 등 파일 View
  • Redirect View
  • Forward View

View에는 데이터를 보여 주는 표현 로직만 포함해야 하며, 가격 계산이나 재고 검사 같은 비즈니스 로직을 넣지 않는 것이 좋다.

Model 데이터
→ View가 화면에 표시

비즈니스 로직
→ Service에서 처리

논리적 View 이름

Controller는 실제 View 파일을 직접 반환하지 않고 논리적인 이름을 반환한다.

@Controller
public class HomeController {

    @GetMapping("/home")
    public String home(Model model) {
        model.addAttribute(
                "message",
                "환영합니다."
        );

        return "home";
    }
}

"home"은 실제 HTML 내용이 아니라 논리적 이름이다.

ViewResolver는 이 이름을 실제 View로 변환한다.

Controller 반환값
→ "home"

ThymeleafViewResolver
→ templates/home.html

InternalResourceViewResolver
→ /WEB-INF/views/home.jsp

ViewResolver는 Controller가 반환한 논리적 View 이름을 실제 View 객체로 변환하며, Controller가 특정 View 기술에 직접 의존하지 않도록 만든다.


REST API에서는 ViewResolver가 아니라 JSON 변환

@RestController를 사용하면 반환값이 View 이름으로 해석되지 않는다.

@RestController
public class ProductController {

    @GetMapping("/api/products/1")
    public ProductResponse product() {
        return new ProductResponse(
                1L,
                "무선 마우스",
                15_000
        );
    }
}

반환된 객체는 Jackson에 의해 JSON으로 변환된다.

{
  "id": 1,
  "name": "무선 마우스",
  "price": 15000
}
@Controller
→ 문자열을 View 이름으로 해석
→ ViewResolver
→ HTML

@RestController
→ 객체를 Response Body로 처리
→ HttpMessageConverter
→ JSON

예제

다음은 Controller–Service–Repository 계층이 적용된 간단한 상품 조회 예제다.

Domain

public class Product {

    private final Long id;
    private final String name;
    private final int price;

    public Product(
            Long id,
            String name,
            int price
    ) {
        if (price < 0) {
            throw new IllegalArgumentException(
                    "가격은 0 이상이어야 합니다."
            );
        }

        this.id = id;
        this.name = name;
        this.price = price;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }
}

Response DTO

public record ProductResponse(
        Long id,
        String name,
        int price
) {
    public static ProductResponse from(
            Product product
    ) {
        return new ProductResponse(
                product.getId(),
                product.getName(),
                product.getPrice()
        );
    }
}

Repository

public interface ProductRepository {

    Optional<Product> findById(Long id);
}
@Repository
public class MemoryProductRepository
        implements ProductRepository {

    private final Map<Long, Product> products =
            Map.of(
                    1L,
                    new Product(
                            1L,
                            "무선 마우스",
                            15_000
                    )
            );

    @Override
    public Optional<Product> findById(Long id) {
        return Optional.ofNullable(
                products.get(id)
        );
    }
}

Service

@Service
public class ProductService {

    private final ProductRepository repository;

    public ProductService(
            ProductRepository repository
    ) {
        this.repository = repository;
    }

    public ProductResponse findById(Long id) {
        Product product = repository.findById(id)
                .orElseThrow(
                        () -> new NoSuchElementException(
                                "상품을 찾을 수 없습니다."
                        )
                );

        return ProductResponse.from(product);
    }
}

Controller

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService service;

    public ProductController(
            ProductService service
    ) {
        this.service = service;
    }

    @GetMapping("/{id}")
    public ProductResponse findById(
            @PathVariable Long id
    ) {
        return service.findById(id);
    }
}

요청이 들어오면 다음 순서로 실행된다.

GET /api/products/1
→ DispatcherServlet
→ ProductController
→ ProductService
→ ProductRepository
→ Product 조회
→ ProductResponse 변환
→ JSON 직렬화
→ HTTP 응답

5. Actuator를 이용한 운영과 모니터링

애플리케이션이 정상적으로 실행된다고 해서 개발이 끝나는 것은 아니다.

운영 환경에서는 다음 질문에 답할 수 있어야 한다.

현재 서버는 정상인가?
DB 연결은 가능한가?
메모리를 얼마나 사용하고 있는가?
HTTP 요청은 얼마나 들어오는가?
어떤 Controller가 등록되어 있는가?
현재 로그 레벨은 무엇인가?

Spring Boot Actuator는 애플리케이션의 운영·모니터링·진단 정보를 Endpoint 형태로 제공한다.

/actuator/health
/actuator/info
/actuator/metrics

Actuator는 애플리케이션 상태와 성능 지표를 확인하고, 로그 레벨 변경과 같은 일부 운영 작업을 수행할 수 있도록 지원한다.


의존성 추가

dependencies {
    implementation(
        'org.springframework.boot:spring-boot-starter-web'
    )

    implementation(
        'org.springframework.boot:spring-boot-starter-actuator'
    )
}

주요 Endpoint

Endpoint 역할
/actuator/health 애플리케이션 상태
/actuator/info 버전·이름 등 애플리케이션 정보
/actuator/metrics JVM, CPU, HTTP 등의 측정 지표
/actuator/beans 등록된 Spring Bean
/actuator/env 환경 설정 정보
/actuator/mappings Controller 요청 매핑
/actuator/loggers 로그 설정 조회와 변경
/actuator/configprops Configuration Properties 목록
/actuator/logfile 로그 파일 조회

Endpoint가 /actuator/{endpoint ID} 형식으로 제공되며, access가 허용되고 HTTP에 expose된 경우에만 외부에서 사용할 수 있다.


Expose와 Access의 차이

Actuator 설정에서 혼동하기 쉬운 부분이 exposure와 access다.

Exposure
→ Endpoint를 HTTP 경로에 공개할 것인가?

Access
→ 공개된 Endpoint에서 어떤 동작까지 허용할 것인가?

Endpoint는 먼저 노출되어야 하며, 그다음 접근 권한이 적용된다.

Expose 안 됨
→ Access를 허용해도 HTTP 접근 불가

Expose 됨
→ Access 수준에 따라 사용 가능

기본적으로 HTTP에는 health 정도만 노출되며, 필요한 Endpoint만 선택적으로 공개하는 것이 안전하다.

management:
  endpoints:
    web:
      exposure:
        include:
          - health
          - info
          - metrics

전체를 공개한 뒤 특정 Endpoint를 제외할 수도 있다.

management:
  endpoints:
    web:
      exposure:
        include: "*"
        exclude:
          - env
          - beans

include와 exclude가 충돌하면 제외 설정이 우선한다.


접근 수준

자료에서는 Endpoint의 접근 수준을 다음 세 가지로 구분한다.

none
→ 접근 차단

read-only
→ 조회만 허용

unrestricted
→ 제한 없이 허용

안전하게 운영하려면 기본 접근을 차단하고 필요한 Endpoint만 읽기 전용으로 여는 방식이 좋다.

management:
  endpoints:
    access:
      default: none

    web:
      exposure:
        include:
          - health
          - info
          - metrics

  endpoint:
    health:
      access: read-only

    info:
      access: read-only

    metrics:
      access: read-only

민감한 설정값 마스킹

/actuator/env와 /actuator/configprops는 애플리케이션 설정을 보여 줄 수 있다.

여기에 비밀번호나 토큰이 포함될 수 있으므로 민감한 값은 마스킹해야 한다.

실제 값
→ secret-password

노출 값
→ ******

자료에서는 다음과 같은 설정 수준을 소개한다.

never
→ 항상 마스킹

always
→ 항상 실제값 표시

when-authorized
→ 권한이 있는 경우 실제값 표시

운영 환경에서 실제 값을 항상 노출하는 설정은 피해야 한다.


Health Endpoint

/actuator/health는 애플리케이션이 정상적으로 동작하는지 확인한다.

{
  "status": "UP"
}

DB, 디스크 공간과 같은 상태는 Spring Boot가 자동으로 확인할 수 있다.

서비스에 맞는 상태 확인이 필요하면 HealthIndicator를 직접 구현할 수 있다.

@Component
public class PaymentHealthIndicator
        implements HealthIndicator {

    @Override
    public Health health() {
        boolean running =
                checkPaymentService();

        if (running) {
            return Health.up()
                    .withDetail(
                            "service",
                            "payment"
                    )
                    .withDetail(
                            "message",
                            "Payment service is healthy"
                    )
                    .build();
        }

        return Health.down()
                .withDetail(
                        "service",
                        "payment"
                )
                .withDetail(
                        "message",
                        "Payment service check failed"
                )
                .build();
    }

    private boolean checkPaymentService() {
        return true;
    }
}

세부 상태를 확인하려면 다음과 같이 설정할 수 있다.

management:
  endpoints:
    web:
      exposure:
        include: health

  endpoint:
    health:
      show-details: always

실제 운영에서는 상세 상태에 내부 시스템 정보가 포함될 수 있으므로 접근 권한과 함께 설정해야 한다.


Info Endpoint

/actuator/info는 애플리케이션의 이름, 버전과 설명 같은 정보를 제공한다.

info:
  app:
    name: shop-api
    version: 1.2.4
    description: 온라인 쇼핑몰 API
management:
  endpoints:
    web:
      exposure:
        include:
          - health
          - info

  info:
    env:
      enabled: true

직접 정보를 추가하려면 InfoContributor를 구현한다.

@Component
public class CustomInfoContributor
        implements InfoContributor {

    @Override
    public void contribute(
            Info.Builder builder
    ) {
        builder.withDetail(
                        "service",
                        "Payment API"
                )
                .withDetail(
                        "maintainer",
                        "backend@example.com"
                );
    }
}

Info 정보는 여러 InfoContributor Bean에서 수집되며, 환경·Java·OS·프로세스·Git·Build 정보도 설정에 따라 포함할 수 있다.


Metrics Endpoint

Metrics는 애플리케이션 상태나 성능을 숫자로 측정한 데이터다.

JVM 메모리 사용량
활성 스레드 수
HTTP 요청 처리 시간
DB Connection 수
CPU 사용률
현재 세션 수
로그 이벤트 수

대표적인 Metrics는 다음과 같다.

분야 Metric 예시 의미
JVM jvm.memory.used 사용 중인 메모리
JVM jvm.threads.live 활성 스레드
HTTP http.server.requests 요청 횟수와 처리 시간
DB hikaricp.connections.active 활성 DB 연결
System system.cpu.usage CPU 사용률
Tomcat tomcat.sessions.active.current 활성 세션
Logback logback.events 로그 이벤트

자료는 Metrics를 상태와 성능을 수치화한 데이터로, Meter를 이러한 값을 수집하는 측정 도구로 설명한다.


Meter 종류

Meter 용도 예시
Counter 계속 증가하는 누적값 요청 횟수, 오류 횟수
Gauge 현재 시점의 값 큐 크기, 작업 수
Timer 실행 횟수와 소요 시간 API 처리 시간
Distribution Summary 값의 분포 요청·응답 크기
Long Task Timer 장시간 작업 시간 배치 작업

예를 들어 현재 처리 중인 작업 수는 Gauge가 적합하다.

@Component
public class PaymentMetrics
        implements MeterBinder {

    private final AtomicInteger runningTasks =
            new AtomicInteger();

    @Override
    public void bindTo(
            MeterRegistry registry
    ) {
        Gauge.builder(
                        "payment.running.tasks",
                        runningTasks,
                        AtomicInteger::get
                )
                .description(
                        "현재 처리 중인 결제 작업 수"
                )
                .tag(
                        "service",
                        "payment"
                )
                .register(registry);
    }

    public void increase() {
        runningTasks.incrementAndGet();
    }

    public void decrease() {
        runningTasks.updateAndGet(
                value -> Math.max(0, value - 1)
        );
    }
}

MeterBinder로 custom.sample.value Gauge를 등록하고, 별도의 Controller에서 값을 증가·감소시킨 뒤 /actuator/metrics에서 결과를 확인한다.


전체 흐름 다시 보기

이번 범위의 내용을 하나의 Spring Boot 애플리케이션으로 연결하면 다음과 같다.

application.yml
→ 공통 설정 정의

application-dev.yml
application-prod.yml
→ 환경별 설정 분리

Environment
→ 여러 설정 소스 통합

@ConfigurationProperties
→ 설정을 타입 안전한 객체로 변환

Validation
→ 잘못된 설정이면 기동 중단

Spring MVC
→ HTTP 요청 처리

DispatcherServlet
→ 요청의 단일 진입점

Controller
→ 요청·응답 처리

Service
→ 비즈니스 로직과 트랜잭션

Repository
→ 데이터 접근

ViewResolver
→ 논리적 View 이름을 실제 화면으로 변환

HttpMessageConverter
→ Java 객체를 JSON으로 변환

Actuator
→ 상태·정보·성능 지표 제공

핵심 정리

Configuration

실행 환경에 따라 달라지는 값을
코드가 아닌 외부 설정으로 분리한다.

PropertySource 우선순위

명령줄
> JVM 옵션
> 환경변수
> application.yml
> 기본값

설정값 주입

@Value
→ 간단한 단일 값

@ConfigurationProperties
→ 구조화된 여러 값
→ 타입 변환과 검증 지원

Profile

application.yml
→ 공통 설정

application-dev.yml
→ 개발 환경

application-prod.yml
→ 운영 환경

@Profile
→ 환경별 Bean 구성

Spring MVC

Client
→ DispatcherServlet
→ Controller
→ Service
→ Repository
→ DB
→ View 또는 JSON

계층별 책임

Controller
→ HTTP 요청과 응답

Service
→ 비즈니스 규칙과 트랜잭션

Repository
→ 데이터 저장과 조회

View 처리

@Controller
→ 논리적 View 이름
→ ViewResolver
→ HTML

@RestController
→ Java 객체
→ HttpMessageConverter
→ JSON

Actuator

health
→ 서비스 상태

info
→ 애플리케이션 정보

metrics
→ 성능과 사용량 수치

복습 질문

  1. 설정값을 Java 코드 안에 하드코딩하면 어떤 문제가 발생하는가?
  2. Environment와 PropertySource는 어떤 관계인가?
  3. 같은 설정 Key가 여러 위치에 있을 때 최종 값은 어떻게 결정되는가?
  4. @Value와 @ConfigurationProperties는 각각 언제 사용하는 것이 적절한가?
  5. 설정값을 애플리케이션 시작 시 검증하면 어떤 장점이 있는가?
  6. Profile별 설정 파일과 @Profile 어노테이션의 역할은 어떻게 다른가?
  7. DispatcherServlet이 Front Controller라고 불리는 이유는 무엇인가?
  8. @RequestParam과 @PathVariable은 어떤 상황에서 구분해 사용하는가?
  9. Controller, Service와 Repository가 각각 가져야 하는 책임은 무엇인가?
  10. Repository 인터페이스를 분리하면 데이터 접근 기술을 바꾸기 쉬운 이유는 무엇인가?
  11. @Controller와 @RestController의 반환값은 각각 어떻게 처리되는가?
  12. View와 ViewResolver는 어떤 차이가 있는가?
  13. Actuator의 Exposure와 Access는 무엇이 다른가?
  14. /actuator/env와 같은 Endpoint를 운영 환경에서 주의해야 하는 이유는 무엇인가?
  15. Counter, Gauge와 Timer는 각각 어떤 종류의 값을 측정하는가?
반응형

'개발 > SKALA 4기' 카테고리의 다른 글

[SKALA] 입력값 검증부터 Spring JPA와 API 문서화  (0) 2026.07.31
[SKALA] 컴포넌트 스캔과 Spring 컨테이너, HTTP 요청 바인딩 정리  (0) 2026.07.29
[SKALA] 객체지향(OOP)과 Spring Boot 핵심 정리  (0) 2026.07.27
[SKALA] LLM 모델과 Transformer 아키텍처 이해  (0) 2026.07.23
[SKALA] Prompt 설계 및 Context Engineering  (1) 2026.07.23
'개발/SKALA 4기' 카테고리의 다른 글
  • [SKALA] 입력값 검증부터 Spring JPA와 API 문서화
  • [SKALA] 컴포넌트 스캔과 Spring 컨테이너, HTTP 요청 바인딩 정리
  • [SKALA] 객체지향(OOP)과 Spring Boot 핵심 정리
  • [SKALA] LLM 모델과 Transformer 아키텍처 이해
danieLee
danieLee
개발일지
  • danieLee
    Code log
    danieLee
  • 전체
    오늘
    어제
    • 분류 전체보기 (77)
      • 개발 (76)
        • C++ (3)
        • java (6)
        • JavaScript (9)
        • python (0)
        • AWS (2)
        • Docker (6)
        • git (0)
        • 백엔드 (4)
        • Spring (7)
        • Django (2)
        • AI (3)
        • 코테 준비 (13)
        • 알고리즘 (6)
        • SKALA 4기 (13)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    알고리즘
    프로그래머스
    파이썬
    대학생
    spring
    JavaScript
    코테
    개발
    java
    js
    서버
    API
    백엔드
    Ai
    4기
    개념
    개발자
    vue.js
    프론트
    skala
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.1
danieLee
[SKALA] Spring Boot 설정 관리와 MVC·Actuator 정리
상단으로

티스토리툴바