프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 규약 형태로 만들어놓은 것
하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴
class Singleton {
private static class singleInstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return singleInstanceHolder.INSTANCE;
}
}
public class HelloWorld {
public static void main(String[] args) {
Singleton a = Singleton.getInstance();
Singleton b = Singleton.getInstance();
System.out.println(a.hashCode());
System.out.println(b.hashCode());
if (a == b) {
System.out.println(true);
}
}
}
TDD(Test Driven Development)를 할 때 걸림돌이 됨의존성이란?
종속성이라고도 하며, ‘A가 B에 의존성이 있다’라는 것은 B의 변경사항에 대해 A 또한 변해야하는 것을 의미
싱글톤 패턴은 사용하기가 쉽고 실용적이지만, 모듈 간의 결합을 강하게 만들 수 있다는 단점을 만들 수 있음
이 때, 의존성 주입(DI, Dependency Injection)을 통해 모듈 간의 결합을 조금 더 느슨하게 만들어 해결할 수 있음
의존성 주입 상세 설명
쇼핑몰에서는 고객이 제품을 주문하고, 다양한 결제 수단(카드, 페이팔, 은행 송금 등)을 통해 결제를 처리
class PaymentService {
public void processPayment(String paymentType) {
if (paymentType.equals("CARD")) {
System.out.println("카드 결제 처리");
} else if (paymentType.equals("PAYPAL")) {
System.out.println("PayPal 결제 처리");
} else if (paymentType.equals("BANK_TRANSFER")) {
System.out.println("은행 송금 결제 처리");
} else {
throw new UnsupportedOperationException("지원되지 않는 결제 방식");
}
}
}
유연성 부족
새로운 결제 방식이 추가될 때마다 PaymentService 코드를 수정해야함. 결제 방식이 늘어날 수록 조건문도 늘어나며, 코드도 방대해짐.
테스트의 어려움
각 결제 방식을 테스트하려면 PaymentService의 여러 조건문을 계속 테스트해야 함
의존성 주입을 적용하면, PaymentService는 결제 방식에 대해 전혀 몰라도 되고, 단지 추상화된 PaymentProcessor 인터페이스에 의존하게 됨
// 1. 결제 처리 인터페이스 (추상화 레이어)
interface PaymentProcessor {
void processPayment();
}
// 2. 카드 결제 구현체
class CardPaymentProcessor implements PaymentProcessor {
@Override
public void processPayment() {
System.out.println("카드 결제 처리");
}
}
// 3. PayPal 결제 구현체
class PayPalPaymentProcessor implements PaymentProcessor {
@Override
public void processPayment() {
System.out.println("PayPal 결제 처리");
}
}
// 4. 은행 송금 결제 구현체
class BankTransferPaymentProcessor implements PaymentProcessor {
@Override
public void processPayment() {
System.out.println("은행 송금 결제 처리");
}
}
// 5. 결제 서비스 클래스 (의존성 주입)
class PaymentService {
private PaymentProcessor paymentProcessor;
// 생성자를 통한 의존성 주입
public PaymentService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public void processPayment() {
paymentProcessor.processPayment();
}
}
public class Main {
public static void main(String[] args) {
// 카드 결제 주입
PaymentProcessor cardProcessor = new CardPaymentProcessor();
PaymentService cardPaymentService = new PaymentService(cardProcessor);
cardPaymentService.processPayment(); // 카드 결제 처리
// PayPal 결제 주입
PaymentProcessor paypalProcessor = new PayPalPaymentProcessor();
PaymentService paypalPaymentService = new PaymentService(paypalProcessor);
paypalPaymentService.processPayment(); // PayPal 결제 처리
// 은행 송금 결제 주입
PaymentProcessor bankProcessor = new BankTransferPaymentProcessor();
PaymentService bankPaymentService = new PaymentService(bankProcessor);
bankPaymentService.processPayment(); // 은행 송금 결제 처리
}
}
**유연성**
- `PaymentService`는 다양한 결제 방식의 구체적인 내용을 전혀 모른 채로, 단순히 `PaymentProcessor` 인터페이스를 통해 작업
- 새로운 결제 방식이 추가되더라도 `PaymentService` 코드를 수정할 필요가 없음
**테스트 용이**
- 각 결제 방식에 대해 별도의 구현체가 있으므로, 이 구현체만 따로 **단위 테스트**가 가능
PaymentService 코드는 전혀 건드리지 않고, 새로운 PaymentProcessor 구현체만 추가하면 됨PaymentService는 단순히 결제를 처리할 뿐, 결제 방식의 세부 사항을 몰라도 됨. 이 추상화 덕분에 서비스 코드는 매우 깔끔하고 읽기 쉬워짐PaymentProcessor 구현체가 필요PaymentService는 CardPaymentProcessor나 PayPalPaymentProcessor 같은 하위 모듈의 구체적인 구현에 의존하지 않고, PaymentProcessor 인터페이스에 의존함PaymentService)과 하위 모듈(CardPaymentProcessor, PayPalPaymentProcessor 등)은 모두 PaymentProcessor라는 추상화에 의존PaymentProcessor)는 구체적인 결제 방식에 대해 전혀 의존하지 않음객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴
상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴
라떼 레시피와 아메리카노 레시피, 우유 레시피라는 구체적인 내용이 들어 있는 하위 클래스가 컨베이어 벨트를 통해 전달되고, 상위 클래스인 바리스타 공장에서 이 레시피들을 토대로 우유 등을 생산하는 생산 공정을 생각하기
enum CoffeeType {
LATTE,
ESPRESSO
}
abstract class Coffee {
protected String name;
public String getName() {
return name;
}
}
class Latte extends Coffee {
public Latte() {
name = "latte";
}
}
class Espresso extends Coffee {
public Espresso() {
name = "Espresso";
}
}
class CoffeeFactory {
public static Coffee createCoffee(CoffeeType type) {
switch (type) {
case LATTE:
return new Latte();
case ESPRESSO:
return new Espresso();
default:
throw new IllegalArgumentException("Invalid coffee type : " + type);
}
}
}
public class Main {
public static void main(String[] args) {
Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE);
System.out.println(coffee.getName()); // latte
}
}
Main 클래스)는 아무것도 수정할 필요가 없음 class Mocha extends Coffee {
public Mocha() {
name = "Mocha";
}
}
class CoffeeFactory {
public static Coffee createCoffee(CoffeeType type) {
switch (type) {
case LATTE:
return new Latte();
case ESPRESSO:
return new Espresso();
case MOCHA:
return new Mocha(); // Mocha 추가
default:
throw new IllegalArgumentException("Invalid coffee type : " + type);
}
}
}
- 이렇게 되면 팩토리 클래스의 코드가 점점 커지게 됨