이번 포스팅에서는 GoF 디자인 패턴 중 하나인 옵저버 패턴에 대해 정리할 것이다.

예제를 직접 작성해보고 패턴의 로직을 익혀볼 것이다!

 

옵저버 패턴이란?

출처 : https://velog.io/@weekbelt/%EC%98%B5%EC%A0%80%EB%B2%84-Observer-%ED%8C%A8%ED%84%B4

소프트웨어 디자인 패턴 중 하나로, 객체 간의 일대다(one-to-many) 의존 관계를 정의하는 패턴이다.

이 패턴은 어떤 객체의 상태 변화가 발생했을 때 그 객체에 의존하는 다른 객체들에게 자동으로 알림을 보내고 상태 변화에 대한 처리를 할 수 있도록 해준다. 이는 객체 간의 결합도를 낮추고 유연한 구조를 만들어준다.

주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용되어, Pub/Sub (발행/구독) 모델로도 알려져 있다.

ex) 발행자(Subject): 유튜브 채널, 관찰자(Observer): 구독자들

 

주요 요소

1. Subject (주체) 

  • 상태 변화를 감지하고, 옵저버들을 관리하는 객체이다.
  • 상태 변화가 발생하면 등록된 옵저버들에게 알림을 보낸다.

2. Observer (옵저버)

  • Subject의 상태 변화를 감지하고, 상태 변화에 대한 처리를 수행하는 인터페이스이다.

3. ConcreteSubject (구체적인 주체)

  • 실제 상태 변화가 일어나는 객체로서 Subject를 구현한 클래스이다.

4. ConcreteObserver (구체적인 옵저버)

  • 실제로 상태 변화를 감지하고 처리하는 객체로서 Observer를 구현한 클래스이다.

 

  • 옵저버 패턴에서는 한개의 관찰 대상자(Subject)와 여러개의 관찰자(Observer A, B, C)로 일 대 다 관계로 구성되어 있다.
  • 관찰 대상자(Subject)의 상태가 바뀌면 변경사항을 옵저버 한태 통보해준다. (notify)
  • 대상자로부터 통보를 받은 관찰자(Observer)는 값을 바꾸거나 삭제하는 등 적절히 대응한다. (update)
  • 관찰자(Observer)들은 언제든 관찰 대상자(Subject)의 그룹에서 추가/삭제 될 수 있다.

 

 

옵저버 패턴 예제

이번 옵저버 패턴 예시로 쓸 소재는 온라인 주문 알림 시스템이다. 온라인 쇼핑몰에서 주문이 들어왔을 때, 옵저버 패턴을 활용해 알림 시스템을 만들어볼 것이다.

 

 

관찰 대상자(Subject): 주문 클래스

import java.util.ArrayList;
import java.util.List;

public class Order {
    private List<Observer> observers = new ArrayList<>();
    private String status;

    public void attach(Observer observer) {
        observers.add(observer);
    }

    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(this);
        }
    }

    public void setStatus(String status) {
        this.status = status;
        notifyObservers();
    }

    public String getStatus() {
        return status;
    }
}

 

관찰자(Observer) 인터페이스

public interface Observer {
    void update(Order order);
}

 

구체적인 관찰 대상자(ConcreteSubect): 주문 상태 변경

public class OrderNotification implements Observer {
    @Override
    public void update(Order order) {
        System.out.println("주문 상태 업데이트: " + order.getStatus());
    }
}

 

구체적인 관찰자(ConcreteObserver): 옵저버 등록 및 주문 상태 변화에 대한 처리

public class Main {
    public static void main(String[] args) {
        Order order = new Order();
        OrderNotification notification = new OrderNotification();

        order.attach(notification);

        order.setStatus("배송 중");
        order.setStatus("배송 완료");
    }
}

Order 객체와 OrderNotification 객체를 각각 생성한 후, 주문 상태의 변화에 따라 주문 알림이 옵저버에게 전달되고 출력된다.

order.attach(notification)과 같이 Order 클래스의 attach 메서드를 통하여 여러개의 관찰자 리스트에 추가한다.

관찰자 리스트에 포함된 객체들에 상태 변경 알림 setStatus를 통해 상태 변화를 전달하고 출력할 수 있다.

 

이로써 개념과 예제를 통해 옵저버 패턴에 대해 알아보았다.

객체의 느슨한 결합을 유지하면서 상태 변화를 처리할 수 있다는 점에서 확장 가능한 코드를 작성할 수 있었다.

주로 이벤트 핸들링이나 MVC 아키텍쳐에서 사용된다고 하니, 패턴 활용해서 나중에 코드 작성해보려고 한다!

'java > Design Pattern' 카테고리의 다른 글

전략 패턴 (Strategy Pattern)  (0) 2023.08.01

이번 포스팅에서는 GoF 디자인 패턴 중 하나인 전략 패턴에 대해 알아보며, 예제를 직접 작성해보고 패턴의 로직을 익혀보겠다.

 

 

전략 패턴이란?

출처 : https://velog.io/@y_dragonrise/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%A0%84%EB%9E%B5-%ED%8C%A8%ED%84%B4Strategy-Pattern

GoF 디자인 패턴 중 하나로, 전략 패턴은 전략을 쉽게 바꿀 수 있도록 하는 디자인 패턴이다.

여기에서 전략이란 어떤 목적을 달성하기 위해 일을 수행하는 방식, 비즈니스 규칙, 문제를 해결하는 알고리즘 등으로 이해할 수 있다.

프로그램에서 알고리즘의 집합을 정의하고 각각을 캡슐화하여 상호 교환 가능하도록 만든다.

알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경하거나 확장할 수 있으며, 코드의 재사용성과 유연성을 높일 수 있다.

 

 

 

전략 패턴 예제

어떤 소재를 가지고 전략 패턴 알고리즘을 작성해볼까 고민하다가 GPT가  게임 캐릭터와 공격 방식을 추천해줬다.

이를 바탕으로 게임 캐릭터, 그리고 캐릭터의 공격방식을 바탕으로 각각 클래스를 작성하였다!

 

 

 

공격 방식

// 전략 인터페이스: 공격 방식을 정의하는 인터페이스
interface AttackStrategy {
    void attack();
}

비어있는 attack() 함수만 정의되어있는 공격 전략 인터페이스다. 

 

공격 방식 클래스들

class FighterAttack implements AttackStrategy {
    @Override
    public void attack() {
        System.out.println("검으로 공격!");
    }
}

class ArcherAttack implements AttackStrategy {
    @Override
    public void attack() {
        System.out.println("활로 원거리 공격!");
    }
}

class MageAttack implements AttackStrategy {
    @Override
    public void attack() {
        System.out.println("마법으로 공격!");
    }
}

AttackStrategy 인터페이스를 구현한 구체적인 공격방식 클래스이다.

클래스 별로 attack()을 구체적으로 구현하였다!

 

 

게임 캐릭터 클래스

class GameCharacter {
    private AttackStrategy attackStrategy;

    public void setAttackStrategy(AttackStrategy attackStrategy) {
        this.attackStrategy = attackStrategy;
    }

    public void attack() {
        attackStrategy.attack();
    }
}

게임 캐릭터 클래스이다.

AttackStrategy 인터페이스를 활용하여 공격전략을 바꿀 수 있는 setAttackStrategy와 AttackStrategy의 공격 방식을 출력하는 attack()이 있다.

 

 

Main문

public class StrategyPatternExample {
    public static void main(String[] args) {
        // 캐릭터 생성
        GameCharacter character = new GameCharacter();

        // 초기 공격 방식 설정
        character.setAttackStrategy(new FighterAttack());

        // 캐릭터가 검으로 공격
        character.attack();

        // 캐릭터가 활로 공격으로 전환
        character.setAttackStrategy(new ArcherAttack());
        character.attack();

        // 캐릭터가 마법으로 공격으로 전환
        character.setAttackStrategy(new MageAttack());
        character.attack();
    }
}

캐릭터를 생성하여 공격 방식을 설정하고 공격한다.

다른 공격 방식으로 전환한 뒤 공격을 취하는 구조이다.

 

게임 캐릭터가 원하는 공격 방식을 취하고 공격을 할 때마다 다르게 출력됨을 확인할 수 있다.

 

이렇게 전략 패턴을 사용하면 새로운 공격 방식을 추가하거나 기존 방식을 변경하는 작업이 캐릭터 클래스를 수정하지 않고도 가능하다.

스프링에서 UserDao를 통해 Connection객체를 변경할 수 있는 예시에도 전략패턴이 사용되는데, 추후에 학습 후 직접 작성해봐야겠다!

'java > Design Pattern' 카테고리의 다른 글

옵저버 패턴 (observer pattern)  (0) 2023.08.23

오늘은 자바에서 쓰이는 스트림에 관해 정리해보려고 한다.

 

Stream은 자바 8부터 사용할 수 있다.

기존에 자바 컬렉션이나 배열의 원소를 가공할 때 for문, foreach 등으로 원소 하나씩 골라내서 가공했다면,

Stream을 이용해 람다 함수 형식으로 간결하게 요소들의 처리가 가능하다.

 

 

Stream이란!

스트림의 사전적 의미는 '흐르다' 또는 '개울'로 프로그래밍에서의 스트림도 사전적 의미가 크게 다르지 않다.

여기서는 '데이터의 흐름'을 뜻한다.

 

출처 : https://steady-coding.tistory.com/309

stream을 설명하기에 정말 적합한 사진이 있어서 출처란에 기입한 블로그 작성자 분의 이미지를 가져왔다. (짱짱)

물고기와 같은 어류의 이동처럼 Stream도 이에 비유하여 이해할 수 있다.

 

먼저 어부들이 그물망으로 잡고 싶은 물고기들을 잡는다. 이 행위를 filter라고 하며, 중간 연산자라고 한다.

그리고 이 물고기들을 포장하여 판매해야하기에 상자에 담는데, 이 행위를 map이라고 한다. 이 또한 중간 연산자라고 한다.

마지막으로 상자들을 운반하여 다른 곳으로 이동하며 끝난다. 이 행위를 collect라고 하며 이 연산자는 최종 연산자라고 한다.

 

이처럼 스트림은 수많은 데이터 흐름 속 각각의 원하는 값을 가공하여 최종 소비자에게 제공하는 역할을 한다.

 

 

Stream의 특징

스트림 내 요소들에 대해 함수가 적용된 결과의 새로운 요소로 매핑한다.

기능적인 측면에서 스트림은 컬렉션(배열포함)의 저장 요소를 하나씩 참조해 람다식으로 처리할 수 있도록 해주는 반복자이다.

사실 우리는 반복자를 스트림이 아니더라도 계속 사용해왔다. (ex. Iterator 반복자 ,,)

 

 public static void main(String[] args) {
   List<Integer> list = Arrays.asList(1, 2, 3);
   Iterator<Integer> it = list.iterator();
   while (it.hasNext()) {
     int num = it.next();
     System.out.println(num);
   }
 }

상단의 코드는 Iterator 반복자를 사용한 예시로, 정수가 있는 리스트를 하나씩 순회하면서 값을 출력한다.

 

이를 스트림으로 바꿔보면, 

 public static void main(String[] args) {
   List<Integer> list = Arrays.asList(1, 2, 3);
   Stream<Integer> stream = list.stream();
   stream.forEach(System.out::println);
 }

코드가 매우 간결해진 것을 확인해볼 수 있다.

 

이제 본격적으로 스트림의 특징에 대해 알아보겠다.

 

  • 람다식으로 요소 처리 코드를 제공한다.
  • 내부 반복자를 사용하므로 병렬 처리가 쉽다. (Iterator의 경우 외부 반복자)
  • 중간 처리와 최종 처리가 존재한다.
    • 중간 처리 (매핑, 필터링, 정렬) / 최종 처리 (반복, 카운팅, 평균, 총합 등 집계 처리)

 

배열의 원소를 가공하는데 있어 map, filter, sorted 등이 있다.

 

map

요소들을 특정 조건에 해당하는 값으로 변환

요소들을 대/소문자 변형 등의 작업을 하고 싶을 때 사용 가능

 

filter

요소들을 조건에 따라 걸러내는 작업을 해줌

길이의 제한, 특정 문자 포함 등의 작업을 하고 싶을 때 사용 가능

 

sorted

요소들을 정렬해주는 작업

요소들의 가공이 끝났다면 리턴해줄 결과를 collect을 통해 만들어줌

 

 


Test Set

ArrayList<string> list = new ArrayList<>(Arrays.asList("Apple","Banana","Melon","Grape","Strawberry"));

System.out.println(list);

//[Apple, Banana, Melon, Grape, Strawberry]

 

Map

list.stream().map(s->s.toUpperCase());
list.stream().map(String::toUpperCase);

리스트의 요소들을 대문자로 변경한다.

요소들을 대문자로 가공하였다면 collect 를 이용하여 결과를 리턴받을 수 있고, forEach 를 이용하여 바로 출력해볼수 있다.

 

System.out.println(list.stream().map(s->s.toUpperCase()).collect(Collectors.joining(" "))); //APPLE BANANA MELON GRAPE STRAWBERRY

System.out.println(list.stream().map(s->s.toUpperCase()).collect(Collectors.toList())); //[APPLE, BANANA, MELON, GRAPE, STRAWBERRY]
System.out.println(list.stream().map(String::toUpperCase).collect(Collectors.toList())); //[APPLE, BANANA, MELON, GRAPE, STRAWBERRY]

list.stream().map(String::toUpperCase).forEach(s -> System.out.println(s));
//APPLE
//BANANA
//MELON
//GRAPE
//STRAWBERRY

 

filter

list.stream().filter(t->t.length()>5)

filter 는 요소를 특정 기준으로 걸러낼 수 있다.

요소의 크기가 5이상인 값만 뽑아낸다.

 

System.out.println(list.stream().filter(t->t.length()>5).collect(Collectors.joining(" "))); //Banana Strawberry

System.out.println(list.stream().filter(t->t.length()>5).collect(Collectors.toList())); //[Banana, Strawberry]

마찬가지로 filter로 가공한 결과를 얻을 수 있다!

 

Sorted

list.stream().sorted()

리스트의 요소를 정렬한다.

 

System.out.println(list.stream().sorted().collect(Collectors.toList())); //[Apple, Banana, Grape, Melon, Strawberry]

사용 예시

public class Human implements Comparable<Human> {

    private Long idx;
    private String name;
    private Integer money;
    private LocalDate birth;
    private List<String> travelDestinations;
}

기본 사용법

@DisplayName("이름만 가져와서 List 만들기")
void mapTest1() {
    List<String> humanNames = humans.stream()
            .map(h -> h.getName())
            .collect(Collectors.toList());

    for (String humanName : humanNames) {
        System.out.print(humanName + " ");
    }
}

 

이렇게 자바 스트림의 개념, 사용하는 방법들에 대해 알아봤다.

 

'java' 카테고리의 다른 글

[Java] 자바의 interface  (0) 2023.01.25

프로젝트를 진행하며 Interface를 활용해 메서드를 만들고 호출하였다.

Interface는 객체지향 개발 5대 원칙을 만족시킬 수 있는 도구라고 하는데, 구체적으로 무엇이고 어떻게 구현하는지 알아보자!

 

1. 인터페이스란? (Interface)

Interface는 추상 메서드들을 나열한 형태이다. 쉽게 말하자면 객체의 사용방법을 가이드라인 하는 것이다.

구현도 되지 않는 메서드들의 나열을 왜 사용할까?

1. 추상 클래스를 통해 객체들 간 네이밍을 통일할 수 있고, 이로 인해 소스의 가독성과 유지보수가 향상된다.
2. 확장에는 열려있고 변경에는 닫혀있는 객체 간 결합도 (코드 종속성)를 낮춘 유연한 방식의 개발이 가능하다.

인터페이스의 특징으로는,

  • 다중 상속 가능
    • 껍데기만 존재하여 클래스 상속시 발생했던 모호함이 없기에 다중 상속이 가능하다.
  • 추상 메서드와 상수만 사용 가능
    • 인터페이스는 구현 소스를 생성할 수 없다.
  • 생성자 사용 불가
    • 인터페이스 객체가 아니므로 생성자를 사용할 수 없다.
  • 메서드 오버라이딩 필수
    • 자식 클래스는 부모 인터페이스의 추상 메서드를 모두 오버라이딩해야 한다.
코드 종속성을 낮춘다 
코드 종속성은 각각의 메서드 간의 결합도를 의미하며 인터페이스를 활용하면 한 메서드를 수정하였을 때, 다른 메서드도 수정해야 하는 상황을 줄여준다는 의미이다.
 인터페이스로 추상 메서드를 지정하면 메서드의 input값과 output값이 고정되어 있다. 예를 들자면 
 public abstract String myName(String name)이라는 메서드는 input값, output값 모두 String으로 고정되어 있다. 이 메서드를 구현하는 객체에서 아무리 수정하더라도 input값과 output 값은 String으로 고정되어 있어 변경에 대한 영향도가 작다. 그래서 인터페이스는 "변경에 강하다", "확장에는 열려있고 변경에는 닫혀있다" 라고 말하는 것입니다.

 

2. 인터페이스 선언

 인터페이스 클래스 작성방식은 아래와 같다. 

인터페이스에서 포함될수 있는 것들은 상수와 메서드 원형뿐이다. 그리고 두가지 멤버 변수는 public static final  public abstract 이어야 한다. (보통 생략한다)  

interface 인터페이스이름 {     
    public static final 타입 상수이름 = 값;     
    public abstract 메서드이름(매개변수목록);
}

 

3. 인터페이스 구현

  • 인터페이스는 구현 코드가 없고 클래스가 이 인터페이스를 구현한다라고 한다.
  • 코드에서 인터페이스의 메서드를 호출하면 인터페이스는 구현 객체의 메서드를 찾아서 호출한다.
  • 객체는 추상 메서드를 구현한 메서드를 갖고 있어야 한다. 이 객체를 구현 객체라고 한다.
  • 구현 객체를 생성하는 클래스는 구현 클래스라고 하다.

자바 클래스는 부모클래스 하나만 상속할수 있다.  extends 를 이용한 클래스 상속은 하나만 되고 두개 이상은 인터페이스만 된다. 인터페이스를 implements 하는 것은 상속이 아니라 구현한다고 해야 한다. 이렇게 두개 이상의 구현 가능한 인터페이스를 콤마(,) 로 구분해서 추가한다.

 

단일 인터페이스 구현

단일 인터페이스는 아래와 같이 구현한다.

public class ClassName impements InterfaceName{
	// 인터페이스의 추상 메서드 구현
}

 

 

다중 인터페이스 구현

 자바 클래스는 하나의 부모 클래스를 상속할수 있지만 인터페이스는 다중 상속이 가능하다. extends 키워드와 콤마를 이용해서 여러 개 인터페이스를 지정하면 된다.

public interface Animals extends IBird, IFlay {
	public void go();
	public void run();
}

 

 

4. 인터페이스 사용하는 이유

 인터페이스 클래스는 DB 연결에 많이 사용한다.

프로젝트에서 전체 프로그램을 완성해 가는 시점에 MySql 에서 Oracle 로 바뀔 경우를 생각해보자.

인터페이스로 구현하지 않았다면 모든 참조 소스는 다 바꿔야 한다. 그러나 인터페이스를 참조해서 구현했다면 Oracle 연결 클래스만 바꾸면 끝난다. 

 

 

5. 인터페이스끼리의 상속

인터페이스끼리도 상속을 통해서 확장을 시켜나갈 수 있고 하위 인터페이스에서 네이밍을 강제하여 객체 간의 통일성을 추구할 수 있다. 인터페이스로 객체를 구현할 때는 implements라는 키워드를 쓰지만, 인터페이스끼리 상속을 할 때는 클래스와 마찬가지로 extneds 키워드를 사용한다.

 

이렇게 자바에서의 인터페이스에 대하여 알아보았다.

다음 포스팅에서는 스프링에서의 인터페이스 특징, 역할 등에 대해 알아볼 예정이다!

 

 

+ Recent posts