어댑터라는 단어를 들으면 저는 제일 먼저 일명 '돼지코'(110v 변환기)가 떠오릅니다.
외국에 처음 나갔을 때 우리나라와 다르게 생긴 콘센트 모양을 보고 큰 충격에 빠졌고,
심지어 사용하는 전압이 다른 것을 보고 크게 당황했던 기억이 있네요.
다른 친구가 돼지코의 존재를 알려줘서 다행히 가져갔던 전자기기를 무사히 쓰다가 돌아왔답니다.
오늘 다루어볼 네번째 디자인 패턴, 어댑터(Adapter) 패턴은 이러한 상황에서 활용할 수 있는 디자인패턴입니다.
* Adapter
- 다른 전기나 기계 장치를 서로 연결해서 작동할 수 있도록 만들어 주는 결합 도구
1. Definition
The adapter pattern convert the interface of a class into another interface clients expect.
Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.
- 한 클래스의 인터페이스를 클라이언트에서 사용하려는 다른 인터페이스로 변환
- 인터페이스의 호환성 문제를 해결하는 디자인 패턴
서론에 말했던 사례를 가지고 설명해보겠습니다.
저는 기존의 220v식 한국에서 생산된 전자기기(사용하려는 인터페이스)를 가지고 있었습니다.
근데 외국에 나가보니 콘센트의 모양과 전압이 다른 문제(다른 인터페이스와의 호환성 문제)가 발생했습니다.
이를 해결하기 위해서 '돼지코(어댑터)'를 사용했습니다.
결과적으로 다른 모양과 전압을 가진 전자기기와 콘센트 연결(변환)해서 사용할 수가 있었습니다.
이처럼 다른 인터페이스간의 호환성 문제를 해결하는 디자인패턴이 어댑터패턴이랍니다.
2. Structure
- Target : Adapter가 구현하는 다른 인터페이스(110v 콘센트)
- Adaptee : 기존 인터페이스(220v 전자기기)
- Adapter : Client가 호출하는 대상. Adaptee(기존 인터페이스)와 Target(다른 인터페이스)를 연결
호출 과정을 보면서 좀더 자세히 살펴봅시다.
1. 먼저 클라이언트에서 Target 인터페이스로 요청(request)를 보냅니다.
2. 이는 Target 인터페이스를 구현한 Adapter 클래스로 전달됩니다.
3. Adapter 클래스는 자신이 감싸고있는(Wrapper) Adaptee에게 실질적인 요청을 위임합니다.
4. 결국 Target.request()와 Adaptee.specificRequest()가 Adapter.request() 안에서 연결됩니다.
이 과정을 코드로 표현해보면 아래와 같습니다.
/**
* Client
*/
public class Client {
public void main() {
Adaptee adaptee = new Adaptee();
// Adaptee를 주입받은 Target 인터페이스 구현체 Adapter
Target target = new Adapter(adaptee);
target.request();
}
}
/**
* 대상 인터페이스
*/
public interface Target {
public void request();
}
/**
* 대상 인터페이스의 구현체
*/
public class Adapter implements Target {
private Adaptee adaptee;
// 기존 인터페이스를 구성(composition)을 활용하여 주입받음
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@overrid
public void request() {
this.adaptee.specificRequeest();
}
}
/**
* 기존 인터페이스
*/
public class Adaptee {
public void specificRequeest() {
// do something
}
}
3. Example
자바의 대표적인 로깅 라이브러리에는 log4j2, java.util.logging, logback 등이 있습니다.
라이브러리마다 특색이 조금 있지만, api, 용법, 사상 등이 크게 차이는 나지 않습니다.
하지만 로깅 라이브러리를 바꾸려고 한다면 많은 클라이언트 코드가 변경되어야 하는 것은 사실입니다.
logger를 설정한 모든 클래스 파일을 일일이 확인해서,
logger 변수와 import 항목을 변경해주는 것은 어렵지는 않지만 상당한 노가다 작업이지요.
또한 기존에 사용하고 있던 라이브러리에서 사용하던 메서드가
변경하고자 하는 라이브러리의 메서드 스펙과 다르다면 이것 또한 일일히 찾아내서 수정해줘야겠네요.
(log4j 보안 이슈 때문에 대응하면서 진행해본 경험이 있는데 썩 유쾌하진 않았습니다.)
이런 문제를 해결하고자 일반적으로 많이 활용하는 라이브러리가 있습니다.
바로 SLF4J(The Simple Logging Facade for Java) 라이브러리입니다.
이 라이브러리는 다양한 로깅 프레임워크의 상위 추상체이자, Facade(파사드)입니다.
아래의 그림을 보시면 이해가 빠를 것 같네요.
Library | Role | Description |
slf4j-api.jar | Adaptee | slf4j의 주요 api(.info(), .debug() etc) |
log4j-slf4j-impl.jar | Adapter | slf4j에서 log4j2로 연결하는 어댑터 |
log4j-core.jar | Target Interface | log4j 라이브러리 |
결국, 다양한 로깅 라이브러리(Adaptee)와 slf4j(Target)을 slf4j-log4j12.jar와 같은 Adpater로 연결하여 사용한다고 생각하시면 될 것 같습니다.
Client에서는 slf4j의 method를 호출하면 Adpater에서 시스템에서 사용하는 로깅 라이브러리의 메서드로
처리를 위임하는 것이죠.
이러면 로깅 라이브러리가 변경되더라도 클라이언트 코드는 수정할 필요가 없을 것입니다.
아래 UML 살펴보면서 어댑터 패턴이 실제적으로 어떻게 적용되는지 살펴보도록 하겠습니다.
- Client에서 LoggerFactory.getLogger()를 호출
- log4j-slf4j-impl.jar(from slf4j to log4j로 연결)에 구현된 Log4jLogger Adapter를 호출
- Log4jLogger Adapter에서는 log4j의 ExtendedLogger를 내부변수로 구성(compostion)하여 해당 인스턴스 반환
아래는 각 클래스의 코드 중 일부분을 발췌한 것입니다.
1) Client
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* client 예시
* logger 변수 선언 및 사용하는 클라이언트
*/
public class Client {
private static final Logger logger = LoggerFactory.getLogger(CommonController.class);
public static void main(String[] args) {
logger.debug('logger debug level');
logger.info('logger info level');
}
}
- slf4j-api.jar에 위치한 org.slf4j.Logger 및 org.slf4j.LoggerFactory 클래스를 활용하여 logger 변수를 선언
- org.slf4j.Logger 인터페이스에 정의된 debug(), info() method를 호출
2) Target
package org.slf4j;
/**
* Target(대상) 인터페이스
* trace, debug, info, warn 등 level에 따른 로깅 메서드 추상화
*/
public interface Logger {
public void trace(String msg);
public void debug(String msg);
public void info(String msg);
public void warn(String msg);
}
- Target(대상) 인터페이스
- trace, debug, info, warn 등 level에 따른 로깅 메서드 추상화
3) LoggerFactory.getLogger(Class)
public final class LoggerFactory {
// 1. LoggerFactory.getLogger(class) -> getLogger(class.getName())
public static Logger getLogger(Class clazz) {
return getLogger(clazz.getName());
}
// 2. getLogger(class.getName()) -> getILoggerFactory().getLogger()
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
// 3. SUCCESSFUL_INITIALIZATION에 주목할 것
// StaticLoggerBinder.getSingleton().getLoggerFactory()를 반환
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITIALIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION:
// support re-entrant behavior.
// See also http://bugzilla.slf4j.org/show_bug.cgi?id=106
return TEMP_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}
}
- LoggerFactory.getLogger(class) -> getLogger(class.getName()) -> getILoggerFactory().getLogger()
이러한 순서로 호출
최종적으로 StaticLoggerBinder.getSingleton().getLoggerFactory()에서 반환하는 객체를 사용
4) StaticLoggerBinder
/**
* slf4j에서 정의한 LoggerFactory의 상위 인터페이스
*/
public interface LoggerFactoryBinder {
public ILoggerFactory getLoggerFactory();
}
/**
* LoggerFactoryBinder를 log4j-slf4j-impl.jar에서 구현한 클래스
*/
public final class StaticLoggerBinder implements LoggerFactoryBinder {
private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
private StaticLoggerBinder() {
loggerFactory = new Log4jLoggerFactory();
}
// 해당 클래스의 싱글톤 객체 반환
public static StaticLoggerBinder getSingleton() {
return SINGLETON;
}
// 최종적으로 Log4jLoggerFactory 인스턴스 반환
@Override
public ILoggerFactory getLoggerFactory() {
return loggerFactory;
}
}
- slf4j에서 정의한 LoggerFactory의 상위 인터페이스 LoggerFactoryBinder를 log4j-slf4j-impl.jar에서 구현한 모습
- 최종적으로 Log4jLoggerFactory 인스턴스를 반환
5) Log4jLoggerFactory
/**
* Log4jLoggerFactory는 ILoggerFactory, AbstractLoggerAdapter의 구현체이다
*/
public class Log4jLoggerFactory extends AbstractLoggerAdapter<Logger> implements ILoggerFactory {
private static final StatusLogger LOGGER = StatusLogger.getLogger();
private static final String SLF4J_PACKAGE = "org.slf4j";
private static final String TO_SLF4J_CONTEXT = "org.apache.logging.slf4j.SLF4JLoggerContext";
private static final Predicate<Class<?>> CALLER_PREDICATE = clazz ->
!AbstractLoggerAdapter.class.equals(clazz) && !clazz.getName().startsWith(SLF4J_PACKAGE);
@Override
protected Logger newLogger(final String name, final LoggerContext context) {
final String key = Logger.ROOT_LOGGER_NAME.equals(name) ? LogManager.ROOT_LOGGER_NAME : name;
return new Log4jLogger(validateContext(context).getLogger(key), name);
}
}
/**
* AbstractLoggerAdapter는 Logger type의 제네릭을 소유
* newLogger()를 내부적으로 호출
*/
public abstract class AbstractLoggerAdapter<L> implements LoggerAdapter<L>, LoggerContextShutdownAware {
@Override
public L getLogger(final String name) {
final LoggerContext context = getContext();
final ConcurrentMap<String, L> loggers = getLoggersInContext(context);
final L logger = loggers.get(name);
if (logger != null) {
return logger;
}
loggers.putIfAbsent(name, newLogger(name, context));
return loggers.get(name);
}
protected abstract L newLogger(final String name, final LoggerContext context);
}
/**
* slf4j에서 Log4j 객체 구현체
* ExtendedLogger 내부 변수를 통해 구성(composition)을 사용
*/
public class Log4jLogger implements LocationAwareLogger, Serializable {
private final boolean eventLogger;
private transient ExtendedLogger logger;
private final String name;
public Log4jLogger(final ExtendedLogger logger, final String name) {
this.logger = logger;
this.eventLogger = "EventLogger".equals(name);
this.name = name;
}
}
- 상위 추상클래스 AbstractLoggerAdapter<Logger>에서 구현한 getLogger() 호출
- 해당 메서드에서는 초기화 시에 내부적으로 newLogger() 호출
- newLogger() 메서드에서는 Log4jLogger() 생성자를 호출
- Log4jLogger 클래스는 log4j에서 정의한 Logger 인터페이스를 내부 변수로 구성(composition)
5) Log4jLogger
package org.apache.logging.log4j;
/**
* log4j에서 정의한 Logger의 최상위 인터페이스
*/
public interface Logger {
void trace(String msg);
void debug(String msg);
void info(String msg);
void warn(String msg);
}
public interface ExtendedLogger extends Logger {
boolean isEnabled(Level level, Marker marker, Message message, Throwable t);
}
- Log4jLogger 클래스의 생성자가 호출될 때 내부적으로 구성하는 log4j logger의 최상위 인터페이스
4. Pros vs Cons
*) Pros
- 기존 코드를 변경하지 않고, 재활용이 가능합니다.
*) Cons
- 구성요소를 위해 클래스를 증가시켜야 하기 때문에 복잡도가 증가할 수 있습니다.
5. Appendix
- Adapter 패턴은 크게 클래스 Adapter / 객체 Adapter 방식이 있습니다.
- 오늘 다룬 방식은 객체 Adapter 방식에 해당합니다.
'Design Pattern' 카테고리의 다른 글
Decorator (0) | 2022.03.15 |
---|---|
Observer (0) | 2022.03.02 |
Strategy (0) | 2022.02.17 |