반응형

람다로 객체지향 디자인 패턴 리팩터링하기-지선학

디자인 패턴에 람다 표현식이 더해지면 색다른 기능을 발휘할 수 있다. 즉, 람다를 이용하면 이전에 디자인 패턴으로 해결하던 문제를 더 쉽고 간단하게 해결할 수 있다. 또한 람다 표현식으로 기존의 많은 객체지향 디자인 패턴을 제거하거나 간결하게 재구현할 수 있다.

람다 미적분학(Lambda Calculus)?

수학과 컴퓨터 과학에서 함수와 계산을 표현하고 연구하는 데 사용되는 형식 체계(formal system)입니다. 알론조 처치(Alonzo Church)가 1930년대에 개발했으며, 프로그래밍 언어 이론함수형 프로그래밍의 기초가 되는 이론입니다.

1급 객체(First-Class Citizen)*

사회학적 배경

  • 1급 객체 : 1급 객체(First-Class Citizen)라는 표현은 원래 법률 및 사회학적인 맥락에서 나온 용어로, 특정 대상이 동등한 권리와 특권을 가지는 것을 의미합니다.

예를 들어, 민주주의 사회에서는 모든 시민이 법 앞에서 평등하다는 개념을 의미하는데, "1급 시민"이라는 용어는 이런 평등을 강조하는 데 사용되었습니다.

결과적으로 함수를 1급 객체로 취급하면 유연성, 재사용성, 추상화 수준이 높아집니다. 함수가 변수처럼 자유롭게 전달되고 반환될 수 있기 때문에, 복잡한 계산이나 데이터 흐름을 더 간단하고 선언적으로 표현할 수 있습니다.

Lambda calculus

 

Lambda calculus - Wikipedia

From Wikipedia, the free encyclopedia Mathematical-logic system based on functions Lambda calculus (also written as λ-calculus) is a formal system in mathematical logic for expressing computation based on function abstraction and application using variabl

en.wikipedia.org

 

람다란 무엇인가?

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다. 람다 표현식에는 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있다.

람다 표현식은 파라미터, 화살표, 바디로 이루어진다.

(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
사용 사례 람다 예제
불리언 표현식 (List list) → list.isEmpty()
객체 생성 () → new Apple(10)
객체에서 소비 (Apple a) → { System.out.println(a.getWeight));
객체에서 선택/추출 (String s) → s.length()
두 값을 조합 (int a, int b) → a * b

함수형 인터페이스(Functional Interface)

하나의 추상 메서드를 가지는 인터페이스를 의미합니다.

@FunctionalInterface
interface Calculator {
    int calculate(int x, int y);
}
Calculator add = (x, y) -> x + y;
System.out.println(add.calculate(5, 3)); // 출력: 8

자바에서는 왜 함수형 인터페이스가 필요한가?

  1. Java의 객체 중심 특성
    • Java는 철저히 객체 지향 언어로 설계되었습니다. 모든 메서드는 클래스나 객체의 일부로만 존재할 수 있습니다.
    • 람다식은 함수형 프로그래밍의 핵심 요소로, 함수 자체를 전달하거나 처리해야 합니다.
    • 그러나 Java에서는 함수 자체를 일급 객체로 지원하지 않기 때문에, 람다식을 객체 지향 모델과 통합할 방법이 필요했습니다.
  2. 인터페이스를 통한 함수 표현
    • Java 8에서는 람다식을 사용하여 함수를 표현하기 위해, 메서드 하나만 가진 인터페이스(즉, 함수형 인터페이스)를 사용했습니다.
    • 이 인터페이스를 사용하면, 람다식을 해당 인터페이스의 구현체로 취급할 수 있습니다. 이렇게 하면 Java의 기존 객체 지향 모델과 호환성을 유지할 수 있습니다.

자바에서 제공하는 기본 함수형 인터페이스

[Java] Lazy Evaluation (지연 연산)

 

[Java] Lazy Evaluation (지연 연산)

Lazy Evaluation는 불필요한 연산을 피하기 위해 연산을 지연 시켜 놓았다가 필요할 때 연산하는 방법이다.

velog.io

 

java vs javascript 함수형 인터페이스 비교

람다로 객체지향 디자인 패턴 리팩터링하기

전략(Strategy)

https://ko.wikipedia.org/wiki/%EC%A0%84%EB%9E%B5_%ED%8C%A8%ED%84%B4

 

전략 패턴 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 전략 패턴(strategy pattern) 또는 정책 패턴(policy pattern)은 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다. 전략 패턴은 특정한 계열

ko.wikipedia.org

Legacy 예제

public interface ValidationStrategy {
    boolean execute(String s);
}
public class IsAllLowerCase implements ValidationStrategy {

    @Override
    public boolean execute(String s) {
        return s.matches("[a-z]+");
    }
}
public class IsNumeric implements ValidationStrategy {

    @Override
    public boolean execute(String s) {
        return s.matches("\\d+");
    }
}
public class Validator {

    private final ValidationStrategy strategy;

    public Validator(ValidationStrategy v) {
        this.strategy = v;
    }

    public boolean validate(String s){
        return strategy.execute(s);
    }
}
public class LegacyMain {

    public static void main(String[] args) {

        Validator numericValidator = new Validator(new IsNumeric());
        boolean b1 = numericValidator.validate("aaa");
        System.out.println(b1);

        Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
        boolean b2 = lowerCaseValidator.validate("hello");
        System.out.println(b2);

    }

}

Lambda로 리팩터링 예제

public class LambdaMain {
    public static void main(String[] args) {
        Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+"));
        boolean b1 = numericValidator.validate("aaaaa");
        System.out.println(b1);

        Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+"));
        boolean b2 = lowerCaseValidator.validate("bbbb");
        System.out.println(b2);
    }
}
  • ValidationStrategy 는 함수형 인터페이스며 Predicate과 같은 함수 디스크립터를 갖고 있음을 파악할 수 있음.

템플릿 메서드(Template Method)

https://ko.wikipedia.org/wiki/%ED%85%9C%ED%94%8C%EB%A6%BF_%EB%A9%94%EC%86%8C%EB%93%9C_%ED%8C%A8%ED%84%B4

 

템플릿 메소드 패턴 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 템플릿 메소드 패턴(template method pattern)은 소프트웨어 공학에서 동작 상의 알고리즘의 프로그램 뼈대를 정의하는 행위 디자인 패턴이다.[1] 알고리즘의 구조를

ko.wikipedia.org

 

Legacy 예제

public class Customer {
    private int id;

    private String name;

    public Customer(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getName() {
        return name;
    }

}
public class Database {
    private static final Map<Integer, String> TEMP_DATA = new HashMap<>();

    static {
        TEMP_DATA.put(1, "철수");
        TEMP_DATA.put(2, "영희");
        TEMP_DATA.put(3, "민수");
        TEMP_DATA.put(4, "지영");
    }
    public static Customer getCustomerWithId(int id) throws Exception {
        if (TEMP_DATA.get(id) == null) {
            throw new Exception("db 에 해당 값 없음");
        }
        return new Customer(id, TEMP_DATA.get(id));
    }

}
interface OnlineBanking {
    default void processCustomer(int id) throws Exception {
        Customer c = Database.getCustomerWithId(id);
        makeCustomerHappy(c);
    }

    void makeCustomerHappy(Customer c);
}
public class Kbank implements OnlineBanking{
    @Override
    public void makeCustomerHappy(Customer c) {
        System.out.println(c.getName() + "케이뱅크 오버라이딩");
    }
}
public class NH implements OnlineBanking{
    @Override
    public void makeCustomerHappy(Customer c) {
        System.out.println(c.getName() + "농협 오버라이딩");
    }
}
public class LagacyMain {

    public static void main(String[] args) throws Exception {
        OnlineBanking kBanking = new Kbank();
        kBanking.processCustomer(1);
        kBanking.processCustomer(2);

        OnlineBanking nhBanking = new NH();
        nhBanking.processCustomer(1);
        nhBanking.processCustomer(2);
    }

}

Lambda로 리팩터링 예제

public class OnlineBankingLambda {
    public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) throws Exception {
        Customer c = Database.getCustomerWithId(id);
        makeCustomerHappy.accept(c);
    }
}
public class LambdaMain {

    public static void main(String[] args) throws Exception {
        new OnlineBankingLambda().processCustomer(1, (Customer c) -> {
            System.out.println(c.getName() + "케이뱅크 오버라이딩");
        });
        new OnlineBankingLambda().processCustomer(2, (Customer c) -> {
            System.out.println(c.getName() + "케이뱅크 오버라이딩");
        });

        new OnlineBankingLambda().processCustomer(1, (Customer c) -> {
            System.out.println(c.getName() + "농협 오버라이딩");
        });
        new OnlineBankingLambda().processCustomer(2, (Customer c) -> {
            System.out.println(c.getName() + "농협 오버라이딩");
        });
    }
}

옵저버(Observer)

https://ko.wikipedia.org/wiki/%EC%98%B5%EC%84%9C%EB%B2%84_%ED%8C%A8%ED%84%B4

 

옵서버 패턴 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 옵서버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체

ko.wikipedia.org

Legacy 예제

public interface Observer {
    void notify(String tweet);
}
public class Guardian implements Observer{
    @Override
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("queen")) {
            System.out.println("Yet more news from London..." + tweet);
        }
    }
}
public class LeMonde implements Observer{
    @Override
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("wine")) {
            System.out.println("Today cheese, wine and news!" + tweet);
        }
    }
}
public class NYTimes implements Observer {
    @Override
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("money")) {
            System.out.println("Breaking news in NY!" + tweet);
        }
    }
}
public interface Subject {
    void registerObserver(Observer o);
    void notifyObservers(String tweet);
}
public class Feed implements Subject{
    private final List<Observer> observers = new ArrayList<>();
    @Override
    public void registerObserver(Observer o) {
        this.observers.add(o);
    }

    @Override
    public void notifyObservers(String tweet) {
        observers.forEach(o -> o.notify(tweet));
    }
}
public class LagacyMain {

    public static void main(String[] args) {
        Feed f = new Feed();
        f.registerObserver(new NYTimes());
        f.registerObserver(new Guardian());
        f.registerObserver(new LeMonde());
        f.notifyObservers("The queen said her favourite book is Modern Java in Action!");
    }
}

Lambda로 리팩터링 예제

public class LambdaMain {
    public static void main(String[] args) {
        Feed f = new Feed();
        f.registerObserver((String tweet) -> {
            if (tweet != null && tweet.contains("money")) {
                System.out.println("Breaking news in NY!" + tweet);
            }
        });

        f.registerObserver((String tweet) -> {
            if (tweet != null && tweet.contains("queen")) {
                System.out.println("Yet more news from London..." + tweet);
            }
        });

        f.registerObserver((String tweet) -> {
            if (tweet != null && tweet.contains("wine")) {
                System.out.println("Today cheese, wine and news!" + tweet);
            }
        });

        f.notifyObservers("The queen said her favourite book is Modern Java in Action!");
    }
}

의무 체인(Chain of Responsibility), 책임 연쇄

https://ko.wikipedia.org/wiki/%EC%B1%85%EC%9E%84_%EC%97%B0%EC%87%84_%ED%8C%A8%ED%84%B4

 

책임 연쇄 패턴 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 객체 지향 디자인에서 책임 연쇄 패턴(chain-of-responsibility pattern)은 명령 객체와 일련의 처리 객체를 포함하는 디자인 패턴이다. 각각의 처리 객체는 명령 객체를

ko.wikipedia.org

Legacy 예제

public abstract class ProcessingObject<T> {
    protected ProcessingObject<T> successor;

    public void setSuccessor(ProcessingObject<T> successor) {
        this.successor = successor;
    }
    public T handle(T input){
        T r = handleWork(input);
        if(successor != null){
            return successor.handle(r);
        }
        return r;
    }

    abstract protected T handleWork(T input);
}
public class HeaderTextProcessing extends ProcessingObject<String> {
    @Override
    protected String handleWork(String text) {
        return "From Raoul, Mario and Alan : "+ text;
    }
}
public class SpellCheckerProcessing extends ProcessingObject<String>{
    @Override
    protected String handleWork(String text) {
        return text.replaceAll("labda", "lambda");
    }
}
public class LagacyMain {

    public static void main(String[] args) {
        ProcessingObject<String> p1 = new HeaderTextProcessing();
        ProcessingObject<String> p2 = new SpellCheckerProcessing();
        p1.setSuccessor(p2); //두 작업 처리 객체를 연결
        String result = p1.handle("Aren't labdas really sexy?!!");
        System.out.println(result);
    }
}

Lambda로 리팩터링 예제

public class LambdaMain {
    public static void main(String[] args) {
        UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
        UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");
        Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
        String result = pipeline.apply("Aren't labdas really sexy?!!");
        System.out.println(result);
    }
}

팩토리(Factory)

https://ko.wikipedia.org/wiki/%ED%8C%A9%ED%86%A0%EB%A6%AC_%EB%A9%94%EC%84%9C%EB%93%9C_%ED%8C%A8%ED%84%B4

 

팩토리 메서드 패턴 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. UML로 표현된 팩토리 메서드 LePUS3로 표현된 팩토리 메서드 팩토리 메서드 패턴(Factory method pattern)은 객체지향 디자인 패턴이다. Factory method는 부모(상위) 클래스

ko.wikipedia.org

Legacy 예제

public class Product {
    String name;   
}
public class Loan extends Product {
}
public class Bond extends Product {
}
public class Stock extends Product {
}
public class ProductFactory {
    public static Product createProduct(String name){
        return switch (name) {
            case "loan" -> new Loan();
            case "stock" -> new Stock();
            case "bond" -> new Bond();
            default -> throw new RuntimeException("No such product" + name);
        };
    }
}
public class LagacyMain {
    public static void main(String[] args) {
        Product p = ProductFactory.createProduct("loan");
        System.out.println(p.getClass());
    }
}

Lambda로 리팩터링 예제

public class LambdaProductFactory {

    final static Map<String, Supplier<Product>> map = new HashMap<>();

    static {
        map.put("loan", Loan::new);
        map.put("stock", Stock::new);
        map.put("bond", Bond::new);
    }

    public static Product createProduct(String name) {
        Supplier<Product> p = map.get(name);
        if (p != null) return p.get();
        throw new IllegalArgumentException("No such Product " + name);
    }
}
public class LambdaMain {
    public static void main(String[] args) {
        Product p = LambdaProductFactory.createProduct("loan");
        System.out.println(p.getClass());
    }
}

참고자료

https://www.yes24.com/Product/Goods/77125987?pid=123487&cosemkid=go15646485055614872&utm_source=google_pc&utm_medium=cpc&utm_campaign=book_pc&utm_content=ys_240530_google_pc_cc_book_pc_11906%EB%8F%84%EC%84%9C&utm_term=%EB%AA%A8%EB%8D%98%EC%9E%90%EB%B0%94%EC%9D%B8%EC%95%A1%EC%85%98&gad_source=1&gclid=Cj0KCQiAkJO8BhCGARIsAMkswyhABf_O0DVcohWq0DPlyqkn9RKmtvRNYz_zEfI8vbG7c7jJEJh8-UkaAoF5EALw_wcB

 

모던 자바 인 액션 - 예스24

자바 1.0이 나온 이후 18년을 통틀어 가장 큰 변화가 자바 8 이후 이어지고 있다. 자바 8 이후 모던 자바를 이용하면 기존의 자바 코드 모두 그대로 쓸 수 있으며, 새로운 기능과 문법, 디자인 패턴

www.yes24.com

https://m.blog.naver.com/zzang9ha/222087025042

 

[Java/자바] - Supplier<T> interface

Supplier<T> interface 안녕하세요, 이번시간에 알아볼 함수형 인터페이스는 Supplier<T>...

blog.naver.com

https://velog.io/@minseojo/Java-Lazy-Evaluation-%EC%A7%80%EC%97%B0-%EC%97%B0%EC%82%B0

 

[Java] Lazy Evaluation (지연 연산)

Lazy Evaluation는 불필요한 연산을 피하기 위해 연산을 지연 시켜 놓았다가 필요할 때 연산하는 방법이다.

velog.io

 

728x90
반응형

CountDownLatch와 사이드 프로젝트에서 활용-지선학

CountDownLatch란?

CountDownLatch는 지정된 횟수만큼 countDown() 메서드가 호출될 때까지 현재 스레드를 대기 상태로 만들고, 카운트가 0에 도달하면 대기 중인 스레드를 다시 실행 가능하게 만드는 동기화 도구입니다. 이를 주로 여러 개의 작업 스레드가 완료될 때까지 대기하고, 모든 작업이 끝나면 특정 스레드를 다시 실행하게 할 때 사용합니다.

Method Summary

예시코드

쇼핑몰 사이트에서 특정 사용자가 컴퓨터 부품을 주문하는 상황임

사용자가 물건 주문 요청을 하면 서버가 각각 부품의 재고 상황을 파악하고, 구매 가능하지 파악하는 로직임

사용자가 부품의 재고를 순차적으로 0초, 5초 *4(재고없는 물건) 대략 20초의 시간이 아닌, 다중쓰레드를 이용해서 대략 5초 이내로 처리 속도를 개선할 수 있음

import com.google.gson.Gson;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class ComputerPartsPurchaseSimulation {

    public static void main(String[] args) throws InterruptedException {

        List<String> productOrderRequests = Arrays.asList("CPU", "RAM", "Motherboard", "SSD", "HDD");

        int threadCount = productOrderRequests.size();
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        log.info("서버: 구매 요청 대기 중...");

        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        Queue<PurchaseResult> productOrderRequestResults = new ConcurrentLinkedQueue<>();
        for (String product : productOrderRequests) {
            executorService.submit(() -> {
                try {
                    PurchaseResult response = processPurchase(product);
                    productOrderRequestResults.add(response);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        countDownLatch.await();

        log.info("모든 요청 처리 완료!");
        log.info("구매 요청 처리 결과 : ");
        productOrderRequestResults.forEach(result -> log.info(new Gson().toJson(result)));

        executorService.shutdown();
    }

    private static PurchaseResult processPurchase(String product) {
        boolean isAvailable = checkStockAvailability(product);
        return PurchaseResult.builder()
                .product(product)
                .status(isAvailable ? "success" : "failure")
                .message(product + (isAvailable ? " 구매 성공!" : " 재고 부족으로 구매 실패"))
                .build();
    }

    /**
     * CPU만 재고가 있음, 나머지는 재고 없음 재고 없어서 오래 걸림
     * @param product
     * @return
     */
    private static boolean checkStockAvailability(String product) {
        if ("CPU".equals(product)) {
            log.info("CPU 구매 처리 중... 0초 대기-{}", Thread.currentThread().getName());
            return true;
        }

        try {
            log.info("{} 구매 처리 중... 5초 대기-{}", product, Thread.currentThread().getName());
            Thread.sleep(5000);  // 5초 대기
        } catch (InterruptedException interruptedException) {
            interruptedException.printStackTrace();
        }
        return false;
    }

    @Setter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static class PurchaseResult {
        private String product;
        private String status;
        private String message;
    }

}
15:45:42.833 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- 서버: 구매 요청 대기 중...
15:45:42.841 [pool-1-thread-1] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- CPU 구매 처리 중... 0초 대기-pool-1-thread-1
15:45:42.841 [pool-1-thread-2] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- RAM 구매 처리 중... 5초 대기-pool-1-thread-2
15:45:42.841 [pool-1-thread-3] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- Motherboard 구매 처리 중... 5초 대기-pool-1-thread-3
15:45:42.842 [pool-1-thread-5] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- HDD 구매 처리 중... 5초 대기-pool-1-thread-5
15:45:42.841 [pool-1-thread-4] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- SSD 구매 처리 중... 5초 대기-pool-1-thread-4
15:45:47.847 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- 서버: 모든 요청 처리 완료!
15:45:47.848 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- 구매 결과 : 
15:45:47.914 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- {"product":"CPU","status":"success","message":"CPU 구매 성공!"}
15:45:47.914 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- {"product":"RAM","status":"failure","message":"RAM 재고 부족으로 구매 실패"}
15:45:47.914 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- {"product":"Motherboard","status":"failure","message":"Motherboard 재고 부족으로 구매 실패"}
15:45:47.914 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- {"product":"HDD","status":"failure","message":"HDD 재고 부족으로 구매 실패"}
15:45:47.914 [main] INFO com.dodam.dicegame.dicegame.util.ComputerPartsPurchaseSimulation -- {"product":"SSD","status":"failure","message":"SSD 재고 부족으로 구매 실패"}

이 밖에 쓰레드 협업용 동기화 도구

CyclicBarrier

  • 목적: 지정된 수의 스레드가 모두 도달할 때까지 기다린 후, 동시에 작업을 진행합니다.
  • CountDownLatch와 달리, 반복적으로 사용 가능.
  • 사용 사례:
    • 여러 스레드가 서로 독립적인 작업을 완료한 후, 다음 단계로 함께 진행해야 할 때.
    • 스레드풀에서 일정 개수의 스레드가 모였을 때 새로운 작업을 시작해야 하는 경우.
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("All threads reached the barrier. Proceeding...");
});

Runnable task = () -> {
    try {
        System.out.println(Thread.currentThread().getName() + " waiting at barrier");
        barrier.await();
        System.out.println(Thread.currentThread().getName() + " proceeding");
    } catch (Exception e) {
        e.printStackTrace();
    }
};

ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
    executor.submit(task);
}
executor.shutdown();

Semaphore

  • 목적: 스레드의 접근을 제어하는 데 사용되며, 허용된 작업의 개수를 제한.
  • 스레드 협업에 사용할 수 있으며, 신호 용도로 활용 가능.
  • 사용 사례:
    • 리소스에 대한 동시 접근을 제한하고 싶을 때.
    • 일정한 스레드 수가 접근해야 작업을 완료하고 신호를 보낼 때.
Semaphore semaphore = new Semaphore(3); // 허용된 동시 작업 수

Runnable task = () -> {
    try {
        semaphore.acquire();
        System.out.println(Thread.currentThread().getName() + " acquired permit");
        Thread.sleep(1000); // 작업 수행
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println(Thread.currentThread().getName() + " releasing permit");
        semaphore.release();
    }
};

ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
    executor.submit(task);
}
executor.shutdown();

Phaser

  • 목적: CyclicBarrier와 유사하게 단계별 동기화를 제공하며, 단계마다 참여 스레드 수를 동적으로 조정할 수 있습니다.
  • CountDownLatch와 달리 단계를 명시적으로 관리하고, 재사용 가능.
  • 사용 사례:
    • 여러 단계로 나뉜 스레드 작업을 동기화해야 할 때.
    • 단계별로 스레드의 참여를 동적으로 변경할 수 있어야 할 때.
import java.util.concurrent.Phaser;

public class PhaserTest {

    /**
     * 3개의 작업을 3단계로 걸쳐서 함
     * @param args
     */
    public static void main(String[] args) {
        // Phaser 생성, 3개의 쓰레드를 등록
        Phaser phaser = new Phaser(3);

        // Runnable 작업 정의
        Runnable task = () -> {
            String threadName = Thread.currentThread().getName();

            // 1단계 작업
            System.out.println(threadName + " completed Phase 1");
            phaser.arriveAndAwaitAdvance(); // 1단계 완료 후 대기

            // 2단계 작업
            System.out.println(threadName + " completed Phase 2");
            phaser.arriveAndAwaitAdvance(); // 2단계 완료 후 대기

            // 3단계 작업
            System.out.println(threadName + " completed Phase 3");
            phaser.arriveAndAwaitAdvance(); // 3단계 완료 후 대기*/

            System.out.println(threadName + " finished all phases");
        };

        // 3개의 쓰레드 생성 및 실행
        Thread t1 = new Thread(task, "Thread-1");
        Thread t2 = new Thread(task, "Thread-2");
        Thread t3 = new Thread(task, "Thread-3");

        t1.start();
        t2.start();
        t3.start();
    }
}

Exchanger

  • 목적: 두 스레드가 데이터를 교환하는 데 사용.
  • 사용 사례:
    • 한 스레드가 데이터를 생성하고 다른 스레드가 이를 소비하는 생산자-소비자 패턴.
    • 두 스레드 간의 데이터 교환이 필요한 경우.
Exchanger<String> exchanger = new Exchanger<>();

Runnable producer = () -> {
    try {
        String data = "Data from producer";
        System.out.println("Producer: Sending data");
        String response = exchanger.exchange(data);
        System.out.println("Producer: Received response - " + response);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

Runnable consumer = () -> {
    try {
        String data = exchanger.exchange(null);
        System.out.println("Consumer: Received data - " + data);
        exchanger.exchange("Acknowledgement from consumer");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(producer);
executor.submit(consumer);
executor.shutdown();

SynchronousQueue

  • 목적: 한 번에 하나의 작업만 전송하고 수신되도록 동기화된 대기열.
  • 생산자가 데이터를 큐에 넣으려면 소비자가 즉시 데이터를 받아야 함.
  • 사용 사례:
    • 스레드 간의 단방향 데이터 교환.
SynchronousQueue<String> queue = new SynchronousQueue<>();

Runnable producer = () -> {
    try {
        System.out.println("Producer: Sending data");
        queue.put("Data from producer");
        System.out.println("Producer: Data sent");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

Runnable consumer = () -> {
    try {
        System.out.println("Consumer: Waiting for data");
        String data = queue.take();
        System.out.println("Consumer: Received data - " + data);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(producer);
executor.submit(consumer);
executor.shutdown();
728x90
반응형

웹 소켓이란?

WebSocket은 웹 앱과 서버 간의 지속적인 연결을 제공하는 프로토콜입니다. 이를 통해 서버와 클라이언트 간에 양방향 통신이 가능해집니다. HTTP와는 달리, WebSocket 연결은 한 번 열린 후 계속 유지되므로, 서버나 클라이언트에서 언제든지 데이터를 전송할 수 있다는 것이 특징입니다. 그렇기에 실시간으로 진행되는 통신에서 적극적으로 사용하고 있습니다.

특징

  1. 양방향 통신
  • 데이터 송수신을 동시에 처리할 수 있는 통신 방법
  • 클라이언트와 서버가 서로에게 원할 때 데이터를 주고 받을 수 있따.
  • 통상적인 Http 통신은 Client가 요청을 보내는 경우에만 Server가 응답하는 단방향 통신
  1. 실시간 네트워킹
  • 웹 환경에서 연속된 데이터를 빠르게 노출 ex) 채팅, 주식, 비디오 데이터
  • 여러 단말기에 빠르게 데이터를 교환

웹 소켓 이전의 비슷한 기술

Polling vs Long polling vs Streaming

  • 결과적으로 이 모든 방법이 HTTP를 통해 통신하기 떄문에 Request, Response 둘다 Header가 불필요하게 큼

핸드 쉐이킹

클라이언트 요청 주요 헤더

Get / HTTP/1.1/r/n : 요청 메서드와 요청 파일정보. HTTP 버전을 의미

Host : 서버의 도메인 네임으로 Host 헤더는 반드시 존재해야 한다.

User-Agent : 사용자가 어떤 클라이언트를 통해 요청을 보냈는지 알 수 있다.

Origin: 요청을 보낼 때, 요청이 어느 주소에서 시작되었는지 나타낸다.

Connection: 클라이언트와 서버가 connection에 대한 옵션을 정할 수 있게 알려줌.

Cache-Control: 메시지와 함께 캐시 지시자를 전달하기 위해 사용하는 헤더이다.

Upgrade: websocket이라는 단어를 꼭 사용해야 한다. (이유는 WebSocket 연결 과정에 서술)

Sec-WebSocket-Key: 유효한 요청인지를 확인하기 위한 키로 base64로 인코딩한다.(클라이언트에서 생성)

서버 응답 주요 헤더

Connection: 클라이언트와 서버가 connection에 대한 옵션을 정할 수 있게 알려줌. Upgrade가 반드시 포함되어야 함

Date: HTTP 메시지를 생성한 일시를 나타냄.

Sec-WebSkcoet-Accept: 클라이언트로부터 받은 Sec-WebSocket-Key를 사용하여 계산된 값

Server: 서버 소프트웨어 정보를 나타냄.

Access-Control-Allow-Credentials:클라이언트 요청이 쿠키를 통해서 자격 증명을 해야 하는 경우에 true를 응답받은 클라이언트는 실제 요청 시 서버에서 정의된 규격의 인증값이 담긴 쿠키를 함께 보내야 한다.

Access-Control-Allow-Headers: 요청을 허용하는 헤더.

Upgrade: websocket이라는 단어를 꼭 사용해야함. (이유는 WebSocket 연결 과정 서술)

WebSocket 핸드셰이크 과정에서 Sec-WebSocket-Key 검증 원리

  1. 클라이언트는 핸드셰이크 요청 헤더에 Sec-WebSocket-Key를 포함합니다.
    예:
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
  2. 서버는 다음 과정을 통해 클라이언트의 요청을 검증합니다:
    • 클라이언트가 제공한 Sec-WebSocket-Key 값에 RFC 6455에서 정의된 고정 문자열 258EAFA5-E914-47DA-95CA-C5AB0DC85B11을 결합합니다.
    • 이 결합된 문자열을 SHA-1 알고리즘으로 해시한 후, 그 결과를 Base64로 인코딩합니다.
    • 생성된 결과가 서버 응답 헤더의 Sec-WebSocket-Accept에 포함됩니다.
  3. 클라이언트는 서버로부터 응답을 수신한 후, Sec-WebSocket-Accept 값을 확인하여 핸드셰이크가 성공적으로 완료되었는지 검증합니다.

이 후 웹 소켓이 열리게 되면 메시지를 전송할 수 있음

프레임 헤더 구조

126bytes이하일 경우

127 byte이상, 65535 bytes 보다 작은 경우

65535 bytes 이상일 경우

FIN : 이 프레임이 전체 메시지의 끝인지 나타내는 플래그

RSV1, RSV2, RSV3 : WebSocket 확장을 위해 예약된 필드

OPCODE :

MSK : 마스킹 필드로 1로 세팅되어 있으면 Payload 데이터에 마스크로 4바이트가 설정된다. 보통 프레임마다 랜덤하게 만들어지고 XOR 로 적용한다. 마스킹을 하는 이유는 cache-poisoning attack을 방지하기 위함이다.

LENGTH : 이 프레임에 포함된 데이터의 총 길이를 나타내는 단위

웹 소켓 프로토콜 특징

  • 최초 접속에서만 http 프로토콜 위에서 handshaking을 하기 때문에 http header를 사용한다.
  • 웹소켓을 위한 별도의 포트는 없으며, 기존 포트(http-80, https-443)
  • 프레임을 구성된 메시지라는 논리적 단위로 송수신
  • 메시지에 포함될 수 있는 교환 가능한 메시지는 텍스트와 바이너리

웹소켓 한계

  1. HTML 5 이후에 나온기술(이전에는 Socket.io, SockJs 활용해야함)
    즉, 브라우저와 웹 서버의 종류와 버전을 파악하여 가장 적합한 기술을 선택하여 사용해야 함
  2. 형식이 정해져 있기 않음
    → sub-portocols를 사용해서 주고 받는 메시지의 형태를 약속
    → STOMP(Simple Text Oriented Message Protocol)
    채팅 통신을 하기 위한 형식을 정의
    HTTP와 유사하게 간단히 정의되어 해석하기 편한 프로토콜

  3. 모든 것은 Server에 달렸다!
    당신이 채팅방에 입장하게 되면 당신은 친구들과 연결된 거시 아니라 모두가 같은 Socket Server에 연결된 것이다.

그렇기 때문에, 누군가가 메세지를 보낸다면 그것은 다른 친구한테 보낸게 아니라 서버에 전달하게 된 것이고 서버는 이를 다른 다른 사용자들에게 전달만 할 뿐이다.

여기서 문제가 발생하는데, WebSocket Server는 모든 통신을 추척하기 위한 "메모리 파워"가 중요하다. 이 말은 유저가 많아질수록, 더 많은 메모리가 필요하고, 메모리가 많이 필요할 수록 서버에 더 많은 돈을 써야함을 의미한다. 또한, 서버를 빠르게 유지해야하는데 이미 수많은 통신이 서버를 통해 이루어지고 있다면 이는 지연(Latency)를 유발할 수 있다. 그리고 이는 최악의 유저경험이 되는 것이다. 또한 서버에 문제가 생기면 누구도 대화 할 수 없다는 문제도 있다.
이러한 문제를 해결하기 위해 Socket Server가 아닌 브라우저끼리 연결하여 데이터를 주고 받는 것이 "WebRTC" 이다.

Spring Boot 환경에서의 WebSocket

요구사항 : 특정 게임방이 있고 각각의 플레이어 게임방에 입장하면 방장은 입장한 플레이어를 실시간으로 확인할 수 있게 구현해야하는 상황

  1. build.gradle 설정
implementation 'org.springframework.boot:spring-boot-starter-websocket'
  1. @Configuration이용한 WebSocketConfigurer 구현
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new RoomWebSocketHandler(new WebSocketRoomManager()), "/ws/room").setAllowedOrigins("*"); // 클라이언트 도메인을 설정 (CORS)
    }
}
  1. TextWebSocketHandler 구현
package com.dodam.dicegame.dicegame.sockethandler;

import com.dodam.dicegame.dicegame.vo.SocketPayloadVO;
import com.google.gson.Gson;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.dodam.dicegame.dicegame.manager.WebSocketRoomManager;

import java.io.IOException;
import java.util.Set;

@Slf4j
@RequiredArgsConstructor
public class RoomWebSocketHandler extends TextWebSocketHandler {

    private final WebSocketRoomManager roomManager;
    private final Gson gson = new Gson();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("WebSocket connection established: {}", session.getId());
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        log.info("Received message from {}: {}", session.getId(), message.getPayload());
        SocketPayloadVO socketPayloadVO = gson.fromJson(message.getPayload(), SocketPayloadVO.class);

        if ("joinRoom".equals(socketPayloadVO.getAction())) {
            roomManager.addSessionToRoom(socketPayloadVO.getRoomId(), session.getId(), session);
            broadcastToRoom(socketPayloadVO.getRoomId(), session.getId(), socketPayloadVO.getNickName() + "님이 입장하였습니다.");
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        log.info("WebSocket connection closed: {}", session.getId());
        roomManager.removeSessionById(session.getId());
    }

    private void broadcastToRoom(String roomId, String selfSessionId, String message) {
        Set<String> sessionIds = roomManager.getSessionsInRoom(roomId); // 방에 있는 세션 목록 조회
        if (sessionIds == null) {
            log.warn("No sessions found for room {}", roomId);
            return;
        }

        sessionIds.forEach(sessionId -> {
            WebSocketSession session = roomManager.getSessionById(sessionId);
            if (isSkipSession(session, selfSessionId, sessionId))
                return;

            try {
                session.sendMessage(new TextMessage(message));
            } catch (IOException e) {
                throw new RuntimeException("Failed to send message", e);
            }
        });
    }

    private boolean isSkipSession(WebSocketSession session, String selfSessionId, String sessionId) {
        return session == null || !session.isOpen() || selfSessionId.equals(sessionId);
    }

}
@Slf4j
@Component
@Getter
@Setter
@NoArgsConstructor
public class WebSocketRoomManager {

    public final Map<String, Set<String>> roomSessionIdMap = new ConcurrentHashMap<>(); //roomId,sessionId
    public final Map<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>(); //sessionId,WebSocketSession

    public void addSessionToRoom(String roomId, String sessionId, WebSocketSession session) {
        roomSessionIdMap.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()).add(sessionId);
        sessionMap.put(sessionId, session);
    }

    public WebSocketSession getSessionById(String sessionId) {
        return sessionMap.get(sessionId);
    }

    public void removeSessionById(String sessionId) {
        sessionMap.remove(sessionId);
        roomSessionIdMap.values().forEach(sessions -> sessions.remove(sessionId));
    }

    public Set<String> getSessionsInRoom(String roomId) {
        return roomSessionIdMap.getOrDefault(roomId, ConcurrentHashMap.newKeySet());
    }
}
  1. Client 접속

https통신이면 wss지원

  1. 크롬 개발자 도구 확인

 

참고자료 및 출처

https://www.youtube.com/watch?v=MPQHvwPxDUw&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=98

https://alnova2.tistory.com/915

https://velog.io/@wnduf8922/etc-WebSocket에-대하여

728x90

+ Recent posts