안녕하세요! Devloo 입니다. 저는 자바 백엔드 개발자로서 Spring Boot와 Spring Framework를 10년 동안 사용하면서, 디자인 패턴이 견고하고 확장 가능한 애플리케이션을 구축하는 데 있어 얼마나 중요한 역할을 하는지 깨닫게 되었습니다. 이 글에서는 다섯 가지 필수 디자인 패턴을 탐구해 보고, 이를 효과적으로 Spring Boot 프로젝트에 적용하는 방법을 살펴보겠습니다. 각 패턴은 구현 예제와 함께 제공드리도록 하겠습니다. 🙂
Singleton Pattern (싱글톤 패턴)
싱글톤 패턴은 클래스의 인스턴스가 오직 하나만 존재하도록 보장하며, 이를 위한 전역 접근 지점을 제공합니다. 이는 데이터베이스 연결이나 캐싱 객체와 같은 자원을 관리하는 데 특히 유용합니다. Spring Boot에서 싱글톤 패턴을 구현하는
방법은 다음과 같습니다:
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private DatabaseConnection() {
// 인스턴스 생성을 방지하기 위한 private 생성자
}
public static DatabaseConnection getInstance() {
if (instance == null) {
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
}
Spring의 Bean을 활용한 싱글턴의 구현은 아래 링크를 확인하세요:
https://www.baeldung.com/spring-boot-singleton-vs-beans#singleton-beans-in-spring
Factory Method Pattern (팩토리 메서드 패턴)
팩토리 메서드 패턴은 슈퍼클래스에서 객체를 생성하는 인터페이스를 제공하고, 서브클래스에서 생성할 객체의 타입을 변경할 수 있게 합니다. 이는 객체 생성 로직을 클라이언트 코드에서 분리하는 데 유용합니다.
Spring Boot에서의 예시는 다음과 같습니다:
/*
인터페이스 및 구현 클래스
*/
public interface PaymentProcessor {
void processPayment();
}
@Component("creditCardProcessor")
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment() {
// 신용카드 결제 처리 로직
}
}
@Component("paypalProcessor")
public class PayPalProcessor implements PaymentProcessor {
@Override
public void processPayment() {
// PayPal 결제 처리 로직
}
}
/*
팩토리 인터페이스 및 구현 클래스
*/
public interface PaymentProcessorFactory {
PaymentProcessor createPaymentProcessor(String type);
}
@Component
public class PaymentProcessorFactoryImpl implements PaymentProcessorFactory {
private final ApplicationContext context;
@Autowired
public PaymentProcessorFactoryImpl(ApplicationContext context) {
this.context = context;
}
@Override
public PaymentProcessor createPaymentProcessor(String type) {
return context.getBean(type + "Processor", PaymentProcessor.class);
}
}
/*
사용 예시
*/
@Service
public class PaymentService {
private final PaymentProcessorFactory paymentProcessorFactory;
public PaymentService(PaymentProcessorFactory paymentProcessorFactory) {
this.paymentProcessorFactory = paymentProcessorFactory;
}
public void processPayment(String type) {
PaymentProcessor processor = paymentProcessorFactory.createPaymentProcessor(type);
processor.processPayment();
}
}
Observer Pattern (옵저버 패턴)
옵저버 패턴은 객체 사이의 일대다 의존성을 정의하여, 하나의 객체 상태가 변경될 때 그 의존 객체들이 자동으로 통지되고 갱신되도록 합니다. 이는 이벤트 기반 시스템에서 자주 사용됩니다.
Spring Boot에서의 구현 방법은 다음과 같습니다:
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class OrderListener implements ApplicationListener<OrderEvent> {
@Override
public void onApplicationEvent(OrderEvent event) {
// 주문 이벤트 처리
}
}
public class OrderEvent extends ApplicationEvent {
public OrderEvent(Object source) {
super(source);
}
}
@Service
public class OrderService {
private ApplicationEventPublisher eventPublisher;
public OrderService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void placeOrder() {
// 주문 로직
// 주문 이벤트 발행
eventPublisher.publishEvent(new OrderEvent(this));
}
}
Decorator Pattern (데코레이터 패턴)
데코레이터 패턴은 동일한 클래스의 다른 객체의 동작에 영향을 주지 않고, 객체에 동작을 동적으로 추가할 수 있게 합니다. 이는 로깅, 캐싱, 암호화 등의 기능을 기존 클래스에 추가하는 데 유용합니다.
Spring Boot에서의 구현 방법은 다음과 같습니다:
public interface DataService {
void fetchData();
}
@Component
public class DataServiceImplementation implements DataService {
@Override
public void fetchData() {
// 데이터 가져오기 구현
}
}
@Component
public class LoggingDecorator implements DataService {
private DataService delegate;
public LoggingDecorator(DataService delegate) {
this.delegate = delegate;
}
@Override
public void fetchData() {
// 데이터 가져오기 전에 로깅
delegate.fetchData();
// 데이터 가져온 후에 로깅
}
}
Strategy Pattern (전략 패턴)
전략 패턴은 알고리즘 군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만드는 패턴입니다. 이는 여러 알고리즘을 상호 교환하여 사용할 수 있을 때 유용합니다.
Spring Boot에서의 구현 방법은 다음과 같습니다:
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.List;
// 압축 전략 인터페이스
public interface CompressionStrategy {
void compress(String file);
String getType();
}
// Zip 압축 전략 구현체
@Component
public class ZipCompressionStrategy implements CompressionStrategy {
@Override
public void compress(String file) {
// Zip 압축 로직
System.out.println("Compressing using ZIP strategy: " + file);
}
@Override
public String getType() {
return "zip";
}
}
// RAR 압축 전략 구현체
@Component
public class RarCompressionStrategy implements CompressionStrategy {
@Override
public void compress(String file) {
// RAR 압축 로직
System.out.println("Compressing using RAR strategy: " + file);
}
@Override
public String getType() {
return "rar";
}
}
// 압축 컨텍스트
@Component
public class CompressionContext {
private final List<CompressionStrategy> strategies;
// 여러 전략을 주입받음
public CompressionContext(List<CompressionStrategy> strategies) {
this.strategies = strategies;
}
// 파일 압축 메소드 (전략 설정 포함)
public void compressFile(String file, String type) {
CompressionStrategy strategy = strategies.stream()
.filter(s -> type.equals(s.getType()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Strategy not found: " + type));
strategy.compress(file);
}
}
결론
디자인 패턴은 특히 Spring Boot와 같은 프레임워크를 사용할 때 모든 Java 백엔드 개발자가 반드시 갖춰야 할 지식입니다. 이러한 패턴을 숙달하고 프로젝트에 신중하게 적용하면 유지 보수성과 확장성이 뛰어나며 이해하기 쉽고 확장 가능한 코드를 작성할 수 있습니다.
이번 시간에는 다섯 가지 필수 디자인 패턴에 대해 알아보았습니다. 궁금하신 점은 댓글로 달아주세요 🙂
감사합니다 !! (_ _)