파사드 패턴이란?
파사드(Facade)는 '건축물의 외관, 겉면'을 뜻하는 단어로, 프랑스어(façade)에서 건너온 단어이다. 원래 어원은 라틴어에서 얼굴(Face)을 뜻하는 facies(파시에스) 라고 하는데 건출물 외관 = 얼굴 로 이해해볼 수 있다.
디자인 패턴에서 말하는 파사드 패턴은 내부의 코드를 가려놓고 전면에 단순화된 인터페이스를 내세우는 구조가 마치 내부의 인테리어나 구조를 덮어놓은 건축물의 외벽과 유사하다는 데서 차용되었다. 여러 클래스들을 직접 가져와서 쓰게 되면 가독성도 떨어지고 의존관계도 복잡해지니, 하나의 단순화된 클래스를 만들어서 이를 쉽게 이용할 수 있도록 만드는 것이다. 아래 그림과 같이 말이다.
언뜻 듣기에 너무 쉬운 개념이라서 '이걸 따로 배울 필요가 있었나?' 라는 생각이 들었지만 실제로 이걸 배우거나 예제로 적용해보지 않은 상태로 이 패턴을 스스로 떠올려서 적용하기는 쉽지 않을 것 같다. 나도 디자인 패턴을 적용해볼게 있을까? 라는 생각으로 코드를 뒤적이다가 그제서야 보였다.
적용 예시
현재 진행중인 프로젝트에서는 아래와 같이 Jwt 관련 유틸리티 클래스들이 있다.
기존에는 위 유틸리티들을 적절히 이용하여 Authorization 필터에서 Jwt 를 검증하고 Authentication 객체를 생성하고 있었다. 아래는 파사드 패턴을 적용하기 전의 클래스. (사실 거진 3 ~ 4달 전부터 리펙토링이 꾸준히 진행되어 와서 지금이랑은 로직 자체가 많이 다르다.)
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private final JwtValidator jwtValidator;
private final JwtGenerator jwtGenerator;
private final UserRepository userRepository;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, JwtValidator jwtValidator, JwtGenerator jwtGenerator, UserRepository userRepository) {
super(authenticationManager);
this.jwtValidator = jwtValidator;
this.jwtGenerator = jwtGenerator;
this.userRepository = userRepository;
}
/**
* JwtAuthorizationFilter
* 인증이 필요한 api 접근시, jwt 검증 수행
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("JwtAuthorization Filter");
String accessToken = this.getAccessToken(request);
// access token 의 소유 여부 확인
if (accessToken != null) {
// access token 이 있는 경우
// access token 의 유효성을 검증
TokenValidateResponse accessTokenValidateResponse = jwtValidator.validateAccessToken(accessToken);
if (accessTokenValidateResponse.isValidToken()) {
authenticate(accessTokenValidateResponse.getUserKey());
chain.doFilter(request, response);
return;
}
}
// access token 이 없는 경우
// refresh token 소유 여부를 확인
String refreshToken = this.getRefreshToken(request);
// refresh token 이 있는 경우
if (refreshToken != null) {
User user = getUser(refreshToken);
TokenValidateResponse refreshTokenValidateResponse = jwtValidator.validateRefreshToken(refreshToken, user);
// refresh token 의 유효성을 검증
// refresh token이 유효한 경우
if (refreshTokenValidateResponse.isValidToken()) {
// access token 을 재발행하여 header 에 담아서 응답
String reissuedAccessToken = jwtGenerator.generateAccessToken(user);
response.setHeader(JwtProperties.ACCESS_TOKEN_HEADER, JwtProperties.ACCESS_TOKEN_PREFIX + reissuedAccessToken);
authenticate(user.getUserKey());
chain.doFilter(request, response);
return;
}
}
// refresh token 이 없는 경우
chain.doFilter(request, response);
}
/**
* authenticate
* Authentication 객체를 생성하여 SecurityContext 에 넣음
* Authentication : UsernamePasswordAuthenticationToken
* exception : jwtToken을 지니고 있는데 해당 회원이 없는 경우
*/
private void authenticate (String userKey){
User user = this.getUser(userKey);
PrincipalDetails principalDetails = new PrincipalDetails(user);
Authentication authentication = new UsernamePasswordAuthenticationToken(
principalDetails,
null,
principalDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
public User getUser(String userKey) {
return userRepository.findByUserKey(userKey).orElseThrow(()->{
throw new IllegalArgumentException("회원을 찾지 못하였습니다.");
});
}
public String getAccessToken(HttpServletRequest request) {
String authorizationHeader = request.getHeader(JwtProperties.ACCESS_TOKEN_HEADER);
if (authorizationHeader != null && authorizationHeader.startsWith(JwtProperties.ACCESS_TOKEN_PREFIX)) {
return authorizationHeader.replace(JwtProperties.ACCESS_TOKEN_PREFIX, "");
}
return null;
}
public String getRefreshToken(HttpServletRequest request) {
String refreshTokenCookie = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie != null && cookie.getName().equals(JwtProperties.REFRESH_TOKEN_HEADER)) {
refreshTokenCookie = cookie.getValue();
}
}
}
if (refreshTokenCookie != null && refreshTokenCookie.startsWith(JwtProperties.REFRESH_TOKEN_PREFIX)) {
return refreshTokenCookie.replace(JwtProperties.REFRESH_TOKEN_PREFIX, "");
}
return null;
}
}
언뜻 봐도 코드가 엄청 길고 복잡하다. 게다가 3개의 빈을 주입하고 있기 때문에 의존관계도 조금 복잡하다 할 수 있다.
여기서 파사드 패턴을 적용하여 아래와 같이 바꿀 수 있었다.
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
private final JwtAuthenticator jwtAuthenticator;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtAuthenticator jwtAuthenticator) {
super(authenticationManager);
this.jwtAuthenticator = jwtAuthenticator;
}
/**
* JwtAuthenticationFilter
* jwt를 검증하고 authenticate 해주는 필터
* jwtAuthenticator 에서 jwt 가 유효함이 검증되면 authentication 을 생성해주며
* jwt 가 없거나 유효하지 않으면 아무 작업을 수행하지 않고 그냥 반환
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
jwtAuthenticator.authenticateRequest(request);
chain.doFilter(request, response);
}
}
길고 지루한 코드를 전부 JwtAuthenticator 로 가려버려서 아주 간단해졌음을 알 수 있다.
추가 의견
파사드 패턴을 이전에 적용하였을 때는 단순히 코드의 가독성이 더 좋아지는구나. 라고만 생각했다. 그런데 이래저래 격변(?)의 리팩토링을 겪고, 토비의 스프링을 읽고 난 뒤로 코드를 볼 때 "추상화", "캡슐화" 등 객체지향적 측면에서 더 접근 하게된다. 파사드 패턴을 적용하고 나니 의존관계도 아주 심플해졌으며, JwtAuthenticator 클래스의 내부 로직도 전혀 알지 못하는 상태가 되었다.
이렇게 변경하고 나면 물론 가독성이라는 측면에서도 이점이 있겠지만 그보다도 변화가 생겼을 때, 더 적은 코드의 수정으로 이에 대처할 수 있다는 더 큰 장점이 있다. 실제로 이후 채팅기능을 도입하면서 웹소켓에서의 인증 처리를 다시 할 일이 생겼었는데, 이러한 패턴을 적용하지 않았더라면 Filter 나 각각의 유틸리티 클래스를 어떻게 변경해야할지 혼란을 겪었을 것 같다.
그러나 파사드 패턴을 도입했기 때문에 기존의 Filter 에 어떠한 변화를 주지 않고도 웹소켓의 Authorization 을 수행할 수 있도록 변경할 수 있었다. 즉, 변경에는 닫혀 있고 확장에는 닫혀있다는 OCP 원칙에 합당하다고 할 수 있다.
패턴을 하나씩 적용해보면서 객체지향 설계에 대해 좀더 가까워지는 느낌이 든다.