본문 바로가기

CS지식

[Java] 디자인패턴

반응형

자주 발생하는 소프트웨어 설계 문제를 해결하기 위해 고안된 모범 사례 모음입니다.

이를 잘 이해하고 활용하면 코드의 재사용성을 높이고,호환성, 유지보수성을 향상시킬 수 있습니다.

 

하지만

디자인 패턴은 아이디어이며, 특정한 구현이 아닙니다.

프로젝트에서 항상 적용해야하는 것은 아니지만 추후 재사용, 효환, 유지보수발생하는

문제 해결을 예방하지위해 만들어진 패턴을 만들어 둔 것입니다.

 

디자인 패턴을 깊이 있게 이해고 적용하기 위해서는 SOLID 원칙과 GRASP패턴같은

기초적인 설계원칙을 이해하는것이 중요합니다.

 

SOLID 원칙

로버트 C. 마틴( Robert C. Martin )이 정의한 소프트웨어 설계 원칙으로, 객체지향 설계다섯 가지 중요한 원칙을 포함합니다. 이 원칙들은 유지보수성이 높고 확장 가능한 소프트웨어를 만드는 데 필수적입니다.

 

 

단일 책임 원칙 (Single Responsibility Principle, SRP)

클래스는 하나의 책임만 가져야 하며, 변경의 이유가 하나여야 합니다.

  • 예시: 사용자 데이터를 처리하는 클래스가 데이터베이스와의 통신까지 담당한다면, 이를 분리하여 하나의 클래스는 데이터 처리만, 다른 하나는 데이터베이스 통신만 담당하도록 합니다.
  • 장점: 코드의 가독성과 유지보수성이 높아집니다.

개방-폐쇄 원칙 (Open/Closed Principle, OCP)

소프트웨어 개체(클래스, 모듈 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 합니다.

  • 예시: 새로운 기능을 추가할 때 기존 코드를 수정하지 않고도 확장할 수 있는 구조를 만듭니다. 인터페이스나 추상 클래스를 사용하여 새로운 기능을 추가할 수 있습니다.
  • 장점: 코드의 안정성을 유지하면서 기능을 확장할 수 있습니다.

리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

서브 클래스는 언제나 자신의 기반 클래스를 대체할 수 있어야 합니다. 즉, 서브 클래스는 부모 클래스의 계약을 준수해야 합니다.

  • 예시: 부모 클래스의 기능을 서브 클래스에서 재정의할 때, 부모 클래스의 기능을 보장해야 합니다.
  • 장점: 다형성을 유지하고, 코드의 예측 가능성을 높입니다.

인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

하나의 범용 인터페이스보다 여러 개의 구체적 인터페이스가 낫다는 원칙입니다.

  • 예시: 큰 인터페이스를 여러 개의 작은 인터페이스로 나누어, 클라이언트가 필요로 하는 기능만을 사용하도록 합니다.
  • 장점: 불필요한 코드 의존성을 줄이고, 유지보수성을 높입니다.

의존성 역전 원칙 (Dependency Inversion Principle, DIP)

고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다.

  • 예시: 구체적인 구현이 아닌 인터페이스나 추상 클래스를 통해 의존성을 관리합니다.
  • 장점: 시스템을 유연하게 만들고, 변경에 강한 구조를 만듭니다.

 


GRASP 패턴

GRASP( General Responsibility Assignment Software Patterns )은 객체지향 설계에서 책임을 분배하기 위한 패턴의 모음입니다. 9가지 패턴으로 구성되어 있으며, 객체에게 책임을 할당할 때 고려해야 할 일반적인 지침을 제공합니다.

 

 

Information Expert

특정 책임을 수행하는 데 필요한 정보를 가지고 있는 객체가 그 책임을 가지는 것이 좋습니다.

  • 예시: 고객 정보를 관리하는 클래스가 고객 데이터를 업데이트할 책임을 지는 것.

 

Creator

어떤 객체가 특정 다른 객체의 생성을 책임져야 하는지에 대한 가이드라인을 제공합니다.

  • 예시: A 객체가 B 객체를 포함하거나 참조하는 경우, A 객체가 B 객체를 생성하는 것이 합리적입니다.

 

Controller

시스템 이벤트를 처리하는 책임을 가지는 객체를 정의합니다.

  • 예시: 애플리케이션의 주요 흐름을 관리하는 컨트롤러 객체.

 

Low Coupling

클래스 간의 의존성을 줄여 변경에 강한 시스템을 만듭니다.

  • 예시: 의존성을 인터페이스로 추상화하여 변경 시 영향을 최소화합니다.

 

High Cohesion

클래스나 모듈이 관련된 책임을 가지고 있어야 합니다.

  • 예시: 유사한 기능을 가진 메서드들이 하나의 클래스에 모여 있는 것.

 

Lazy Initialization

객체의 초기화를 필요한 시점까지 지연시킵니다.

  • 예시: 데이터베이스 연결을 처음 요청이 들어올 때까지 지연시키는 것.

 

Polymorphism

객체들이 공통된 인터페이스를 통해 상호작용하도록 하여 구현을 숨깁니다.

  • 예시: 인터페이스를 통해 서로 다른 객체들이 같은 메서드를 호출할 수 있게 합니다.

 

Protected Variations

변경이 발생할 수 있는 지점을 인터페이스나 추상 클래스로 보호합니다.

  • 예시: 특정 모듈의 변경이 다른 모듈에 영향을 미치지 않도록 인터페이스로 보호하는 것.

 

Pure Fabrication

시스템의 책임을 분배하는데 필요한, 도메인 모델에 속하지 않는 인위적인 클래스를 정의합니다.

  • 예시: 데이터베이스 연결을 관리하는 독립적인 유틸리티 클래스.

 


Design Smells

코드에서 발견되는 나쁜 설계의 징후

유지보수, 확장성을 저하시키며, 종종 스파게티 코드와 같은 혼잡한 코드베이스를 초래합니다.

 

Rigidity(경직성)
시스템이 변경하기 어렵다. 

하나의 변경을 위해서 다른 것들을 변경 해야할 때 경직성이 높다. ( -> 이부분 5px 정도만 이동가능할까요 ? )

경직성이 높다면 non-critical한 문제가 발생했을 때 관리자는 개발자에게 수정을 요청하기가 두려워진다.

Fragility(취약성)
취약성이 높다면 시스템은 어떤 부분을 수정하였는데 관련이 없는 다른 부분에 영향을 준다. 

수정사항이 관련되지 않은 부분에도 영향을 끼치기 떄문에 관리하는 비용이 커지며 시스템의 credibility 또한 잃는다.

Immobility(부동성)
부동성이 높다면 재사용하기 위해서 시스템을 분리해서 컴포넌트를 만드는 것이 어렵다. 

주로 개발자가 이전에 구현되었던 모듈과 비슷한 기능을 하는 모듈을 만들려고 할 때 문제점을 발견한다.

Viscosity(점착성)
점착성은 디자인 점착성과 환경 점착성으로 나눌 수 있다.


스파게티 코드(Spaghetti Code)

스파게티 코드는 의존성이 얽히고설켜서 이해하기 어렵고, 수정하기 힘든 코드를 말합니다.

마치 스파게티면 처럼 엉킨 모습을 비유한 표현입니다.

 

주로 나쁜 설계로 인해 발생하며, Design Smells의 결과물일 수 있습니다.

이를 방지하지 위해서  SOLID 원칙과 GRASP 패턴을 따르고, 주기적으로 코드를 리팩토링해야 함.


싱글톤 패턴 (Singleton Pattern)

싱글톤 패턴은 애플리케이션에서 클래스의 인스턴스를 하나만 생성하도록 보장하는 디자인 패턴

주로 하나의 객체만 필요한 상황에서 사용됩니다. 예를 들어, 데이터베이스 연결 객체, 설정 파일 관리 객체 등

 

public class Singleton {
    // 클래스의 유일한 인스턴스를 static으로 선언
    private static Singleton instance;

    // 생성자를 private으로 설정해 외부에서 객체 생성을 막음
    private Singleton() {}

    // 유일한 인스턴스를 반환하는 메서드
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

 

장점

메모리 절약 : 인스턴스를 하나만 생성하기 때문에 메모리 사용이 절약

글로벌 접근점 : 싱글톤 객체는 어디서든 접근할수 있다.

 

단점

테스트가 어려움 : 의존성 주입을 통한 테스트가 어려울수 있음.

전역 상태를 가짐 : 전역적으로 상태를 공유하게 되어 프로그램이 복잡해질 수 있음.

 


어댑터 패턴 (Adapter Pattern)

어댑터 패턴은 호환되지 않는 인터페이스를 가진 두 개의 객체를 연결해주는 디자인 패턴입니다.

예를 들어, 서로 다른 형식의 데이터 소스를 통합하거나, 기존 클래스를 새로운 인터페이스와 함께 사용할 때 유용합니다.

 

// 기존 인터페이스
interface Target {
    void request();
}

// 기존 클래스: Adaptee
class Adaptee {
    public void specificRequest() {
        System.out.println("Adaptee's specific request.");
    }
}

// 어댑터 클래스
class Adapter implements Target {
    private Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void request() {
        adaptee.specificRequest();
    }
}

 

장점

호환성 제공 : 기존 코드를 수정하지 않고도 새로운 인터페이스를 사용가능.

코드 재사용성 : 기존 코드를 재사용하여 새로운 인터페이스와 통합 가능

 

단점

코드 복잡성 : 어댑터 클래스를 추가로 생성해야 하므로 코드가 복잡해질 수 있습니다.

 


프록시 패턴 ( Proxy Pattern )

프록시 패턴은 실제 객체에 대한 접근을 제어하는 대리자(프록시)를 제공하는 패턴.

프록시는 실제 객체에 대한 인터페이스를 제공하고, 필요할 때만 실제 객체를 생성하거나 접근함.

이 패턴은 접근 제어, 리소스 관리, 로깅, 캐싱 등의 상황에서 유용하게 사용됩니다.

 

프록시 ( Proxy )

실제 객체의 인터페이스를 구현하고 실제 객체에 대한 접근을 제어

 

실제 객체 ( Real Subject )

프록시가 대리하는 실제 객체로, 모든 작업을 수행하는 클래스

 

// 공통 인터페이스
interface Image {
    void display();
}

// 실제 객체
class RealImage implements Image {
    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }

    private void loadFromDisk(String fileName) {
        System.out.println("Loading " + fileName);
    }

    public void display() {
        System.out.println("Displaying " + fileName);
    }
}

// 프록시 클래스
class ProxyImage implements Image {
    private RealImage realImage;
    private String fileName;

    public ProxyImage(String fileName) {
        this.fileName = fileName;
    }

    public void display() {
        if (realImage == null) {
            realImage = new RealImage(fileName);  // 실제 객체 생성
        }
        realImage.display();  // 실제 객체 메서드 호출
    }
}

// 클라이언트 코드
public class ProxyPatternDemo {
    public static void main(String[] args) {
        Image image = new ProxyImage("test_image.jpg");
        image.display();  // 이미지 로드 후 표시
        image.display();  // 이미 로드된 이미지 표시
    }
}

 

 

ProxyImage는 RealImage의 객체 생성과 접근을 제어합니다.

실제 이미지는 처음 요청될 때만 로드되고, 이후에는 캐싱된 이미지가 사용됩니다.

 

메모리 절약, 성능 최적화, 접근 제어를 위해 사용

 


데코레이터 패턴 (Decorator Pattern)

데코레이터 패턴객체에 추가적인 기능을 동적으로 추가할 수 있는 패턴입니다.

이는 상속 대신 조합을 통해 기능을 확장하며, 다수의 데코레이터를 조합하여 다양한 기능을 추가할 수 있습니다.

 

컴포넌트 ( Component )

추가 기능을 부여할 기본 객체의 인터페이스

 

콘크리트 컴포넌트 ( Concrete Component )

기본 기능을 구현하는 클래스

 

데코레이터 ( Decorator )

컴포넌트의 인터페이스를 구현하고, 추가 기능을 부여하는 클래스

 

// 컴포넌트 인터페이스
interface Coffee {
    String getDescription();
    double getCost();
}

// 기본 커피 클래스
class SimpleCoffee implements Coffee {
    public String getDescription() {
        return "Simple Coffee";
    }

    public double getCost() {
        return 2.00;
    }
}

// 데코레이터 클래스
class MilkDecorator extends SimpleCoffee {
    public String getDescription() {
        return super.getDescription() + ", Milk";
    }

    public double getCost() {
        return super.getCost() + 0.50;
    }
}

// 클라이언트 코드
public class DecoratorPatternDemo {
    public static void main(String[] args) {
        Coffee coffee = new MilkDecorator();
        System.out.println(coffee.getDescription() + " $" + coffee.getCost());
    }
}

 

 

MilkDecorator는 SimpleCoffee에 우유 기능을 추가

데코레이터는 객체의 기본 기능을 확장할 때 유용하게 사용

 


옵저버 패턴 (Observer Pattern)

옵저버 패턴객체의 상태 변화를 관찰하는 옵저버들에게 그 변화를 통보하는 디자인 패턴입니다.

주로 이벤트 기반 시스템이나 실시간 업데이트가 필요한 상황에서 사용됩니다.

 

주제 ( Subject )

상태 변화를 관리하며, 옵저버들을 등록/삭제/통보하는 역할

 

옵저버 ( Observer )

주제의 상태 변화를 감지하고 반응하는 객체

 

import java.util.*;

interface Observer {
    void update(String message);
}

class Subject {
    private List<Observer> observers = new ArrayList<>();

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}

class ConcreteObserver implements Observer {
    private String name;

    public ConcreteObserver(String name) {
        this.name = name;
    }

    public void update(String message) {
        System.out.println(name + " received: " + message);
    }
}

// 클라이언트 코드
public class ObserverPatternDemo {
    public static void main(String[] args) {
        Subject subject = new Subject();
        subject.addObserver(new ConcreteObserver("Observer 1"));

        subject.notifyObservers("Hello!");
    }
}

 

 

Subject 클래스는 상태가 변경될 때 notifyObservers를 호출하여 옵저버들에게 알림.

ConcreteObserver는 상태 변경을 통지받아 동작을 수행


 

반응형

'CS지식' 카테고리의 다른 글

[Java] 데드락 ( DeadLock )  (2) 2024.08.06
[Java] 가비지 컬렉션 ( Garbage Collection ) 개념 및 동작  (1) 2024.08.05
클라우드 ( AWS ) 지식  (1) 2024.08.02
운영체제와 WAS 정리  (1) 2024.08.02
네트워크 CS지식  (2) 2024.08.01