디자인 패턴(Design Pattern)은 소프트웨어 개발에서 자주 발생하는 문제를 해결하기 위한 일반적인 방법이나 접근 방식을 정의한 것입니다. 디자인 패턴을 사용하면 코드의 재사용성을 높이고, 유지보수성을 개선하며, 개발 시간을 단축할 수 있습니다. 또한, 여러 개발자들 간에 공통된 언어를 제공하여 협업을 용이하게 합니다.
디자인 패턴은 크게 세 가지 범주로 나눠집니다:
- 생성적 패턴(Creational Patterns): 객체 생성에 관한 패턴으로, 객체를 생성하는 방법을 추상화하여 클라이언트 코드에서 객체 생성을 더 효율적으로 할 수 있도록 돕습니다.
- 구조적 패턴(Structural Patterns): 클래스나 객체의 구조를 조직하는 방법을 다룹니다. 객체들 간의 관계를 설계하여 효율적인 데이터 처리와 구성을 도와줍니다.
- 행위적 패턴(Behavioral Patterns): 객체 간의 상호작용과 책임 분배를 다루는 패턴입니다. 객체들이 어떻게 협력하고 메시지를 주고받는지 정의합니다.
1. 생성적 패턴
싱글톤 패턴 (Singleton Pattern)
public class Singleton {
// private static instance 변수
private static Singleton instance;
// private 생성자
private Singleton() {
}
// 인스턴스를 가져오는 메서드
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 최초 요청 시 객체 생성
}
return instance;
}
}
싱글톤 패턴은 클래스의 인스턴스를 하나만 생성하고, 그 인스턴스를 전역에서 공유하는 패턴입니다. 이 패턴은 시스템에 단 하나의 인스턴스만 필요할 때 사용됩니다.
싱글톤 패턴 예시 (Java)
위 예시에서 Singleton 클래스는 private 생성자를 사용하여 외부에서 객체를 생성할 수 없도록 합니다. getInstance 메서드를 통해서만 인스턴스를 가져올 수 있으며, 이 메서드는 최초 호출 시 객체를 생성하고 이후에는 같은 객체를 반환합니다.
팩토리 메서드 패턴 (Factory Method Pattern)
팩토리 메서드 패턴은 객체를 생성하는 인터페이스를 정의하고, 서브클래스에서 인스턴스를 생성하는 방법을 구현하도록 하는 패턴입니다. 객체 생성 방법을 캡슐화하여 클라이언트 코드가 객체 생성 과정을 알 필요 없도록 합니다.
팩토리 메서드 패턴 예시 (Java)
// 제품 인터페이스
interface Product {
void use();
}
// 구체적인 제품 클래스
class ConcreteProductA implements Product {
@Override
public void use() {
System.out.println("Using ConcreteProductA");
}
}
class ConcreteProductB implements Product {
@Override
public void use() {
System.out.println("Using ConcreteProductB");
}
}
// 제품을 생성하는 팩토리 인터페이스
abstract class Creator {
public abstract Product createProduct();
}
// ConcreteCreatorA 클래스
class ConcreteCreatorA extends Creator {
@Override
public Product createProduct() {
return new ConcreteProductA();
}
}
// ConcreteCreatorB 클래스
class ConcreteCreatorB extends Creator {
@Override
public Product createProduct() {
return new ConcreteProductB();
}
}
public class FactoryMethodExample {
public static void main(String[] args) {
Creator creatorA = new ConcreteCreatorA();
Product productA = creatorA.createProduct();
productA.use();
Creator creatorB = new ConcreteCreatorB();
Product productB = creatorB.createProduct();
productB.use();
}
}
위 예시에서 Creator 클래스는 createProduct 메서드를 정의하고, ConcreteCreatorA와 ConcreteCreatorB 클래스가 각기 다른 Product 객체를 생성합니다. 클라이언트는 Creator를 통해 객체를 생성하고, 객체 생성 방법에 대해 알 필요가 없습니다.
2. 구조적 패턴 - 어댑터 패턴 (Adapter Pattern)
어댑터 패턴은 기존 클래스의 인터페이스를 변경하지 않고, 그 인터페이스를 변환하여 사용하는 패턴입니다. 주로, 서로 다른 인터페이스를 가진 클래스를 호환되게 만드는 데 사용됩니다.
어댑터 패턴 예시 (Java)
// 기존 인터페이스
interface MediaPlayer {
void play(String audioType, String fileName);
}
// 고급 기능을 제공하는 클래스
class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) {
if(audioType.equalsIgnoreCase("vlc") ){
advancedMusicPlayer = new VlcPlayer();
} else if(audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if(audioType.equalsIgnoreCase("vlc")){
advancedMusicPlayer.playVlc(fileName);
}
else if(audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer.playMp4(fileName);
}
}
}
// 고급 기능을 제공하는 인터페이스
interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: " + fileName);
}
@Override
public void playMp4(String fileName) {
// do nothing
}
}
class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
// do nothing
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file. Name: " + fileName);
}
}
위 예시에서 MediaAdapter는 MediaPlayer 인터페이스와 AdvancedMediaPlayer 인터페이스를 연결해주는 역할을 합니다. MediaAdapter는 VlcPlayer와 Mp4Player를 변환하여 MediaPlayer 인터페이스에 맞는 형식으로 데이터를 전달할 수 있습니다.
컴포지트 패턴 (Composite Pattern)
컴포지트 패턴은 객체들을 트리 구조로 구성하여, 개별 객체와 객체들의 집합을 동일하게 다룰 수 있게 만드는 패턴입니다. 주로 트리 구조의 데이터 구조에서 사용됩니다.
컴포지트 패턴 예시 (Java)
import java.util.ArrayList;
import java.util.List;
// 컴포넌트 인터페이스
interface Component {
void showDetails();
}
// 리프 클래스
class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
@Override
public void showDetails() {
System.out.println("Leaf: " + name);
}
}
// 복합 클래스 (트리 구조)
class Composite implements Component {
private List<Component> children = new ArrayList<>();
public void add(Component component) {
children.add(component);
}
public void remove(Component component) {
children.remove(component);
}
@Override
public void showDetails() {
for (Component child : children) {
child.showDetails();
}
}
}
public class CompositePatternExample {
public static void main(String[] args) {
Leaf leaf1 = new Leaf("Leaf 1");
Leaf leaf2 = new Leaf("Leaf 2");
Composite composite = new Composite();
composite.add(leaf1);
composite.add(leaf2);
Composite root = new Composite();
root.add(composite);
root.add(new Leaf("Leaf 3"));
root.showDetails();
}
}
위 예시에서 Leaf는 개별 객체이고, Composite는 자식 Component 객체를 포함할 수 있는 복합 객체입니다. 클라이언트는 Composite와 Leaf를 동일하게 다룰 수 있습니다.
3. 행위적 패턴 - 옵저버 패턴 (Observer Pattern)
옵저버 패턴은 객체의 상태 변화를 다른 객체들에게 자동으로 통지하는 패턴입니다. 주로 이벤트 시스템에서 사용됩니다.
옵저버 패턴 예시 (Java)
import java.util.ArrayList;
import java.util.List;
interface Observer {
void update(String message);
}
class ConcreteObserver implements Observer {
private String name;
public ConcreteObserver(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + " received message: " + message);
}
}
interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
private String message;
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(message);
}
}
public void setMessage(String message) {
this.message = message;
notifyObservers();
}
}
public class ObserverPatternExample {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
Observer observer1 = new ConcreteObserver("Observer1");
Observer observer2 = new ConcreteObserver("Observer2");
subject.registerObserver(observer1);
subject.registerObserver(observer2);
subject.setMessage("New Message!");
}
}
위 예시에서 ConcreteSubject는 상태를 변경할 때마다 등록된 옵저버들에게 상태 변화를 통지합니다. ConcreteObserver는 Subject의 상태 변화를 받아 출력합니다.
전략 패턴 (Strategy Pattern)
전략 패턴은 알고리즘을 클래스로 정의하고, 각 알고리즘을 클래스 내부에서 캡슐화하여 동적으로 교환할 수 있게 하는 패턴입니다. 클라이언트는 원하는 전략을 선택하여 사용할 수 있습니다.
전략 패턴 예시 (Java)
// 전략 인터페이스
interface Strategy {
int execute(int a, int b);
}
// 구체적인 전략 클래스 1
class AddStrategy implements Strategy {
@Override
public int execute(int a, int b) {
return a + b;
}
}
// 구체적인 전략 클래스 2
class MultiplyStrategy implements Strategy {
@Override
public int execute(int a, int b) {
return a * b;
}
}
// 컨텍스트 클래스
class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public int executeStrategy(int a, int b) {
return strategy.execute(a, b);
}
}
public class StrategyPatternExample {
public static void main(String[] args) {
Context context = new Context(new AddStrategy());
System.out.println("Add: " + context.executeStrategy(5, 3)); // 8
context.setStrategy(new MultiplyStrategy());
System.out.println("Multiply: " + context.executeStrategy(5, 3)); // 15
}
}
위 예시에서 Strategy 인터페이스는 다양한 계산 방법을 정의합니다. AddStrategy와 MultiplyStrategy는 그 구현체로, Context 클래스는 현재 전략을 설정하고 이를 통해 계산을 수행합니다. 클라이언트는 실행 중에 전략을 변경할 수 있습니다.
디자인 패턴은 특정 문제를 해결하기 위한 효율적인 방법을 제시하며, 개발자들이 공통된 문제를 해결할 때 유용한 참고 자료가 됩니다. 각각의 패턴은 상황에 맞게 사용해야 하며, 너무 많은 패턴을 적용하는 것보다는 필요한 곳에 적절히 사용하는 것이 중요합니다 !
'IT 개발 라이프 > Back_End' 카테고리의 다른 글
자바(JAVA) 컴파일 과정 (0) | 2025.01.10 |
---|---|
Jackson 기반 JSON 유틸리티와 커스텀 ObjectMapper 적용하기 (0) | 2025.01.09 |
자바 HashMap vs HashTable vs ConcurrentHashMap (1) | 2024.12.31 |
자바(JAVA) try-with-resources (0) | 2024.12.31 |
자바에서 Generic(제네릭)을 쓰는 이유 (1) | 2024.12.27 |