본문 바로가기

TIL

[TIL] 🌱 2023.04.19 - ControllerAdvice, Actuator

🛹 목표

목표 난이도 달성 여부
이펙티브 자바 ITEM 15, 16
- 클래스와 멤버의 접근권한을 최소화하라
- public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
😘 ✔️
패스트 캠퍼스 어드민 서비스 마무리
😘 ✔️

📋 공부 내용 & 기록

이펙티브 자바 핵심정리

ITEM 15

프로그램 요소의 접근성은 가능한 한 최소한으로 하라. 꼭 필요한 것만 골라 최소한의 public API를 설계하자. 그 외에는 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야한다. public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안된다. public static final 필드가 참조하는 객체가 불변인지 확인하라. 

 

 

1. 스프링의 ControllerAdvice

@ControllerAdvice는 @Controller 또는 @RestController에서 발생한 예외를 한 곳에서 관리하고 처리할 수 있도록 도와주는 애노테이션이다. 해당 애노테이션은 클래스에 적용되며, 해당 클래스는 예외 처리 메소드를 정의한다. 예외 처리 메소드는 @ExceptionHandler 애노테이션과 함께 사용되며, 특정 예외가 발생했을 때 해당 메소드가 실행되어 예외를 처리한다. ControllerAdvice는 AOP 구현 방식 중 하나로 이러한 방식은 중복코드를 제거하고, 일관적인 예외 처리를 할 수 있어 편리하다.

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Bad Request: " + ex.getMessage());
    }
    
    // 1차적으로 자신이 생각하는 예외를 처리해주고 예상치 못하게 발생할 수 있는 예외상황에 대해 @ExceptionHandler(Exception.class)로 다시한번 처리해준다.
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception e) {
  	    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Unknown Exception: " + ex.getMessage());
    }
    
}
 

[Spring Boot] @ControllerAdvice을 이용한 Exception 처리

오류 처리는 프로그램을 개발하는데 있어서 매우 큰 부분을 차지한다. 오류를 예측해서 비정상적인 상황이 발생하지 않게 하는 것은 정말 중요하다. 1. @ControllerAdvice 란? @Controller나 @RestController

bamdule.tistory.com

2. 스프링 Actuator [참고]

패캠의 어드민 서비스에서 방문자 수 집계를 위해 스프링의 Actuator를 사용하였다. 서비스의 모든 uri에서 방문한 사람의 집계를 해야하기 때문에 전체 컨트롤러에 적용되는 설정이어서 ControllerAdvice로 접근을 하였다.

 

스프링부트의 Actuator[공식문서 참고]

 

CHAP 16. 스프링 부트 액추에이터 사용하기 - Incheol's TECH BLOG

"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"

incheol-jung.gitbook.io

스프링 부트 애플리케이션의 모니터링 및 관리를 위한 라이브러리로 애플리케이션의 상태정보를 노출하고, 애플리케이션을 관리하기 위한 여러 기능을 제공.

  • 애플리케이션의 정보를 알기 위해 엔드포인트를 호출하면, JSON 형식으로 제공한다.
  • 메모리 사용량, CPU 사용량 등과 같은 메트릭 정보를 수집할 수 있다. (EndPoint : /actuator/metric)
  • 애플리케이션의 실행 정보, 로그 정보를 알 수 있다. 

 

스프링 Actuator를 사용하기 위한 초기 설정

 

1. 디펜던시 추가

아래의 디펜던시에는 Actuator 관련 라이브러리와 정보 수집에 도움을 주는 마이크로미터사의 라이브러리도 포함되어 있다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

2. Properties.yaml에 노출될 엔드포인트 설정하기

  • 엔드포인트에는 민감한 정보가 포함될 수 있으므로 엔드포인트를 언제 노출할지 고려해야한다.
  • 기본적으로 Actuator는 `health`와 `info`엔드포인트를 제외한 모든 엔드포인트를 노출한다.
  • management.endpoints.web.exposure.exclude → 노출되지 않아야 하는 엔드포인트
  • management.endpoints.web.exposure.include → 노출되어야 하는 엔드포인트
management.endpoints.web.exposure.include: "*"  // 전체 Actuator 엔드포인트들이 오픈된 상태

3. 메트릭을 수집할 주소 방문해서 내용 확인

메트릭 : 엔티티의 속성으로 정해진 수치의 데이터들을 말하며 스프링 Actuator를 사용하여 메트릭을 시각화하고 모니터링 할 수 있게끔 한다.

// http://localhost:8081/actuator/metrics/http.server.requests

{
  "name": "http.server.requests",
  "description": null,
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 774.0
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 5.189345373
    },
    {
      "statistic": "MAX",
      "value": 0.577150083
    }
  ],
  "availableTags": [
    {
      "tag": "exception",
      "values": [
        "None"
      ]
    },
    {
      "tag": "method",
      "values": [
        "POST",
        "GET"
      ]
    },
    {
      "tag": "uri",
      "values": [
        "/management/user-accounts",
        "/actuator/caches",
        "/management/articles/{articleId}",
        "/admin/members",
        "REDIRECTION",
        "/management/user-accounts/{userId}",
        "/management/articles",
        "/chat/**",
        "/management/article-comments",
        "/**",
        "/actuator/metrics/{requiredMetricName}",
        "/api/admin/members",
        "/actuator",
        "/management/article-comments/{articleCommentId}",
        "/actuator/beans",
        "root",
        "/actuator/health",
        "/webjars/**",
        "/actuator/metrics",
        "/actuator/heapdump"
      ]
    },
    {
      "tag": "outcome",
      "values": [
        "REDIRECTION",
        "INFORMATIONAL",
        "SUCCESS"
      ]
    },
    {
      "tag": "status",
      "values": [
        "302",
        "200",
        "101"
      ]
    }
  ]
}

4. 방문자를 확인할 엔드포인트만 정의 후 방문자 집계 메서드 구현

MeterRegistry는 애플리케이션에서 수집되는 메트릭의 인스턴스를 생성하고 관리한다. 아래에서는 Timer 인스턴스를 생성하여 등록했기에 해당 타이머에서 측정한 지연 시간, 카운트,  평균 지연 시간등을 추적할 수 있다.

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.search.MeterNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@RequiredArgsConstructor
@Service
public class VisitCounterService {

    private final MeterRegistry meterRegistry;

    private static final List<String> viewEndpoints = List.of(
            "/management/articles",
            "/management/article-comments",
            "/management/user-accounts",
            "/admin/members"
    );

    public long visitCount() {
        long sum;

        try {
            sum = meterRegistry.get("http.server.requests") // 해당 주소의 메트릭을 수집
                    .timers()
                    .stream()
                    .filter(timer -> viewEndpoints.contains(timer.getId().getTag("uri")))
                    .mapToLong(Timer::count)
                    .sum();  //히트수 계산 후 반환
        } catch (MeterNotFoundException e) {  // 최초 실행시에는 카운트가 없으므로 에러를 발생한다. catch를 통해 해결
            sum = 0L;
        }
        return sum;
    }

}

5. 결과값을 보내줄 컨트롤러 구현

import com.fastcampus.projectboardadmin.service.VisitCounterService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;

@RequiredArgsConstructor
@ControllerAdvice
public class VisitCounterControllerAdvice {

    private final VisitCounterService visitCounterService;

    @ModelAttribute("visitCount")
    public long visitCount() {
        return visitCounterService.visitCount();
    }
}