Skip to content
dlsnfl0615 edited this page Jul 29, 2026 · 2 revisions

📄 원본(노션): https://app.notion.com/p/3ab19935b8ae80afa1e9c7e3d4482e2f — 최종 동기화 2026-07-29

07-28 (2주차 화)

  • 회의 날짜: 2026-07-28
  • 점심: 돈카춘 (카레)

다음 회의 안건

  1. 로컬 H2 DB용 application.properties 양식 통합 각자 개발 환경 .env 공유
  2. 에이전트 md
  3. 이미지 업로드 - presignedURL vs 서버로 이미지를 보내고 서버에서 S3로

요약

Matching: 매칭 관련 논의를 하였다. 아키텍처를 수도코드로 확정하고 페어프로그래밍을 통해 MVP의 초기 단계를 설계했다.

UI: 배송, 배달 상태인 경우 사용자는 부르미/드리미 토글이 불가능한 것으로 정했다.

AWS, User, Address: AWS 설정, User 인증 구현 완료, Kakao API를 통한 위치 정보 받아오기 구현이 완료되었다.


아침 스크럼

  • 석희
    • 깃허브 위키를 썼다.

      ❓페이지의 사진 아이콘 다리가 3개인게 있음. 고쳐야 함.

  • 현성
    • 어제 한 내용 정리
    • 휴식
  • 동혁
    • 다음날을 위한 에너지 충전
  • 현서
    • 코드 수정 사항 수정
    • ERD의 치명적 문제 발견‼️

오늘 할 작업

  • 석희
    • 오늘 세션 PR 올린거 리뷰 반영하기, USER 도메인 마무리
      • Spring Security에 있어서 사용은 못함. 가능한 방법들:
      1. 서비스 레이어에 권한 인가 관련된 코드 상단에 작성
      2. Custom Annotation

      업데이트 (07-28): 석희 - Custom Annotation으로 해결. BE/Feat : 세션 기반 인증 및 로그인/로그아웃/내 정보 API- #42

    • 부르미 도메인 시작,
    • 프론트 개발 수정
  • 현성, 동혁
    • 어제 못한 알고리즘 마무리
    • API 분배
    • 구현 시작
    • 𝓟𝓪𝓲𝓻 𝓟𝓻𝓸𝓰𝓻𝓪𝓶𝓶𝓲𝓷𝓰
  • 현서
    • S3가 필요하기 때문에 AWS만드는 부분 고민.
    • BusinessException으로 예외 던지는 부분 수정
    • swagger

공통

  • AWS 인프라 설정 관련 회의
    • 이건 추상화는 못하나? A: 업로드를 하고 URL을 하는거라서. 배포를 하는 것이 제일 쉬움
    • ✅ EC2안의 JAR? Docker?
      • Docker + Github Action Docker Build → Docker Hub → Docker Hub polling → 변경사항 발견 시 Update 장점: 장애 대응이 빠르고 배포가 굉장히 쉬움.
      • JAR 파일 내부에서 빌드하다 죽을 수 있음. EC2 2개: Spring EC2 + MySQL EC2 AWS 인프라 구축 가이드 (노션 하위 문서)
image1
  • ✅ ERD의 문제 발견!‼️ 주문 테이블에 주소 테이블이 있음. 생일이 FK로 설정이 되어있었음.
  • ✅ 시간 (Timestamp)는 LocalDateTime기준.
  • AWS 리소스 Naming Convention: symboorm-<리소스명>

오늘 스케줄

없음


  • 초안 버전을 우리 ERD에 맞게 바꾸기
    • GeoPoint 같은 것들
  • 메소드 이름 수정
  • generetaedUUId 함수 인라인화
  • 클래스 파일 분리 및 실제 내용 끼워넣기
  • 일단 1차 점검
  • 큐의 처리 순서를 직렬화 (줄을 세워서 시킨다)
  • claude --resume f057e9bd-5316-48ce-b03b-6636205c7856
    • 4번 해결
    • 5번 해결
    • 6번 해결
  • 로거 바꾸기 (slf4j 안되어있는거)
  • 타이머 기반 딜레이 큐 구현
  • 블로킹 큐 구현
  • 락을 새로 적용하기
  • 실제 backend 코드에 적용하기
MatchingService.java 초안 (Lock X)

Troubleshoot: 제안을 받은 사람이 아닌 모든 사람에 대해서 거절상태로 바꾸는 문제가 있음.

문제 시나리오:
1. 드리미 B가 주문1에서 거절 → B의 오퍼는 DREAMI_REJECTED, B는 MATCHING
2. B가 주문2 매칭에 선정됨 → B는 PROPOSED (주문2 방에서 응답 대기 중)
3. 주문1에서 드리미 A가 수락 → 루프가 주문1 방의 B 오퍼(DREAMI_REJECTED)까지 순회하며 dreamiMap.get(B).changeStatus(MATCHING) 실행
4. 주문2에서 대기 중이던 B가 갑자기 MATCHING으로 바뀜 → B는 주문3에도 중복 선정 가능

해결법

, else 분기는 현재 OFFERED 상태인 오퍼에만 적용해야 합니다:
} else if (offer.status() == MatchOfferStatus.OFFERED) {
    offer.changeStatus(MatchOfferStatus.WITHDRAWN);
    dreamiMap.get(offer.dreamiId()).changeStatus(WaitingDreamiStatus.MATCHING);
    ...
}
기존 (한글) 변경 (영어)
액션_수행 processAction
드리미_등록 registerDreami
드리미_삭제 removeDreami
매칭_시작 startMatching
드리미의_수락 acceptByDreami
드리미의_거절 rejectByDreami
부르미의_수락 acceptByBoormi
부르미의_거절 rejectByBoormi
드리미가_시간내에_수락안누름 expireDreamiOffer
부르미가_시간내에_수락안누름 expireBoormiOffer
정렬기준 orderingComparator
새로운_UUID_할당 generateUuid
실제_배달_로직으로_진행 proceedToDelivery
주문건 (파라미터) order
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;

/**
 * 부르미 - 드리미 매칭 로직 스켈레톤.
 * 로직 자체는 원본 그대로 두고, 컴파일/자료구조/네이밍 일관성만 보정한 버전.
 */
public class MatchingService {

    /** 드리미 응답 제한시간. TODO: 정책 확정 후 조정 */
    private static final Duration OFFER_TTL = Duration.ofSeconds(30);
    /** 한 주문에 동시에 제안할 최대 드리미 수 */
    private static final int MAX_OFFER_COUNT = 3;

    // ────────────────────────────── 도메인 타입 ──────────────────────────────

    public record GeoPoint(double lat, double lng) {
    }

    /** TODO: 실제 주문 도메인으로 교체 */
    public record Order(UUID orderId, UUID boormiId, GeoPoint destination) {
    }

    /** 큐에 쌓이는 액션. 타입별로 필요한 payload가 달라서 sealed interface로 분리 */
    public sealed interface Action permits DreamiRegister, DreamiRemove {
    }

    public record DreamiRegister(UUID dreamiId, GeoPoint location) implements Action {
    }

    public record DreamiRemove(UUID dreamiId) implements Action {
    }


    /**
     * status가 계속 바뀌므로 record가 아닌 가변 클래스로 변경.
     * (record로 유지하려면 withStatus()로 새 인스턴스를 만들어 맵에 다시 넣어야 함)
     */
    public static final class MatchOffer {
        private final UUID offerId;
        private final UUID orderId;
        private final UUID dreamiId;
        private final LocalDateTime expiresAt;
        private MatchOfferStatus status;

        public MatchOffer(UUID offerId, UUID orderId, UUID dreamiId,
                          MatchOfferStatus status, LocalDateTime expiresAt) {
            this.offerId = offerId;
            this.orderId = orderId;
            this.dreamiId = dreamiId;
            this.status = status;
            this.expiresAt = expiresAt;
        }

        public UUID offerId() {
            return offerId;
        }

        public UUID orderId() {
            return orderId;
        }

        public UUID dreamiId() {
            return dreamiId;
        }

        public LocalDateTime expiresAt() {
            return expiresAt;
        }

        public MatchOfferStatus status() {
            return status;
        }

        public void changeStatus(MatchOfferStatus status) {
            this.status = status;
        }
    }

    public enum MatchOfferStatus {
        OFFERED,                        // 드리미에게 제안이 전달되어 응답 대기 중
        PENDING_BOORMI_CONFIRMATION,    // 해당 드리미가 수락하여 부르미의 승낙을 대기중
        MATCHED,                        // 해당 드리미가 수락했고 부르미도 수락해 매칭 후보로 확정됨
        BOORMI_REJECTED,                // 해당 부르미가 명시적으로 거절함
        DREAMI_REJECTED,                // 해당 드리미가 명시적으로 거절함
        BOORMI_EXPIRED,                 // 제한 시간 내 부르미가 응답하지 않아 만료됨
        DREAMI_EXPIRED,                 // 제한 시간 내 드리미가 응답하지 않아 만료됨
        WITHDRAWN                       // 다른 드리미가 먼저 수락했거나 서버가 제안을 회수함
    }

    /** 기다리고 있는 드리미 (콜 대기중인 드리미). 마찬가지로 status가 바뀌므로 가변 클래스 */
    public static final class WaitingDreami {
        private final UUID dreamiId;
        private GeoPoint location;
        private WaitingDreamiStatus status;
        private LocalDateTime updatedAt;

        public WaitingDreami(UUID dreamiId, GeoPoint location,
                             WaitingDreamiStatus status, LocalDateTime updatedAt) {
            this.dreamiId = dreamiId;
            this.location = location;
            this.status = status;
            this.updatedAt = updatedAt;
        }

        public UUID dreamiId() {
            return dreamiId;
        }

        public GeoPoint location() {
            return location;
        }

        public WaitingDreamiStatus status() {
            return status;
        }

        public LocalDateTime updatedAt() {
            return updatedAt;
        }

        public void changeStatus(WaitingDreamiStatus status) {
            this.status = status;
            this.updatedAt = LocalDateTime.now();
        }
    }

    public enum WaitingDreamiStatus {
        MATCHING,   // 지금 매칭 중
        PROPOSED    // 지금 Match Offer 방 안에 들어감
    }

    // ────────────────────────────── 저장소 ──────────────────────────────

    private final Map<UUID, MatchOffer> offersById = new HashMap<>();           // Map<OfferUUID, MatchOffer>
    private final Map<UUID, Set<UUID>> offerIdsByDreamiId = new HashMap<>();    // Map<DreamiUUID, Set<OfferUUID>>
    private final Map<UUID, Set<UUID>> offerIdsByOrderId = new HashMap<>();     // Map<OrderUUID, Set<OfferUUID>>

    /** 하나의 주문에 대해 동시에 뿌린 제안 묶음 = "방". 원본의 offerMap을 orderId 기준으로 정리 */
    private final Map<UUID, List<MatchOffer>> offersByOrderId = new HashMap<>();

    private final Map<UUID, WaitingDreami> dreamiMap = new HashMap<>();

    private final Queue<Action> q = new ArrayDeque<>();

    // ────────────────────────────── 액션 처리 ──────────────────────────────

    void 액션_수행() {
        Action action = q.poll();
        if (action == null) {
            return;
        }
        switch (action) {
            case DreamiRegister a -> 드리미_등록(a.dreamiId(), a.location());
            case DreamiRemove a -> 드리미_삭제(a.dreamiId());
        }
    }

    void alarmBySocket(String message) {
        // 소켓을 통해 알림보내기
        // 아직 코드 구현X
    }

    void 드리미_등록(UUID dreamiId, GeoPoint location) {
        dreamiMap.put(dreamiId,
                new WaitingDreami(dreamiId, location, WaitingDreamiStatus.MATCHING, LocalDateTime.now()));
    }

    void 드리미_삭제(UUID dreamiId) {
        dreamiMap.remove(dreamiId);
    }

    // 액션 하나 처리
    void 매칭_시작(Order 주문건) {
        List<WaitingDreami> top3List = dreamiMap.values().stream()
                .filter(dreami -> dreami.status() == WaitingDreamiStatus.MATCHING)
                .sorted(정렬기준())
                .limit(MAX_OFFER_COUNT)
                .toList();

        // 상황 : 상위 3명 알림 보낼 드리미 완성
        LocalDateTime expiresAt = LocalDateTime.now().plus(OFFER_TTL);
        List<MatchOffer> matchOfferList = new ArrayList<>();
        for (WaitingDreami dreami : top3List) {
            UUID offerId = 새로운_UUID_할당(); // 제안UUID (드리미 1명당 1개)
            MatchOffer offer = new MatchOffer(
                    offerId, 주문건.orderId(), dreami.dreamiId(), MatchOfferStatus.OFFERED, expiresAt);
            matchOfferList.add(offer);

            offersById.put(offerId, offer);
            offerIdsByDreamiId.computeIfAbsent(dreami.dreamiId(), k -> new HashSet<>()).add(offerId);
            offerIdsByOrderId.computeIfAbsent(주문건.orderId(), k -> new HashSet<>()).add(offerId);
        }

        // 드리미 3명에게 status를 변경 요청
        for (WaitingDreami dreami : top3List) {
            dreamiMap.get(dreami.dreamiId()).changeStatus(WaitingDreamiStatus.PROPOSED);
        }
        offersByOrderId.put(주문건.orderId(), matchOfferList); // 방안에상위3명넣기 (사실상 방 만들기)
        alarmBySocket("드리미에게_제안_팝업_띄우기");
    }

    // 팝업에서 수락을 눌렀다는 가정
    void 드리미의_수락(WaitingDreami dreami, UUID offerId) {
        // 이미 자신 matchOffer상태가 WITHDRAWN이면? -> 실패메시지
        MatchOffer matchOffer = offersById.get(offerId);
        if (matchOffer.status() == MatchOfferStatus.WITHDRAWN) {
            alarmBySocket("이미 다른 드리미가 수락한 주문입니다.");
            return;
        }
        // TODO : 추후에 여기서 다른 matchOffer.status에 대한 분기도 작성 필요

        // 수락한 사람의 상태를 PENDING_BOORMI_CONFIRMATION로 변경
        // 나머지 매칭오퍼 상태를 WITHDRAW로 변경
        for (MatchOffer offer : offersByOrderId.get(matchOffer.orderId())) {
            // 수락한사람은 PENDING_BOORMI_CONFIRMATION
            // 나머지 사람은 WITHDRAWN
            if (offer.dreamiId().equals(dreami.dreamiId())) {
                offer.changeStatus(MatchOfferStatus.PENDING_BOORMI_CONFIRMATION);
                // 드리미의 status는 PROPOSED 유지
                assert dreami.status() == WaitingDreamiStatus.PROPOSED;
                alarmBySocket("부르미한테_드리미정보_팝업넘기기");
            } else {
                offer.changeStatus(MatchOfferStatus.WITHDRAWN);
                // 선착순에서 패배한 드리미를 다시 매칭 수락가능한 상태로 변경
                WaitingDreami otherDreami = dreamiMap.get(offer.dreamiId());
                otherDreami.changeStatus(WaitingDreamiStatus.MATCHING);
                alarmBySocket("팝업꺼지게(선착순패배)");
            }
        }
    }

    // 드리미가 거절하면, DREAMI_REJECTED로 변경 및 다시 대기상태로
    void 드리미의_거절(WaitingDreami dreami, UUID offerId) {
        dreami.changeStatus(WaitingDreamiStatus.MATCHING);
        MatchOffer offer = offersById.get(offerId);
        offer.changeStatus(MatchOfferStatus.DREAMI_REJECTED);
        alarmBySocket("거절했으니 팝업끄면됨");
    }

    void 부르미의_수락(UUID offerId) {
        MatchOffer matchOffer = offersById.get(offerId);
        assert matchOffer.status() == MatchOfferStatus.PENDING_BOORMI_CONFIRMATION;
        matchOffer.changeStatus(MatchOfferStatus.MATCHED); // 부르미까지 수락 완료
        실제_배달_로직으로_진행(matchOffer);
    }

    void 부르미의_거절(UUID offerId) {
        MatchOffer matchOffer = offersById.get(offerId);
        assert matchOffer.status() == MatchOfferStatus.PENDING_BOORMI_CONFIRMATION;
        matchOffer.changeStatus(MatchOfferStatus.BOORMI_REJECTED);

        // 거절당한 드리미의 상태를 배달가능 상태로 변경
        WaitingDreami waitingDreami = dreamiMap.get(matchOffer.dreamiId());
        alarmBySocket("거절당한_드리미에게_부르미가_거절했다고_알려주기");
        waitingDreami.changeStatus(WaitingDreamiStatus.MATCHING);
    }

    // 이건 액션에 의해 실행 되어야함
    void 드리미가_시간내에_수락안누름(UUID offerId) {
        MatchOffer matchOffer = offersById.get(offerId);

        // 해당 match가 OFFERED 상태가 아니라면 다른 로직에 의해서 처리가 된거임
        if (matchOffer.status() != MatchOfferStatus.OFFERED) {
            return;
        }

        matchOffer.changeStatus(MatchOfferStatus.DREAMI_EXPIRED);
        dreamiMap.get(matchOffer.dreamiId()).changeStatus(WaitingDreamiStatus.MATCHING);
    }

    void 부르미가_시간내에_수락안누름(UUID offerId) {
        MatchOffer matchOffer = offersById.get(offerId);
        // TODO: 드리미쪽과 달리 status 가드가 없음. PENDING_BOORMI_CONFIRMATION 체크 필요 여부 확인

        // 드리미가 다시 배달이 가능하게 바꿔야함
        matchOffer.changeStatus(MatchOfferStatus.BOORMI_EXPIRED);
        dreamiMap.get(matchOffer.dreamiId()).changeStatus(WaitingDreamiStatus.MATCHING);
    }

    // ────────────────────────────── 미구현 ──────────────────────────────

    /** TODO: 거리순 등 실제 정렬 기준 확정 전까지는 대기 오래한 순 */
    private Comparator<WaitingDreami> 정렬기준() {
        return Comparator.comparing(WaitingDreami::updatedAt);
    }

    private UUID 새로운_UUID_할당() {
        return UUID.randomUUID();
    }

    private void 실제_배달_로직으로_진행(MatchOffer matchOffer) {
        // 아직 코드 구현X
    }
}
로그 관련된 부분

Lombok의 @Slf4j

private static final Logger log =
        LoggerFactory.getLogger(TestController.class);

그저 자동적으로 작성하는 역할을 함. Spring Boot는 SLF4J의 구현체로 Logback을 사용한다.

TRACE < DEBUG < INFO < WARN < ERROR

순서이다. 기본적으로는 순서가 INFO이기 때문에 보이지 않는다.

# logging.level.root=DEBUG
# logging.level.com.example=DEBUG
# logging.level.com.quick.naengsam=DEBUG
# logging.level.com.example.demo.controller.TestController=DEBUG

와같이 적용시킬 수 있다.

Logging 정보를 파일에 저장하기

logging.file.path=logs # 폴더명만 지정하기
logging.file.name=logs/application.log # 경로가지 지정하기

기본적으로 프로젝트 루트에 폴더명이 적혀있을 것.

예: 콘솔은 INFO, 파일은 DEBUG

다만 application.properties의 단순 설정만으로는 보통 로거 자체의 레벨만 지정합니다.

팀별 스크럼

logging.level.com.example=DEBUG

이 설정은 "DEBUG 이상의 로그를 생성한다"는 의미이지, 콘솔과 파일의 출력 레벨을 각각 분리하는 설정은 아닙니다. 콘솔과 파일을 다르게 설정하려면 일반적으로 logback-spring.xml을 사용합니다.

logback-spring.xml

다음 파일을 생성합니다.

src/main/resources/logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <property name="LOG_PATH" value="logs"/>
    <property name="LOG_FILE" value="${LOG_PATH}/application.log"/>

    <!-- 콘솔 출력 -->
    <appender name="CONSOLE"
              class="ch.qos.logback.core.ConsoleAppender">

        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level
                [%thread] %logger{36} - %msg%n
            </pattern>
        </encoder>

        <!-- 콘솔에는 INFO 이상만 출력 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
    </appender>

    <!-- 파일 출력 -->
    <appender name="FILE"
              class="ch.qos.logback.core.rolling.RollingFileAppender">

        <file>${LOG_FILE}</file>

        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level
                [%thread] %logger{36} - %msg%n
            </pattern>
        </encoder>

        <!-- 파일에는 DEBUG 이상 출력 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>DEBUG</level>
        </filter>

        <rollingPolicy
                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>
                ${LOG_PATH}/application.%d{yyyy-MM-dd}.log
            </fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>

    <!-- DEBUG 로그가 생성될 수 있도록 root를 DEBUG로 설정 -->
    <root level="DEBUG">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>

</configuration>

결과는 다음과 같습니다.

로그 호출 콘솔 파일
log.trace() 출력 안 됨 출력 안 됨
log.debug() 출력 안 됨 출력됨
log.info() 출력됨 출력됨
log.warn() 출력됨 출력됨
log.error() 출력됨 출력됨

아무래도 이정도까지는 굳이 같음.

Troubleshoot: 한글 깨짐 issue
  • 현상

    🖼️ (스크린샷은 노션 원본 페이지에서 확인)

  • 원인 보통 React 프론트에서는 문제 없을 가능성이 큽니다. fetch나 Axios는 JSON 응답을 사실상 UTF-8로 처리하므로:

    const response = await fetch("/api");
    const data = await response.json();
    

    이 경우에는 한글이 정상적으로 들어올 가능성이 높습니다. 실제로 Safari 개발자 도구에서도 정상 JSON으로 보이므로, 서버가 보낸 바이트 자체는 UTF-8인 것으로 보입니다. 다만 Content-Type은 정확히 주는 것이 좋습니다.

    Content-Type: application/json;charset=UTF-8
    

    이유는 다음과 같습니다.

    • 브라우저에서 API URL을 직접 열면 Safari처럼 깨질 수 있음
    • 일부 라이브러리, 프록시, 테스트 도구가 문자셋을 다르게 추론할 수 있음
    • JSON이 아니라 response.text()로 읽는 코드에서 문제가 생길 수 있음
    • 에러 응답만 헤더가 다르면 정상 응답과 예외 응답의 동작이 달라질 수 있음

    즉 현재 상태를 정리하면:

    React에서 response.json() 사용
    → 대부분 정상
    
    Safari에서 API 주소 직접 접근
    → 한글이 깨질 수 있음
    
    서버 응답 헤더에 charset=UTF-8 추가
    → 둘 다 안정적으로 정상
    

    프론트에서 별도로 디코딩하거나 문자열을 변환할 필요는 없습니다. 서버의 응답 헤더를 수정하는 게 맞습니다. Content-Type: application/json;charset=UTF-8 이거만 문제지? 네. 지금 보이는 한글 깨짐 현상만 보면 핵심 문제는 응답 헤더에 charset이 빠진 것입니다.

    Content-Type: application/json;charset=UTF-8
    

    로 내려가면 됩니다. 다만 두 가지는 구분해야 합니다.

    • 404 자체: /에 매핑된 컨트롤러나 정적 파일이 없어서 발생
    • 한글 깨짐: Content-Type에 UTF-8 정보가 없어서 Safari가 잘못 해석

    /의 404는 정상일 수 있고, 수정 대상은 문자 인코딩 헤더입니다. 프론트에서 response.json()이나 Axios를 쓰는 경우에는 대개 정상 처리되지만, 서버에서 헤더를 정확히 주는 편이 맞습니다.

  • 해결 방법 ErrorHandler에 application/json;charset=UTF-8를 붙인다.

TTL → Delay Queue → Action Queue

맞습니다. 다만 static으로 만드는 것과 싱글톤으로 관리하는 것은 구분하는 것이 좋습니다. Spring이라면 static 클래스보다는 싱글톤 Bean으로 만드는 것을 추천합니다. 예를 들어

@Component
public class MatchingEngine {

    private final BlockingQueue<MatchingAction> queue = new LinkedBlockingQueue<>();

    public void submit(MatchingAction action) {
        queue.offer(action);
    }

    @PostConstruct
    public void start() {
        Thread.ofVirtual().start(this::run);
    }

    private void run() {
        while (true) {
            MatchingAction action = queue.take();
            action.execute();

        }
    }
}

그러면 어디서든

@RequiredArgsConstructor
@Service
public class MatchingService {

    private final MatchingEngine matchingEngine;

    public void registerDreami(...) {
        matchingEngine.submit(new DreamiRegisterAction(...));
    }
}

처럼 사용할 수 있습니다. 현재 코드 안에 있는 액션 큐를 한 스레드에서만 실행할 수 있게 하는 방법이 있을까?

왜 Action Queue에서 Virtual Thread를 썼는가?

JDK 21을 사용한다면 Virtual Thread를 써도 충분히 괜찮다. 다만 "성능 향상"을 기대해서 쓰는 것은 아니고, 단순히 최신 Java의 스레드 모델을 사용하는 이유.

Sealed interface에서 상속 바꾸기?

  • ✅ 고민 포인트:
    • 드리미의 경우 드림을 수행할 때 부르미로 전환할 수 있는가?
      • Client의 경우 Toggle을 바꿨을 때 지금 어떤 상태인지 서버에 알려야 함. → 못넘어가면? 구현을 위해서 기능을 제한하는 것이 좋겠다.
    • 반대의 경우? ex) 부르미가 부름을 하고 있을 때 부르미로 전환할 수 있는가? 이것도 일관적으로 없다고 하자.
    • 추가적으로 드리미 → 부르미로 토글을 넘어갈 때 Server의 검증이 필요한 것으로.
  • claude local 환경 설정 https://lucas.codesquad.kr/Softeer-8th/course/u/웹-백엔드/2주차-수업-자료/AI랑-코딩하기2

저녁 회고

  • 석희
    • 유저도메인 구현 완료
    • 프론트 UI 작성 PR
    • 테스트 커버리지 논의
  • 현성, 동혁
    • 어제 구상했던 매칭 로직 부분을 자바 코드로 구현 (delivery 도메인)
    • 큐 동시성 고민

이미지 업로드 문제점

파일 업로드 구현 전략 ⇒ 여기 나온 거랑 동일한 문제..

  • 내일 할 일: Github Discussion 참고

🛵 쉼, 부름

Softeer Bootcamp 8th · WEB Team 3


🏠 시작하기

🤝 협업

📋 기획안

🧠 기술 결정 기록 (ADR)

🕸️매칭 시스템

🧪 부하 테스트

🔧 트러블슈팅

🗓️ 데일리 스크럼


Repo

Clone this wiki locally