소프트웨어 프로젝트가 성장함에 따라 코드를 정리하고 유지 관리하며 확장 가능하게 만드는 것이 점점 더 중요해집니다. 이때 디자인 패턴이 중요하게 작용합니다. 디자인 패턴은 일반적인 소프트웨어 디자인 문제에 대한 검증된 재사용 가능한 솔루션을 제공하여 코드를 더 효율적이고 관리하기 쉽게 만듭니다.
이 가이드에서는 가장 인기 있는 디자인 패턴에 대해 깊이 있게 살펴보고 이를 Spring Boot에 구현하는 방법을 보여줍니다. 마지막에는 이러한 패턴을 개념적으로 이해할 뿐만 아니라, 자신의 프로젝트에 자신 있게 적용할 수 있을 것입니다.
목차
디자인 패턴 소개
디자인 패턴은 공통 소프트웨어 디자인 문제에 대한 재사용 가능한 해결책입니다. 이를 일종의 모범 사례로 볼 수 있으며, 코드의 구체적인 도전 과제를 해결하기 위해 적용할 수 있는 템플릿으로 추출되었습니다. 언어에 구애받지 않지만, 객체 지향적인 성질을 가진 Java에서 특히 강력한 기능을 발휘할 수 있습니다.
이 가이드에서는 다음을 다룰 것입니다:
-
싱글톤 패턴: 클래스가 하나의 인스턴스만 가지도록 보장합니다.
-
팩토리 패턴: 정확한 클래스를 지정하지 않고 객체를 생성합니다.
-
전략 패턴: 알고리즘을 런타임에 선택할 수 있게 합니다.
-
옵저버 패턴: 발행-구독 관계를 설정합니다.
이 패턴들이 어떻게 작동하는지 뿐만 아니라, 실제 애플리케이션에서 Spring Boot에서 어떻게 적용할 수 있는지에 대해서도 살펴볼 것입니다.
Spring Boot 프로젝트 설정 방법
패턴을 다루기 전에 Spring Boot 프로젝트를 설정해봅시다:
준비물
다음이 있는지 확인해주세요:
-
Java 11+
-
메이븐
-
스프링 부트 CLI (선택사항)
-
Postman 또는 curl (테스트용)
프로젝트 초기화
스프링 이니셜라이저를 사용하여 스프링 부트 프로젝트를 신속하게 생성할 수 있습니다:
curl https://start.spring.io/starter.zip \
-d dependencies=web \
-d name=DesignPatternsDemo \
-d javaVersion=11 -o design-patterns-demo.zip
unzip design-patterns-demo.zip
cd design-patterns-demo
싱글턴 패턴이란?
싱글턴 패턴은 클래스가 단 하나의 인스턴스만 가지도록 보장하며, 이를 전역적으로 접근할 수 있는 지점을 제공합니다. 이 패턴은 로깅, 구성 관리 또는 데이터베이스 연결과 같은 서비스에 일반적으로 사용됩니다.
스프링 부트에서 싱글턴 패턴 구현하기
스프링 부트 빈은 기본적으로 싱글턴으로, 이는 스프링이 이러한 빈의 생명 주기를 자동으로 관리하여 단 하나의 인스턴스만 존재하도록 보장함을 의미합니다. 그러나 스프링이 관리하지 않는 빈을 사용하거나 인스턴스 관리에 대한 더 많은 제어가 필요할 때 싱글턴 패턴이 내부에서 어떻게 작동하는지를 이해하는 것이 중요합니다.
싱글턴 패턴의 수동 구현을 통해 애플리케이션 내에서 단일 인스턴스의 생성을 제어하는 방법을 살펴보겠습니다.
1단계: LoggerService
클래스 생성
이 예제에서는 싱글턴 패턴을 사용하여 간단한 로깅 서비스를 생성합니다. 목표는 애플리케이션의 모든 부분이 동일한 로깅 인스턴스를 사용하도록 보장하는 것입니다.
public class LoggerService {
// 단일 인스턴스를 보유하기 위한 정적 변수
private static LoggerService instance;
// 외부에서 인스턴스화를 방지하기 위한 비공개 생성자
private LoggerService() {
// 이 생성자는 다른 클래스가 인스턴스를 생성하지 못하도록 의도적으로 비어 있습니다
}
// 단일 인스턴스에 접근을 제공하는 공개 메서드
public static synchronized LoggerService getInstance() {
if (instance == null) {
instance = new LoggerService();
}
return instance;
}
// 예제 로깅 메서드
public void log(String message) {
System.out.println("[LOG] " + message);
}
}
-
정적 변수 (
instance
): 이는LoggerService
의 단일 인스턴스를 보유합니다. -
비공개 생성자: 이 생성자는 다른 클래스가 직접 새로운 인스턴스를 생성하지 못하도록 비공개로 표시되어 있습니다.
-
동기화된
getInstance()
메서드: 이 메서드는 스레드 안전성을 보장하기 위해 동기화되어 있으며, 여러 스레드가 동시에 접근하더라도 단 하나의 인스턴스만 생성됩니다. -
게으름 초기화: 인스턴스는 처음 요청될 때만 생성됩니다 (
게으름 초기화
), 이는 메모리 사용 측면에서 효율적입니다.
실제 활용: 이 패턴은 공유 리소스에 대해 일반적으로 사용되며, 예를 들어 로깅, 구성 설정 또는 데이터베이스 연결 관리 등에서 애플리케이션 전체에서 하나의 인스턴스만 사용되도록 액세스를 제어하고 보장하려는 경우에 사용됩니다.
단계 2: Spring Boot Controller에서 싱글턴 사용
이제, LoggerService
싱글턴을 Spring Boot 컨트롤러 내에서 어떻게 사용할 수 있는지 살펴봅시다. 이 컨트롤러는 액세스될 때마다 메시지를 기록하는 엔드포인트를 노출합니다.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LogController {
@GetMapping("/log")
public ResponseEntity<String> logMessage() {
// LoggerService의 싱글턴 인스턴스에 액세스
LoggerService logger = LoggerService.getInstance();
logger.log("This is a log message!");
return ResponseEntity.ok("Message logged successfully");
}
}
-
GET 엔드포인트: 접속 시
LoggerService
를 사용하여 메시지를 기록하는/log
엔드포인트를 생성했습니다. -
싱글턴 사용:
LoggerService
의 새 인스턴스를 생성하는 대신 항상 같은 인스턴스를 사용하도록getInstance()
를 호출합니다. -
응답: 로깅 후, 엔드포인트는 성공을 나타내는 응답을 반환합니다.
단계 3: 싱글톤 패턴 테스트
이제, Postman이나 브라우저를 사용하여 이 엔드포인트를 테스트해 봅시다:
GET http://localhost:8080/log
예상 출력:
-
콘솔 로그:
[LOG] This is a log message!
-
HTTP 응답:
Message logged successfully
엔드포인트를 여러 번 호출해도, 일관된 로그 출력에 따라 동일한 LoggerService
인스턴스가 사용되는 것을 확인할 수 있습니다.
싱글톤 패턴의 실제 사용 사례
다음은 실제 응용 프로그램에서 싱글톤 패턴을 사용해야 하는 경우입니다:
-
구성 관리: 특히 파일이나 데이터베이스에서 로드된 설정이 일관된 설정 집합을 사용하도록 보장합니다.
-
데이터베이스 연결 풀: 제한된 수의 데이터베이스 연결에 대한 액세스를 제어하여 동일한 풀이 응용 프로그램 전체에서 공유되도록 합니다.
-
캐싱: 일관되지 않은 데이터를 피하기 위해 단일 캐시 인스턴스를 유지합니다.
-
로깅 서비스: 이 예제에서 보여진 대로, 응용 프로그램의 다른 모듈들 사이에 로그 출력을 중앙 집중화하기 위해 단일 로깅 서비스를 사용합니다.
주요 포인트
-
싱글톤 패턴은 클래스의 단일 인스턴스만 생성되도록 보장하는 간단한 방법입니다.
-
여러 스레드가 싱글톤에 액세스하는 경우 스레드 안전이 중요하며, 이를 위해 예제에서
synchronized
를 사용했습니다. -
Spring Boot 빈은 기본적으로 싱글톤이지만, 수동으로 구현하는 방법을 이해하면 필요할 때 더 많은 제어를 얻을 수 있습니다.
이는 싱글톤 패턴의 구현과 사용을 다룹니다. 이어서 팩토리 패턴을 살펴보고 객체 생성을 어떻게 간소화할 수 있는지 살펴보겠습니다.
팩토리 패턴이란 무엇인가요?
팩토리 패턴은 정확한 클래스를 지정하지 않고 객체를 생성할 수 있게 해줍니다. 이 패턴은 일부 입력을 기반으로 인스턴스화해야 하는 다른 유형의 객체가 있는 경우 유용합니다.
Spring Boot에서 팩토리 구현하는 방법
팩토리 패턴은 특정 기준에 따라 객체를 생성해야 하지만 객체 생성 프로세스를 주요 응용 프로그램 로직과 분리하고 싶을 때 매우 유용합니다.
이 섹션에서는 이메일이나 SMS를 통해 알림을 보내기 위한 NotificationFactory
를 만드는 방법을 살펴보겠습니다. 이는 푸시 알림이나 앱 내 알림과 같은 더 많은 알림 유형을 추가할 것으로 예상되는 경우에 특히 유용합니다. 기존 코드를 변경하지 않고 새로운 알림 유형을 추가할 수 있습니다.
단계 1: Notification
인터페이스 만들기
첫 번째 단계는 모든 알림 유형이 구현할 공통 인터페이스를 정의하는 것입니다. 이렇게 함으로써 각 알림 유형(이메일, SMS 등)이 일관된 send()
메서드를 갖게 됩니다.
public interface Notification {
void send(String message);
}
-
목적:
Notification
인터페이스는 알림을 보내기 위한 계약을 정의합니다. 이 인터페이스를 구현하는 모든 클래스는send()
메서드에 대한 구현을 제공해야 합니다. -
확장성: 인터페이스를 사용함으로써 기존 코드를 수정하지 않고 나중에 다른 유형의 알림을 포함시킬 수 있습니다.
단계 2: EmailNotification
과 SMSNotification
을 구현하세요
이제 이메일을 보내는 하나의 구체적인 클래스와 SMS 메시지를 보내는 다른 클래스를 구현해봅시다.
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending Email: " + message);
}
}
public class SMSNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}
단계 3: NotificationFactory
생성
NotificationFactory
클래스는 지정된 유형에 기반하여 Notification
인스턴스를 생성하는 책임이 있습니다. 이 설계는 NotificationController
가 객체 생성의 세부 정보를 알 필요가 없도록 합니다.
public class NotificationFactory {
public static Notification createNotification(String type) {
switch (type.toUpperCase()) {
case "EMAIL":
return new EmailNotification();
case "SMS":
return new SMSNotification();
default:
throw new IllegalArgumentException("Unknown notification type: " + type);
}
}
}
팩토리 메소드 (createNotification()
):
-
팩토리 메소드는 문자열 (
type
)을 입력으로 받아 해당 알림 클래스의 인스턴스를 반환합니다. -
스위치 문: 스위치 문은 입력에 따라 적절한 알림 유형을 선택합니다.
-
오류 처리: 제공된 유형이 인식되지 않으면
IllegalArgumentException
을 throw합니다. 이를 통해 잘못된 유형이 일찍 발견됩니다.
팩토리를 왜 사용해야 하나요?
-
결합도 감소: 팩토리 패턴은 객체 생성을 비즈니스 로직과 분리합니다. 이를 통해 코드가 더 모듈화되고 유지보수가 쉬워집니다.
-
확장성: 새로운 알림 유형을 추가하려면 컨트롤러 로직을 변경하지 않고 팩토리만 업데이트하면 됩니다.
단계 4: 스프링 부트 컨트롤러에서 팩토리 사용하기
이제 사용자 요청에 따라 알림을 보내는 NotificationFactory
를 사용하는 스프링 부트 컨트롤러를 만들어 모든 것을 함께 봅시다.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NotificationController {
@GetMapping("/notify")
public ResponseEntity<String> notify(@RequestParam String type, @RequestParam String message) {
try {
// 팩토리를 사용하여 적절한 Notification 객체 생성
Notification notification = NotificationFactory.createNotification(type);
notification.send(message);
return ResponseEntity.ok("Notification sent successfully!");
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}
GET 엔드포인트 (/notify
):
-
컨트롤러는 두 개의 쿼리 매개변수인
type
(“EMAIL” 또는 “SMS”)와message
를 받아들이는/notify
엔드포인트를 제공합니다. -
NotificationFactory
를 사용하여 적절한 알림 유형을 생성하고 메시지를 전송합니다. -
오류 처리: 잘못된 알림 유형이 제공되면, 컨트롤러는
IllegalArgumentException
을 포착하고400 Bad Request
응답을 반환합니다.
5단계: 팩토리 패턴 테스트
Postman 또는 브라우저를 사용하여 엔드포인트를 테스트해 보겠습니다:
-
이메일 알림 보내기:
GET http://localhost:8080/notify?type=email&message=Hello%20Email
출력:
이메일 전송: Hello Email
-
문자 메시지 알림 전송:
GET http://localhost:8080/notify?type=sms&message=Hello%20SMS
출력:
SMS 전송 중: Hello SMS
-
유효하지 않은 유형으로 테스트:
GET http://localhost:8080/notify?type=unknown&message=Test
출력:
잘못된 요청: 알 수 없는 알림 유형: unknown
팩토리 패턴의 실제 사용 사례
팩토리 패턴은 다음과 같은 시나리오에서 특히 유용합니다:
-
동적 객체 생성: 사용자 입력에 기반하여 객체를 생성해야 하는 경우, 다양한 형식으로 보고서 생성, 다양한 결제 방법 처리 등
-
객체 생성의 분리: 팩토리를 사용하여 주요 비즈니스 로직을 객체 생성과 분리함으로써 코드를 유지보수하기 쉽게 만들 수 있습니다.
-
확장성: 기존 코드를 수정하지 않고 새로운 알림 유형을 지원하도록 응용 프로그램을 쉽게 확장할 수 있습니다. 단순히
Notification
인터페이스를 구현하는 새 클래스를 추가하고 팩토리를 업데이트하면 됩니다.
전략 패턴이란 무엇인가요?
전략 패턴은 여러 알고리즘 또는 동작 사이를 동적으로 전환해야 할 때 완벽합니다. 각 알고리즘을 별도의 클래스로 캡슐화하고 런타임에서 쉽게 교체할 수 있도록 알고리즘 패밀리를 정의합니다. 특정 조건에 따라 알고리즘을 선택하는 데 특히 유용하며 코드를 깔끔하고 모듈화되고 유연하게 유지할 수 있습니다.
실제 사용 사례: 여러 결제 옵션을 지원해야 하는 전자 상거래 시스템을 상상해보십시오. 신용 카드, PayPal 또는 은행 송금과 같은 다양한 결제 방법을 지원해야 합니다. 전략 패턴을 사용하면 기존 코드를 수정하지 않고 결제 방법을 쉽게 추가하거나 수정할 수 있습니다. 이 접근 방식은 새로운 기능을 도입하거나 기존 기능을 업데이트할 때 애플리케이션이 확장 가능하고 유지 관리 가능하도록 보장합니다.
우리는 신용 카드 또는 PayPal 전략을 사용하여 결제를 처리하는 Spring Boot 예제로 이 패턴을 설명하겠습니다.
단계 1: PaymentStrategy
인터페이스 정의
우리는 모든 결제 전략이 구현할 공통 인터페이스를 만들어 시작합니다:
public interface PaymentStrategy {
void pay(double amount);
}
이 인터페이스는 모든 결제 방법에 대한 계약을 정의하여 구현 사이의 일관성을 보장합니다.
단계 2: 결제 전략 구현
신용 카드 및 PayPal 결제에 대한 구체적인 클래스를 만듭니다.
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " with Credit Card");
}
}
public class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " via PayPal");
}
}
각 클래스는 특정 동작을 가진 pay()
메서드를 구현합니다.
단계 3: Controller에서 전략 사용
사용자 입력에 기반하여 동적으로 결제 전략을 선택하는 컨트롤러를 만듭니다:
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PaymentController {
@GetMapping("/pay")
public ResponseEntity<String> processPayment(@RequestParam String method, @RequestParam double amount) {
PaymentStrategy strategy = selectPaymentStrategy(method);
if (strategy == null) {
return ResponseEntity.badRequest().body("Invalid payment method");
}
strategy.pay(amount);
return ResponseEntity.ok("Payment processed successfully!");
}
private PaymentStrategy selectPaymentStrategy(String method) {
switch (method.toUpperCase()) {
case "CREDIT": return new CreditCardPayment();
case "PAYPAL": return new PayPalPayment();
default: return null;
}
}
}
이 엔드포인트는 쿼리 매개변수로 method
와 amount
를 허용하고 적절한 전략을 사용하여 결제를 처리합니다.
단계 4: 엔드포인트 테스트
-
신용카드 결제:
GET http://localhost:8080/pay?method=credit&amount=100
출력:
신용카드로 $100.0 결제함
-
PayPal 결제:
GET http://localhost:8080/pay?method=paypal&amount=50
출력:
PayPal을 통해 $50.0 결제함
-
잘못된 방법:
GET http://localhost:8080/pay?method=bitcoin&amount=25
출력:
잘못된 결제 방법
전략 패턴의 사용 사례
-
결제 처리: 다양한 결제 게이트웨이 간에 동적으로 전환합니다.
-
정렬 알고리즘: 데이터 크기에 따라 최적의 정렬 방법을 선택합니다.
-
파일 내보내기: 다양한 형식(PDF, Excel, CSV)으로 보고서를 내보냅니다.
주요 포인트
-
전략 패턴은 코드를 모듈화하고 개방/폐쇄 원칙을 따릅니다.
-
새 전략을 추가하는 것은 쉽습니다—단순히
PaymentStrategy
인터페이스를 구현하는 새 클래스를 만들면 됩니다. -
실행 시간에 유연한 알고리즘 선택이 필요한 시나리오에 이상적입니다.
다음으로 옵저버 패턴을 살펴보겠습니다. 이는 이벤트 주도 아키텍처를 다루는 데 완벽합니다.
옵저버 패턴이란 무엇인가요?
옵서버 패턴은 하나의 객체(주체)가 자신의 상태 변화에 대해 여러 다른 객체(옵서버)에게 알릴 필요가 있을 때 이상적입니다. 이는 업데이트가 다양한 구성 요소에 푸시되어야 하며, 서로 간의 강한 결합을 만들지 않아야 하는 이벤트 기반 시스템에 완벽합니다. 이 패턴은 시스템의 서로 다른 부분이 독립적으로 변화에 반응해야 할 때, 깔끔한 아키텍처를 유지할 수 있도록 해줍니다.
실제 사용 사례: 이 패턴은 알림이나 경고를 보내는 시스템에서 일반적으로 사용됩니다. 예를 들어, 채팅 애플리케이션이나 주식 가격 추적기와 같이 업데이트가 실시간으로 사용자에게 푸시되어야 하는 경우입니다. 옵서버 패턴을 사용하면 핵심 논리를 변경하지 않고도 알림 유형을 쉽게 추가하거나 제거할 수 있습니다.
우리는 사용자 등록 시 이메일 및 SMS 알림이 모두 전송되는 간단한 알림 시스템을 구축하여 Spring Boot에서 이 패턴을 구현하는 방법을 시연할 것입니다.
1단계: Observer
인터페이스 생성
먼저 모든 옵서버가 구현할 공통 인터페이스를 정의합니다:
public interface Observer {
void update(String event);
}
이 인터페이스는 모든 옵서버가 주체가 변경될 때마다 호출되는 update()
메서드를 구현해야 하는 계약을 수립합니다.
2단계: EmailObserver
및 SMSObserver
구현
다음으로, 이메일 및 SMS 알림을 처리하기 위해 Observer
인터페이스의 두 가지 구체적인 구현체를 생성합니다.
EmailObserver
클래스
public class EmailObserver implements Observer {
@Override
public void update(String event) {
System.out.println("Email sent for event: " + event);
}
}
EmailObserver
는 이벤트에 대한 알림을 받을 때마다 이메일 알림을 전송하는 역할을 합니다.
SMSObserver
클래스
public class SMSObserver implements Observer {
@Override
public void update(String event) {
System.out.println("SMS sent for event: " + event);
}
}
SMSObserver
는 알림을 받을 때마다 SMS 알림을 처리합니다.
단계 3: UserService
클래스(주제) 생성
이제 주제로 작용하는 UserService
클래스를 만들어 사용자가 등록될 때 등록된 옵저버에게 알림을 보냅니다.
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserService {
private List<Observer> observers = new ArrayList<>();
// 옵저버 등록 메서드
public void registerObserver(Observer observer) {
observers.add(observer);
}
// 이벤트 발생 시 모든 등록된 옵저버에게 알림을 보내는 메서드
public void notifyObservers(String event) {
for (Observer observer : observers) {
observer.update(event);
}
}
// 새 사용자 등록 및 옵저버에게 알림 보내는 메서드
public void registerUser(String username) {
System.out.println("User registered: " + username);
notifyObservers("User Registration");
}
}
-
옵저버 목록: 모든 등록된 옵저버를 추적합니다.
-
registerObserver()
메서드: 새로운 옵저버를 목록에 추가합니다. -
notifyObservers()
메서드: 이벤트 발생 시 모든 등록된 옵저버에게 알림을 보냅니다. -
registerUser()
메서드: 새 사용자 등록 및 모든 옵저버에게 알림을 트리거합니다.
단계 4: 컨트롤러에서 옵저버 패턴 사용
마지막으로, 사용자 등록을 위한 엔드포인트를 노출하는 Spring Boot 컨트롤러를 생성할 것입니다. 이 컨트롤러는 UserService
에 EmailObserver
와 SMSObserver
를 모두 등록할 것입니다.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
public UserController() {
this.userService = new UserService();
// 옵저버 등록
userService.registerObserver(new EmailObserver());
userService.registerObserver(new SMSObserver());
}
@PostMapping("/register")
public ResponseEntity<String> registerUser(@RequestParam String username) {
userService.registerUser(username);
return ResponseEntity.ok("User registered and notifications sent!");
}
}
-
엔드포인트 (
/register
):username
매개변수를 받아들이고 사용자를 등록하여 모든 옵저버에게 알립니다. -
옵저버:
EmailObserver
와SMSObserver
둘 다UserService
에 등록되어 있으므로 사용자가 등록될 때마다 알림을 받습니다.
옵저버 패턴 테스트
이제 Postman이나 브라우저를 사용하여 구현을 테스트해 봅시다:
POST http://localhost:8080/api/register?username=JohnDoe
콘솔에 기대되는 출력:
User registered: JohnDoe
Email sent for event: User Registration
SMS sent for event: User Registration
시스템이 사용자를 등록하고 Email 및 SMS 옵저버에게 알림을 보내며 Observer 패턴의 유연성을 보여줍니다.
Observer 패턴의 실제 응용
-
알림 시스템: 특정 이벤트가 발생할 때 사용자에게 다양한 채널(이메일, SMS, 푸시 알림)을 통해 업데이트를 보내는 것입니다.
-
이벤트 주도 아키텍처: 사용자 활동이나 시스템 경고와 같은 특정 작업이 발생했을 때 여러 서브시스템에 알림을 보냅니다.
-
데이터 스트리밍: 실시간으로 데이터 변경 사항을 다양한 소비자에게 방송합니다(예: 라이브 주식 가격 또는 소셜 미디어 피드).
Spring Boot의 의존성 주입 사용 방법
지금까지 디자인 패턴을 보여주기 위해 수동으로 객체를 만들고 있습니다. 그러나 실제 세계의 Spring Boot 애플리케이션에서는 의존성 주입(DI)이 객체 생성을 관리하는 선호하는 방법입니다. DI를 사용하면 Spring이 클래스의 인스턴스화와 연결을 자동으로 처리하여 코드를 더 모듈식, 테스트 가능하고 유지보수 가능하게 만듭니다.
Spring Boot의 강력한 DI 기능을 활용하여 전략 패턴 예제를 리팩터링해봅시다. 이를 통해 Spring의 어노테이션을 사용하여 의존성을 관리하여 동적으로 결제 전략을 전환할 수 있게 됩니다.
Spring Boot의 DI를 사용한 업데이트된 전략 패턴
우리가 리팩토링한 예제에서는 @Component
, @Service
, @Autowired
와 같은 Spring의 어노테이션을 활용하여 의존성 주입 프로세스를 간소화할 것입니다.
단계 1: 결제 전략에 @Component
어노테이션 달기
먼저, 우리는 전략 구현체에 @Component
어노테이션을 붙여 Spring이 자동으로 감지하고 관리할 수 있도록 할 것입니다.
@Component("creditCardPayment")
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " with Credit Card");
}
}
@Component("payPalPayment")
public class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using PayPal");
}
}
-
@Component
어노테이션:@Component
를 추가함으로써, 이 클래스들을 Spring에서 관리되는 빈으로 취급하도록 합니다. 문자열 값("creditCardPayment"
및"payPalPayment"
)은 빈 식별자 역할을 합니다. -
유연성: 이 설정을 통해 적절한 빈 식별자를 사용하여 전략 간에 전환할 수 있습니다.
단계 2: PaymentService
리팩토링하여 의존성 주입 사용하기
이제, PaymentService
를 수정하여 @Autowired
와 @Qualifier
를 사용하여 특정 결제 전략을 주입하도록 할 것입니다.
@Service
public class PaymentService {
private final PaymentStrategy paymentStrategy;
@Autowired
public PaymentService(@Qualifier("payPalPayment") PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void processPayment(double amount) {
paymentStrategy.pay(amount);
}
}
-
@Service
어노테이션:PaymentService
를 Spring에서 관리되는 서비스 빈으로 표시합니다. -
@Autowired
: Spring이 필요한 의존성을 자동으로 주입합니다. -
@Qualifier
: 어떤PaymentStrategy
구현을 주입할지 지정합니다. 이 예제에서는"payPalPayment"
를 사용하고 있습니다. -
구성 용이성:
@Qualifier
값을 변경하기만 하면 비즈니스 로직을 변경하지 않고도 결제 전략을 전환할 수 있습니다.
3단계: 컨트롤러에서 리팩토링된 서비스 사용
리팩토링의 이점을 보기 위해 컨트롤러를 업데이트하여 PaymentService
를 사용해 보겠습니다:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class PaymentController {
private final PaymentService paymentService;
@Autowired
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@GetMapping("/pay")
public String makePayment(@RequestParam double amount) {
paymentService.processPayment(amount);
return "Payment processed using the current strategy!";
}
}
-
@Autowired
: 컨트롤러는 주입된 결제 전략과 함께PaymentService
를 자동으로 받습니다. -
GET 엔드포인트 (
/pay
): 접근 시 현재 구성된 전략(이 예제에서는 PayPal)을 사용하여 결제를 처리합니다.
DI를 사용한 리팩토링된 전략 패턴 테스트
이제 Postman이나 브라우저를 사용해 새 구현을 테스트해 보겠습니다:
GET http://localhost:8080/api/pay?amount=100
예상 출력:
Paid $100.0 using PayPal
PaymentService
에서 한정자를 "creditCardPayment"
로 변경하면 출력이 그에 따라 변경됩니다:
Paid $100.0 with Credit Card
의존성 주입 사용의 이점
- 느슨한 결합: 서비스와 컨트롤러는 결제가 처리되는 방식에 대한 세부 정보를 알 필요가 없습니다. 그들은 단순히 Spring이 올바른 구현을 주입하도록 의존합니다.
- 모듈성:
@Component
로 주석이 달린 새 클래스를 생성하고@Qualifier
를 조정하여 새로운 결제 방법(예:BankTransferPayment
,CryptoPayment
)을 쉽게 추가할 수 있습니다. - 구성 가능성: Spring 프로파일을 활용하여 환경(예: 개발 vs. 프로덕션)에 따라 전략을 전환할 수 있습니다.
예시: @Profile
를 사용하여 활성 프로필에 따라 자동으로 다른 전략을 주입할 수 있습니다:
@Component
@Profile("dev")
public class DevPaymentStrategy implements PaymentStrategy { /* ... */ }
@Component
@Profile("prod")
public class ProdPaymentStrategy implements PaymentStrategy { /* ... */ }
주요 요점
-
Spring Boot의 DI를 사용하면 객체 생성을 단순화하고 코드의 유연성을 향상시킬 수 있습니다.
-
DI와 결합된 전략 패턴을 사용하면 핵심 비즈니스 논리를 변경하지 않고도 서로 다른 전략 간에 쉽게 전환할 수 있습니다.
-
@Qualifier
와 Spring Profiles를 사용하면 서로 다른 환경이나 요구사항에 따라 애플리케이션을 구성할 수 있는 유연성을 제공합니다.
이 접근 방식은 코드의 가독성을 높일 뿐만 아니라 향후 더 고급 구성 및 확장 가능성을 대비하게 합니다. 다음 섹션에서는 Spring Boot 애플리케이션을 한 단계 끌어올리기 위한 모범 사례 및 최적화 팁을 살펴보겠습니다.
모범 사례 및 최적화 팁
일반적인 모범 사례
-
패턴을 과도하게 사용하지 마십시오: 필요할 때만 사용하십시오. 과도한 엔지니어링은 코드를 유지 관리하기 어렵게 만들 수 있습니다.
-
상속보다는 구성을 선호하십시오: 전략 및 관찰자와 같은 패턴이 이 원칙의 좋은 예입니다.
-
패턴을 유연하게 유지하십시오: 코드를 분리하기 위해 인터페이스를 활용하십시오.
성능 고려사항
-
싱글톤 패턴:
synchronized
또는빌 퓨 싱글톤 디자인
을 사용하여 스레드 안전성 보장 -
팩토리 패턴: 비용이 많이 드는 경우 객체를 캐시하십시오.
-
관찰자 패턴: 많은 관찰자가 있을 경우 블로킹을 방지하기 위해 비동기 처리를 사용하십시오.
고급 주제
-
팩토리 패턴을 사용하여 동적 클래스 로딩을 위해 Reflection 사용.
-
환경에 따라 전략을 전환하기 위해 Spring Profiles 활용.
-
API 엔드포인트에 Swagger 문서 추가.
결론 및 주요 포인트
이 튜토리얼에서는 싱글톤, 팩토리, 전략, 옵저버와 같은 강력한 디자인 패턴을 탐색하고 Spring Boot에서 구현하는 방법을 소개했습니다. 각 패턴을 간단히 요약하고 어떤 상황에 가장 적합한지 강조해 보겠습니다:
싱글톤 패턴:
-
요약: 클래스가 하나의 인스턴스만 가지도록 보장하고 전역 액세스 지점을 제공합니다.
-
가장 적합한 상황: 설정, 데이터베이스 연결, 또는 로깅 서비스와 같은 공유 리소스를 관리할 때 적합합니다. 전체 애플리케이션 전체에서 공유 인스턴스에 대한 액세스를 제어하려는 경우 이상적입니다.
팩토리 패턴:
-
요약: 정확한 클래스를 지정하지 않고 객체를 생성하는 방법을 제공합니다. 이 패턴은 객체 생성을 비즈니스 로직에서 분리합니다.
-
최적: 입력 조건에 따라 다른 유형의 객체를 생성해야 하는 시나리오에 적합합니다. 예를 들어 이메일, SMS 또는 푸시 알림을 보내는 경우입니다. 코드를 더 모듈식으로 만들고 확장 가능하게 하는 데 좋습니다.
전략 패턴:
-
요약: 일련의 알고리즘을 정의하고 각각을 캡슐화하며 교환 가능하게 만드는 것을 허용합니다. 이 패턴은 런타임에 알고리즘을 선택하는 데 도움이 됩니다.
-
최적: 동적으로 다른 동작이나 알고리즘을 전환해야 하는 경우에 적합합니다. 예를 들어 전자 상거래 애플리케이션에서 다양한 결제 방법을 처리해야 하는 경우입니다. 코드를 유연하게 유지하고 개방/폐쇄 원칙을 준수합니다.
옵저버 패턴:
-
요약: 객체 간의 일대다 종속성을 정의하여 한 객체의 상태가 변경되면 모든 종속 요소가 자동으로 알림을받습니다.
-
최적인 경우: 알림 서비스, 채팅 앱의 실시간 업데이트 또는 데이터 변경에 대응해야 하는 시스템과 같은 이벤트 주도 시스템에 적합합니다. 구성 요소를 분리하고 시스템을 확장 가능하게 만드는 데 이상적입니다.
다음 단계는?
이러한 필수 디자인 패턴을 배웠으니, 기존 프로젝트에 통합하여 코드 구조와 확장성을 향상시킬 수 있는지 확인해보세요. 추가 탐구를 위한 몇 가지 제안은 다음과 같습니다:
-
실험: 데코레이터, 프록시, 빌더와 같은 다른 디자인 패턴을 구현해 보세요. 도구 상자를 확장할 수 있습니다.
-
실습: 이러한 패턴을 사용하여 기존 프로젝트를 리팩토링하고 유지보수성을 향상시키세요.
-
공유: 궁금한 점이 있거나 경험을 공유하고 싶다면 언제든지 연락해 주세요!
이 안내서가 여러분이 자바에서 디자인 패턴을 효과적으로 활용하는 방법을 이해하는 데 도움이 되었기를 바랍니다. 계속해서 실험하고 코딩을 즐기세요!
Source:
https://www.freecodecamp.org/news/how-to-use-design-patterns-in-java-with-spring-boot/