본문 바로가기
IT/Java

[Java] Java Adapter Pattern

by kyu-nahc 2024. 9. 15.
반응형

Adapter Pattern

어댑터 패턴(Adaptor Pattern)이란 이름 그대로 클래스를 어댑터로써 사용되는 구조 패턴이다.

즉, 서로 호환이 되지 않은 단자를 어댑터로 호환시켜 작동시키게끔 하는 것이 어댑터의 역할이다.

이를 객체 지향 프로그래밍에 접목해 보면, 호환성이 없는 인터페이스 때문에 함께

동작할 수 없는 클래스들을 함께 작동해 주도록 변환 역할을 해주는 행동 패턴이라고 보면 된다.  

예를 들어 기존에 있는 시스템에 새로운 써드파티 라이브러리를 추가하고 싶거나,

Legacy 인터페이스를 새로운 인터페이스로 교체하는 경우에 어댑터 패턴을 사용하면

코드의 재사용성을 높일 수 있다. 즉, 어댑터란 이미 구축되어 있는 것을 새로운 어떤 것에 사용할 때

양 쪽 간의 호환성을 유지해 주기 위해 사용하는 것으로서, 기존 시스템에서 새로운 업체에서 

제공하는 기능을 사용하려고 할 때 서로 간의 인터페이스를 어댑터로 일치시켜 줌으로써 

호환성 및 신규 기능 확장을 할 수 있다고 보면 된다.

위의 그림을 통해 더욱 이해를 높일 수 있다.

이미 존재하는 프로그램에 대해 새로운 라이브러리 코드나 외부에서 생성한 코드를 병합시키고자 할 때,

호환성이 없는 인터페이스 때문에 작동할 수 없는 경우가 있다. 

 

예를 들어 MediaPlayer라는 인터페이스를 구현한 기존 음악 플레이어 애플리케이션을 가지고 있다.

이 애플리케이션은 play라는 메서드를 호출하여 MP3 파일을 재생할 수 있다.

그러나 새로운 요구사항으로 인해 MP4 파일과 VLC 파일도 재생할 수 있어야 하는데,

MP4 및 VLC 재생 클래스는 play라는 메서드를 가지고 있지 않고, 각기 다른 인터페이스를 사용한다.

이런 경우 인터페이스 호환 문제로 바로 작동이 불가하고, 새로운 요구사항 클래스를 변경해야 한다.

하지만 어댑터 패턴을 이용하면 기존 프로그램과 새로운 요구사항 클래스를 변경할 필요 없이,

추가 기능을 확장하여 사용할 수 있는 것이다.

따라서 위의 그림처럼 Adapter 클래스를 제공함으로써

기존 코드의 변경 없이 확장이 가능하고, OCP원리에 부합하게 된다.

 

 

Adapter Pattern 구조

 

 

  • Adaptee
    어댑터 대상 객체. 기존 시스템 / 외부 시스템 / 써드파티 라이브러리
  • Target(Client Interface)
    Adapter가 구현하는 인터페이스.
  • Adapter
    Client와 Adaptee 중간에서 호환성이 없는 둘을 연결시켜 주는 역할을 담당.
    Object Adaptor 방식에선 합성을 이용해 구성한다.
    Adaptee를 따로 클래스 멤버로 설정하고 위임을 통해 동작을 매치시킨다.
  • Client
    기존 시스템을 어댑터를 통해 이용하려는 쪽.
    Client Interface를 통하여 Service를 이용할 수 있게 된다.

이를 코드를 통해 살펴보면 다음과 같다.

// Client Interface
// 클라이언트가 접근해서 사용할 추상화된 어댑터 모듈
public interface Target {
    void request();
}
// Adapter
// Adaptee 서비스를 클라이언트에서 사용하게 할 수 있도록 호환 처리 해주는 클래스
public class Adapter implements Target{

    private Adaptee adaptee;

    // 생성자를 통해 Adaptee 클래스를 초기화
    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    // Adapter의 메소드가 호출되면 Adaptee의 서비스 로직을 사용
    @Override
    public void request() {
        adaptee.specificRequest();
    }
}
// Adaptee
// 클라이언트에서 사용하고 싶은 서비스 (하지만 호환이 안되서 바로 사용 불가능)
public class Adaptee {

    public void specificRequest(){
        System.out.println("This is Adaptee Class");
        System.out.println("Do Something Request");
    }
}
public class Client {

    public static void main(String[] args) {
        // 어댑터 생성 (Adaptee를 인자로 받아 호환 작업 처리)
        Target adapter = new Adapter(new Adaptee());
        // Client Interface 규격에 따라 메소드 호출 시
        // Adapter에 의해 Adaptee 메소드가 호출된다.
        adapter.request();
    }
}

 

Adapter Pattern 특징

패턴 적용 시기

  • 레거시 코드를 사용하고 싶지만 새로운 인터페이스가 레거시 코드와 호환되지 않을 때
  • 이미 만든 것을 재사용하고자 하나 이 재사용 가능한 라이브러리를 수정할 수 없을 때
  • 이미 만들어진 클래스를 새로운 인터페이스(API)에 맞게 개조할 때
  • 소프트웨어의 구 버전과 신 버전을 공존시키고 싶을 때

기존의 클래스를 새로운 인터페이스에 맞게 개조하고 싶을 때, 

우리는 기존 클래스의 소스를 바꾸어 수정을 해왔었다. 

하지만 이런 식으로 운용하면 이미 동작 테스트가 끝난 기존의 클래스를 다시 한번 테스트해야 한다. 

만일 클래스 소스가 몇천 줄이라면 운용하기 꽤나 힘들 것이다.

이러한 관점에서 어댑터(Adapter) 패턴은 기존의 클래스를 수정하지 않고 

새로운 인터페이스에 맞게 호환작업을 중계하는 것이다.

 

패턴 장점

  • 프로그램의 기본 비즈니스 로직에서 인터페이스 또는 데이터 변환 코드를 분리할 수 있기 때문에
    단일 책임 원칙(SRP)을 만족한다.
  • 기존 클래스 코드를 건들지 않고 클라이언트 인터페이스를 통해 어댑터와 작동하기 때문에
    개방 폐쇄 원칙(OCP)을 만족한다.
  • 만일 추가로 필요한 메서드가 있다면 어댑터에 빠르게 만들 수 있다.
    만약 버그가 발생해도 기존의 클래스에는 버그가 없으므로
    Adapter 역할의 클래스를 중점적으로 조사하면 되고, 프로그램 검사도 쉬워진다.

패턴 단점

  • 새로운 인터페이스와 어댑터 클래스 세트를 도입해야 하기 때문에 코드의 복잡성이 증가한다. 
  • 때로는 직접 서비스(Adaptee) 클래스를 변경하는 것이 간단할 수 있다.

 

Adapter Pattern 적용 예시

Adapter 패턴의 적용 예시로는 데이터베이스를 연동하는 Repository를 예시로 들 수 있다.

MySQL과 MongoDB 두 가지 데이터베이스를 사용하는 애플리케이션을 관리하고 있다고 가정한다.

두 데이터베이스는 각기 다른 API를 가지고 있으며,

이를 동일한 방식으로 처리하기 위해 어댑터 패턴을 사용할 수 있다.

// 공통 Repository 인터페이스
interface Repository {
    void save(String data);
    String findById(String id);
}

// MySQL을 위한 구현 (JDBC 사용 예시)
class MySQLRepository {
    public void insertIntoTable(String data) {
        System.out.println("Saving data in MySQL: " + data);
    }

    public String selectFromTableById(String id) {
        System.out.println("Fetching data from MySQL with ID: " + id);
        return "MySQL Data";
    }
}

// MongoDB를 위한 구현 (MongoDB API 사용 예시)
class MongoDBRepository {
    public void insertDocument(String data) {
        System.out.println("Saving data in MongoDB: " + data);
    }

    public String findDocumentById(String id) {
        System.out.println("Fetching data from MongoDB with ID: " + id);
        return "MongoDB Data";
    }
}

 

Repository 인터페이스는 MySQL 및 MongoDB 두 가지 데이터베이스에 대한

공통 인터페이스로 정의된다. 이 인터페이스는 데이터베이스에 데이터를 저장(save)하고,

ID로 데이터를 검색(findById)하는 메서드를 제공한다.

MySQLRepository와 MongoDBRepository는 각각 MySQL과 MongoDB에서

데이터를 저장하고 조회하는 로직을 구현한 클래스이다.

MySQLRepository와 MongoDBRepository는 서로 다른 API 규격을 가지고 있으며,

이를 통합하여 관리해 줄 필요가 있다. 이를 어댑터 패턴을 이용하여 구현할 수 있다.

// 어댑터 클래스
class RepositoryAdapter implements Repository {
    private MySQLRepository mySQLRepository;
    private MongoDBRepository mongoDBRepository;
    private String dbType;

    public RepositoryAdapter(String dbType) {
        this.dbType = dbType;
        if (dbType.equalsIgnoreCase("mysql")) {
            mySQLRepository = new MySQLRepository();
        } else if (dbType.equalsIgnoreCase("mongodb")) {
            mongoDBRepository = new MongoDBRepository();
        }
    }

    @Override
    public void save(String data) {
        if (dbType.equalsIgnoreCase("mysql")) {
            mySQLRepository.insertIntoTable(data);
        } else if (dbType.equalsIgnoreCase("mongodb")) {
            mongoDBRepository.insertDocument(data);
        }
    }

    @Override
    public String findById(String id) {
        if (dbType.equalsIgnoreCase("mysql")) {
            return mySQLRepository.selectFromTableById(id);
        } else if (dbType.equalsIgnoreCase("mongodb")) {
            return mongoDBRepository.findDocumentById(id);
        }
        return null;
    }
}

// 클라이언트 코드
public class AdapterPatternDemo {
    public static void main(String[] args) {
        // MySQL에 저장
        Repository mysqlRepository = new RepositoryAdapter("mysql");
        mysqlRepository.save("MySQL Data Example");
        System.out.println(mysqlRepository.findById("1"));

        // MongoDB에 저장
        Repository mongoRepository = new RepositoryAdapter("mongodb");
        mongoRepository.save("MongoDB Data Example");
        System.out.println(mongoRepository.findById("2"));
    }
}

 

RepositoryAdapter는 이 두 클래스를 통합하여 Repository 인터페이스에 맞는 형식으로 변환한다.

dbType에 따라 MySQL 또는 MongoDB 리포지토리를 사용할 수 있다.

클라이언트 코드에서는 MySQL과 MongoDB에 관계없이

동일한 Repository 인터페이스를 통해 데이터를 저장하고 조회할 수 있다.

이 패턴을 사용함으로써 클라이언트는 데이터베이스의 종류에 상관없이

같은 방식으로 데이터를 처리할 수 있는 것이다.

새로운 데이터베이스가 추가되거나 기존 데이터베이스가 변경되더라도 어댑터만 수정하면 된다.

즉, 어댑터 패턴을 통해 확장성이 뛰어나고 코드 재사용성이 높아지는 효과를 볼 수 있다.

 

 

 

 

 

참고자료

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%96%B4%EB%8C%91%ED%84%B0Adaptor-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

반응형

'IT > Java' 카테고리의 다른 글

[Java] Java Template Method Pattern  (1) 2024.09.16
[Java] Java Iterator Pattern  (0) 2024.09.15
[Java] Java Singleton Pattern  (2) 2024.09.14
[Java] Java Command Pattern  (0) 2024.09.13
[Java] Java Builder Pattern  (2) 2024.09.12

loading