

결제 기능 자체는 동일하지만 결제 방식만 변경하는 경우
ShoppingCart (Context)
↓
PaymentStrategy (Strategy)
↙ ↘
LUNACard KAKAOCard
PaymentStrategy → 결제 전략 인터페이스LUNACardStrategy → LUNACard 결제 전략KAKAOCardStrategy → KAKAOCard 결제 전략ShoppingCart → 선택된 결제 전략을 사용하는 Contextimport java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
// 1. 전략 인터페이스
interface PaymentStrategy {
void pay(int amount);
}
// 2-1. 구체적인 전략 1: KAKAOCard
class KAKAOCardStrategy implements PaymentStrategy {
private String name;
private String cardNumber;
private String cvv;
private String dateOfExpiry;
public KAKAOCardStrategy(String nm, String ccNum, String cvv, String expiryDate) {
this.name = nm;
this.cardNumber = ccNum;
this.cvv = cvv;
this.dateOfExpiry = expiryDate;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using KAKAOCard.");
}
}
// 2-2. 구체적인 전략 2: LUNACard
class LUNACardStrategy implements PaymentStrategy {
private String emailId;
private String password;
public LUNACardStrategy(String email, String pwd) {
this.emailId = email;
this.password = pwd;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using LUNACard.");
}
}
// 3. 상품(Item) 클래스
class Item {
private String name;
private int price;
public Item(String name, int cost) {
this.name = name;
this.price = cost;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
// 4. 쇼핑카트 (컨텍스트) 클래스
class ShoppingCart {
List<Item> items;
public ShoppingCart() {
this.items = new ArrayList<Item>();
}
public void addItem(Item item) {
this.items.add(item);
}
public void removeItem(Item item) {
this.items.remove(item);
}
public int calculateTotal() {
int sum = 0;
for (Item item : items) {
sum += item.getPrice();
}
return sum;
}
// 전략 객체를 매개변수로 받아 결제를 수행
public void pay(PaymentStrategy paymentMethod) {
int amount = calculateTotal();
paymentMethod.pay(amount);
}
}
// 5. 실행 클래스
public class HelloWorld {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
Item A = new Item("kundolA", 100);
Item B = new Item("kundolB", 300);
cart.addItem(A);
cart.addItem(B);
// LUNACard로 결제 (100 + 300 = 400원)
cart.pay(new LUNACardStrategy("kundol@example.com", "pukubababo"));
// KAKAOCard로 결제 (100 + 300 = 400원)
cart.pay(new KAKAOCardStrategy("Ju hongchul", "123456789", "123", "12/01"));
}
}
cart.pay(new LUNACardStrategy(...));
cart.pay(new KAKAOCardStrategy(...));
ShoppingCart의 결제 로직을 변경하지 않고 전달하는 전략 객체만 변경하여 다른 결제 방식 사용
400 paid using LUNACard.
400 paid using KAKAOCard.
컨텍스트(Context)
특정 작업을 수행하는 데 필요한 상황이나 관련 정보를 의미하며, 전략 패턴에서는 전략 객체를 사용하여 실제 작업을 수행하는 객체를 의미함.
변하는 행위를 전략으로 분리하여, Context의 코드를 수정하지 않고 전략만 교체할 수 있도록 하는 패턴
즉, 인증 로직 자체를 변경하지 않고 사용할 인증 전략만 교체하는 구조
var passport = require('passport')
, LocalStrategy = require('passport-local').Strategy;
passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function(err, user) {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));
핵심
Passport는 인증 방법을 각각 전략으로 분리하여 LocalStrategy, OAuth 전략 등을 상황에 따라 선택할 수 있음.

Subject
│
│ 상태 변경
↓
notify()
│
├──→ Observer A
├──→ Observer B
└──→ Observer C
어떤 사용자를 팔로우한 상황
사용자(Subject)
│
│ 게시물 업로드
↓
알림
↙ ↓ ↘
팔로워 A B C
Subject와 Observer를 반드시 별도의 객체로 명확히 구분해야 하는 것은 아니며, 구현 방식에 따라 상태를 가진 객체가 직접 옵저버들을 관리할 수도 있음.
옵저버 패턴은 이벤트 기반 시스템이나 MVC와 같은 구조에서 활용될 수 있음
Model
│
│ 상태 변경
↓
notify / update
│
↓
View
Model의 데이터 변경View에 변경 사실 전달View가 새로운 상태를 기반으로 화면 갱신※ MVC 전체가 옵저버 패턴인 것은 아니며, Model의 변경을 View에 전달하는 부분에서 옵저버 패턴을 활용할 수 있음
한 객체의 상태가 변하면 해당 객체를 관찰하고 있는 여러 객체에게 자동으로 변화를 전달하는 패턴
interface Subject {
void register(Observer obj);
void unregister(Observer obj);
void notifyObservers();
Object getUpdate(Observer obj);
}
interface Observer {
void update();
}
Subject → 옵저버 등록·삭제·알림을 담당하는 주체 인터페이스Observer → 상태 변경 알림을 받기 위한 인터페이스update() → 주체의 상태가 변경되었을 때 옵저버가 실행하는 메서드class Topic implements Subject {
private List<Observer> observers;
private String message;
@Override
public void register(Observer obj) {
if (!observers.contains(obj))
observers.add(obj);
}
@Override
public void unregister(Observer obj) {
observers.remove(obj);
}
@Override
public void notifyObservers() {
observers.forEach(Observer::update);
}
@Override
public Object getUpdate(Observer obj) {
return this.message;
}
public void postMessage(String msg) {
this.message = msg;
notifyObservers();
}
}
Topic → 상태가 변경되는 Subjectobservers → 현재 등록된 옵저버 목록register() → 옵저버 등록unregister() → 옵저버 제거notifyObservers() → 모든 옵저버의 update() 호출postMessage() → 새로운 메시지 저장 후 모든 옵저버에게 변경 알림class TopicSubscriber implements Observer {
private String name;
private Subject topic;
@Override
public void update() {
String msg = (String) topic.getUpdate(this);
System.out.println(name + ":: got message >> " + msg);
}
}
TopicSubscriber → Observer를 구현한 실제 구독자update() 호출 시 Topic의 최신 메시지 조회Topic topic = new Topic();
Observer a = new TopicSubscriber("a", topic);
Observer b = new TopicSubscriber("b", topic);
Observer c = new TopicSubscriber("c", topic);
topic.register(a);
topic.register(b);
topic.register(c);
topic.postMessage("amumu is op champion!!");
Topic에 메시지 등록
↓
notifyObservers()
↓
┌──────┼──────┐
↓ ↓ ↓
a b c
update update update
a, b, c를 Topic의 옵저버로 등록Topic에 새로운 메시지 발생notifyObservers() 실행update() 호출핵심:
Topic의 상태가 변경되면 등록된 모든TopicSubscriber에게 자동으로 변경 사실 전달
extendsclass Child extends Parent {
}
implementsclass Topic implements Subject {
}
옵저버 예제에서는
Topic implements Subject,TopicSubscriber implements Observer형태로 인터페이스를 구현
Proxy 객체를 이용해 객체의 변화를 감지하는 방식으로 구현 가능const p = new Proxy(target, handler);
target → Proxy가 감싸는 원본 객체handler → 원본 객체의 동작을 가로챘을 때 수행할 로직get() → 속성 조회 가로채기set() → 속성 변경 가로채기has() → in 연산자 사용 가로채기예를 들어,
const handler = {
get: function(target, name) {
return name === 'name'
? `${target.a} ${target.b}`
: target[name];
}
};
const p = new Proxy(
{ a: 'KUNDOL', b: 'IS AMUMU ZANGIN' },
handler
);
console.log(p.name);
KUNDOL IS AMUMU ZANGIN
name 속성이 존재하지 않음p.name 접근 발생get()이 접근을 가로챔a, b 값을 이용해 새로운 값 반환function createReactiveObject(target, callback) {
const proxy = new Proxy(target, {
set(obj, prop, value) {
if (value !== obj[prop]) {
const prev = obj[prop];
obj[prop] = value;
callback(
`${prop}가 [${prev}] >> [${value}] 로 변경되었습니다.`
);
}
return true;
}
});
return proxy;
}
사용 예시
const a = {
"형규": "솔로"
};
const b = createReactiveObject(a, console.log);
b.형규 = "솔로";
b.형규 = "커플";
형규가 [솔로] >> [커플] 로 변경되었습니다.
b.형규 값 변경
↓
Proxy의 set() 호출
↓
기존 값과 새로운 값 비교
↓
값이 변경된 경우 callback 실행
↓
변경 사실 전달
set()이 변경 감지옵저버 패턴과의 연결: 객체의 상태 변화를 감지하고, 변화가 발생했을 때 등록된 동작을 실행한다는 점에서 옵저버 패턴과 유사한 구조
Proxy를 활용하여 객체의 상태 변화를 감지reactive() 등으로 생성한 반응형 데이터가 변경되면 관련된 화면도 자동 갱신데이터 변경
↓
Proxy가 변경 감지
↓
관련 반응형 효과 실행
↓
DOM 갱신
get() → 어떤 값이 사용되는지 추적하는 데 활용set() → 값의 변경 감지에 활용DOM(Document Object Model)
웹 페이지를 구성하는 HTML 요소들을 객체 형태로 표현한 구조
※ 정확히 말하면 Vue 3의 반응성 시스템 전체를 단순한 GoF 옵저버 패턴 하나로만 설명하기는 어렵고, Proxy를 통한 변경 감지와 의존성 추적·효과 실행이 결합된 반응형 시스템으로 이해하는 것이 더 정확함.
옵저버 패턴은 Subject의 상태 변화를 감지하여 관련 Observer들에게 자동으로 알리고, Java에서는 인터페이스와 옵저버 목록으로, JavaScript에서는 Proxy 등을 활용해 비슷한 반응형 구조를 구현할 수 있음.