CS Study 13주차: [디자인패턴] 어댑터 패턴(Adapter Pattern)

hjern·2024년 5월 27일
0

CS Study

목록 보기
10/10

디자인 패턴의 목적과 특징

  • 디자인 패턴 : 소프트웨어 개발에 빈번하게 발생하는 문제를 해결하기 위한 일종의 설계 방식
  • SW 재사용성, 호환성, 유지 보수성을 보장
  • 프로젝트에 항상 적용해야 하는 것은 아니지만 위의 목적들로 발생하는 문제를 예방하기 위해 특정 패턴을 만들어 둔 것

SOLID 객체지향 설계 원칙

1. Single Responsibility Principle
하나의 클래스는 하나의 역할(=하나의 책임, axis of change)을 수행하는데 집중 해야 함

ex. Guitar 에 관한 클래스를 구성할 때 고유 정보인 serialNumber 와 변화 요소인 price, Maker, Type, model, backWood, topWood, stringNum 등을 GuitarSpec 클래스로 분리하여 담는다.

2. Open-Close Principle
확장(상속)에는 열려 있지만, 수정엔 닫혀 있어야 함. 변경(확장)될 것과 변하지 않을 것을 엄격히 구분하며 이 두 모듈이 만나는 지점에 인터페이스를 정의함. 구현에 의존하기 보다 정의한 인터페이스에 의존하도록 코드를 작성해야 함

ex. Guitar 와 유사한 특징을 가지는 Violin 클래스를 1번 원칙을 적용해 만들 수 있을 것이다. 하지만, 유사한 특징을 가지는 클래스가 늘어날 때마다 Ooo 와 OooSpec 클래스를 계속해 만드는 건 번거로운 일이다. 이를 해결하기 위해 Guitar 와 Violin 의 공통 특성을 추상화한다. Spec 클래스도 동일하다. 이를 통해 새로운 악기를 추가할 때마다 코드의 수정을 최소화할 수 있다.

3. Liskov Substitution Principle
자식이 부모의 자리에 항상 교체될 수 있어야 함(= 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다.). 즉, 서브 타입은 언제나 기반 타입과 호환될 수 있어야 함을 의미하는데, 달리 말하며 서브 타입은 기반 타입이 약속한 규약(public 인터페이스, 물론 메서드가 던지는 예외까지)을 지켜야 함. LSP원리도 역시 서브 클래스가 확장에 대한 인터페이스를 준수해야 함을 의미함.

4. Interface Segregation Principle
인터페이스가 잘 분리되어서, 클래스가 꼭 필요한 인터페이스만 구현하도록 해야 함. 즉 어떤 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스만 사용해야 한다는 뜻(반대로 하나의 일반적인 인터페이스보다는, 여러 개의 구체적인 인터페이스가 낫다). 인터페이스의 단일 책임을 강조하지만 어떤 클래스 혹은 인터페이스가 여러 책임 혹은 역할을 갖는 것을 인정함. SPR가 클래스 분리를 통해 변화에의 적응성을 획득하는 반면, ISP에서는 인터페이스 분리를 통해 같은 목표에 도달하려 함.

5. Dependency Inversion Property
상위 모듈은 하위 모듈에 의존해선 안 되며 둘다 추상화에 의존하지만, 추상화는 세부 사항에 의존해선 안 됨. 구조적인 디자인에서 발생하던 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의 역전. 실제 사용 관계는 바뀌지 않으며, 추상을 매개로 메시지를 주고 받음으로써 관계를 최대한 느슨하게 만드는 원칙. 세 가지 키워드 : 'IOC', '훅 메소드(슈퍼 클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 서브 클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메소드. 서브 클래스에서 추상 메소드를 구현하거나 훅 메소드를 오버라이드하는 방법을 이용해 기능의 부를 확장시킴)', '확장성'

패턴 분류

  1. 생성 패턴(Creational) : 객체의 생성 방식 결정
  • 객체 생성에 관련된 패턴
  • 객체의 생성과 조합을 캡슐화해 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공함
  • 예 : DBConnection을 관리하는 Instance를 하나만 만들 수 있도록 제한하여, 불필요한 연결을 막음.
  1. 구조 패턴(Structural) : 객체 간의 관계를 조직
  • 클래스나 객체를 조합해 더 큰 구조를 만드는 패턴
  • 예 : 서로 다른 인터페이스를 지닌 2개의 객체를 묶어 단일 인터페이스를 제공하거나 객체들을 서로 묶어 새로운 기능을 제공
  1. 행위 패턴(Behavioral) : 객체의 행위를 조직, 관리, 연합
  • 객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴
  • 한 객체가 혼자 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하는지, 또 그렇게 하면서 객체 사이의 결합도를 최소하시키는 것에 중점을 둠.
  • 예 : 위 클래스에서 구현해야 하는 함수 및 알고리즘들을 미리 선언하여, 상속시 이를 필수로 구현하도록 함.

어댑터 패턴

  • 호환되지 않는 인터페이스들을 연결하는 디자인 패턴
  • 기존의 클래스를 수정하지 않고도, 특정 인터페이스를 필요로 하는 코드에 사용할 수 있게 해줌
  • 또한, 클래스의 인터페이스를 다른 인터페이스로 변환시킬 수 있는데, 이를 통해 서로 다른 인터페이스를 가진 클래스들이 상호 작용할 수 있도록 하여 코드의 재사용성을 증대시킴

0. 이용 배경

  • 아이폰에 이어폰을 꽂으려고 하는데 이어폰과 아이폰이 호환되지 않는다.
  • 어댑터를 구매해 연결시켜 호환될 수 있도록 한다
  • 즉, 업체에서 제공한 클래스가 기존 시스템과 맞지 않다면 기존 시스템을 수정하는 게 아니라 어댑터를 활용해 유연하게 해결하는데 사용하자.

1. 주요 구성 요소

  • 타겟(Target), 어댑티(Adaptee), 어댑터(Adapter), 클라이언트(Client)
  • 타겟 : 클라이언트가 직접적으로 호출하는 인터페이스
  • 어댑티 : 아직 호환되지 않은 기존 클래스(또는 인터페이스)
  • 클라이언트 : 특정 작업을 요청하는 클래스
  • 어댑터 : 타겟 인터페이스를 구현하여 클라이언트 요청을 어댑티로 전달하는 클래스

2. 구현 방법

1) 패턴을 적용하고자 하는 인터페이스 식별하기
2) 어댑터 클래스 작성
3) 클라이언트 코드에서 호출

4) 두 인터페이스와 클래스를 호환시키기 위해서 아래 그림처럼 어댑터 클래스를 작성, 어댑터 패턴을 적용할 수 있음. 어댑터 클래스는 타겟 인터페이스를 구현했으며, 어댑티 클래스를 멤버 변수로 갖고 있음

5) 클라이언트에서 Target 인터페이스의 doSomething() 메서드를 호출하여 Adaptee 클래스의 performAction() 메서드를 호출할 수 있게 됨. 이를 통해 클라이언트는 직접 Adaptee 클래스를 호출하지 않고도 자신이 원하는 인터페이스를 통해 Adaptee 클래스의 기능을 사용할 수 있음.

3. 적용방법

  • 오리 객체가 부족하면 칠면조 객체로 대체하여 사용해야 한다

  • 그러나 두 개의 인터페이스는 '다르'므로 바로 칠면조 객체로 대체될 수 없다

  • 따라서 칠면조 어댑터를 생성해 활용할 것

  • Duck.java

package AdapterPattern;

public interface Duck {
	public void quack();
	public void fly();
}
  • Turkey.java
package AdapterPattern;

public interface Turkey {
	public void gobble();
	public void fly();
}
  • WildTurkey.java
package AdapterPattern;

public class WildTurkey implements Turkey {

	@Override
	public void gobble() {
		System.out.println("Gobble gobble");
	}

	@Override
	public void fly() {
		System.out.println("I'm flying a short distance");
	}
}
  • TurkeyAdapter.java
package AdapterPattern;

public class TurkeyAdapter implements Duck {

	Turkey turkey;

	public TurkeyAdapter(Turkey turkey) {
		this.turkey = turkey;
	}

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

	@Override
	public void fly() {
		turkey.fly();
	}

}
  • DuckTest.java
package AdapterPattern;

public class DuckTest {

	public static void main(String[] args) {

		MallardDuck duck = new MallardDuck();
		WildTurkey turkey = new WildTurkey();
		Duck turkeyAdapter = new TurkeyAdapter(turkey);

		System.out.println("The turkey says...");
		turkey.gobble();
		turkey.fly();

		System.out.println("The Duck says...");
		testDuck(duck);

		System.out.println("The TurkeyAdapter says...");
		testDuck(turkeyAdapter);

	}

	public static void testDuck(Duck duck) {

		duck.quack();
		duck.fly();

	}
}

4. 어댑터 패턴의 장단점?

1) 장점
기존 클래스를 수정하지 않고도 클라이언트에서 새로운 인터페이스를 사용할 수 있음. 이는 기존의 코드를 재사용하고 코드 중복을 줄여주는데 도움이 됨. 또한 클래스 간의 결합도를 줄여주어 소스 코드 변경이 필요할 때 쉽게 수정할 수 있다는 장점이 있음

2) 단점
어댑터 클래스를 추가로 작성해야 하기 때문에 소스 코드가 늘어나게 됨. 이는 코드의 복잡성을 증가시키고, 유지 보수를 어렵게 만들 수 있으며 어댑터가 중간에 데이터를 변화하는 과정에서 추가적인 처리 시간과 오버 헤드가 발생할 수 있음

3) 필요한 경우
무분별한 사용을 권장하진 않지만, 호환되지 않는 인터페이스를 가진 클래스들이 함께 작동해야 하거나 이미 존재하는 클래스의 인터페이스가 요구 사항과 맞지 않거나 또는 기존 클래스에 원하는 인터페이스가 없는 경우 어댑터 패턴을 고려해볼 수 있음

예를 들어 서드파티 라이브러리나 API를 사용하는데 그 인터페이스가 애플리케이션 코드와 잘 맞지 않는 경우, 어댑터 패턴을 사용해 서드파티 라이브러리 및 API 내부 구현에 영향을 받지 않으면서 프로젝트에 필요한 인터페이스를 생성할 수 있음.

참고자료
객체지향 개발 5대 원리: SOLID
[Design Pattern] 디자인 패턴 종류
자바 어댑터 패턴은 어떻게 쓰일까?

profile
주니어의 굴레는 언제 벗어날 것인가

0개의 댓글