본문 바로가기

우아한테크코스

책임 연쇄 패턴을 사용해서 지하철 추가 요금 정책을 반영해보자

지하철 미션 3단계를 구현해보았다.

추가 요금 정책을 적용하는 것으로, 노선별 추가 요금 정책과 연령별 요금 할인 정책을 반영하여 요금을 산정해야 한다.

 

그리고 요금을 계산할 때, 책임 연쇄 패턴을 사용하였다. 

 

책임 연쇄 패턴 (Chain of Responsibility) 이란

요청을 처리하는 객체들 사이에 연결을 형성하여 요청을 처리할 수 있는 객체를 순차적으로 찾아가는 패턴으로

일련의 객체들을 연결된 체인으로 구성하여 체인 상에 있는 객체들은 순차적으로 요청을 처리한다.

 

온라인 주문 시스템을 개발할 때, 요청이 인증되어야 주문을 할 수 있게끔 하고, 인증된 유저들에게 권한을 부여해주고자 한다.

 

 

이 과정에서 인증에 실패하면 권한 부여를 하는 로직을 거칠 필요가 없다.

또 요청을 검증하고, 반복 요청에 대해서 캐시를 하는 로직을 추가했다고 가정해보면

검사 코드는 너무나 복잡해지고 많은 책임을 가지게 된다. 

이로 인해 한 로직의 코드를 바꾸면 다른 검사 로직의 코드도 영향을 받을 가능성이 커지고, 재사용성과 유지보수성이 나빠진다.

 

이때 책임 연쇄 패턴을 도입해보자.

 

특정 행동들을 라는 독립 실행형 객체들로 변환한다. 그리고 각 검사는 검사를 수행하는 단일 메서드가 있는 자체 클래스로 추출한다. 

이렇게 구성함으로써 가장 큰 장점은 핸들러에서 검증이 실패한다면 요청을 체인 아래로 더 이상 전달하지 않고 추가 처리를 중지할 수 있다는 것이다.

 

책임 연쇄 패턴의 구조는 이런 식으로 나타낼 수 있다. 

 

이를 코드로 작성해보자면,

// Handler 인터페이스
public interface Logger {
    void setNextLogger(Logger nextLogger);
    void logMessage(LogLevel level, String message);
}

// BaseHandler
public abstract class BaseLogger {
	private Logger nextLogger;
    
    @Override
    public void setNextLogger(Logger nextLogger) {
    	this.nextLogger = nextLogger;
    }
    
    abstract void logMessage(LogLevel level, String message);
}

// 구체적인 처리자 구현
public class ConsoleLogger extends BaseLogger {
    @Override
    public void logMessage(LogLevel level, String message) {
        if (level == LogLevel.INFO) {
            System.out.println("ConsoleLogger: " + message);
        } else if (nextLogger != null) {
            nextLogger.logMessage(level, message);
        }
    }
}

public class FileLogger extends BaseLogger {
    @Override
    public void logMessage(LogLevel level, String message) {
        if (level == LogLevel.WARNING) {
            System.out.println("FileLogger: " + message);
        } else if (nextLogger != null) {
            nextLogger.logMessage(level, message);
        }
    }
}

public class EmailLogger extends BaseLogger {
    @Override
    public void logMessage(LogLevel level, String message) {
        if (level == LogLevel.ERROR) {
            System.out.println("EmailLogger: " + message);
        } else if (nextLogger != null) {
            nextLogger.logMessage(level, message);
        }
    }
}

// 로그 레벨 열거형
public enum LogLevel {
    INFO,
    WARNING,
    ERROR
}

// 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        // 처리자 객체 생성
        Logger consoleLogger = new ConsoleLogger();
        Logger fileLogger = new FileLogger();
        Logger emailLogger = new EmailLogger();

        // 처리자 체인 구성
        consoleLogger.setNextLogger(fileLogger);
        fileLogger.setNextLogger(emailLogger);

        // 로그 메시지 전달
        consoleLogger.logMessage(LogLevel.INFO, "This is an information.");
        consoleLogger.logMessage(LogLevel.WARNING, "This is a warning.");
        consoleLogger.logMessage(LogLevel.ERROR, "This is an error.");
    }
}

 

이렇게 구성하면 ConsoleLogger -> FileLogger -> EmailLogger 순으로 처리가 진행될 것이고,

"ConsoleLogger: This is an information."
"FileLogger: This is a warning."
"EmailLogger: This is an error."

의 결과가 콘솔에 출력될 것이다. 

 

추가 요금 정책의 코드 작성해 적용해보자

 

우선 요금 정책의 처리 순서는 

거리 기반 요금 계산 -> 노선별 추가 요금 계산 -> 연령별 요금정책 반영 순이다. 

 

그래서 추상 클래스인 FareCalculator 를

public abstract class FareCalculator {
    private FareCalculator nextFareCalculator = null;

    public FareCalculator setNextFareCalculator(FareCalculator fareCalculator) {
        this.nextFareCalculator = fareCalculator;
        return nextFareCalculator;
    }

    public Fare calculate(ShortestPath shortestPath, Passenger passenger, Fare baseFare) {
        Fare fare = this.internalCalculate(shortestPath, passenger, baseFare);
        if (nextFareCalculator != null) {
            fare = nextFareCalculator.calculate(shortestPath, passenger, fare);
        }
        return fare;
    }

    abstract Fare internalCalculate(ShortestPath shortestPath, Passenger passenger, Fare fare);
}

 

로 구현해주고, 해당 클래스를 상속하는 요금 계산 정책을 구현한 구현체들을 만들어주었다

(인터페이스는 생략했다)

 

 

Client 인 PathService 에서는 생성자에서 DistanceFareCalculator 를 의존성 주입받고, LineFareCalculator와 AgeFareCalculator 를 생성하여 체이닝해주었다. 

@Service
public class PathService {
    private final PathRepository pathRepository;
    private final FareCalculator fareCalculator;

    public PathService(PathRepository pathRepository, FareCalculator fareCalculator) {
        this.pathRepository = pathRepository;

        FareCalculator ageCalculator = new AgeFareCalculator();
        FareCalculator lineCalculator = new LineFareCalculator();

        fareCalculator.setNextFareCalculator(lineCalculator).setNextFareCalculator(ageCalculator);

        this.fareCalculator = fareCalculator;
    }

    public PathResponse getShortestPath(Long departureStationId, Long arrivalStationId, Long age) {
        Station departure = pathRepository.findStationById(departureStationId);
        Station arrival = pathRepository.findStationById(arrivalStationId);

        Sections sections = pathRepository.findAll();
        Subway subway = Subway.from(sections);

        SubwayGraph subwayGraph = SubwayGraph.from(subway);
        ShortestPath shortestPath = subwayGraph.getDijkstraShortestPath(departure, arrival);
        Passenger passenger = new Passenger(age);
        Fare fare = fareCalculator.calculate(shortestPath, passenger, new Fare(0));

        return PathResponse.of(shortestPath, fare);
    }
}

 

여러 복잡한 요금 계산 정책들이 추가되었지만 간결하게 calculate() 하나로 요금 정책을 계산해줄 수 있다.

또한 새로운 요금 정책이 추가되면 FareCalculator 의 새로운 구현체를 만들어 체이닝해주면 될 것이고 요금 정책이 빠지게 된다면 핸들러 체이닝에서 빼주면 될 것이다. 

 

스프링 프레임워크에서의 책임 연쇄 패턴

 

스프링 프레임워크에서 인터셉터를 구현하는데 책임 연쇄패턴이 사용된다. 인터셉터는 컨트롤러가 요청을 처리하기 전/후에 특정 작업을 수행하도록 한다. 이때 인터셉터 체인을 구성하는데 책임 연쇄 패턴이 사용된다. 

 

스프링에서 인터셉터 체인은 HandlerInterceptor 인터페이스를 구현하여 정의듸고, HandlerInterceptor는 다음과 같은 메서드를 포함한다.

 

preHandle(): 컨트롤러가 요청을 처리하기 전에 실행되는 메서드로, 요청 전 처리 작업을 수행합니다.
postHandle(): 컨트롤러가 요청을 처리한 후, 뷰를 렌더링하기 전에 실행되는 메서드로, 요청 후 처리 작업을 수행합니다.
afterCompletion(): 컨트롤러와 뷰의 처리가 완료된 후에 실행되는 메서드로, 요청 완료 후 처리 작업을 수행합니다.

 

또한 스프링 시큐리티에서도 필터 체인 구성에 사용된다. 

 

전체 코드는 https://github.com/yenawee/jwp-subway-path/tree/step3 에서 확인할 수 있다.

 

GitHub - yenawee/jwp-subway-path

Contribute to yenawee/jwp-subway-path development by creating an account on GitHub.

github.com

참고 사이트

https://refactoring.guru/ko/design-patterns/chain-of-responsibility

'우아한테크코스' 카테고리의 다른 글

2차 데모데이 회고  (0) 2023.07.31
1차 데모데이 회고  (0) 2023.07.24
h2 database 를 사용해서 InMemoryDao Test 하기  (0) 2023.04.10
레벨 1 - 레벨 로그  (0) 2023.03.29
[우테코 프리코스] 5기 합격  (0) 2023.01.04