7. 어댑터 패턴 (Adapter Pattern) & 퍼사드 패턴 (Facade Pattern)

Kim Dong Kyun·2023년 8월 6일
1
post-thumbnail

어댑터 패턴 (Adapter Pattern)

지난 시간 이야기

  • 우리가 개발해준 DuckSimulator 가 엄청난 성공을 거둔 덕분에, 회사는 공격적으로 사업을 확장하면서 M&A를 진행했습니다.

  • 그 결과, 유서 깊은 프로그램인 TurkeySimulator 를 제공하는 회사와 합병해서 해당 기능을 DuckSimulator 에 병합시키려고 합니다.


1. Duck

public class MallardDuck implements Duck{
    @Override
    public void quack() {
        System.out.println("꽥");
    }

    @Override
    public void fly() {
        System.out.println("날고 있어요!");
    }
}
  • 우리가 이미 작성했었던 덕 클래스입니다.

  • 꽥! 하는 소리를 내는 것과 날 수 있는 기능을 각각 클래스마다 구현하거나, 전략 패턴을 통해 인터페이스를 활용 할 수 있었죠.

2. Turkey

public class WildTurkey implements Turkey{
    @Override
    public void gobble() {
        System.out.println("골골");
    }

    @Override
    public void fly() {
        System.out.println("짧게밖에 못납니다요");
    }
}
  • 그리고 이번에 합병된 터키시뮬레이터의 터키 구상클래스입니다.

  • gobble() 이라는 다른 매서드가 보이네요!


어떻게 합쳐요?

public class TurkeyAdapter implements Duck {
    // Duck 으로 변형시켜주는 어댑터입니다. 따라서 "원하는" 타입을 상속받아줍니다.
    // 이 어댑터를 사용하면 Turkey 는 Duck 의 타입을 상속하게 되는것이죠.
    Turkey turkey;

    public TurkeyAdapter(Turkey turkey) { // 생성자에서 기존 Turkey 객체의 레퍼런스를 가져옵니다.
        this.turkey = turkey;
    }

    @Override
    public void quack() {
        turkey.gobble();
    }

    /**
     * 공통 인터페이스 부분입니다. 칠면조와 오리가 공유하지만 다른 인터페이스(행동)입니다.
     * 터키의 fly 는 짧으므로 좀 더 많이 호출해서 오리와 비슷한 거리를 날 수 있도록 조정했습니다.
     */
    @Override
    public void fly() {
        for (int i = 0; i < 5; i++) {
            turkey.fly();
        }
    }
}
  • 어댑터 역할을 하는 클래스를 하나 작성해봅시다

  • 상속받고자 하는 타입을 구현한 클래스입니다.

  • 그리고, 변환이 필요한 ( 이 경우에는 터키 -> 오리 가 되어야 하므로 터키 ) 객체를 필드 변수로 가집니다.

  • 생성자 매서드에서 패러미터로 변환이 필요한 객체(터키)를 가져온 후, 우리가 구현하고자 하는 변환할 클래스(오리) 의 매서드들로 재정의해주는 과정을 거칩니다.


테스트

public class AdapterTest {
    public static void main(String[] args) {
        Duck duck = new MallardDuck();

        Turkey turkey = new WildTurkey();
        Duck turkeyAdapter = new TurkeyAdapter(turkey);
        // 터키 객체를 어댑터로 감싸서 Duck 객체처럼 보이게 만듭니다.

        System.out.println("=====칠면조입니다=====");
        turkey.gobble();
        turkey.fly();
        System.out.println("==================");

        System.out.println("=====오리입니다=====");
        duck.quack();
        duck.fly();
        System.out.println("==================");

        System.out.println("=====칠면조 어댑터입니다=====");
        turkeyAdapter.quack();
        turkeyAdapter.fly();
        System.out.println("==================");
    }
}

  • 나름 오리같이 변모했습니다.

아니, 이걸 대체 어따써요?

조금 더 활용할법한 예를 들어볼까요?

Enumeration -> Iterator

자바의 초기 컬렉션들은 (Vector, Stack, HashTable) 은 Enumeration 을 리턴하는 elements() 매서드가 구현되어 있습니다.

  • asIterator 라는 매서드가 정의되어 있긴 합니다만, 우리는 모른척 해봅시다 (패턴 실습을 위해)

그리고, 최근에는 Enumeraion 과 마찬가지로 컬렉션에 있는 일련의 항목에 접근하고, 그 항목을 제거할 수 있게 해주는 Iterator 인터페이스를 사용합니다.

두 녀석들은 remove() 매서드 외에는 별 차이가 없네요. 이거 어댑터로 변환하기 딱 좋아보입니다.


Turkey -> Duck 과 같은 방식으로 시도합니다.

public class EnumerationIterator implements Iterator<Object> {
    Enumeration<?> enumeration;

    public EnumerationIterator(Enumeration<?> enumeration) {
        this.enumeration = enumeration;
    }

    @Override
    public boolean hasNext() {
        return enumeration.hasMoreElements();
    }

    @Override
    public Object next() {
        return enumeration.nextElement();
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
        // Enumerations 는 remove 를 지원하지 않으므로 예외를 던짐
    }
}

결론

어댑터 패턴은?

  • 호환되지 않는 인터페이스들을 연결해서 사용할 수 있게 한다!

  • Duck 과 Turkey 는 서로 다른 인터페이스를 가지지만, 어댑터를 통해서 이 두 인터페이스를 적절히 호환되게 변경 가능하다.


퍼사드 패턴

파사드(프랑스어: Façade)는 건물의 출입구로 이용되는 정면 외벽 부분을 가리키는 말이다. 한글화하여 순화하려면 '정면'(正面)이 무난할 것으로 여겨진다. 건축에서 파사드의 궁극적 목적은 '소통'이다. 건물의 입면이 다양해지면서 파사드는 건물 외피 전체를 의미하기도 한다.

출처 : 위키백과

홈시어터로 영화 한편 볼까요?

public class TheaterTest {
    public static void main(String[] args) {
        Popper popper = new Popper(); // 팝콘기계
        Projector projector = new Projector();
        Movie movie = new Movie("아저씨");
        Screen screen = new Screen();
        Lights lights = new Lights();

        System.out.println("========영화 한편 볼까?========");

        popper.on();
        popper.pop();
        lights.dim(10);
        projector.on();
        screen.down();
        screen.movieOn(movie);
    }
}

  • 영화 한편 보기 너무 힘드네요

  • 이렇게 매번 하나하나 할 필요 없이, 이렇게 해봅시다


전자동 홈씨어터!

public class HomeTheaterFacade {
    Lights lights;
    Popper popper;
    Projector projector;
    Screen screen;

    public HomeTheaterFacade(Lights lights, Popper popper, Projector projector, Screen screen) {
        this.lights = lights;
        this.popper = popper;
        this.projector = projector;
        this.screen = screen;
    }

    public void watchMovie(Movie movie){
        System.out.println("홈씨어터 영화보기 시퀀스 시작!");
        popper.on();
        popper.pop();
        lights.dim(10);
        projector.on();
        screen.down();
        screen.movieOn(movie);
    }

    public void endMovie(){
        System.out.println("홈시어터 끄는 중");
        popper.off();
        // ...
        // 전부 .off() 매서드로 꺼버리기
    }
}

  • 클라이언트 입장에서는 훨씬 더 편하게 바뀌었습니다!

  • 그리고 추가로, 각각 객체들( 프로젝터, 팝콘기계 ... ) 들도 여전히 개별적으로 사용 가능합니다.


알겠는데, 이건 어따써요?

  • 아래는 부트캠프 최종프로젝트 (올해 2?월) 코드입니다.
@Service
public class UserChannelServiceImpl implements UserChannelService {

    private final UserChannelRepository userChannelRepository;
    private final RedisDao redisDao;
    private final ChannelService channelService;
    private final UserService userService;

...

    @Override
    @CacheEvict(cacheNames = CacheNames.CHANNELS, key = "#user.id")
    @Transactional
    public void inviteUser(User user, Long channelId, String email) {
        if (!isUserJoinedByChannel(user, channelId)) {
            throw new CustomException(USER_CHANNEL_FORBIDDEN);
        }
        Channel channel = channelService.getChannelById(channelId);
        User invitedUser = userService.getUserByEmail(email);
        if (isUserJoinedByChannel(invitedUser, channelId)) {
            throw new CustomException(USER_CHANNEL_DUPLICATED);
        }
        UserChannel userChannel = UserChannel.builder().user(invitedUser).channel(channel).build();
        userChannelRepository.save(userChannel);
        //invitedUser 쪽의 CHANNELS cache data 삭제
        redisDao.deleteValues("CACHE_CHANNELS::" + invitedUser.getId());
    }
  • 다대다 (@ManyToMany) 연관관계를 중간 테이블(FK를 들고있는)을 사용해서 풀어낸 부분입니다.

  • 이 매서드에서는 userService, ChannelService 를 둘 다 의존주입 받습니다.

  • 그리고 각각의 매서드를 적절히 조합해서 하나의 기능으로 리턴합니다.

  • 이를 통해서 컨트롤러 측에서는 인증, 인가를 처리하고, 서비스 쪽의 복잡한 부분은 신경쓰지 않은 채로 위의 매서드를 호출하면 됩니다.

  • API의 스펙의 변경 없이(URI를 바꾸거나, 여러번의 호출이 있어야 하거나) 처리할 수 있게 됐습니다.


결론

어댑터 패턴?
: 특정 클래스 인터페이스를 클라이언트에서 요구하는 다르 인터페이스로 변환합니다. 인터페이스가 호환되지 않아 같이 쓸 수 없었던 클래스를 사용할 수 있게 도와줍니다.

퍼사드 패턴?
: 서브시스템에 있는 일련이 인터페이스를 통합 인터페이스로 묶어줍니다. 또한 고수준 인터페이스도 정의하므로(ex : 홈싸어터) 서브시스템을 더 편하게 사용할 수 있습니다.

0개의 댓글