🙊

깨달음 줍줍 ~

이채민

카드 컴포넌트 스타일링 재사용 방식 비교

tailwindcss @components vs 스타일링된 컴포넌트 별도 정의 방식 <Card></Card>
1.
@components 는 가독성은 후자에 비해 떨어지지만 태그, 스타일링 등을 확장하기 수월
// global.css @layer components { .card { ... } } // 활용 <div className="card 덮어쓸_스타일">...</div> <section className="card">...</section>
JavaScript
복사
2.
컴포넌트 정의시 가독성은 높지만 커스텀을 위해서는 props로 넘겨야한다는 불편함과 다른 태그 사용 안됨.
function Card({className, children}){ // className을 상속받아 추가 return <div className={`스타일 ${className}`}>{children}</div> } // 활용 <Card></Card>
JavaScript
복사
→ 단순 배경색 뿐 아니라 커스텀되어야하는 스타일링이 많아 tailwindcss @components 채택

필터링을 가지는 리스트 모두 클라이언트 컴포넌트로 구현해야하는가?

1.
CSR을 최소화하자 - 서버 컴포넌트만을 활용 할 순 없을까?
→ path variable을 활용하여 서버 컴포넌트로 일괄 변경 /funding/{category}
2.
카테고리와 정렬을 모두 가지는 리스트의 경우는 어떻게 처리할 것인가?
모두 path variable로 나타내기에는 URL 설계 방식에 어울리지 않아보임
e.g. payments/{category}/{filter}
→ searchParams를 함께 활용하자 /all?sort=latest
3.
잘못된 params와 searchParams를 판별하는 validation 로직 필요
Layout에서 해당 로직 추가 시도
→ layout.tsx 에서는 params만, page.tsx에서는 searchParams를 validate하기로 함
4.
뒤로가기 클릭시 홈으로 이전 페이지로 이동하길 바라는 사용자의 기대와는 달리 이전에 선택한 카테고리 탭으로 이동하는 문제 발생
영상자료
라우팅 히스토리를 관리하기 위해서는 클라이언트 컴포넌트여야함.
→ 모든 필터가 삽입된 리스트페이지를 클라이언트 컴포넌트로 변경
use client 를 사용하는 것에 죄책감을 갖지 말자!
클라이언트/서버 컴포넌트는 각각의 장점이 존재하며,
이를 적절히 섞어 사용할 수 있는 것이 Next.js의 특징. 잘 활용하도록 하자

커스텀 타입 체크

const fruit = ["apple", "banana", "grape"] as const; type Fruit = (typeof fruit)[number]; const isFruit = (x: any): x is Fruit => fruit.includes(x);
JavaScript
복사

학습내용

타입스크립트 자바스크립트에 타입 시스템을 추가하고, 코드가 트랜스파일될 때 삭제된다.
typeof 의 결과가 저장되는 변수 키워드(let, type)에 따라 사용되는 오퍼레이터가 다름 (즉, 결과값이 다름)
as const
array, object와 같은 리터럴 타입을 as const 을 통해 타입의 범위를 구체적으로 제한할 수 있다
const ab = ["a", "b"]; // const ab = string [] const ab = ["a", "b"] as const; // const ab = readonly ["a", "b"]
JavaScript
복사
is
사용자 정의 타입인지 확인하는 용도로 사용되는 키워드로,
특정 요소의 타입을 체킹하는 함수의 리턴 타입으로 사용된다
// a의 타입이 string인 경우, 즉 리턴값이 true인 경우 value는 string 타입이라 정의 function isString(value: unknown): value is string { return typeof a === "string"; }
JavaScript
복사

useForestField 커스텀 훅으로 분리

카드 이미지 방향 설정 util 함수 구현

서승주

MSA: 영향이 가장 작은 것 → 부하가 가장 많은 것 순서대로 쪼갬

1. 결제 시스템(Pay-Transaction-Payment)

결합도

상황: 결제/환불 요청 흐름에서 로직간 결합도가 높았음
문제: 유저가 결제를 요청하면 성공/실패 응답 받기까지 시간 오래 걸림
해결: 트랜잭션 생성 완료되자마자 유저에게 결제 상태 변경(pending) 반환
트랜잭션 성공 → Settlement, Notification, Schedule 서비스는 kafka로 통신
 Pay Service가 죽었을 경우?
상황: 유저가 결제 요청 → (Transaction Service → …) 로직 진행 중에 Pay Service 죽음 ⇒ 유저가 결제 성공/실패 어떻게 알 건지
잘못하면 결제 업체가 돈 먹을 수 있음
⇒ Pay Service가 죽었는지 살아있는지 매번 확인? retryCount?

외부 클라이언트

상황: 내부 결제 시스템만 생각해서 단순히 결제하는 내부 클라이언트가 요청하는 것으로 생각
결제 과정 프론트엔드가 존재하는데 결제 클라이언트와 별개로 생각 X
= (클라이언트) 결제 과정 페이지에서 결제 요청 → (서버) 결제 처리라고 생각
(외부 클라이언트) 결제 요청 → (결제 과정 클라이언트) 결제 페이지 → (결제 서버) 결제
문제: (외부 클라이언트 → 결제 과정 클라이언트 → 서버) 로직은 보안 위험
최종적으로 결제 완료 후 외부 클라이언트에게 반환했을 때도 외부 클라이언트가 자신의 서버에 결제 안된 것처럼 속일 위험 있음
해결: (외부 클라이언트) 결제 요청 → (외부 서버) 유저인증, 결제 요청 → (결제 서버) 결제, 리다이렉트 (결제 과정 클라이언트) 결제 성공/실패 응답

실제 결제할 때 한 번 더 확인?

 결제 요청을 하고 카드 어플이나 웹사이트로 결제 확인을 하고 다시 결제 요청 화면으로 돌아왔을 때
유저가 체크 표시 O → “진짜” 결제 / X → 결제 취소
⇒ 결제 요청을 한 것 자체는 트랜잭션에 기록 되어야 함(상태 변경: Pending → Succss/Faild/Canceld)
이 때, 유저의 계좌에서 진짜 실제 돈을 빼서 들고 있을까 아님 시스템상으로만 들고 있을까
이 경우 만약 유저가 체크 표시 화면에서 나가서 다른 상품을 결제하면 어떻게 될까? ⇒ 유저가 체크 표시를 하고 최종 버튼을 눌러야 “진짜” 결제가 될 때 실제 돈이 빠져나가야 함
결제 요청 금액을 어떻게 관리하는가
은행의 역할까지 수행 → 고객 계좌에 holding된 금액을 별도 관리 → 금액 이상만큼 다른데서 못찾아가게 처리
플랫폼의 역할만
진자 결제를 할때 돈을 차감해오는 과정에서 돈 부족하다면 모두 롤백처리
⇒ 최종 결제시에는 무조건 해당 금액이 있는지 체크하고 없다면 롤백
→ 이전: pay service에서 결제 진입 시에만 1차 금액 확인 후 금액이 없다면 결제 X
→ 수정: payment service에서 실제 결제(최종 결제)시에 다시 한 번 해당 금액 확인
상황: 결제 요청하면 바로 알아서 흘러가서 결제되는 형식
문제: 결제할 때 결제 프론트엔드에서 최종적으로 결제 버튼을 눌러야 “진짜” 결제가 됨
즉, 최종 결제 버튼 누르지 않고 나가면 결제 취소
해결: 로직 한 번 더 분리
v1/payment 처음 결제 요청 → 트랜잭션 Pending 상태, 결제 프론트엔드로 리다이렉트
v1/payment/verify 리다이렉트 url에 있던 토큰으로 상태 확인
v1/payment (결제 프론트엔드) 최종 결제 버튼 → 트랜잭션 ID 찾고 실제 결제 로직
payment service에서 최종 결제 전에 한 번 더 페이에 돈이 있는지 확인
현재 로직에서는 pay service에 account, pay, point가 있어서 이렇게 그림이 나올 수 밖에 없음

포인트 및 실결제

상황: 포인트 사용, 결제 API가 각각 존재
PUT /v1/pay/points/use
POST /v1/pay/payment/{sellerId}
클라이언트가 결제 전 포인트 사용 API 호출 → (원래 금액 - 포인트 사용 금액) 결제 API 호출
문제: 클라이언트에서 포인트 사용 API를 호출하지 않고 (원래 금액 - 포인트 사용 금액)을 넣은 결제 API 호출할 수 있음
해결: 포인트 사용은 결제할 때만 사용 → 결제 로직 내부에서 포인트 사용까지 처리

계좌 예외 처리

상황: 계좌 생성, 수정, 삭제 API 존재
문제: 페이는 무조건 한 개 이상의 계좌를 가지고 있어야 함
같은 계좌 번호가 여러 개 등록되는 상황(은행API를 연결하지 않았기 때문)
해결: 페이 생성 시 계좌 생성 로직 재활용하기
커스텀 예외 처리 활용하기: 같은 계좌 번호 사용 X 계좌가 1개일 때 삭제 X

취소 및 환불

 한 서비스 내에 로직이 많아지는 것 vs. 통신이 많아지는 것 → 어떤 게 효율적?
각 서비스가 어디서부터 어디까지 담당할 것인가 명확하게 표현되어야 함
상황: payment service와 refund service 분리
payment service: 결제
refund service: 취소와 환불
해결: http 통신이 적어서 더 효율적이라고 판단 → payment service와 refund service 통합

2. Acitivity History

Admin Service에는 관리자(manger, admin)의 행동 히스토리 작성 (OpenFeign 사용)
상황: Item/Funding Service에서 Admin Service(ActivityHistory) 호출해서 작성
v1/items, v1/fundings
문제: Admin Service가 죽으면 Item 서비스에도 영향 O
해결: Admin Service에서 Item Service 호출
→ Admin Service가 과부화될 위험 있음 But, 죽어도 다른 서비스에 영향 X
→ URI가 v1/admin/으로 시작하게 할 수 있음
AdminItemController: v1/admin/items
AdminFundingController: v1/admin/fundings

3. 관리자

상황: 관리자와 유저의 URI가 같고 안에 서비스 로직에서 권한 확인 후 각각의 결과 반환
문제: 관리자와 유저는 반환 값이 다름
해결: 유저는 Page, 관리자는 List로 반환, URI 분리
단, 유저는 상황마다 다름
펀딩 리스트는 정보량이 많아 페이지네이션 적용하기
아이템이나 기부사 리스트는 리스트로 반환
Before
After

4. 코드 분리

서비스 내부에서 작동하는 코드 → Internal 인터페이스로 분리
ex. public class ItemServiceImpl Implements ItemService, InternalService {...}

5. 아이템 반환 with 채민

상황: MyItem은 UPDATE가 많아 MyItem 중 현재 사용 중인 아이템은 UsingItem 테이블로 분리(일대일 관계)
사용 중 아이템 한 개마다 서버를 호출하는 구조
POST /v1/items/my-items/using-items, PUT DELETE /v1/items/my-items/using-items/{usingItemId}
문제: 서버 요청이 빈번한 상황 → 서버 부하 문제
고민: 새로운 API 만들어서 → action으로 CREATE, UPDATE, DELETE 넣어 보내기
/v1/items/my-items/using-items/action
문제: 서버가 action에 다른 값 들어오는 상황을 처리해야 함
고민: 편집 모드를 넣기
편집 버튼 클릭 → 아이템 수정 → 완료 버튼 클릭 + 리스트로 보내기
문제: 서버 부하는 적어지나 사용자가 바로바로 아이템을 수정할 수 없어 유저 친화적 X
해결: GraphQL 도입
REST보다 네트워크 트래픽 감소
하나의 mutation → 단일 엔드포인트 ⇒ 복잡한 작업(CREATE, UPDATE, DELETE) 처리
subscription 기능 사용 → 실시간으로 UI에 반영

6. 채팅방

상황: 채팅 AI 모델 SpringBoot 서버(WebSocket) 클라이언트
→ 실시간성, 다중 사용자 동시 처리 및 성능이 중요함
문제
1.
동기적 I/O 사용하는 Tomcat으로 설계 → 동시 요청 처리량 제한 ⇒ 성능 저하
2.
비효율적인 비동기 처리
Thread.sleep(500): 메시지 응답 생성에서 불필요한 블로킹 호출
CompletableFuture.runAsync : 별도의 스레드풀 X or 최적화 X → 요청량 ↑ ⇒ 병목 현상
CompletableFuture.runAsync(() -> { try { Thread.sleep(500); // 불필요한 블로킹 호출 ChatMessage aiResponse = ChatMessage.builder() .sender("AI") .message(chatService.generateResponse(chatMessage.getMessage())) .type(MessageType.CHAT) .build(); messagingTemplate.convertAndSend("/topic/public", aiResponse); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } });
Java
복사
3.
AI 응답 생성 병목
RestTemplate 사용해 동기적으로 처리 → 호출량 ↑ ⇒ 병목 현상
@Service @RequiredArgsConstructor public class ChatServiceImpl implements ChatService { private final RestTemplate restTemplate; @Value("${fastapi.url}") private String FASTAPI_URL; @Override public String generateResponse(String userMessage) { try { Map<String, String> requestPayload = new HashMap<>(); requestPayload.put("question", userMessage); HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(requestPayload); ResponseEntity<Map> responseEntity = restTemplate.postForEntity(FASTAPI_URL, requestEntity, Map.class); if (responseEntity.getStatusCode() == HttpStatus.OK && responseEntity.getBody() != null) { Map<String, Object> responseBody = responseEntity.getBody(); return responseBody.get("answer").toString(); } else { return "서버로부터 유효하지 않은 응답"; } } catch (Exception e) { return extractDetailFromErrorMessage(e.getMessage()); } } }
Java
복사
해결
WebFlux + Netty 사용
WebFlux: 비동기 논블로킹 방식
Netty: 적은 스레드로 높은 동시 요청 처리할 수 있음
AI 응답에 gRPC 사용
네트워크 오버헤드 줄이고 요청-응답 시간 단축
HTTP/2
Protocol Buffers → 데이터 직렬화 최적화
JSON/텍스트 기반 프로토콜보다 데이터 크기 작고 전송 속도 빠름
Kafka도 비동기
메시지 브로커 거쳐 메시지 전달 → 지연 발생
이벤트 기반 동작 → 소비자 지속적으로 polling하는 구조 추가로 구현
⇒ 수십만 명 사용자 동시접속하는 대규모 채팅 애플리케이션에서 확장성 위해 사용 O

최호윤

추천 시스템 응답시간 최적화

추천 시스템에서 request를 받았을 때, 응답 시간이 6~7초 소요되는 문제 발생
문제 원인 → 추천 응답을 받을 때 마다 tf-idf 로 벡터라이징 하는 과정을 반복하고 있었음
해결 방안 → HuggingFace 임베딩 모델로 혜택 및 카테고리를 미리 임베딩해서 저장해서 사용
추천된 카드 결과에 llm을 사용해서 필요한 키워드를 추출
문제 원인 → 결과를 추출하고 굳이 formatting 함수를 한 번 더 거쳐서 결과를 출력하는 것은 비효율적
해결 방안 → Prompt Engineering을 통해 해결 가능한 부분, 이외에도 Prompting으로 해결할 수 있는 부분은 최대한 Prompting으로 해결할 수 있는 방법을 찾아서 해결할 것
결과: 변경 이후 응답 시간을 3~4초로 줄일 수 있었음

챗봇 수정

기존에 구현했던 VectorDB를 사용하여 RAG 구성한 방식이 구현하려는 서비스에서 챗봇의 역할과 데이터를 보았을 때 적합하지 않다는 평가.
제안 받은 개선 방향
Text-to-SQL 개념을 적용 → 입력 받은 사용자의 질문을 SQL로 변환
프롬프트에 질문에 대한 View를 생성하도록 DDL을 정의
백엔드로 api를 호출할지, DB를 직접 호출할지
호출해서 조회한 내용을 토대로 답변을 생성
답변 예시가 다채롭게 있어야, 다양한 View를 만들 수 있음

1. Text-to-SQL 기반 View 생성 흐름

단계별 설명:

1.
View 생성
Data Warehouse에서 챗봇에 사용할 데이터의 요소들을 뽑아 View를 생성
2.
사용자 질문 입력
사용자의 자연어 질문을 입력받음
3.
Text-to-SQL 변환
질문을 기반으로 적절한 SQL 쿼리를 생성
생성된 SQL은 데이터베이스의 View 조회 DDL 형태로 변환
4.
질문에 대한 답변 생성
생성된 View를 조회하여 데이터를 기반으로 답변 생성
예: SQL 결과를 요약하거나 자연어 형태로 포맷

2. API 호출 vs DB 직접 호출

비교

API 호출
장점: DB와의 분리 유지, API 게이트웨이를 통한 보안 및 로깅 강화
단점: API의 추가적인 호출 비용과 지연 가능성
적용: 외부 데이터 시스템이나 통합 서비스에서 주로 사용
DB 직접 호출
장점: 실시간 응답성 우수, 추가적인 중간 계층 없이 간결
단점: 직접적인 DB 접근이 보안적으로 취약할 수 있음
적용: 사내 전용 시스템이나 직접 접근이 필요한 고성능 요구 환경

3. 답변 다양성 확보