-
Notifications
You must be signed in to change notification settings - Fork 3
07 21
📄 원본(노션): https://app.notion.com/p/3a319935b8ae80b499f7e8c0ffda1aeb — 최종 동기화 2026-07-21
제미나이는 안되는걸로 ㄱ-
- 현서 - JPA, JDBC 차이점 정리
- 석희 - 슬랙 방에 영상 올림. JDBC, ORM 공부함.
- 노션 → README 스킬을 만들었지만 멤버라 권한이 없었음. → 복제해서 진행
- 추가적으로 이미지가 노션에서 migrate가 안돼서 할 것을 해야 함.
- 노션 → README 스킬을 만들었지만 멤버라 권한이 없었음. → 복제해서 진행
- 현성 - WAS 할 때 정리 못한 것 정리
- 동혁 - 스프링 강의 API 부분 시청.
ERD, 웹 뷰 프로토타입 진행해야 함. 점심은 한식 뷔페 (땅땅땅)

현성: 로그 찍을 때는 @Slf4j 어노테이션을 통해서 로깅하자.
package com.naengsam.quick;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@Slf4j
@SpringBootApplication
public class QuickApplication {
public static void main(String[] args) {
log.debug("abcd");
SpringApplication.run(QuickApplication.class, args);
}
}Nest js → 모듈끼리 의존하는 방식.
폴더 구조를 어떻게 나눌 것인지
- 계층형 구조
- MVC가 각 폴더에 나뉘어 있음.
-
도메인형 구조
- 작업 단위당으로 폴더를 나눔
- 석희 - 코드의 규모가 커지고 많은 사람들이 협업하는 방식에서는 유리함
- 참고 자료
| 비교 항목 | 도메인 중심 (Package by Feature) | 계층 중심 (Package by Layer) |
|---|---|---|
| 코드 탐색 | 도메인 단위로 모여있어 매우 직관적 | 서비스/레포지토리 폴더를 계속 넘나듦 |
| 응집도 / 결합도 | 높은 응집도, 낮고 명확한 결합도 | 도메인 간 경계가 무너지기 쉬움 |
| 접근 제어 (캡슐화) | Package-private 활용 가능 | 대부분 public으로 열어야 함 |
| 확장성 | 멀티 모듈 및 MSA 분리 용이 | 모듈 분리 시 의존성 해소가 매우 어려움 |
| 추천 대상 | 실무 프로젝트, 중대형 서비스 | 간단한 CRUD 튜토리얼, 소규모 토이 프로젝트 |
🔖 (노션 원본의 북마크 링크 — 상세는 노션에서 확인)
- global 폴더에 config, auth
- Spring Security에서는 built-in 기능이지만 Servlet Filter를 먼저 잡아서 패턴 매칭 - Whitelist도 필요.
- 스프링 시큐리티가 없으니깐, 우리가 직접 시큐리티 Servelt Filter을 이용해서 도입하자.
- JWT?
- 의존성 주입
- 의존성 주입은 생성자 주입 사용 ⇒ 가장 권장되는 방식 (테스트 코드 작성의 편의성 등..)
-
@RequiredArgsConstructor사용 권장
- API는 버전을 도입해서 쓰기.
ex)
api/v1/user
🔖 (노션 원본의 북마크 링크 — 상세는 노션에서 확인)
검색 결과를 보니, Spring Boot 4.x 환경에서 실질적인 선택지는 생각보다 많지 않습니다. 정리해드릴게요.
가장 먼저 말씀드려야 할 건, springdoc-openapi 자체는 Boot 4를 지원한다는 겁니다. 다만 버전을 3.x대로 올려야 합니다(2.6.0이 아니라). 앞서 드린 답변처럼:
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.2'"다른 라이브러리"를 찾으신다기보다, 같은 라이브러리의 맞는 버전을 쓰는 게 먼저 확인할 부분입니다.
과거 대안이었지만, 2020년 이후 업데이트가 없어 사실상 폐기된 상태이며 Spring Boot 3.x 이상과 호환되지 않습니다. Boot 4에서는 당연히 동작하지 않을 가능성이 매우 높습니다. Medium
springdoc-openapi 내에 Scalar라는 대체 UI가 통합되어 있습니다. Swagger UI 대신 좀 더 모던한 인터페이스로 API 문서를 보여줍니다.
- Swagger UI는 개발팀이나 최종 사용자가 구현 로직 없이도 API 리소스를 시각화하고 상호작용할 수 있게 해주는데, Scalar도 같은 역할을 하되 디자인이 다릅니다. SpringDoc
- springdoc 의존성만 있으면 별도 라이브러리 추가 없이 설정으로 전환 가능합니다:
- 실제 서비스가 운영될 때 기존 클라이언트를 깨트리지않고 API를 수정할 수 있음
-
/v1/api/..vs/api/v1..-
/api/v1..방식이 대중적 - 스프링 시큐리티 등에서 매핑할 때도 http.securityMatcher("/api/**") 이렇게 설정할 수 있는 장점이 있음
-
석희 : api 응답을 특정 타입으로 통일해서 클라이언트 처리를 수월하게 하자
{
"resultType": "SUCCESS",
"error": null,
"success": {
data...
}
}-
장점
- 모든 api가 같은 스키마를 사용해 응답하기에 클라이언트에서 처리하는게 편리하다.
- 에러 처리를 한곳에서 모아서 처리 할 수 있다.
- 팀 내 커뮤니케이션 비용이 감소한다.
-
단점
- http 상태코드 자체의 의미가 퇴색 될 수 있다.
- 불필요한 응답크기가 증가된다.
- 과도판 표준화로 오버엔지니어링이 이뤄 질 수 있다.
-
보완점
- 글로벌 예외도 동일한 형식으로 응답하도록 따로 설정 필요함
- 대소문자(SUCCESS) 및 오타 및 공백 문자 오류, resultType의 null등의 문제가 있을 수 있으니 resultType은 boolean 타입으로 하는 것이 좋을 것 같음
- 완전히 Custom한 객체를 만들면 추후에 어떤 예상하지 못한 다른 문제가 생길 수 있음 ⇒ 스프링에서 제공하는 ResponseBodyAdvice를 사용
- (선택) 에이전트 코드 작성 자동화에 응답 반환에 대한 지침 추가 필요 (ResponseEntity를 사용하지말고 CommonResponse를 사용해라)
-
DTO 설계시에는 Java의 record를 사용하기 (불변성 및 편의성)
-
ResponseBodyAdvice를 사용해서 Json응답체를 반환하는 방향으로 결정. ResponseEntity 타입을 반환하는 메소드가 있을 때 응답을 customize 가능하게 됨. https://colabear754.tistory.com/231#ResponseBodyAdvice_구현체를_작성하자
구현 방식: ResponseBodyAdvice 채택 (확정)
대안 대비 채택 이유
- 컨트롤러마다 직접
CommonResponse로 감싸는 방식은 반복 작업이 많고, 감싸는 걸 누락하면 그 API만 스키마가 깨진 채 나가는 위험이 있음 -
ResponseBodyAdvice를 쓰면 컨트롤러는 원래 도메인 객체를 그대로 반환하면 되고, 포장은 전역에서 한 번에 처리 → 누락 가능성 자체가 사라짐 -
@ResponseBody뿐 아니라ResponseEntity반환 메소드에도 적용 가능해서 기존 코드와 공존 가능 - 포맷 변경 시
CommonResponseAdvice한 곳만 수정하면 되므로 유지보수 지점 단일화 - 결론: 스키마 통일이라는 목표를 "코드 컨벤션"이 아니라 프레임워크 레벨에서 강제할 수 있다는 점이 핵심 채택 근거
만약 스키마가 통일되어 있지 않다면?
API마다 응답 형태가 제각각이면, 프론트엔드는 API별로 다른 파싱 로직을 짜야 합니다.
// API A: 데이터만 반환
const resA = await axios.get('/api/a'); // { id: 1, name: "..." }
// API B: data로 감싸서 반환
const resB = await axios.get('/api/b'); // { data: { id: 1 } }
// API C: 에러 시 message 필드, 성공 시 result 필드 사용
const resC = await axios.get('/api/c'); // { result: {...} } or { message: "error" }
// 결과: 매 API 호출마다 이런 코드가 반복됨
if (resC.message) {
// 에러 처리
} else if (resC.result) {
// 성공 처리
}스키마가 통일되어 있다면?
resultType / success / error 구조가 모든 API에서 동일하므로, 공통 처리 함수 하나로 모든 API를 다룰 수 있습니다.
// 공통 응답 타입 정의 (한 번만)
interface CommonResponse<T> {
success: boolean;
error: { code: string; message: string } | null;
data: T | null;
}
// 공통 처리 함수 (한 번만 작성하면 모든 API에 재사용)
async function request<T>(url: string): Promise<T> {
const res = await axios.get<CommonResponse<T>>(url);
const { resultType, error, success } = res.data;
if (resultType === false) {
throw new ApiError(error!.code, error!.message);
}
return success as T;
}
// 실제 사용 - 어떤 API든 동일한 방식으로 호출
const workspace = await request<WorkspaceResponse>('/api/workspaces/123');
const users = await request<User[]>('/api/users');→ API가 몇 개든, 어떤 팀이 만들었든 호출부 코드가 똑같아집니다.
스키마가 통일되어 있으면 개별 API 호출마다 try-catch를 반복할 필요 없이, 인터셉터 하나로 전역 에러 처리가 가능합니다.
axios.interceptors.response.use(
(response) => {
const { success, error, data } = response.data;
if (!success) {
// 에러 코드별 공통 처리 (토스트, 리다이렉트 등)
if (error.code === "AUTH_EXPIRED") {
router.push("/login");
} else {
toast.error(error.message);
}
return Promise.reject(error);
}
// success 데이터만 뽑아서 그대로 반환 → 호출부는 wrapping 신경 안 써도 됨
response.data = success;
return response;
}
);
// 이후 모든 컴포넌트에서는 이렇게 깔끔하게 사용 가능
const { data } = await axios.get('/api/workspaces/123');
console.log(data.workspaceId); // wrapping 벗겨진 상태로 바로 접근axios.interceptors.response.use(
(response) => {
const { success, error, success } = response.data;
if (!resultType) {
// 에러 코드별 공통 처리 (토스트, 리다이렉트 등)
if (error.code === "AUTH_EXPIRED") {
router.push("/login");
} else {
toast.error(error.message);
}
return Promise.reject(error);
}
// success 데이터만 뽑아서 그대로 반환 → 호출부는 wrapping 신경 안 써도 됨
response.data = success;
return response;
}
);
// 이후 모든 컴포넌트에서는 이렇게 깔끔하게 사용 가능
const { data } = await axios.get('/api/workspaces/123');
console.log(data.workspaceId); // wrapping 벗겨진 상태로 바로 접근axios.interceptors.response.use(
(response) => {
const { resultType, error, success } = response.data;
if (!resultType) {
// 에러 코드별 공통 처리 (토스트, 리다이렉트 등)
if (error.code === "AUTH_EXPIRED") {
router.push("/login");
} else {
toast.error(error.message);
}
return Promise.reject(error);
}
// success 데이터만 뽑아서 그대로 반환 → 호출부는 wrapping 신경 안 써도 됨
response.data = success;
return response;
}
);
// 이후 모든 컴포넌트에서는 이렇게 깔끔하게 사용 가능
const { data } = await axios.get('/api/workspaces/123');
console.log(data.workspaceId); // wrapping 벗겨진 상태로 바로 접근적용 시 반드시 처리해야 할 예외 케이스 (참고 자료 기반)
-
예외 응답 이중 포장 문제:
ExceptionHandler가 이미CommonResponse로 감싸 반환해도 Advice가 재포장하면 에러인데SUCCESS로 덮여버림 →body타입이 이미 공통 응답 DTO면 재포장하지 않도록 조건 분기 필요 -
String 반환 메소드에서
ClassCastException:StringHttpMessageConverter는 제네릭이String으로 고정되어 있어 객체로 감싸면 캐스팅 에러 발생 →supports()에서StringHttpMessageConverter(및 서브클래스)는 적용 대상에서 제외 필요
- 가짜 데이터를 넣어놓고 이 사람이 요청했을 때 근거리에 얼마나 사람들이 있나?
- 발표 영상
- 부스를 하면 시연을 어떻게 해야하나?
- 가짜로 움직이는걸 만들어야할 필요가 있음.
- GPS의 정확도 문제
- 부스가 작고 실내면 GPS가 잘 안됨.
- 모바일 웹으로 위치가 잡히나?
- 비대면 대면 인증은 어떻게?
- 가짜로 움직이는걸 만들어야할 필요가 있음.
-
우선순위 결정을 할 때 시연 중심으로 설정하는 것이 좋음.
- 파손, 분실 등은 후순위로!
웹소켓 구현 방식
- 제안1 : 소켓은 오직 매칭과정에서만 사용하자
- 복구 위험성
- 만약에 이미 배송중에 소켓이 끊기면 웹소켓 연결 복구 로직이 필요함
- 매칭 과정에서는 소켓이 끊겨도 다시 연결하면 됨
- 구현 난이도가 훨씬 쉬움
- 상대의 위치는 실시간 0.1초 단위로 정확해야할 필요가 없음
- 예를 들어 5초마다 요청 → 5초 사이의 시간 평균을 이용해서 애니메이션으로 부드럽게 이동처리 가능
- 복구 위험성
드리미가 배달 중에 웹사이트(브라우저)를 꺼버린 경우
- 재접속 대기
- 알림 보내기 (FCM)
- 문자 보내기
- 고객센터에서 전화
휴대전화를 생각 할 때는 상단을 생각한다.
Delivery 파트 할 일
- 매칭 후에 배달 중에 소켓 or API
- 우선순위 로직
- 우선순위 알고리즘
- 소켓에서 채널 하나에 N명 넣고 여기서 몇명뽑아서 보낼지 or 채널분리
- 비용 계산
- 우체국 유사하게 고정 비용
- 사용자가 어느정도 비용 정할 수 있게
- 비용 추천 → 올리거나 내리거나? 일정 이상 시간이 지나면 가격이 바뀔 수 있게?
- 픽업, 수령은 대면 기준.
서명. 클라이언트에서
svg형식으로 전송 한다고 가정하고 작업 → 넣을 순 있다. - 클라이언트, 서버에서 → heartbeat
추가적으로 드리미가 배달 중에 웹사이트(브라우저)를 꺼버린 경우
- 재접속 대기
- 알림 보내기 (FCM)
- 문자 보내기
- 고객센터에서 전화
- current state → next state로 이동 할 때 필요한 input, output 확실히 정의
가짜 위치 정보 or 이걸 먼저 구현 하나?
Team 3 · 석희 · 현성 · 동혁 · 현서
Softeer Bootcamp 8th · WEB Team 3 · 이 위키는 팀 노션에서 동기화됩니다 📄
🏠 시작하기
🤝 협업
📋 기획안
🧠 기술 결정 기록 (ADR)
- 세션 vs 토큰
- 세션 및 다중 탭 처리 정책
- 실시간 배달 상태 전달에 SSE를 선택한 이유
- SSE 연결은 왜 1개였다가 5개가 되고, 다시 1개가 되었을까
- 드리미 GPS 끊김 감지 정책
- Redis 기반 로그인 대기열 도입기
- 카카오 API 응답 Redis 캐시 설계
- 활동 내역 조회를 커서 기반 페이지네이션으로 진행
- 알림 - WebPush 도입 이유와 SSE의 한계
- S3 Presigned URL 도입 결정
- 포인트, 머니 시스템 설계
🕸️매칭 시스템
- 매칭 시스템 설계
- Command와 Producer Consumer를 조합한 single writer 구조의 매칭 엔진
- 제안은 누구에게 해야 할까
- 쉼, 부름의 부르미‐드리미 매칭은 어떻게 동작할까
- 배정 정책에 Greedy 배정을 선택한 이유
- 매칭 시스템 구현
🧪 부하 테스트
- 매칭 부하테스트
- 매칭 부하테스트 보고서 (8.12)
- 매칭 부하테스트 보고서 (8.13)
- 매칭 부하테스트 보고서 (8.13 · 외부 API 트랜잭션)
- 매칭 부하테스트 보고서 (8.18 · 배포환경 720명)
🔧 트러블슈팅
- UploadSession - 옛 키 재사용 공격 방어 설계
- 8/6 · EC2 SSH 접속 불가
- 8/7 · 매칭 확정 후 Delivery가 생성되지 않는 고아 Order 문제
- 8/8 · 영속성 컨텍스트 detach로 dirty checking 미반영 문제 (@Modifying(clearAutomatically = true))
- 매칭 확정 후 Delivery가 생성되지 않는 고아 Order 문제
- 매칭이 성공하지 않았음에도 Orders 테이블에 PENDING으로 상태 변경하는 문제
- 부르미 확인 타임아웃 시 주문 DB·매칭 메모리 불일치
- 주변 콜 조회 시 반복되던 주문 조회 개선
- 드리미 오프라인 조회에 필요한 복합 인덱스 추가
- 매칭 단계 취소시 포인트 이중 환불 문제
- 로컬 DevStorage 사용시 로그인 상태에서도 401 UNAUTHORIZED 뜨던 문제
🗓️ 데일리 스크럼