디자인 패턴 공부 - 책임 연쇄 패턴

이혁진·2023년 1월 10일

책임 연쇄 패턴

요청을 여러 단계로 처리하는 경우에, 요청의 연쇄 과정을 캡슐화하여, 요청을 보내는 클래스(Client)와 요청을 처리하는 클래스(Filter)를 분리하는 패턴이다.

구현

기존에 여러 필터를 거처 요청을 처리하는 과정에서, 다음과 같이 구현하는 것이 일반적이다.

public class ClientA {
	public void send() {
    	String request = "  r e q u e s t 1  ";
    	request = new FilterA().handle(request);
    	request = new FilterB().handle(request);
    	request = new FilterC().handle(request);
    	request = new PrintFilter().handle(request);
        // 회원가입 처리 과정
	}
}

public class ClientB {
	public void send() {
    	String request = "  r e q u e s t 1   ";
    	request = new FilterA().handle(request);
    	request = new FilterB().handle(request);
    	request = new FilterC().handle(request);
    	request = new PrintFilter().handle(request);
        // 회원가입 처리 과정
	}
}

public class ClientC {
	public void send() {
    	String request = "  re que st 1";
    	request = new FilterA().handle(request);
    	request = new FilterC().handle(request);
    	request = new PrintFilter().handle(request);
        // 로그인 처리 과정
	}
}

원래 요청 처리가 위처럼 되었다고 해보자. 로그인과 회원가입 필터 연쇄가 바뀌었다고 하면, 요청자들(ClientA, ClientB, ClientC)에 가서 다 일일이 바꿔줘야 한다. 지금이야 할만하지만 필터 연쇄가 몇백개의 클래스에서 사용되고 있다면 꽤나 힘들 것이다. 따라서, 다음과 같이 필터의 연쇄를 캡슐화한다.

public abstract class Filter {

	private final Filter nextFilter;

	public Filter(Filter nextFilter) {
    	this.nextFilter = nextFilter;
	}

	public void handle(String request) {
    	if(nextFilter != null) {
        	nextFilter.handle(request);
    	}
	}
}

nextFilter.handle(request) 부분을 보면, 현재 필터의 handle이 다음 필터의 handle을 호출하는, 연쇄 과정을 캡슐화한 것을 알 수 있다.(어떤 필터이든지 상관없이 일반화) 이후, 그것을 상속하는 필터 클래스를 만든다.

public class FilterA extends Filter {
	public FilterA(Filter nextFilter) {
    	super(nextFilter);
	}

	@Override
	public void handle(String request) {
    	super.handle(request.toUpperCase(Locale.ROOT));
	}	
}

public class FilterB extends Filter {
	public FilterB(Filter nextFilter) {
    	super(nextFilter);
	}

	@Override
	public void handle(String request) {
    	super.handle(request.trim());
	}
}

public class FilterC extends Filter {
	public FilterC(Filter nextFilter) {
    	super(nextFilter);
	}

	@Override
	public void handle(String request) {
    	super.handle(request.replace("1", "2"));
	}
}

public class PrintFilter extends Filter {
	public PrintFilter(Filter nextFilter) {
    	super(nextFilter);
	}

	@Override
	public void handle(String request) {
    	System.out.println(request);
    	super.handle(request);
	}
}

중요한 부분은 super.handle로 부모의 handle을 호출하여 다음 필터를 작동시키는 것이다. 이 필터 부분에서 여러 변형을 줄 수도 있다. 특정 필터에서 super의 handle을 호출하지 않음으로써 필터 체인의 작동을 멈추거나, 앞의 필터를 다시 거치거나, 특정 조건에 따라 어떤 필터를 다음에 통과시킬 지 분기하는 것도 가능하다. 또, 타입을 검사해서 필터를 넘길지 적용할지, 순서를 유지한 채로 수행할지 아무튼 요청 처리하는 경우 꽤나 요긴하게 쓰이는 패턴이란다.

이렇게 하면, 아까 연쇄 작업을 Filter의 handle로 캡슐화한 덕에 Client로부터 분리가 가능해졌다. 가령 이런 식이다.

<Client>
Filter filter = new FilterA(new FilterB(new FilterC)));
filter.handle(request);

원래라면 request를 여러 필터를 Client에서 거치면서 받아야 했는데, 이제는 체인의 캡슐화로 분리가 가능해졌다. 마치 DI와 비슷한 느낌이다.

public class ChainContainer {
	public static Filter getRegisterChainFilter() {
    	return new FilterA(new FilterB(new FilterC(
            	new PrintFilter(null)
    	)));
	}

	public static Filter getLoginChainFilter() {
    	return new FilterA(new FilterC(
            	new PrintFilter(null)
    	));
	}
}

public class ClientA {
	public void send() {
    	String request = "  r e q u e s t 1  ";
    	ChainContainer.getRegisterChainFilter().handle(request);
        // 회원가입 처리 과정
	}
}

public class ClientB {
	public void send() {
    	String request = "  r e q u e s t 1   ";
    	ChainContainer.getRegisterChainFilter().handle(request);
        // 회원가입 처리 과정
	}
}

public class ClientC {
	public void send() {
    	String request = "  re que st 1";
    	ChainContainer.getLoginChainFilter().handle(request);
        // 로그인 처리 과정
	}
}

이러면 로그인 부분을 바꾸고 싶을때, 클라이언트 다 가서 바꾸는 게 아니라 로그인 필터 구성 부분만 컨테이너에서 수정해주면 되는 것이다.

장단점

장점은 아까 위에서랑 같다. 클라이언트가 여러개면 특정 체인의 로직 변경 시 변경 시 고칠 부분이 적어진다. 또한 캡슐화와 인터페이스 기반 설계로 어떤 필터가 추가되면, 클래스 구현해서 체인을 새로 만들거나 하면 된다. 물론 이전 구현 역시도 인터페이스에 기반하므로 큰 차별점은 아니다.
그리고 필터 체인의 흐름을 제어할 수 있다. 위에처럼 순서를 바꿔도 되도록 체인을 구성할 수도 있지만, 필터에서 순서를 강제하거나, 필터에서 다음 필터를 분기하거나, 이전 필터를 한번 더 거치거나, 특정 필터에서 체인을 멈추거나 할 수 있다.
이러한 장점으로 요청을 처리하는 로직에서 엄청 많이 쓴다고 한다. 반면 단점은 역시 구조가 복잡해지고, 디버깅이 어렵다는 점을 꼽을 수 있겠다.

예시 1 - 서블릿 필터

사용자 인증, 혹은 로깅 같은 공통 기능을 서블릿의 요청 처리 이전에 전처리하거나, 호출 후 후처리하고싶다면 서블릿 필터로 구현하면 된다.

아래처럼 Filter를 구현해서 만든다.

@WebFilter(urlPatterns = "/hello")
public class MyFilter implements Filter {
	@Override
    public void doFilter(
    	ServletRequest request, ServletResponse response, FilterChain chain
    ) throws IOException, ServletException {
    	// 전처리
		request.setCharacterEncoding("UTF-8");
		System.out.println("doFilter() 전....");
        
        // 다음 필터로 요청이랑 응답 객체 넘기고, 필터 처리 수행
		chain.doFilter(request, response);
        
		// 후처리
		System.out.println("doFilter() 후....");
    }
}

아까 패턴이랑 거의 똑같다. super.handle()이 chain.doFilter()랑 같은 역할을 한다.

이렇게 만든 필터를 여러개 등록해 체인으로 쓰고 싶다면,

<filter>
	<filter-name>firstFilter</filter-name>
	<filter-class>test.FirstFilter</filter-class>
</filter>
<filter>
	<filter-name>secondFilter</filter-name>
	<filter-class>test.SecondFilter</filter-class>
</filter>

이렇게 xml로 쓰믄 된단다. 순서를 지정하고 싶으면 이렇게

<filer-mapping>
	<filter-name>firstFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filer-mapping>
secondFilter /* thirdFilter /third

예시 2 - 스프링 시큐리티

스프링 시큐리티는 스프링에서 인증(누구인지)가 인가(권한)을 담당하는 스프링 하위 프레임워크이다. 서블릿 필터와 그 체인으로 구현되어있고, 보안과 관해서 옵션이 제공되므로 개발자는 보안 로직을 일일히 작성할 필요가 없다.

생각해보니까, 그냥 필터가 아니라 인증 인가 관련 필터만 있고, 전처리 필터는 서블릿으로 따로 구현하나보다.

보면 시큐리티 필터랑 그냥 필터가 있는데, 그냥 필터가 서블릿인듯 하다. 시큐리티 필터는 스프링 시큐리티에 해당되는 모양. 서블릿 필터 안에 DelegatingFilterProxy를 넣어서 인증과 인가를 하면 되나보다. 저 프록시가 필터체인에 대한 제어권을 가지고 있는 모양

그리고 그 프록시가 제어하는 시큐리티 필터 체인은 이렇게 생겨 먹었다.

아무튼 이렇고, 구현은 다음과 같이 할 수 있다.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception {
    	http.authorizeRequests()
        	.anyRequest()
            .permitAll()
            .and()
        	.addFilter(new MyFilter());
    }
}

아까 구현한 서블릿 필터를 저렇게 new MyFilter()로 넣을 수 있다는 것을 확인할 수 있다. 아까 본 것처럼, 이거 말고도 필터가 많다. 잘 모르겠는데, addFilter로 서블릿 필터 넣는 부분이 프록시에서 서블릿 체인으로 제어권이 넘어오는 모양이다. 아님 말고.

profile
한양대학교 정보시스템학과 22학번 이혁진입니다. 컴퓨터 아키텍처와 시스템 등 다양한 주제로 공부한 내용을 기록합니다.

0개의 댓글