와이파이 공유기의 관리자 페이지에서 제공하는 연결 기기 정보를 기반으로 연결된 기기, 연결 시간울 관리하는 프로젝트인 WifiObserver라는 프로젝트에서는 관리자 페이지에 접속하기 위해 쿠키 값을 획득하여야 했습니다.
이를 위해서는 아래와 같은 행위가 필요합니다.
- 공유기 관리자 페이지에 로그인한다.
- 로그인 이후 페이지에서
setCookie(XXX)
형태의 값을 찾는다. setCookie(XXX)
에서XXX
를 분리한다.
간단히 구현할 수 있는 문제이지만 다양한 방법을 고려해 보았고 이번 글에서는 어떤 이유로 데코레이터 패턴을 활용하였는지 공유해보려 합니다.
하나의 메서드
public class CookieResolver {
public String resolve(String source) {
// setCookie(XXX) 형태의 값을 찾는다.
// setCookie(XXX)에서 XXX를 분리한다.
return cookie;
}
}
우선 위와 같이 하나의 메서드에서 위의 행위를 모두 수행할 수 있습니다.
이때 resolve
메서드는 "setCookie(XXX) 형태의 값을 찾는 것" 그리고 "etCookie(XXX)에서 XXX를 분리하는 것"이라는 두 가지 책임을 가지게 됩니다.
이렇게 하나의 메서드가 여러 책임을 가지게 되면 코드의 가독성이 줄어들고 유지 보수하기 좋지 않은 코드가 되기에 책임을 분리하는 것을 고려할 수 있습니다.
private 메서드
책임을 분리하기 위해 private 메서드를 사용하는 방법을 생각할 수 있습니다.
private 메서드를 사용한다면 시각적으로 코드가 분리되기에 하나의 메서드에서 확인할 수 있는 문제를 해결하였다 생각할 수 있습니다.
하지만 이는 단지 시각적으로 코드가 분리된 것 뿐이지 클래스 입장에서는 여전히 두 가지 책임을 가지고 있기에 올바른 분리라고 생각하지 않습니다.
두 개의 클래스
public class SetCookieResolver {
public String resolve(String source) {
// setCookie(XXX) 형태의 값을 찾는다.
return cookie;
}
}
public class CookieNameResolver {
public String resolve(String source) {
// setCookie(XXX)에서 XXX를 분리한다.
return cookie;
}
}
위의 코드로 확인할 수 있듯 각 클래스가 하나의 책임을 가질 수 있게 되었습니다.
하지만 두 개의 클래스로 분리하며 코드를 통해 각 메서드의 실행 순서를 강제할 수는 없게 되었습니다.
이에 두 개의 클래스로 분리된 메서드의 실행 순서를 강제할 수 있다면 보다 친절한 코드가 되지 않을까 생각하였습니다.
데코레이터 패턴
객체들을 새로운 행동들을 포함한 특수 래퍼 객체들 내에 넣어서 위 행동들을 해당 객체들에 연결시키는 구조적 디자인 패턴입니다.
데코레이터 패턴을 클래스에 적용하여 분리된 메서드의 실행 순서를 강제할 수 있었습니다.
구현
데코레이터 패턴을 적용하기 위해서는 추가적인 구현이 필요하였습니다.
우선 구상 컴포넌트 클래스와 기초 데코레이터 클래스가 추가로 필요하였습니다.
이에 구상 컴포넌트는 StringResolver
라는 인터페이스로 기초 데코레이터 클래스는 StringResolverDecorator
라는 클래스로 구현하였습니다.
public interface StringResolver {
String resolve(String source);
/** 패턴을 통해 원하는 값을 파싱하기 위한 메서드 */
default String findBracketTextByPattern(Pattern pattern, String text) {
...
}
}
@RequiredArgsConstructor
public class StringResolverDecorator implements StringResolver {
private final StringResolver stringPatternResolver;
@Override
public String resolve(String source) {
return stringPatternResolver.resolve(source);
}
}
이후 기존의 SetCookieResolver
는 StringResolver
를 구현하도록 CookieNameResolver
는 StringResolverDecorator
를 상속하도록 구현을 수정해 주었습니다.
@Component
public class SetCookiePatternResolver implements StringResolver {
private static final Pattern SET_COOKIE_PATTERN = Pattern.compile("setCookie\\('[^()]+'\\)");
@Override
public String resolve(String source) {
return findBracketTextByPattern(SET_COOKIE_PATTERN, source);
}
}
@Component
public class CookieNamePatternResolverDecorator extends StringResolverDecorator {
private static final Pattern EXTRACT_COOKIE_NAME_PATTERN = Pattern.compile("([^()]+)");
public CookieNamePatternResolverDecorator(
@Qualifier("setCookiePatternResolver") StringResolver resolver) {
super(resolver);
}
@Override
public String resolve(String source) {
String resolved = super.resolve(source);
return findBracketTextByPattern(EXTRACT_COOKIE_NAME_PATTERN, resolved).replace("\'", "");
}
}
이때 CookieNamePatternResolverDecorator
의 resolve
메서드는 부모 클래스의 resolve
를 먼저 수행하고 이후 그 결과를 활용해 findBracketTextByPattern를
수행하도록 클래스를 설계하였습니다.
이렇게 설계된 CookieNamePatternResolverDecorator
클래스를 생성할 때 SetCookiePatternResolver
를 주입받아 부모 클래스의 생성자로 넘겨주는 방식으로 분리된 메서드의 실행 순서를 강제할 수 있었습니다.
다이어그램
느낀 점
@Component
public class CookieResolver {
private final SetCookieResolver setCookieResolver;
private final CookieNameResolver cookieNameResolver;
public String resolve(String source) {
String resolved = setCookieResolver.resolve(source);
return cookieNameResolver.resolve(resolved);
}
}
데코레이터 패턴을 적용하고 난 이후 위의 구현이 생각났고 데코레이터 패턴을 적용한 코드와 어떤 차이가 있을지 고민할 수 있었습니다.
제가 생각하기에 위의 구현은 행위를 수행하기 위해 필요한 모든 클래스를 알아야 하고 데코레이터 패턴을 적용한 구현은 바로 이전의 구현만 알면 된다는 것이 차이인 것 같습니다.
(스프링 DI를 활용하기 위해서 이전의 구현을 알아야 하는 것이지 본래 데코레이터 패턴은 이전의 주입받을 대상을 알고 있지 않습니다.)
해당 글에서의 예시처럼 행위의 단계가 적다면 데코레이터 패턴은 높아지는 복잡도에 적절하지 않은 선택일 수 있겠다는 생각을 하였습니다.
하지만 단계가 많아진다고 각 단계에서 수행하는 행위가 중요하다면 데코레이터 패턴을 사용하여 각 단계를 명확히 구분하는 것도 좋은 선택이 될 수 있을 것 같다는 생각을 하였습니다.
앞으로도 단순히 구현을 하는 것에 의미를 두기보다는 다양하게 제가 알고 있는 것들을 적용해 보며 저만의 상황을 판단하는 능력을 키워나가 보려 합니다.
감사합니다.
'개발' 카테고리의 다른 글
다중화 어플리케이션 환경에서 동시성 처리하기 (0) | 2024.05.27 |
---|---|
위치 기반 프로젝트를 준비하며 (0) | 2024.04.23 |
인터페이스가 필요한 순간 (0) | 2024.04.09 |
도메인 객체 도입하기 (0) | 2024.04.06 |
GCP로 프로젝트 배포하기 (0) | 2024.04.02 |