JUINTINATION

퍼사드(Facade) 패턴 본문

StudyNote

퍼사드(Facade) 패턴

DEOKJAE KWON 2024. 11. 2. 17:30
반응형

퍼사드 패턴이란? 

퍼사드 패턴(창구 패턴)은 사용하기 복잡한 라이브러리에 대한, 프레임워크에 대한 또는 다른 클래스들의 복잡한 집합에 대한 단순화된 인터페이스를 제공하는 디자인 패턴이다. GOF 디자인 패턴 중 구조 패턴에 해당하며, 서비스 지향 아키텍처(SOA)에서 자주 사용되는 패턴 중 하나이다.

퍼사드 패턴과 인터페이스 설계

인터페이스 개발을 하다 보면 인터페이스를 얼마나 세분화해야 하는지에 대해  고민하게 되는데, 퍼사드 패턴은 그 원리와 구현이 매우 간단할 뿐만 아니라 사용 대상이 비교적 명확하기 때문에 인터페이스 설계에 주로 사용된다.

만약 인터페이스를 재사용할 수 있게 하려면 인터페이스를 최대한 세분화하고 단일 책임을 지도록 설계해야 하지만, 그렇다고 해서 인터페이스 설계를 너무 세분화하면 비즈니스 기능을 개발할 때 너무 많이 세분화된 인터페이스를 호출하도록 작업해야 하기 때문에 매우 번거롭다.

반대로, 특정 비즈니스에 대해 매우 크고 포괄적인 인터페이스를 개발하게 되면 당장 사용에는 편리할지 모르지만, 인터페이스가 담당하는 범위가 너무 넓고, 책임이 단일하지 않기 때문에 범용성이 너무 낮아 코드를 재사용하거나 다른 비즈니스 개발에 적용할 수 없다.

그렇다면 인터페이스의 사용 편의성과 범용성의 균형을 맞추려면 어떻게 해야 할까?

예를 들어 시스템 A는 a, b, c, d라는 4개의 인터페이스를 제공하고, 시스템 B는 시스템 A의 인터페이스 중 a, b, d라는 3가지 인터페이스를 호출해야 한다고 가정하면, 퍼사드 패턴은 시스템 B에서 직접 사용할 수 있도록 인터페이스 a, b, d를 하나로 묶어 인터페이스 x로 제공해준다. 이렇게 퍼사드 패턴을 적용했을 때 다음과 같은 장점이 있다.

  • 복잡성 감소
    • 시스템 B는 a, b, d 세 가지 인터페이스를 개별적으로 호출하는 대신 하나의 통합된 인터페이스 x만 호출하면 되므로, 이는 시스템 B의 코드를 더 간단하고 이해하기 쉽게 복잡한 시스템의 인터페이스를 단순화한다. 
  • 결합도 감소
    • 퍼사드 인터페이스 x를 사용함으로써 시스템 B는 시스템 A의 세부 구현에 대해 알 필요가 없어지므로, 이는 시스템 A와 B 사이의 결합도를 낮춰 시스템 A의 내부 변경이 시스템 B에 미치는 영향을 최소화한다.
  • 유지보수성 향상
    • 인터페이스 x를 통해 시스템 A의 변경사항을 캡슐화하여 시스템 A의 내부 구조나 동작이 변경되더라도 퍼사드 인터페이스 x만 적절히 수정하면 시스템 B의 코드는 변경할 필요가 없어진다.
  • 사용 편의성 증대
    • 퍼사드 인터페이스 x는 시스템 B에 필요한 기능만을 제공하므로 시스템 B 개발자는 불필요한 복잡성에 노출되지 않고 필요한 기능만 쉽게 사용할 수 있다.
  • 모듈화 및 재사용성 향상
    • 퍼사드 패턴을 통해 시스템 A의 기능을 모듈화할 수 있다. 이는 다른 시스템에서도 동일한 퍼사드 인터페이스를 재사용할 수 있게 해, 코드의 재사용성을 높일 수 있다.

퍼사드 인터페이스가 많지 않다면 퍼사드 인터페이스가 아닌 인터페이스와 함께 사용할 수 있으며, 특별히 태그를 달 필요 없이 공통 인터페이스로서 사용하면 된다. 반면에 퍼사드 인터페이스가 많다면 기존 인터페이스 위에 퍼사드 계층을 다시 추상화하고, 원본 인터페이스 레이어와 구별하기 위해 클래스와 패키지의 이름을 특별하게 재배치할 수 있다. 만약 퍼사드 인터페이스가 너무 많은 데다가 여러 시스템에 걸쳐 있다면, 퍼사드 인터페이스를 새로운 시스템으로 분리하여 구축할 수도 있다.

퍼사드 패턴의 단점

  • 유연성 감소
    • 퍼사드는 시스템의 세부 기능에 대한 직접적인 접근을 제한하므로, 이로 인해 클라이언트가 시스템의 특정 부분을 세밀하게 제어하거나 커스터마이즈해야 할 때 제약이 생길 수 있다.
  • 과도한 단순화 위험
    • 퍼사드가 너무 많은 기능을 단순화하면 필요한 세부 기능을 사용하지 못하게 될 수 있다. 이는 특히 복잡한 작업을 수행해야 하는 클라이언트에게 문제가 될 수 있다.
  • 퍼사드 복잡화
    • 시스템이 복잡해질수록 퍼사드 자체가 복잡해질 수 있으며, 퍼사드가 너무 많은 책임을 지게 되면 결국 퍼사드 자체가 관리하기 어려운 복잡한 객체가 될 수 있다.
  • 성능 오버헤드
    • 퍼사드를 통해 시스템에 접근하는 것이 직접 접근하는 것보다 약간의 성능 저하를 초래할 수 있다. 대부분의 경우 이는 무시할 만한 수준이지만, 고성능이 요구되는 시스템에서는 고려해야 할 사항이다.
  • 의존성 증가
    • 클라이언트 코드가 퍼사드에 과도하게 의존하게 될 수 있다. 이는 퍼사드의 변경이 클라이언트 코드에 광범위한 영향을 미칠 수 있음을 의미한다.


이러한 단점들을 고려하여, 퍼사드 패턴을 적용할 때는 시스템의 요구사항과 특성을 잘 파악하고 적절한 균형을 유지하는 것이 중요하다.

실제 프로젝트에 적용해보기

현재 진행중인 프로젝트에 퍼사드 패턴을 적용한 부분은 ConvertController이다. 해당 프로젝트는 동영상을 공간 이미지로 변환해주는 서비스로, ConvertController를 통해 동영상을 저장 및 관리하고, 공간 이미지를 변환 및 반환한다.

해당 패턴을 적용하기 전의 코드는 아래와 같다.

package com.example.vpapi.controller;

import com.example.vpapi.dto.ImageDTO;
import com.example.vpapi.dto.VideoDTO;
import com.example.vpapi.service.ConvertService;
import com.example.vpapi.service.ImageService;
import com.example.vpapi.service.VideoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/convert")
public class ConvertController {

    private final ImageService imageService;
    private final VideoService videoService;
    private final ConvertService convertService;

    @GetMapping("/{vno}")
    public Map<String, Long> convertFile(@PathVariable Long vno) {
        VideoDTO videoDTO = videoService.get(vno);
        ImageDTO imageDTO = convertService.uploadAndConvertFile(videoDTO);
        Long ino = imageService.register(imageDTO);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return Map.of("ino", ino);
    }

}

위의 코드를 보면 해당 컨트롤러는 VideoService, ImageService, 그리고 ConvertService 이렇게 총 3개의 서비스가 필요한 것을 알 수 있다. 위의 예시에서 B 시스템이 A 시스템의 a, b, d 세 가지 인터페이스를 개별적으로 호출하는 방식처럼 매우 복잡하며, 인터페이스의 재사용성만 고려했기 때문에 너무 많이 세분화된 인터페이스를 호출하도록 작업해야 하므로 매우 번거롭다.

그래서 ConvertService에서 VideoService와 ImageService를 사용하도록 코드를 수정하는 과정에서 이러한 과정이 퍼사드 패턴을 적용하는 것이라는 사실을 알게 되었고, 다음과 같이 코드를 수정하였다.

package com.example.vpapi.facade;

import com.example.vpapi.dto.ImageDTO;
import com.example.vpapi.dto.VideoDTO;
import org.springframework.transaction.annotation.Transactional;

import java.util.Map;

@Transactional
public interface ConvertFacade {

    ImageDTO uploadAndConvertFile(VideoDTO videoDTO);

    Map<String, Long> convertAndRegister(Long vno);

}

위의 ConvertFacade를 구현한 ConvertFacadeImpl의 코드는 내 깃허브에서 확인이 가능하다.

아무튼 이렇게 코드를 수정한 뒤에 수정된 ConvertController의 코드는 다음과 같다.

package com.example.vpapi.controller;

import com.example.vpapi.facade.ConvertFacade;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/convert")
public class ConvertController {

    private final ConvertFacade convertFacade;

    @GetMapping("/{vno}")
    public Map<String, Long> convertFile(@PathVariable Long vno) {
        return convertFacade.convertAndRegister(vno);
    }

}

확실히 이전보다 Facade 패턴을 적용하여 코드의 구조와 책임을 더욱 명확하게 만들어진 것을 확인할 수 있다. Convert 프로세스의 세부 사항이 ConvertFacade에 숨어있기 때문에 향후 내부 로직이 변경되더라도 컨트롤러에 영향을 주지 않을 것이다.


결론

사실 퍼사드 패턴은 프로젝트의 회원 정보와 관련해 ProfileImage를 추가하려고 고민할 때 발견한 디자인 패턴이다. Member와 ProfileImage는 일대일 관계로, 각각의 엔티티의 필드에 서로를 참조한다. 이렇게 되면 MemberService와 ProfileImageService의 dtoToEntity 메서드와 같은 부분에서 서로를 동시에 참조하기 때문에 순환 참조 문제가 발생할 수 있는데, 이를 해결하기 위해 Facade 패턴, Mediator 패턴 등과 같은 여러 디자인 패턴을 찾아봤었던 것 같다.

하지만 이러한 디자인 패턴들은 순환 참조 문제를 해결해주지는 못했고, 결국에는 JPA를 통해 생성된 테이블들을 분석하면서 Member 테이블에 ProfileImage의 ID인 pino 필드가 자동으로 생성되지 않는다는 것을 확인하고 MemberService에서 ProfileImageService를 호출하지 않는 방식으로 해결했다.

오픽 공부를 하다가 PPT 수정을 하다가 갑자기 코딩하고.. 글의 순서가 뒤죽박죽이고 어색할 수 있지만 영어 공부가 너무 하기 싫은걸.. 아무튼 이렇게 개발중에 발견된 문제들을 해결하기 위해 디자인 패턴을 찾아보는 과정을 통해서 ConvertFacade를 구현하며 리팩터링을 하는 등 다른 부분에서 적용할 수 있는 설계적인 부분에서 이득을 보게 되어서 내심 뿌듯했다. 그래서 다음 글은 아마 프로젝트에 적용은 하지 않았지만, 또 다른 디자인 패턴에 대한 내용이 아닐까 싶다.

728x90
Comments