

Client
public class Client {
public static void main(String[] args) {
Game game = new Game();
game.setRedTeamScore(10);
game.setBlueTeamScore(20);
// 상태를 클라이언트가 직접 저장
int blueTeamScore = game.getBlueTeamScore();
int redTeamScore = game.getRedTeamScore();
// 상태를 클라이언트가 직접 복구
Game restoredGame = new Game();
restoredGame.setBlueTeamScore(blueTeamScore);
restoredGame.setRedTeamScore(redTeamScore);
}
}
Game
public class Game implements Serializable {
private int redTeamScore;
private int blueTeamScore;
public int getRedTeamScore() {
return redTeamScore;
}
public void setRedTeamScore(int redTeamScore) {
this.redTeamScore = redTeamScore;
}
public int getBlueTeamScore() {
return blueTeamScore;
}
public void setBlueTeamScore(int blueTeamScore) {
this.blueTeamScore = blueTeamScore;
}
}
Game객체의 상태 복구
Game 객체의 내부 상태(스코어)를 가져와 복사한 후 새로운 객체에 설정.
- 문제점
개선 사항
GameSave 클래스 도입:
Game 객체의 상태를 저장하고 복구할 수 있는 불변 객체(GameSave) 도입.Game 내부에서 처리.save()와 restore() 메서드 추가:
Game.save(): 현재 상태를 GameSave 객체로 저장.Game.restore(): GameSave 객체를 기반으로 상태 복구.코드 분석
GameSave
public final class GameSave {
private final int blueTeamScore; // 블루 팀 스코어
private final int redTeamScore; // 레드 팀 스코어
public GameSave(int blueTeamScore, int redTeamScore) {
this.blueTeamScore = blueTeamScore;
this.redTeamScore = redTeamScore;
}
public int getBlueTeamScore() {
return blueTeamScore;
}
public int getRedTeamScore() {
return redTeamScore;
}
}
Game
public class Game {
private int redTeamScore;
private int blueTeamScore;
public int getRedTeamScore() {
return redTeamScore;
}
public void setRedTeamScore(int redTeamScore) {
this.redTeamScore = redTeamScore;
}
public int getBlueTeamScore() {
return blueTeamScore;
}
public void setBlueTeamScore(int blueTeamScore) {
this.blueTeamScore = blueTeamScore;
}
// 현재 상태를 저장
public GameSave save() {
return new GameSave(this.blueTeamScore, this.redTeamScore);
}
// 이전 상태로 복구
public void restore(GameSave gameSave) {
this.blueTeamScore = gameSave.getBlueTeamScore();
this.redTeamScore = gameSave.getRedTeamScore();
}
}
Client
public class Client {
public static void main(String[] args) {
Game game = new Game();
game.setBlueTeamScore(10);
game.setRedTeamScore(20);
// 현재 상태 저장
GameSave save = game.save();
// 상태 변경
game.setBlueTeamScore(12);
game.setRedTeamScore(22);
// 이전 상태로 복구
game.restore(save);
// 출력: 복구된 상태
System.out.println(game.getBlueTeamScore()); // 10
System.out.println(game.getRedTeamScore()); // 20
}
}
이러면 새로운 객체를 생성하지 않고도 상태를 복구할 수 있다!
동작 과정
현재 상태 저장:
Game.save() 메서드를 호출하여 현재 상태를 GameSave 객체로 캡슐화.상태 복구:
Game.restore(GameSave) 메서드를 호출하여 GameSave 객체를 기반으로 상태 복구.상태 변경과 복구:
Game의 상태를 마음대로 변경한 후, 저장된 상태로 되돌릴 수 있음.
구성 요소
Originator (기원자):
save())하거나 복구(restore())하는 객체.Game 클래스가 Originator 역할을 한다.Memento (메멘토):
GameSave 클래스가 Memento 역할을 한다.Caretaker (관리자):
Client 클래스가 Caretaker 역할을 한다.장점
캡슐화를 지키면서 상태 저장
save() 및 restore() 메서드를 통해 상태를 관리.상태 관리의 책임 분리
Caretaker가 담당하므로, 객체의 책임이 단순화된다.클라이언트 코드의 독립성
Memento와 Originator 내부에서 처리.단점
메모리 사용량 증가
Memento 객체를 자주 생성하는 경우 메모리 사용량이 증가할 수 있다.복잡성 증가
Memento, Caretaker)와 로직이 필요하므로, 코드 복잡도가 증가할 수 있다.자바
1) 객체 직렬화, java.io.Serializable
public class MementoInJava {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// TODO Serializable
Game game = new Game();
game.setRedTeamScore(10);
game.setBlueTeamScore(20);
// TODO 직렬화
try(FileOutputStream fileOut = new FileOutputStream("GameSave.hex");
ObjectOutputStream out = new ObjectOutputStream(fileOut))
{
out.writeObject(game);
}
game.setBlueTeamScore(25);
game.setRedTeamScore(15);
// TODO 역직렬화
try(FileInputStream fileIn = new FileInputStream("GameSave.hex");
ObjectInputStream in = new ObjectInputStream(fileIn))
{
game = (Game) in.readObject();
System.out.println(game.getBlueTeamScore());
System.out.println(game.getRedTeamScore());
}
}
}
2) java.util.Date
1) 발행(publish)-구독(subscribe) 패턴을 구현할 수 있다.
2) 상태 변화를 자동으로 알림으로써 클라이언트 간 결합도를 낮추고 확장성을 높인다.

ChatServerpublic class ChatServer {
private Map<String, List<String>> messages;
public ChatServer() {
this.messages = new HashMap<>();
}
public void add(String subject, String message) {
if (messages.containsKey(subject)) {
messages.get(subject).add(message);
} else {
List<String> messageList = new ArrayList<>();
messageList.add(message);
messages.put(subject, messageList);
}
}
public List<String> getMessage(String subject) {
return messages.get(subject);
}
}
UserChatServer를 직접 호출하여 메시지를 저장하거나 가져옴.public class User {
private ChatServer chatServer;
public User(ChatServer chatServer) {
this.chatServer = chatServer;
}
public void sendMessage(String subject, String message) {
chatServer.add(subject, message);
}
public List<String> getMessage(String subject) {
return chatServer.getMessage(subject);
}
}
Clientpublic class Client {
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
User user1 = new User(chatServer);
user1.sendMessage("디자인패턴", "옵저버 패턴입니다.");
user1.sendMessage("롤드컵2021", "LCK 화이팅!");
User user2 = new User(chatServer);
System.out.println(user2.getMessage("디자인패턴")); // 수동 요청
user1.sendMessage("디자인패턴", "코드 분석 중...");
System.out.println(user2.getMessage("디자인패턴")); // 다시 요청
}
}
Before 문제점
구독-발행 구조 부재:
결합도 높음:
User와 ChatServer가 강하게 결합되어 있어 변경 및 확장이 어렵다.확장성 부족:
알림 비효율성:
수정 내용

구독-발행 구조 도입:
ChatServer는 발행자(Subject) 역할, User는 구독자(Observer) 역할을 수행한다.결합도 감소:
Subscriber 인터페이스를 통해 User와 ChatServer 간의 강한 결합을 제거했다.확장성 향상:
알림 자동화:
ChatServerregister), 구독 취소(unregister), 알림 전송(sendMessage) 기능을 제공한다.public class ChatServer {
private Map<String, List<Subscriber>> subscribers = new HashMap<>();
public void register(String subject, Subscriber subscriber) {
subscribers.computeIfAbsent(subject, k -> new ArrayList<>()).add(subscriber);
}
public void unregister(String subject, Subscriber subscriber) {
if (subscribers.containsKey(subject)) {
subscribers.get(subject).remove(subscriber);
}
}
public void sendMessage(User user, String subject, String message) {
if (subscribers.containsKey(subject)) {
String userMessage = user.getName() + ": " + message;
subscribers.get(subject).forEach(s -> s.handleMessage(userMessage));
}
}
}
SubscriberhandleMessage 메서드를 제공한다.public interface Subscriber {
void handleMessage(String message);
}
UserSubscriber 인터페이스를 구현하여 알림을 받을 수 있는 구독자로 동작.public class User implements Subscriber {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void handleMessage(String message) {
System.out.println(message);
}
}
Clientpublic class Client {
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
User user1 = new User("keesun");
User user2 = new User("whiteship");
chatServer.register("디자인패턴", user1);
chatServer.register("디자인패턴", user2);
chatServer.sendMessage(user1, "디자인패턴", "옵저버 패턴 구현 완료!");
}
}
Subject (발행자): 상태 변화를 관리하며, 구독자들에게 알림을 보냄.
ChatServer가 해당한다.Observer)를 등록/해제하며 메시지를 발행하면 구독자들에게 알림을 보낸다.Observer (구독자): 발행자의 상태 변화를 감지하고 처리.
User가 해당한다.Subscriber 인터페이스를 구현하여 알림을 받을 준비가 되어 있는 객체이다.ConcreteObserver (구체적인 구독자): 실제 알림을 처리하는 Observer의 구체적인 구현체.
User 자체가 ConcreteObserver이다.Subscriber 인터페이스를 구현하고, 알림이 올 때 메시지를 출력하는 기능을 구현하였다.장점
• 상태를 변경하는 객체(publisher)와 변경을 감지하는 객체(subsriber)의 관계를 느슨하게 유지할 수 있다.
• Subject의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지할 수 있다.
• 런타임에 옵저버를 추가하거나 제거할 수 있다.
단점
• 복잡도가 증가한다.
• 다수의 Observer 객체를 등록 이후 해지 않는다면 memory leak이 발생할 수도 있다.
1-1. PropertyChangeListener
PropertyChangeListener와 PropertyChangeSupport를 활용해 상태 변화를 감지하고 구독자에게 알림.import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
public class PropertyChangeExample {
static class User implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
System.out.println(evt.getNewValue());
}
}
static class Subject {
PropertyChangeSupport support = new PropertyChangeSupport(this);
public void addObserver(PropertyChangeListener observer) {
support.addPropertyChangeListener(observer);
}
public void removeObserver(PropertyChangeListener observer) {
support.removePropertyChangeListener(observer);
}
public void add(String message) {
support.firePropertyChange("eventName", null, message);
}
}
public static void main(String[] args) {
Subject subject = new Subject();
User observer = new User();
subject.addObserver(observer);
subject.add("자바 PCL 예제 코드");
subject.removeObserver(observer);
subject.add("이 메시지는 볼 수 없지..");
}
}
1-2. Flow API
Publisher와 Subscriber로 발행-구독 구현.import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
public class FlowInJava {
public static void main(String[] args) throws InterruptedException {
Flow.Publisher<String> publisher = new SubmissionPublisher<>();
Flow.Subscriber<String> subscriber = new Flow.Subscriber<String>() {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription) {
System.out.println("Subscribed!");
this.subscription = subscription;
this.subscription.request(1); // 데이터 요청
}
@Override
public void onNext(String item) {
System.out.println("Received: " + item);
subscription.request(1); // 다음 데이터 요청
}
@Override
public void onError(Throwable throwable) {
System.err.println("Error occurred: " + throwable.getMessage());
}
@Override
public void onComplete() {
System.out.println("All items processed");
}
};
publisher.subscribe(subscriber);
((SubmissionPublisher<String>) publisher).submit("Hello, Java Flow!");
System.out.println("Publisher 작업 완료");
}
}
2-1. ApplicationEvent와 ApplicationEventPublisher
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
public class MyEvent {
private String message;
public MyEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
@Component
public class MyEventListener {
@EventListener(MyEvent.class)
public void onApplicationEvent(MyEvent event) {
System.out.println(event.getMessage());
}
}
@Component
public class MyRunner implements ApplicationRunner {
private final ApplicationEventPublisher publisher;
public MyRunner(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@Override
public void run(ApplicationArguments args) throws Exception {
publisher.publishEvent(new MyEvent("Hello Spring Event!"));
}
}
@SpringBootApplication
public class ObserverInSpring {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(ObserverInSpring.class);
app.setWebApplicationType(WebApplicationType.NONE);
app.run(args);
}
}