본문 바로가기
디자인패턴

구조 패턴

by 이상한나라의개발자 2023. 12. 12.

Adapter 패턴

 

Adapter 패턴은 구조적 디자인 패턴 중 하나로, 서로 다른 두 인터페이스간에 호환성을 제공하는 패턴입니다. 어댑터 패턴을 사용하면 기존 코드를 수정하지 않고 새로운 코드와 함께 작동하도록할 수 있습니다.

 

예를 들어, 기존에 사용하던 라이브러리를 사용하는데 있어, 새로운 시스템과 맞지 않는다면 어댑터를 사용하여 오래된 라이브러리의 인터페이스를 새로운 시스템 인터페이스와 호환되게 만들 수 있습니다.

 

많이들 비교하는게 전자기기 단자를 호환해주는 어댑터 입니다. 110V 전용 가전제품에 220V 어댑터를 끼워 사용하는 예를 들 수 있습니다.

이미 100V는 만들어 져있는 라이브러리 이고 이를 상속 또는 인터페이스로 220V 기능을 추가하는 것입니다.

 

Adapter 패턴에는 크게 두가지를 나눌 수 있습니다. 

 

1. 객체 어댑터 패턴 : Adaptee(기존 라이브러리) 를 구현 클래스 내부에 인스턴스로 주입 받아서 사용하합니다.

  • 다양한 Adaptee를 확장 구현 할 수 있는 장점이 있습니다.

2. 클래스 어댑터 패턴 : Adaptee(기존 라이브러리) 클래스를 직접 상속 받아서 사용합니다.

  • 구현체에서 인스턴스를 주입 받거나 생성하지 않아도 부모 클래스의 기능을 쓸 수 있다는 장점 있습니다.

 

객체 어댑터 패턴 

 

Adaptee(기존 라이브러리) 를 객체 인스턴스에 포함 시켜서 사용합니다.

 

public interface Target {

    void request();
}

public class Adaptee100v {
    public void electricitySupply() {
        System.out.println("electricity supply");
    }
}


public class Adapter220V implements Target {

    private final Adaptee100v adaptee100v;

    public Adapter220V(Adaptee100v adaptee100v) {
        this.adaptee100v = adaptee100v;
    }

    @Override
    public void request() {
        adaptee100v.electricitySupply();
    }
}


public class Client {
    public static void main(String[] args) {
        Target target = new Adapter220V(new Adaptee100v());
        target.request();
    }
}

 

 

클래스 어댑터 패턴

 

클래스 어댑터 패턴은 어댑터가 Adaptee100V를 상속 받고 Target 인터페이스를 구현합니다. JAVA는 다중 상속을 지원하지 않기 때문에 이 방법은 한정적인 경우에만 적용될 수 있습니다.

 

 

 

public interface Target {

    void request();
}


public class Adaptee100v {
    public void electricitySupply() {
        System.out.println("electricity supply");
    }
}


public class Adapter220V extends Adaptee100v implements Target {
    @Override
    public void request() {
        electricitySupply();
    }
}

public class Client {
    public static void main(String[] args) {
        Target target = new Adapter220V();
        target.request();
    }
}

 

 

퍼사드 패턴 (Facade Pattern)

 

퍼사드 패턴은 구조 패턴의 하나이며. 복잡한 서브 시스템의 인터페이스를 단순화 하는 패턴입니다. 이 패턴은 클라이언트와 복잡한 서브 시스템 사이에 중간에 위치하여 중개자 역할을 합니다. 이를 통해 클라이언트는 간결하게 서브시스템을 이용할 수 있습니다.

 

예시로 동영상 분석 로직이 있습니다.

 

동영상다운로드 -> 동영상 인코당 -> 동영상 정보 추출 -> 동영상 분석 -> 분석 결과 저장 

 

 

분석 흐름

 

 

 

해당 기능을 구현하면 아래와 같습니다. ( 퍼사트 패턴 x )

 

public class SomeService {

    private final FileDownloader fileDownloader;
    private final FileEncoding fileEncoding;
    private final VideoFeatureExtractor videoFeatureExtractor;
    private final VideoAnalyzer videoAnalyzer;
    private final FeatureRepository featureRepository;

    public AnotherService(FileDownloader fileDownloader, FileEncoding fileEncoding, VideoFeatureExtractor videoFeatureExtractor, VideoAnalyzer videoAnalyzer, FeatureRepository featureRepository) {
        this.fileDownloader = fileDownloader;
        this.fileEncoding = fileEncoding;
        this.videoFeatureExtractor = videoFeatureExtractor;
        this.videoAnalyzer = videoAnalyzer;
        this.featureRepository = featureRepository;
    }

    public VideoAnalystsResult analysis() {
        String filePath = fileDownloader.download(); // 동영상 다운로드
        String encodedFilePath = fileEncoding.encoding(filePath); // 동영상 인코딩
        VideoFeautre videoFeautre = videoFeatureExtractor.extract(encodedFilePath); // 동영상 정보 추출
        VideoAnalystsResult videoAnalystsResult = videoAnalyzer.analyze(videoFeautre); // 동영상 분석
        featureRepository.save(videoAnalystsResult); // 분석 결과 저장

        // 기타 로직

        return videoAnalystsResult;
    }
}

 

 

만약 다른 클래스가 추가 된다면 아래와 같이 중복 로직이 발생하게 됩니다.

 

public class AnotherService {

    private final FileDownloader fileDownloader;
    private final FileEncoding fileEncoding;
    private final VideoFeatureExtractor videoFeatureExtractor;
    private final VideoAnalyzer videoAnalyzer;
    private final FeatureRepository featureRepository;

    public AnotherService(FileDownloader fileDownloader, FileEncoding fileEncoding, VideoFeatureExtractor videoFeatureExtractor, VideoAnalyzer videoAnalyzer, FeatureRepository featureRepository) {
        this.fileDownloader = fileDownloader;
        this.fileEncoding = fileEncoding;
        this.videoFeatureExtractor = videoFeatureExtractor;
        this.videoAnalyzer = videoAnalyzer;
        this.featureRepository = featureRepository;
    }

    public VideoAnalystsResult analysis() {
        String filePath = fileDownloader.download(); // 동영상 다운로드
        String encodedFilePath = fileEncoding.encoding(filePath); // 동영상 인코딩
        VideoFeautre videoFeautre = videoFeatureExtractor.extract(encodedFilePath); // 동영상 정보 추출
        VideoAnalystsResult videoAnalystsResult = videoAnalyzer.analyze(videoFeautre); // 동영상 분석
        featureRepository.save(videoAnalystsResult); // 분석 결과 저장

        // 기타 로직

        return videoAnalystsResult;
    }
}

 

 

퍼사드 패턴을 적용하게 되면 서브 클래스의 로직은 뒤로 감추로 SomeService or AnotherService는 하나의 메서드를 호출합니다. Facade 말 그대로 출입구가 하나인거죠

 

퍼사드 패턴

 

 

 

public class FacadeVideoAnalyzer {

    private final FileDownloader fileDownloader;
    private final FileEncoding fileEncoding;
    private final VideoFeatureExtractor videoFeatureExtractor;
    private final VideoAnalyzer videoAnalyzer;
    private final FeatureRepository featureRepository;

    public FacadeVideoAnalyzer(FileDownloader fileDownloader, FileEncoding fileEncoding, VideoFeatureExtractor videoFeatureExtractor, VideoAnalyzer videoAnalyzer, FeatureRepository featureRepository) {
        this.fileDownloader = fileDownloader;
        this.fileEncoding = fileEncoding;
        this.videoFeatureExtractor = videoFeatureExtractor;
        this.videoAnalyzer = videoAnalyzer;
        this.featureRepository = featureRepository;
    }

    public VideoAnalystsResult analysis() {
        String filePath = fileDownloader.download(); // 동영상 다운로드
        String encodedFilePath = fileEncoding.encoding(filePath); // 동영상 인코딩
        VideoFeautre videoFeautre = videoFeatureExtractor.extract(encodedFilePath); // 동영상 정보 추출
        VideoAnalystsResult videoAnalystsResult = videoAnalyzer.analyze(videoFeautre); // 동영상 분석
        featureRepository.save(videoAnalystsResult); // 분석 결과 저장

        // 기타 로직

        return videoAnalystsResult;
    }
}


public class SomeService {

    private final FacadeVideoAnalyzer facadeVideoAnalyzer;

    public SomeService(FacadeVideoAnalyzer facadeVideoAnalyzer) {
        this.facadeVideoAnalyzer = facadeVideoAnalyzer;
    }

    public VideoAnalystsResult doSomething() {
        return facadeVideoAnalyzer.analysis();
    }
}


public class AnotherService {

    private final FacadeVideoAnalyzer facadeVideoAnalyzer;

    public AnotherService(FacadeVideoAnalyzer facadeVideoAnalyzer) {
        this.facadeVideoAnalyzer = facadeVideoAnalyzer;
    }

    public VideoAnalystsResult doSomething() {
        return facadeVideoAnalyzer.analysis();
    }
}

 

 

프록시 패턴 

 

원래 객체를 대신하여 요청을 받아 원래 객체를 호출하기 전이나 후에 특정 로직을 실행하는 패턴을 말합니다.

어떤 로직을 핵심적인 로직과 부가적인 로직으로 나누고 반복되는 부가적인 로직을 분리하여 감추게 되면 핵심적인 로직만 남게 되므로 로직의 가동성이 올라갑니다.

또한, 프록시 패턴은 스프링의 AOP(관점지향프로그래밍)을 알기 위해 알아야 합니다.

 

아래는 예시 코드입니다.

 

public interface SomeInterface {
    void someMethod();
}


-----


public class Service implements SomeInterface {
    @Override
    public void someMethod() {
        System.out.println("Service.someMethod()");
    }
}


-----


public class Porxy implements SomeInterface {

    private final Service service;

    public Porxy() {
        this.service = new Service();
    }

    @Override
    public void someMethod() {
        System.out.println(" someMethod() before");
        service.someMethod();
        System.out.println(" someMethod() alfter");
    }
}


-----


public class Client {
    private final SomeInterface someInterface;

    public Client(SomeInterface someInterface) {
        this.someInterface = someInterface;
    }

    public void doSomething() {
        someInterface.someMethod();
    }
}


-----


public class ProxyMain {

    public static void main(String[] args) {
        Client client = new Client(new Porxy());
        client.doSomething();
    }
}

 

 

스프링 AOP 기반 예제 

 

스프링에서 BindResult Validation 을 하는데요. 이 부분을 AOP를 사용해서 처리 해보도록 하겠습니다.

 

@Target(ElementType.METHOD)  // 메서드에만 어노테이션을 적용할 수 있도록 설정
@Retention(RetentionPolicy.RUNTIME)  // 런타임시에 어노테이션 정보를 사용하기 위해 설정
public @interface AutoValidator {
    // 어노테이션 속성들을 여기에 정의할 수 있습니다.
    // 예: String value() default "";
}



@Aspect
@Component
public class CustomValidatorAspect {

    @Pointcut("@annotation(sample.coffeekiosk.config.aop.AutoValidator))")
    public void autoValidator() {

    }

    @Around("autoValidator()")
    public Object validationCheck(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if ( arg instanceof BindingResult ) {
                BindingResult bindingResult = (BindingResult) arg;

                if ( bindingResult.hasErrors()) {
                    return ApiResponse.bindError("잘못된 요청입니다.", bindingResult);
                }
            }
        }
        return joinPoint.proceed();
    }
}


@AutoValidator
@PostMapping("/api/v1/user/create")
public ApiResponse<Object> createUser(@RequestBody @Validated CreateUserDto createUserDto, BindingResult bindingResult) {
    CreateUserResponse response = userService.createUser(createUserDto);
    return ApiResponse.of(HttpStatus.CREATED, response);
}

 

 

위 처럼 하게되면 실제 BindResult 구문은 AOP로 분리되여 반복되는 부가적인 로직을 분리하므로써 핵심적인 로직에 집중할 수 있게 됩니다.