Spring

10분만에 구현하는 CircuitBreaker

AlwaysPr 2023. 1. 29. 14:00

Spring AOP를 활용하여 CircuitBreaker를 구현해 보자.

 

Annotation 기반의 AOP를 활용하기 위해 Annotation과 Aspect를 다음과 같이 만들어주자.

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CircuitBreaker {}

 

@Aspect
@Component
public class CircuitBreakerAspect {

    @Around("@annotation(com.ms.circuitbreaker.CircuitBreaker)")
    public Object round(ProceedingJoinPoint joinPoint) throws Throwable {
    	// TODO 세부구현
        return joinPoint.proceed();
    }

}

 

CircuitBreaker를 적용할 코드도 하나 만들자

@Service
public class FooService {

    @CircuitBreaker
    public String getFoo() {
        return "foo";
    }

}

 

실패 개수와 마지막 실패 시간을 가지고 있는 metadata와 Exception을 만들자

class CircuitBreakerMetadata {

        private final AtomicInteger failures = new AtomicInteger();

        private volatile long lastFailure;

        CircuitBreakerMetadata() {
        }

        private long getLastFailure() {
            return this.lastFailure;
        }

        private void setLastFailure(long lastFailure) {
            this.lastFailure = lastFailure;
        }

        private AtomicInteger getFailures() {
            return this.failures;
        }

    }

 

public final class CircuitBreakerOpenException extends RuntimeException {}

 

기본적인 뼈대는 만들어졌다.

이제 CircuitBreaker 동작을 Aspect에서 구현해 보자

 

요구사항은 간단하다.

1. @CircuitBreaker가 선언된 곳에서 에러가 연속으로 5회를 넘게 되면

2. 1초 동안 CircuitBreaker가 발동한다. 

3. 에러 없이 성공하면 실패 개수를 초기화한다. 

 

@Aspect
@Component
public class CircuitBreakerAspect {

    private static final int THRESHOLD = 5;
    
    private static final long HALF_OPEN_AFTER = 1000;
    
    private static final ConcurrentMap<Object, CircuitBreakerMetadata> METADATA_MAP = new ConcurrentHashMap<>();

    @Around("@annotation(com.ms.circuitbreaker.CircuitBreaker)")
    public Object round(ProceedingJoinPoint joinPoint) throws Throwable {

        final Object target = joinPoint.getTarget();

        CircuitBreakerMetadata metadata = METADATA_MAP.get(target);

        if (metadata == null) {
            METADATA_MAP.putIfAbsent(target, new CircuitBreakerMetadata());
            metadata = METADATA_MAP.get(target);
        }

        if (metadata.getFailures().get() >= THRESHOLD &&
                System.currentTimeMillis() - metadata.getLastFailure() < HALF_OPEN_AFTER) {
            throw new CircuitBreakerOpenException();
        }

        try {
            final Object result = joinPoint.proceed();
            metadata.getFailures().set(0);
            return result;
        } catch (Exception e) {
            metadata.getFailures().incrementAndGet();
            metadata.setLastFailure(System.currentTimeMillis());
            throw e;
        }

    }
}

 

CircuitBrekaer를 구현해 보았다.

Spring에서 제공하는 @CircuitBreaker를 사용하다가 어떻게 구현되어있는지 보니 생각에 비해 간단/간결하게 작성되어 있었다.

사실 위에서 작성한 코드는 spring-integration의 RequestHandlerCircuitBreakerAdvice 클래스이다.

 

프레임워크와 라이브러리가 제공해 준다고 해서 그저 가져다 쓰기보다는 어떻게 구현되었는지 궁금해하고 찾다 보면 더 좋은 개발자가 되리라 생각한다.