디자인 패턴이란 ?
디자인 패턴이란 개발 과정에서 자주 발생하던 문제에 대한 해결책을 재사용 가능한 형태로 정리해 놓은 검증된 설계 방법 또는 솔루션이다.
디자인 패턴의 장점으로는 일관된 구조를 사용하기 때문에 팀원 간 의사소통을 쉽게 만들어주고 유지보수성이 좋다.
또한 검증된 방법이기 때문에 신뢰하고 애플리케이션을 개발할 수 있고, 시스템의 확장성이 높아진다.
디자인 패턴은 크게 생성 패턴, 구조 패턴, 행위 패턴 3가지가 있다.
이번 게시글에서는 구조 패턴의 종류에 대해 간략히 알아본다.
구조 패턴
구조 패턴은 클래스나 객체를 조합해서 더 큰 구조로 만들거나 인터페이스를 제공하는 디자인 패턴이다.
주로 상속이나 구성(Composition)을 통해 클래스를 조직화해서 새로운 기능을 제공하거나, 인터페이스를 향상 시키는데 사용된다.
구조 패턴의 주요 패턴으로는 어댑터, 브릿지, 퍼사드, 컴포지트, 프록시, 데코레이터 패턴이 있다.
어댑터 (Adapter)
서로 다른 인터페이스를 갖는 두 클래스를 연결해서 동작할 수 있도록 지원해주는 디자인 패턴이다.
외부 라이브러리 또는 시스템을 현재 시스템에 통합해야 하는 경우 인터페이스를 맞춰주는 용도로 사용된다. (호환성 문제 해결)
- 장점 : 기존 코드를 변경하지 않고 인터페이스 구현체를 만들어 재사용할 수 있다
- 단점 : 새로운 클래스를 구현해야 해서 복잡도가 증가할 수 있다
어댑터 패턴 적용 예시
아래 코드에서는 LegacyInterface와 규격이 다른 TargetInterface가 있을 때 LegacyInterface와 TargetInterface의 구현체를 연결하기 위해 Adapter 클래스를 사용하고 있다.
Adapter클래스는 TargetInterface의 구현체로 newMethod를 호출하면 LegaryInterface의 legacyMethod를 호출하게 구현했다.
// Legacy 코드의 인터페이스
public interface LegacyInterface {
void legacyMethod();
}
// Legacy 코드의 구현
public class LegacyClass implements LegacyInterface {
@Override
public void legacyMethod() {
System.out.println("Legacy 메서드 호출");
}
}
// 새로운 코드가 기대하는 인터페이스
public interface TargetInterface {
void newMethod();
}
// 어댑터 클래스
public class Adapter implements TargetInterface {
private LegacyInterface legacy;
public Adapter(LegacyInterface legacy) {
this.legacy = legacy;
}
@Override
public void newMethod() {
// Legacy 코드의 메서드를 호출하여 어댑터 패턴을 구현
legacy.legacyMethod();
}
}
public class Client {
public static void main(String[] args) {
// Legacy 코드를 새로운 코드의 인터페이스에 맞게 사용
LegacyInterface legacy = new LegacyClass();
TargetInterface adapter = new Adapter(legacy);
// 새로운 코드의 인터페이스를 사용
adapter.newMethod();
}
}
브릿지 (Bridge)
하나의 큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 추상 부분와 구현 부분으로 나눈 후 각각 독립적으로 개발하는 패턴이다.
- 장점
- 추상부분과 구현부분을 분리해서 각자 독립적으로 변형이 가능하기 때문에 유연성과 확장성이 증가한다.
- 추상부분과 구현부분이 독립적으로 진행되기 때문에 복잡성이 감소한다.
- 단점
- 클래스 수가 증가하고 코드 복잡성이 증가 할 수 있다.
- 미리 추상화와 구현을 설계해야 하기 때문에 초기 설계가 중요하다
- 언제 사용할까 ?
- 추상화와 구현부가 각각 독립성이 필요한 경우
- 클래스 계층 구조와 구현의 독립성이 필요한 경우
브릿지 패턴 예시
여기서 Color 인터페이스가 구현 부분 이고(Implementor), Blue / Red 클래스는 Color의 구현체다(ConcreteImplementor)
그리고 Brush가 추상 부분 이고(Abstract), HBPencil / MonoLine이 Brush를 확장한 클래스이다 (RefinedAbstract)
// 구현부의 인터페이스 (Implementor)
public interface Color {
String fill();
}
// 구현부의 실제 구현체 (ConcreteImplementor)
public class Red implements Color {
@Override
public String fill() {
return "빨간색";
}
}
// 구현부의 실제 구현체 (ConcreteImplementor)
public class Blue implements Color {
@Override
public String fill() {
return "파란색";
}
}
// 추상 부분의 최상위 부모 클래스 (Abstraction)
public abstract class Brush {
protected Color color;
protected Brush(Color color) {
this.color = color;
}
public abstract String draw();
}
// 추상 부분에서 새로운 기능을 확장한 자식 클래스 (RefinedAbstract)
public class HBPencil extends Brush {
public static final String type = "[HB 연필]";
public HBPencil(Color color) {
super(color);
}
@Override
public String draw() {
return type + " " + color.fill();
}
}
// 추상 부분에서 새로운 기능을 확장한 자식 클래스 (RefinedAbstract)
public class MonoLine extends Brush {
public static final String type = "[모노라인]";
public MonoLine(Color color) {
super(color);
}
@Override
public String draw() {
return type + " " + color.fill();
}
}
// 클라이언트 코드
class BrushTest {
public static void main(String[] args) {
Brush redBrush = new HBPencil(new Red());
System.out.println("[HB 연필] 빨간색 : " + redBrush.draw());
Brush blueBrush = new MonoLine(new Blue());
System.out.println("[모노라인] 파란색: " + redBrush.draw());
}
}
퍼사드 (Facade)
퍼사드는 "건물의 정면"이라는 의미로, 복잡한 서브 시스템 / 라이브러리 / 클래스들을 사용하기 편하게 인터페이스를 제공하는 패턴이다.
예를 들어, 사용자가 세탁기의 청바지 전용 / 이불 전용 등 여러 옵션 중 한 가지를 선택해서 간편하게 세탁할 수 있는데, 해당 옵션들은 내부적으로 복잡한 과정을 진행한다. 그러나 사용자는 이러한 복잡한 내부 과정은 몰라도 된다.
퍼사드 패턴 예시
아래 예시에서 클라이언트는 Washer 퍼사드 클래스의 start() 메서드 하나만으로 복잡한 세탁 과정을 처리할 수있다.
// Sub System Wash Class
public class Wash {
public Wash() {}
public void startWash() {
System.out.println("Start Washing");
}
}
// Sub System Rinse Class
public class Rinse {
public Rinse() {}
public void startRinse() {
System.out.println("Start Rinsing");
}
}
// Sub System SpanDry Class
public class SpanDry {
public SpanDry() {}
public void startDry() {
System.out.println("Start Drying");
}
}
// Facade class
public class Washer {
private final Wash wash;
private final Rinse rinse;
private final SpanDry spanDry;
public Washer() {
wash = new Wash();
rinse = new Rinse();
spanDry = new SpanDry();
}
public void start() {
wash.startWash();
rinse.startRinse();
spanDry.startDry();
}
}
컴포지트 (Composite)
객체들을 트리 구조로 구성해서 개별 객체와 복합 객체를 동일한 인터페이스에서 다룰 수 있게 하는 패턴이다.
컴포지트 패턴은 클라이언트가 개별 객체와 복합 객체를 따로 관리하지 않아도 일관되게 다룰 수 있게 해준다.
- 주요 구성요소
- Component : 복합 객체와 개별 객체에 대한 공통 인터페이스
- Leaf (단일 객체): 복합 객체의 단말 노드로, 자식을 가지지 않는다.
- Composite (복합 객체) : 하나 이상의 자식을 가지는 복합 객체
컴포지트 패턴 예시
Graphic 인터페이스를 구현하는 단일 객체인 Circle을 복합객체 Group에 넣어서 클라이언트가 일관되게 다룰 수 있게 한다.
단일객체와 복합객체 모두 동일한 인터페이스를 사용한다 (Graphic의 draw())
// Component
interface Graphic {
void draw();
}
// Leaf - 단일 객체
class Circle implements Graphic {
@Override
public void draw() {
System.out.println("그림: 원");
}
}
// Composite - 복합 객체
import java.util.ArrayList;
import java.util.List;
class Group implements Graphic {
private List<Graphic> graphics = new ArrayList<>();
public void add(Graphic graphic) {
graphics.add(graphic);
}
@Override
public void draw() {
System.out.println("그림: 그룹");
for (Graphic graphic : graphics) {
graphic.draw();
}
}
}
// client
public class Client {
public static void main(String[] args) {
// 개별 객체 생성
Graphic circle = new Circle();
// 복합 객체 생성
Group group = new Group();
group.add(new Circle());
group.add(new Circle());
// 클라이언트는 동일한 인터페이스로 다룰 수 있음
circle.draw(); // 출력: 그림: 원
group.draw(); // 출력: 그림: 그룹\n그림: 원\n그림: 원
}
}
프록시 (Proxy)
프록시 패턴은 원본 객체 대신 대리역할을 하는 프록시 객체를 제공해서, 원본 객체 대신 여러 작업을 처리하도록 위임하는 패턴이다.
프록시 객체는 원본 객체와 동일한 인터페이스를 가지고, 클라이언트는 프록시 객체를 통해서 원본 객체에 접근하게 된다.
이때 프록시 객체는 클라이언트의 요청이 유효한지 확인하거나, 보안 처리를 하거나, 로깅 처리를 하는 등의 여러 추가 작업을 수행한다.
- 장점
- 보안 적용 / 캐싱 처리 / 지연 초기화 / 로깅 등 다양한 기능을 앞단에서 대신 처리해준다.
- 단점
- 객체를 생성할 때 한단계 더 거치게 되어서 성능이 저하 될 수 있다.
- 코드 복잡도가 증가해서 가독성이 떨어질 수 있다.
프록시 패턴 예시
interface ISubject {
void action();
}
class RealSubject implements ISubject {
public void action() {
System.out.println("원본 객체 액션 !!");
}
}
class Proxy implements ISubject {
private RealSubject subject; // 대상 객체를 composition
Proxy(RealSubject subject) {
this.subject = subject;
}
public void action() {
subject.action(); // 위임
/* do something */
System.out.println("프록시 객체 액션 !!");
}
}
class Client {
public static void main(String[] args) {
ISubject sub = new Proxy(new RealSubject());
sub.action();
}
}
데코레이터 (Decorator)
데코레이터 패턴이란 객체의 결합을 통해 동적으로 객체의 기능을 유연하게 확장할 수 있게 해주는 패턴이다.
기본 기능은 ConcreteComponent 클래스에 구현하고, 그 외의 추가 동작은 Decorator의 구현클래스에 구현한다.
그리고 클라이언트 코드에서 동적으로 ConcreteComponent 객체와 Decorator 객체를 결합해서 기능을 확장한다
- 장점 : 기능을 조합할 수 있어서 유연성이 높으며 OCP, SRP 객체지향 원칙을 준수한다.
- 단점 : 데코레이터 클래스가 증가할 수록 코드 복잡성이 늘어난다.
- 구성요소
- Component : 기본 클래스와 서브클래스(데코레이터)들이 구현하는 공통 인터페이스
- ConcreteComponent : 기본 클래스 나타내고 , Componet을 구현한다.
- Decorator : 서브클래스인 데코레이터 클래스의 추가 기능을 정의한 인터페이스이다.
- ConcreateDecorator : 데코레이터 구현 클래스로, 기본 객체를 생성자로 받아 추가적인 책임을 부여하는 기능을 구현한다.
데코레이터 패턴 예시
아래 예시는 클라이언트가 커피에 우유와 설탕을 추가하는 데코레이터 패턴의 예시이다.
클라이언트는 SimpleCoffe 인스턴스를 생성하고 MilkDecorator와 SugarDecorator 클래스를 통해 기본 커피에 우유와 설탕을 동적으로 추가하고 있다.
// 커피 인터페이스 (Component)
interface Coffee {
double cost();
String getDescription();
}
// 기본 커피 (ConcreteComponent)
class SimpleCoffee implements Coffee {
@Override
public double cost() {
return 2.0;
}
@Override
public String getDescription() {
return "Simple Coffee";
}
}
// 커피 데코레이터 (Decorator)
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee decoratedCoffee) {
this.decoratedCoffee = decoratedCoffee;
}
@Override
public double cost() {
return decoratedCoffee.cost();
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
}
// 설탕 데코레이터 (ConcreteDecorator)
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public double cost() {
return super.cost() + 0.5;
}
@Override
public String getDescription() {
return super.getDescription() + ", Sugar";
}
}
// 우유 데코레이터 (ConcreteDecorator)
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public double cost() {
return super.cost() + 1.0;
}
@Override
public String getDescription() {
return super.getDescription() + ", Milk";
}
}
// 주문
public class Client {
public static void main(String[] args) {
Coffee coffee = new SimpleCoffee();
System.out.println("Cost: " + coffee.cost() + ", Description: " + coffee.getDescription());
Coffee sugarCoffee = new SugarDecorator(coffee);
System.out.println("Cost: " + sugarCoffee.cost() + ", Description: " + sugarCoffee.getDescription());
Coffee milkAndSugarCoffee = new MilkDecorator(sugarCoffee);
System.out.println("Cost: " + milkAndSugarCoffee.cost() + ", Description: " + milkAndSugarCoffee.getDescription());
}
}
'Study > Etc' 카테고리의 다른 글
[Design Pattern] 디자인 패턴의 개념과 종류 (3) - 행위 패턴 (0) | 2024.01.12 |
---|---|
[Design Pattern] 디자인 패턴의 개념과 종류 (1) - 생성 패턴 (0) | 2024.01.12 |
[AWS] 과금 폭탄 피하기 위한 2가지 설정 (0) | 2023.11.23 |
[IDE] Eclipse에 Spring Boot 프로젝트 생성하기 (0) | 2022.11.04 |
[IDE] IntelliJ 단축키 모음 (0) | 2022.11.04 |