📌 프로젝트 소개
MSA 기반 선착순 구매 프로젝트
대규모 트래픽 가능성을 염두에 둔 e-Commerce 기반의 웹 서비스
사용자는 회원가입 이후 정보확인, 장바구니, 위시리스트, 구매와 같은 기능을 경험할 수 있고
특정 시간에 한정된 수량의 상품을
선착순으로 결제하고 구매할 수 있는 서비스를 사용가능한 서비스입니다.
프로젝트 기간: 24.12.18 ~ 25.01.09
개발인원 : 개인프로젝트 (1명)
관련 도메인
배달의 민족, 무신사, 29cm 등 e-Commerce
주차별 구현 목표
- 프로젝트 기획
- DB 설계
- API 설계
- 프로젝트 구조 설계
- 개발 환경 구축 ERD 설계 및 docker-compose 설치
- 유저 및 상품 관리를 위한 데이터 셋 구현
- PostMan을 활용한 테스트 시나리오 작성
1주차
- Docker를 활용한 로컬 개발 환경 구축
- 모노리스 서비스의 마이크로 서비스화
- API Gateway 생성, 장애 상황 연출 후 회복 탄력성 갖추기
2주차
- 쇼핑몰 목업 사이트 구축을 위한 API 설계
- 자동화 테스트 툴 구축
- Redis를 활용한 대규모 주문 처리 기술 이해
3주차
- 목업 사이트 내 실제 구매, 재고 관리 기능 등 구현
- 인수 조건에 따른 결제 프로세스 구현
- Non Blocking 형태로 API Gateway 전환
4주차
🛠️ 기술스택과 선택이유
백엔드 기술 스택
Java 17:
- LTS(Long Term Support) 버전으로서 안정성이 보장됨
- 향상된 성능과 새로운 기능(Record, Pattern Matching 등)을 활용 가능
- 광범위한 커뮤니티와 풍부한 레퍼런스 존재
Spring Boot 3.4.0:
- 빠른 개발 생산성과 자동 설정 기능 제공
- Spring의 복잡한 설정을 간소화
- 내장 서버(Tomcat)를 통한 독립적인 실행 환경
Spring Data JPA:
- 반복적인 CRUD 작업을 줄여주는 Repository 인터페이스 제공
- 객체 지향적인 데이터 접근 방식
- 데이터베이스 벤더에 독립적인 개발 가능
서버 인프라
Apache Tomcat:
- 가볍고 안정적인 웹 서버/컨테이너
- Spring Boot와의 완벽한 통합
- 대규모 트래픽 처리에 검증된 성능
AWS RDS:
- 관리형 데이터베이스 서비스로 운영 부담 감소
- 자동 백업 및 고가용성 제공
- 손쉬운 스케일링 가능
데이터베이스
MySQL 8.0:
- 오픈소스로 비용 효율적
- 높은 안정성과 성능
- JSON 지원 등 현대적인 기능 제공
Redis:
- 인메모리 캐싱을 통한 성능 향상
- 세션 관리 및 일시적 데이터 저장에 적합
- 분산 락 구현 가능
분산시스템/아키텍처
Eureka Server:
- 마이크로서비스 간의 서비스 디스커버리 제공
- 동적인 서비스 등록과 발견
- 시스템의 안정성과 확장성 향상
Spring Cloud Gateway:
- 마이크로서비스에 대한 통합된 엔트리포인트 제공
- 라우팅, 필터링, 로드밸런싱 기능
- 보안 및 모니터링 통합 용이
MSA:
- 서비스의 독립적인 개발과 배포 가능
- 개별 서비스의 독립적인 스케일링
Docker:
- 일관된 개발/운영 환경 제공
- 컨테이너화를 통한 효율적인 리소스 관리
- 마이크로서비스 배포 용이성
테스트 도구
K6:
- 성능 테스트 및 부하 테스트에 특화
- 코드 기반 테스트 시나리오 작성 가능
- 확장 가능한 테스트 환경 제공
Postman:
- API 테스트 및 문서화 용이
- 팀 협업 기능 제공
- 환경 변수 관리 편리
JUnit5:
- Java 생태계에서 가장 널리 사용되는 테스트 프레임워크
- 풍부한 검증 기능과 확장성
- 테스트 코드의 재사용성과 가독성 향상
형상관리
Git & GitHub:
- 분산 버전 관리로 협업 용이
- CI/CD 파이프라인 통합 편리
- 코드 리뷰와 이슈 트래킹 기능
IDE
IntelliJ IDEA:
- 강력한 코드 분석과 리팩토링 도구
- Spring Boot 프로젝트에 최적화된 지원
- 다양한 플러그인 생태계
🤔 기술적 의사결정
1. Dockerfile과 docker-compose.yml 위치 선정 기준
통합 관리 vs 개별 관리
① 루트 경로에 하나의 Dockerfile과 docker-compose.yml을 둘 경우
- 목적: 전체 프로젝트를 하나의 컨테이너나 통합된 서비스로 관리.
- 특징:
- 단일 Dockerfile로 프로젝트 전체를 컨테이너화.
- docker-compose.yml에 모든 모듈의 실행 정보를 통합 관리.
- 적합한 상황:
- 각 모듈이 완전히 독립된 서비스가 아니며 하나의 애플리케이션처럼 동작.
- 프로젝트 규모가 작아 모듈별로 완전히 독립 배포가 필요하지 않은 경우.
② 각 모듈별 Dockerfile을 둘 경우
- 목적: 각 모듈(서비스)을 독립적으로 컨테이너화하고 배포.
- 특징:
- 모듈별 Dockerfile 생성으로 개별 빌드 및 실행 가능.
- docker-compose.yml은 루트 경로에 두고, 모듈별 Dockerfile을 참조.
- 적합한 상황:
- 모듈 간 완전한 독립성과 분리된 배포가 필요한 경우.
- 각 모듈의 기술 스택이 다르거나, 서로 다른 환경이 필요한 경우.
- 확장성과 독립 배포가 중요한 MSA 환경.
MSA 프로젝트에 적용 가능한 선택과 고려 사항
① 기술 스택이 동일한 경우
- 현황: 모든 모듈이 동일한 기술 스택(Java, Spring Boot)으로 개발될 예정.
- 고려 사항:
- 기술 스택이 동일하더라도 서비스 독립성을 보장하기 위해 모듈별 Dockerfile이 권장됨.
- 개별 빌드와 배포 가능성을 유지하는 것이 장기적으로 유리.
② 특정 모듈만 실행하는 경우의 필요성
- 현황: 현재 프로젝트 구조 상 특정 모듈만 실행할 필요성은 낮음.
- 결론: docker-compose.yml은 각 모듈별로 작성하지 않고 루트 경로에 단일 파일로 관리.
최종 결론 및 진행 방향
- Dockerfile 작성: 각 모듈별 Dockerfile 작성으로 독립성을 유지.
- docker-compose.yml 작성: 루트 경로에 단일 파일을 작성하고, 각 모듈의 Dockerfile을 참조.
- 목표: 서비스 간 독립성을 유지하면서도 전체 서비스 관리의 간결성을 확보.
2. RESTful API 응답 코드 설계
성공 응답 (2xx) 사용 기준
선택 이유
- 클라이언트의 요청 처리 상태를 명확하게 전달
- REST API 표준을 준수하는 일관된 응답 체계 구현
- 클라이언트 측의 효율적인 에러 핸들링 지원
구현 상세
- 200 OK: 일반적인 성공적 요청 처리 완료 (예: 조회 성공, 처리 성공)
- 201 Created: 새로운 리소스 생성 완료 (예: 회원가입, 새로운 리소스 등록)
- 202 Accepted: 비동기 작업 접수 완료 (예: 대용량 파일 업로드, 배치 작업 요청)
- 204 No Content: 성공적 처리 완료되었으나 응답 본문 없음 (예: 리소스 삭제 성공)
클라이언트 오류 응답 (4xx) 사용 기준
선택 이유
- 클라이언트 측 오류를 명확하게 구분하여 전달
- 디버깅 및 문제 해결의 효율성 증대
- 보안 관련 이슈의 명확한 전달
구현 상세
- 400 Bad Request: 잘못된 요청 구문 (예: 잘못된 JSON 형식, 필수 필드 누락)
- 401 Unauthorized: 인증 실패 (예: 로그인 실패, 만료된 JWT 토큰)
- 403 Forbidden: 권한 부족 (예: 관리자 전용 API 접근 시도)
- 404 Not Found: 요청 리소스 부재 (예: 존재하지 않는 URL 또는 데이터 요청)
- 409 Conflict: 리소스 충돌 (예: 중복 데이터 생성 시도)
- 422 Unprocessable Entity: 유효성 검사 실패 (예: 비즈니스 로직 유효성 검증 실패)
서버 오류 응답 (5xx) 사용 기준
선택 이유
- 서버 측 문제를 명확하게 구분하여 전달
- 시스템 모니터링 및 장애 대응의 효율성 향상
- 클라이언트에게 적절한 재시도 전략 제공
구현 상세
- 500 Internal Server Error: 서버 내부 오류 발생 (예: 예기치 않은 서버 오류, 처리되지 않은 예외)
- 502 Bad Gateway: 게이트웨이 오류 (예: 프록시 서버가 잘못된 응답을 수신)
- 503 Service Unavailable: 일시적 서비스 불가 (예: 서버 과부하, 유지보수 중)
- 504 Gateway Timeout: 게이트웨이 응답 시간 초과 (예: 외부 API 응답 지연)
적용 시 고려사항
- 동일한 상황에 대해 일관된 상태 코드 사용
- 상태 코드와 함께 구체적인 에러 메시지 제공
- 민감한 정보가 에러 메시지에 포함되지 않도록 주의
- 향후 추가될 수 있는 상태 코드 고려
3. UserController 검증 로직 개선 작업
기존 검증 로직의 문제점
문제점
- 반복되는 중복 코드 다수 발생.
- 코드 가독성이 떨어지고, 재사용이 어려움.
- 모든 검증 로직이 컨트롤러에 위치하여 비효율적.
기존 코드
@Operation(summary = "회원가입 - 이메일인증", description = "사용자가 이메일을 통해 회원가입을 진행합니다.")
@ApiResponse(responseCode = "202", description = "인증메일 전송 성공")
@PostMapping("/signup")
public ResponseEntity
개선 방향: 검증 로직 변경
① Bean Validation 방식 적용
개요
- Spring Boot의 Bean Validation을 활용하여 DTO에 검증 로직을 선언.
- 컨트롤러에서
@Valid
어노테이션을 사용해 자동으로 검증 수행.
장점
- 가독성 향상 및 검증 로직의 재사용 가능.
- 일관된 검증 방식 제공.
- 자동 예외 처리로 불필요한 코드를 줄임.
단점
- 비즈니스 로직이 포함된 복잡한 검증은 처리 불가.
② 커스텀 유효성 검사 방식 검토
개요
ConstraintValidator
를 사용하여 사용자 정의 어노테이션 생성 후 복잡한 검증 로직 캡슐화.
장점
- 복잡한 로직 처리 가능.
- 재사용성 높고, 검증 로직의 가독성을 보장.
단점
- 구현 복잡성이 증가.
- 추가적인 코드 작성 필요.
③ 최종 선택: Bean Validation
이유
- 프로젝트에서 비즈니스 로직이 복잡하지 않아 Bean Validation으로 충분히 처리 가능.
- 검증 로직을 간결하고 재사용 가능하게 작성할 수 있음.
구현 내용
① DTO 생성: UserSignupRequestDto
목적
- 사용자 회원가입 요청 시 데이터를 전달받고 검증 수행.
- 검증 로직을 DTO 내부에 어노테이션으로 선언.
UserSignupRequestDto
@NotBlank(message = "이름을 입력해주세요.")
private String userName;
@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "유효한 이메일 주소를 입력해주세요.")
private String userEmail;
@NotBlank(message = "비밀번호를 입력해주세요")
@Pattern(
regexp = "^(?=.*[0-9])(?=.*[!@#$%^&*])(?=\\S+$).{8,}$",
message = "비밀번호는 최소 8자 이상이어야 하며, 숫자와 특수 문자를 포함해야 합니다."
)
private String userPw;
@NotBlank(message = "주소를 입력해주세요")
private String userAddress;
@NotBlank(message = "휴대폰번호를 입력해주세요.")
@Pattern(
regexp = "^01[01]-\d{4}-\d{4}$",
message = "유효한 휴대폰 번호를 입력해주세요. (예: 010-1234-5678)"
)
private String userPH;
private String profileImg;
private String description;
@Valid
어노테이션 적용 예시
@Operation(summary = "회원가입 - 이메일 인증", description = "사용자가 이메일을 통해 회원가입을 진행합니다.") 신규
@ApiResponse(responseCode = "202", description = "인증메일 전송 성공")
@PostMapping("/signup")
public ResponseEntity
기대 효과
- 중복 코드 제거 및 가독성 향상.
- 검증 로직의 재사용성을 높이고 유지보수성을 강화.
- 자동 예외 처리로 컨트롤러의 코드 간소화.
- DTO와 컨트롤러 간의 역할 분리로 구조적 일관성 확보.
4. 검증 실패 메시지 개선 작업
기존 코드 문제점
HashMap 사용 문제
HashMap
은 키-값 쌍의 입력 순서를 보장하지 않음.- 사용자에게 검증 실패 메시지가 무작위 순서로 표시되어 가독성이 저하될 가능성 존재.
개선 내용
LinkedHashMap으로 변경
- 입력된 키-값 쌍의 순서를 보장.
- 사용자에게 검증 실패 메시지가 입력된 순서대로 출력되도록 개선.
변경 전 / 후 비교
변경 전
@ExceptionHandler(WebExchangeBindException.class)
public ResponseEntity
변경 후
@ExceptionHandler(WebExchangeBindException.class)
public ResponseEntity
5. 이메일 인증 기반 회원가입 로직 개선 기록
기존 방법
로직
- 사용자 입력 정보를 스태틱 필드에 임시로 저장.
- 이메일 인증이 완료되면 데이터를 데이터베이스에 저장하여 회원가입 처리.
장점
- 간단한 구현: 데이터베이스와의 상호작용 최소화로 구현이 간단.
- DB 부하 감소: 데이터베이스에 불필요한 접근이 줄어 시스템 부하 감소.
단점
- 데이터 손실 위험: 서버가 재시작되거나 예외 상황 발생 시, 저장된 데이터 손실 가능.
- 동시성 이슈: 여러 사용자가 동시 접근 시 스태틱 필드에 데이터가 덮어씌워질 가능성 존재.
변경 방법
로직
- 사용자 입력 정보를 데이터베이스에 임시 저장.
- 이메일 인증 완료 시, 상태값(
not_yet
)을confirm
으로 변경하여 회원가입 완료.
장점
- 영속성: 데이터가 데이터베이스에 저장되므로 손실 위험이 적음.
- 유지보수성: 상태값을 기반으로 회원가입 과정을 명확히 추적 가능.
- 확장성: 이메일 인증 외에 추가 검증 프로세스(예: 전화번호 인증) 도입 시에도 쉽게 확장 가능.
단점
- DB 부하: 데이터베이스에 임시 데이터를 저장하므로 부하 증가 가능성.
결론 및 선택
- 단점(DB 부하)보다 장점(영속성, 유지보수성, 확장성)이 더 크다고 판단하여 데이터베이스를 활용한 임시 정보 저장 방식으로 변경.
- 변경 후 로직:
- 사용자 입력 정보를 데이터베이스에 저장 (상태값:
not_yet
). - 이메일 인증 완료 시, 상태값을
confirm
으로 변경. - 상태값이
confirm
인 경우에만 회원가입 완료 처리.
변경된 로직 예시
데이터베이스 컬럼 구조
ID
: 유저 아이디email
: 이메일name
: 이름password
: 비밀번호state
: 상태값
로직 예시
회원가입 요청 처리
@PostMapping("/signup")
public ResponseEntity signup(@RequestBody SignupRequestDto requestDto) {
userRepository.save(new User(requestDto.getEmail(), requestDto.getName(), requestDto.getPassword(), "not_yet"));
emailService.sendVerificationEmail(requestDto.getEmail());
return ResponseEntity.ok("인증메일이 전송되었습니다.");
}
이메일 인증 완료 처리
@GetMapping("/verify-email")
public ResponseEntity verifyEmail(@RequestParam String token) {
User user = userRepository.findByToken(token);
if (user != null && user.getState().equals("not_yet")) {
user.setState("confirm");
userRepository.save(user);
return ResponseEntity.ok("이메일 인증이 완료되었습니다!");
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("인증 실패");
}
6. 사용자 관리 컨트롤러 개발 목표 및 진행 기록
목표
주요 목표
- 사용자 관리 컨트롤러의 핵심 기능을 빠르게 구현(MVP 방식).
- 디테일한 부분은 건너뛰고 핵심 기능 구현에 집중.
구체적인 계획
- 로그인 기능 구현.
- JWT 기반 인증 처리.
- 로그아웃 및 모든 기기에서 로그아웃 기능 구상.
문제 상황 및 고려 사항
① 초기 계획
- 간단한 로그인 기능 구현: JWT를 따로 저장하지 않고, 생성 후 암호 비교 정도로 간단히 처리 예정.
② 문제 발견
- 로그아웃 기능의 구현 어려움:
- "현재 기기에서 로그아웃"과 "모든 기기에서 로그아웃" 기능을 구현하려면 JWT를 저장 및 관리해야 함.
- JWT를 저장하지 않으면 블랙리스트 및 화이트리스트 관리를 할 수 없어 위 기능 구현 불가.
해결 방안: JWT 저장 방식 결정
① MySQL에 저장
- 장점:
- 데이터 영속성 보장.
- 기존 데이터베이스 환경과 통합 가능.
- 단점:
- 데이터베이스 부하 증가.
- JWT 만료 시 삭제를 위한 추가적인 관리 로직 필요.
② Redis에 저장
- 장점:
- TTL(Time To Live) 기능을 사용해 저장 시 만료 시간을 설정 가능.
- 만료된 JWT를 자동 삭제하여 효율적인 메모리 관리 가능.
- 빠른 읽기/쓰기 성능으로 서버 부하 감소.
- 단점:
- Redis 구축 및 설정 필요.
- 메모리 기반이라 대규모 데이터 저장 시 적합하지 않을 수 있음.
③ 최종 선택
- Redis 채택:
- TTL 기능과 성능 측면에서 Redis가 적합하다고 판단.
- JWT 만료 시 자동 삭제로 관리 로직 간소화.
구현 계획
- JWT 저장 및 관리:
- Redis를 사용해 JWT를 저장하고 TTL 설정.
- 만료된 토큰은 자동 삭제되도록 구성.
- 로그아웃 기능:
- 현재 기기에서 로그아웃: Redis에서 해당 JWT 삭제.
- 모든 기기에서 로그아웃: Redis에 저장된 사용자 관련 JWT 모두 삭제.
- 추가 작업:
- Redis 환경 구축 및 의존성 추가.
- 사용자 관리 컨트롤러에 Redis 기반 JWT 관리 로직 구현.
결론
- MVP 방식: 핵심 기능 구현에 집중하여 빠르게 결과물 확보.
- Redis 채택: TTL 기능과 성능을 고려하여 효율적으로 JWT를 관리.
- 추가 목표: 빠른 로그인 및 로그아웃 기능 구현 후, 필요 시 기능 확장 예정.
7. ConfigServer 설정 관리 방식 결정
ConfigServer 도입 계획
- 프로젝트 루트 디렉토리에 ConfigServer 모듈을 생성.
- 프로젝트 전체의 설정 파일 관리를 ConfigServer에 위임.
설정 관리 방식
① Git 기반 설정 관리
- 특징:
- 설정 파일을 GitHub 저장소에 업로드하여 Git에서 설정을 읽어 적용.
- 사용 이유:
- MSA 환경에서 설정 변경 사항을 실시간으로 반영 가능.
- CI/CD와 연계하여 자동화 배포 가능.
- 설정 변경 이력을 추적하고 롤백이 용이.
② 로컬 파일 기반 설정 관리
- 특징:
- 설정 파일을 로컬에 저장하고 ConfigServer에서 읽어 적용.
- 사용 이유:
- 프로젝트 초기 규모가 작음.
- 설정 변경이 드물며 로컬 접근이 더 효율적.
- 외부 의존성을 줄이고 빠른 설정 접근 가능.
③ 결론
- 초기:
- 프로젝트 개발 초기에는 로컬 파일 기반으로 설정 관리.
- 설정 변경이 드문 현재 상황에 적합.
- 향후:
- 프로젝트 기능 구현 및 개발 완료 후, Git 기반 설정 관리로 전환.
- 설정 변경 이력 관리와 롤백 필요성을 반영하여 점진적 전환.
ConfigServer 설정 방식 요약
- 현재: 로컬 파일 기반으로 설정 파일을 관리.
- 향후: GitHub 저장소에서 설정 파일을 가져오는 방식으로 전환.
8. DB 스키마 분할 결정
고민의 시작
- 현황:
- 유저 관리 서비스(회원가입, 로그인 등)만 구현된 상태.
- 상품 관리 서비스를 새롭게 추가하기 전에 DB 스키마 분할 여부 결정 필요.
초기 걱정
- 연관관계: 서로 다른 스키마로 나누면 JPA에서 연관관계를 어떻게 유지할 것인가?
- 데이터 무결성: 스키마 분할로 인해 데이터 무결성을 보장할 방법은?
- 다중 테이블 조회: 여러 테이블의 데이터를 한 번에 가져오는 쿼리는 어떻게 작성할 것인가?
스키마 분할에 대한 재검토
발견한 사실
- JPA 설정을 통한 스키마 간 연관 관계 지원:
- DB에서 스키마 간 관계를 지원한다면, JPA에서도 약간의 설정 수정으로 스키마 간 연관관계 유지 가능.
- 필요한 경우, 쿼리를 커스터마이징하여 다중 스키마 데이터를 조합 가능.
- MSA와 DB 스키마 분할의 필요성:
- MSA 프로젝트의 본질: 서비스 간 결합도를 최소화하고, 독립적인 확장성과 유지보수성을 보장.
- 스키마 분할 없이 단일 DB를 사용: 서비스 간 결합도가 높아지고, MSA의 장점이 훼손됨.
최종 결정
- 스키마 분할 채택.
- 이유:
- MSA에서 서비스 독립성을 보장하기 위함.
- 단일 DB 사용은 서비스 간 결합도를 높여 유지보수성과 확장성을 저해.
- JPA 설정 및 커스터마이징으로 스키마 간 관계를 유지할 수 있다는 점에서 걱정 해소.
9. 주문 관리 서비스의 유저 검증 로직에 대한 고민
문제 상황
현상
- 현재 로직:
- 주문 서비스에서 상품 주문 시 유저 서비스를 통해 유저 검증.
- 유저 서비스는 자체 스키마와 통신하여 처리 결과를 반환.
- 결과를 주문 서비스로 전달하여 유저 검증을 완료.
- 의문:
- 이 방식이 MSA의 목적에 부합하는지 확신이 서지 않음.
- 다른 서비스(유저 서비스)의 스키마에 직접 접근하지 않는 것이 맞는지 혼란.
해결 과정
① 멘토님 의견
- 같은 방식 사용:
- 멘토님과 동료들 모두 현재와 같은 방식이 MSA 구조에서 적합하다고 판단.
- 다른 서비스 스키마에 직접 접근하는 대신, HTTP 요청을 통해 필요한 정보를 요청.
② 추가 확인
- 검색 및 상담 결과:
- 결론: MSA 아키텍처에서 다른 서비스의 스키마에 직접 접근하는 것은 결합도를 높임.
- 현재처럼 서비스 간에 HTTP 통신을 통해 데이터를 주고받는 방식이 적합함.
최종 결론
- 현재 로직 유지:
- 현재 로직이 MSA의 원칙에 부합.
- 다른 서비스 스키마에 직접 접근하지 않고, 각 서비스의 책임을 명확히 분리.
- 서비스 간의 결합도를 낮추는 구조.
MSA의 본질
- 독립적 서비스: 각 서비스는 독립적으로 자신의 데이터를 관리하고 제공해야 함.
- HTTP 통신: 필요한 정보는 다른 서비스의 API를 통해 요청. 서비스 간 데이터 공유는 API 호출로 제한.
10. 공통 클래스 관리 방식 고민: 개별 관리 vs 중앙 서비스
고민의 핵심
- 공통 로직: 예: 토큰에서 사용자 ID(이메일) 추출, 사용자 확인.
- 고민:
- 각 서비스별로 공통 클래스를 개별적으로 관리할지,
- 중앙 서비스를 만들어 공통 클래스를 API로 제공할지 결정해야 함.
두 가지 접근 방식의 비교
① 각 서비스별 개별 관리
- 특징: 공통 클래스를 각 서비스 내부에서 구현 및 관리.
- 장점:
- 별도 통신 없이 동작 → 성능 우수.
- 서비스 간 결합도 낮음 → 서비스 독립성 유지.
- 네트워크 장애 영향 없음 → 안정성 높음.
- 단점:
- 코드 중복 발생: 모든 서비스에 동일한 로직 작성 필요.
- 유지보수 부담: 공통 로직 변경 시, 각 서비스를 개별적으로 수정.
- 로직 일관성 보장 어려움 → 코드 품질 저하 가능성.
② 공통 클래스 서버 방식
- 특징: 공통 로직을 서비스화하여 API로 제공.
- 장점:
- 로직 변경 시, 중앙 서비스만 수정 → 유지보수 용이.
- 모든 서비스에서 로직과 데이터 처리 방식의 일관성 보장.
- 민감 데이터(예: 암호화 키) 관리 강화.
- 단점:
- 네트워크 통신 필수 → 성능 저하 가능성.
- 대규모 요청 시, 병목 현상 발생 가능.
- 공통 서비스 장애 시, 모든 서비스에서 해당 기능 사용 불가 → 의존성 증가.
요약 및 현재 상황
구분 | 각 서비스 관리 | 공통 클래스 서버 |
---|---|---|
성능 | 뛰어남 | 네트워크 통신으로 성능 저하 가능 |
독립성 | 높음 | 낮음 |
유지보수성 | 낮음 | 높음 |
일관성 보장 | 어려움 | 용이 |
안정성 | 네트워크 영향 없음 | 공통 서비스 장애 시 전체 서비스에 영향 |
결론 및 계획
현 시점(2주차) 결론
- 각 서비스에 공통 클래스를 저장하는 방식 채택.
- 이유:
- 현재 서비스 요청 규모가 작아 성능 우수성이 더 중요.
- 유지보수 부담이 있지만, 현재는 코드 변경이 빈번하지 않음.
- 서비스 독립성 유지가 유리.
3주차 이후 대규모 요청을 고려한 계획
- 공통 클래스 서버 준비:
- 대규모 요청 처리 기술 도입 이후, 공통 로직을 API로 제공하는 공통 클래스 서버로 전환.
- 서버 과부하 방지를 위해 캐싱(Redis) 및 로드 밸런싱 도입 검토.
- 점진적 전환:
- 현재 방식으로 유지하며, 공통 클래스 서버 구축 후 성능 테스트를 거쳐 점진적 전환.
결론
- 현재: 각 서비스별로 공통 클래스를 관리.
- 미래: 대규모 요청을 대비해 공통 클래스 서버 방식으로 전환 검토.
11. 연관 관계 처리 방식 선택: 1안 vs 2안
문제 상황
연관 관계 컬럼 처리
- 1안:
- 각 연관 관계 컬럼(예: 사용자 ID, 상품 ID 등)을 일반 값으로 저장.
- 필요 시, 주기적으로 데이터의 정확성을 검증하는 절차 추가.
- 2안:
- 엔티티에서 데이터가 필요할 때마다 다른 서비스로 HTTP 요청을 보내 데이터의 유효성을 검증 후 저장하거나 사용.
- 간단한 조회에도 서비스 간 통신이 여러 번 발생.
장단점 비교
구분 | 1안: 일반 값 저장 | 2안: HTTP 요청 검증 |
---|---|---|
성능 | 뛰어남 (별도 통신 없음) | 느림 (통신 횟수 증가) |
서비스 독립성 | 낮음 (데이터 무결성 관리 필요) | 높음 (검증 로직을 서비스별로 분리) |
구현 복잡성 | 낮음 (직접 값 저장, 추가 로직 단순) | 높음 (HTTP 요청 및 예외 처리 필요) |
확장성 | 낮음 (데이터 일관성 문제 가능성 있음) | 높음 (서비스 간 결합도 낮음) |
즉시성 | 빠름 (데이터 저장 및 조회 속도 빠름) | 느림 (외부 통신 응답 시간 영향) |
선택: 1안
선택 이유
- 속도 우선:
- 현재는 빠른 구현과 작업 속도 향상이 가장 중요.
- 1안은 별도의 통신 없이 데이터를 저장하므로 성능 우위.
- 미래의 확장성 고려:
- DB 분할 및 서비스 독립성 강화 계획이 있기 때문에, 현재 방식이 장기적으로 결합도를 낮추는 방향으로 개선 가능.
- 복잡성 최소화:
- 현재 프로젝트 규모에서는 통신 기반의 데이터 검증 방식(2안)이 과도한 복잡성을 초래.
결론
- 선택: 1안: 각 연관 관계 컬럼을 일반 값으로 저장하고, 주기적으로 정확성 검증.
- 추후 계획: 서비스 분리 및 DB 독립이 필요해질 경우, 데이터 검증 로직을 통신 기반으로 전환.
12. 선착순 구매 서비스 설계 및 구성 방향
독립적인 설계 방식 채택 이유
목표
- 높은 트래픽 처리 능력 확보
- WebFlux 기반 리액티브 프로그래밍을 활용하여 실시간 트래픽을 효율적으로 처리.
- 비동기적 처리로 대규모 동시 요청에 대한 성능 보장.
- 서비스 독립성 확보
- 다른 서비스의 장애로부터 선착순 구매 서비스(PurchaseService)를 보호.
- 실시간성과 안정성 유지.
- 트래픽 병목 감소
- 인증, 인가, 로그인 여부 확인 등 필요한 모든 로직을 내부에서 처리하여 외부 호출 의존성 제거.
- 서비스 간 통신 오류와 네트워크 지연 문제를 방지.
설계 구성
1) PurchaseService 모듈
- 기능: 선착순 구매 관련 API 및 로직을 독립적으로 처리.
- 구조:
- 컨트롤러: 선착순 구매 요청 처리.
- 서비스: 선착순 로직 구현, 어뷰징 사용자 필터링.
- 레포지토리: 데이터 저장 및 조회.
2) 인증 및 인가 처리
- 내부 인증 및 인가 구현:
- 외부 인증 서비스 호출 대신 JWT 토큰 검증 로직을 내장.
- 로그인 여부 확인 로직도 서비스 내부에서 처리.
- 장점:
- 외부 호출로 인한 병목 방지.
- 네트워크 지연에 대한 영향 최소화.
3) 데이터베이스 설계
- 단일 DB, 스키마 분할 사용:
- 장점: 데이터베이스 장애 시 전체 서비스 셧다운 방지.
- 가정: 동일 DB 사용으로 인한 리스크는 무시.
- 선착순 구매 테이블:
- 컬럼: purchase_id, user_id, product_id, timestamp.
4) 비정상 사용자 접근 관리
- 오픈 시간 이전 구매 요청 차단:
- 오픈 시간 검증 로직 추가.
- Redis에 TTL 설정으로 잘못된 요청을 빠르게 필터링.
- 어뷰징 방지:
- 한 사용자당 초당 요청 수 제한 (Rate Limiting).
- Redis를 활용한 IP 및 사용자 ID 기반 요청 제한.
단점과 타협
- 단점:
- 서비스 내부 로직이 복잡해지고 유지보수 비용 증가.
- 중복된 인증/인가 로직으로 인해 다른 서비스와의 일관성 저하 가능.
- 타협:
- 독립성과 안정성이 중요하므로 일관성 저하와 유지보수 비용은 감수.
추가 고려 사항
- 부하 테스트: K6 또는 Gatling을 사용하여 대규모 트래픽 시뮬레이션 테스트.
- 캐싱 전략: Redis 캐시를 활용해 인기 상품의 재고 상태를 실시간으로 반영.
- 장애 복구 계획: 구매 요청 처리 실패 시 재시도 로직 추가.
13. 선착순 구매 서비스(FF) 설계 방향 고민 기록
기존 설계 방향
- 목표: 오로지 성능 극대화를 위해 MSA 구조를 배제.
- 방법:
- FF 모듈에서 모든 스키마에 직접 접근.
- 다른 모듈과의 요청 주고받기 없이 독립적으로 처리.
- 예상 장점:
- 서비스 간 통신으로 인한 병목 현상 제거.
- 단일화된 비즈니스 로직으로 성능 최적화.
- 예상 단점:
- MSA 설계 원칙 훼손.
- 서비스 간 강한 결합으로 확장성과 유지보수성 저하.
- 팀 협업 및 코드 일관성 저해 가능성.
새로운 설계 방향
- 목표: MSA 원칙 존중과 성능 간의 균형.
- 방법:
- FF는 기존 MSA 구조에 따라 독립적 모듈로 유지.
- 필요한 경우 다른 서비스(User, Product 등)와의 통신은 최소화 및 비동기화.
- Redis, Kafka 등 캐싱/비동기 처리 기술 적극 활용.
- 장점:
- MSA 설계 원칙을 준수하여 확장성, 유지보수성 확보.
- 통신 병목을 줄이면서 성능도 최적화 가능.
- 단점:
- 구현 복잡도 증가.
- 성능 극대화를 목표로 한 단일화 방식 대비 낮은 퍼포먼스 가능성.
결정 및 진행 방향
- MSA 존중 방향 채택:
- 확실한 성능 증명이 없는 상황에서 모놀리틱 접근은 위험 부담이 크다고 판단.
- MSA 아키텍처 설계 원칙을 최대한 준수하면서, 성능 최적화 기법을 도입하는 방향으로 진행.
- 진행 중 작업:
- FF 모듈 독립성 유지.
- User, Product 서비스와의 통신은 비동기 방식으로 처리.
- 필요한 정보는 Redis 캐싱을 통해 로컬에서 재사용.
추가 작업 목록
- 성능 최적화 검토:
- 캐싱 활용 및 FF 모듈 내 처리 로직 최소화.
- 요청 수와 처리 속도에 따른 트래픽 분석.
- 테스트 기반 설계:
- 대규모 트래픽 환경을 시뮬레이션하여 성능 테스트 진행.
- 통신 지연 및 병목 현상을 최소화하는 비동기 방식 테스트.
- 리스크 대비책:
- MSA 설계 원칙을 훼손하지 않는 범위 내에서 성능 극대화 방안 지속 모색.
- 필요 시, MSA와 모놀리틱 간의 절충안으로 추가 설계 검토.
결론
- 지금은 MSA 아키텍처를 존중하는 방향으로 설계를 진행하되,
- 성능 최적화를 위해 캐싱, 비동기 요청, 병렬 처리 등을 적극 활용하며 설계를 개선해나갈 예정.
🔍 트러블슈팅
1. 로그인 검증 로직 오류 및 해결
문제 상황
현상
- 로그인 검증 로직:
- 상품 주문 등 로그인 필수 서비스 이용 시, 토큰 값을 UserService에 보내 검증 후 boolean 값 반환.
- 모든 로직이 올바르게 작성된 것처럼 보였으나, 항상 false 반환.
- 문제:
- 모든 로그를 확인했음에도 오류 원인을 찾지 못함.
- false 반환이 지속적으로 발생.
원인
조건문 오류
- 로그인 검증 로직에서 조건문이 잘못 작성됨:
if (로그인이 되었다면?) { // "로그인이 필요한 서비스입니다" 메시지 반환 }
- 문제:
- "로그인 되었다면" 조건에서 로그인 여부를 부정해야 하는데, 부정 연산자(!)가 누락.
- 결과적으로, 로그인 상태에서도 로그인 필요 메시지가 반환.
- 유저 서비스 로그아웃 로직과 혼동:
- UserService에서 검증 로직은 주로 로그아웃 시 사용되었으므로, 로그인 검증 로직에 대한 부정 조건이 빠져 있었음.
해결 과정
- 조건문 수정: 로그인 검증 조건에 부정 연산자(!) 추가:
if (!로그인이 되었다면?) { // "로그인이 필요한 서비스입니다" 메시지 반환 }
- 결과 확인: 조건문 수정 후, 로그인 여부를 올바르게 판단하여 정상 처리.
교훈
- 조건문 작성 시 항상 재확인:
- 로그인, 검증, 부정 조건 등은 헷갈리기 쉬우므로 철저히 검토 필요.
- 특히, 유사한 로직(UserService의 로그아웃 검증 등)이 영향을 줄 수 있음.
- 디버깅의 중요성: 로그 확인과 디버깅 과정을 통해, 로직 오류를 점진적으로 좁혀가는 과정이 중요.
결론
- 문제 해결: 조건문 수정으로 로그인 검증 로직 정상 작동.
- 추후 방지: 코드 리뷰 및 테스트 코드 작성으로 조건문 오류 재발 방지.
2. 동적 라우팅 문제 분석 및 해결 방안
문제 상황
- 현상:
- 게이트웨이의 라우팅이 유레카 서버에 등록된 서비스 이름을 기준으로 동적 라우팅되도록 설정.
- 도커 환경에서 빌드 시 동적 서비스 이름을 읽어오지 못하는 문제 발생.
- 임시 해결 방법:
- 게이트웨이의
application.yml
파일을 수정하여 정적 라우팅 방식으로 설정 → 문제 해결. - 하지만 동적 라우팅의 장점을 잃게 됨.
- 게이트웨이의
발생 가능한 원인
- 도커 네트워크 문제:
- 도커 컨테이너 간 네트워크 연결이 제대로 설정되지 않음.
- 도커에서 기본적으로 사용하는 Bridge 네트워크가 유레카의 서비스 디스커버리를 방해할 가능성.
- 유레카 서버와의 통신 문제:
- 게이트웨이가 유레카 서버와 통신하여 등록된 서비스 정보를 가져오는 과정에서 실패.
- 도커 내부의 호스트 이름(예:
localhost
)이 맞지 않거나 IP 주소가 잘못된 경우.
- 도커 환경 변수 누락:
- 도커에서 환경 변수(예:
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE
)가 제대로 전달되지 않아 유레카 서버를 인식하지 못함.
- 도커에서 환경 변수(예:
- 게이트웨이 설정 문제:
- 게이트웨이가 유레카 서버에서 서비스를 가져오는 로직이 제대로 동작하지 않음.
spring.cloud.gateway.discovery.locator.enabled
설정이 비활성화되었거나, 유레카 서버와의 연결이 끊김.
해결 방안
1) 도커 네트워크 구성 확인
- Custom Network 사용:
docker network create gateway-network
- 모든 서비스와 유레카 서버, 게이트웨이를 동일한 네트워크에 연결:
networks: default: name: gateway-network
- 각 컨테이너의
application.yml
에 유레카 서버의 IP 또는 DNS를 명시적으로 설정:eureka: client: serviceUrl: defaultZone: http://eureka-server:8761/eureka/
2) 게이트웨이 설정 확인
- 게이트웨이에서 동적 라우팅 활성화 설정 확인:
spring: cloud: gateway: discovery: locator: enabled: true
- 유레카 서버에 등록된 서비스 이름이 올바르게 사용되고 있는지 확인:
- 유레카 서버에 등록된 서비스 이름은 대문자로 변환되므로 게이트웨이가 이를 인식할 수 있도록 확인 필요.
- 예:
userservice
→USERSERVICE
3) 유레카와 게이트웨이 통신 문제 해결
- 도커 환경에서
localhost
를 사용하지 말고 컨테이너 이름으로 통신:eureka: client: serviceUrl: defaultZone: http://eureka-server:8761/eureka/
- 게이트웨이가 유레카 서버에서 서비스 정보를 가져오지 못하는 경우, 컨테이너 실행 순서를 보장:
docker-compose.yml: services: eureka-server: image: eurekaserver gateway: image: gateway depends_on: - eureka-server userservice: image: userservice depends_on: - eureka-server
4) 디버깅 및 로그 확인
- 게이트웨이의 디버깅 로그를 활성화:
logging: level: org.springframework.cloud.gateway: DEBUG
- 확인할 내용:
- 유레카 서버에 정상적으로 등록된 서비스 목록.
- 게이트웨이가 유레카에서 가져온 서비스 이름과 라우팅 정보를 올바르게 읽는지.
5) 정적 설정으로 임시 해결
- 문제가 장기적으로 해결되지 않을 경우, 아래와 같이 정적 라우팅 설정 유지:
spring: cloud: gateway: routes: - id: user-service uri: http://userservice:8080 predicates: - Path=/users/**
결론
- 동적 라우팅 문제는 도커 네트워크, 환경 변수, 실행 순서, 서비스 이름 변환 등 여러 요인에서 발생 가능.
- 위 해결 방안을 단계별로 적용하며 원인을 파악.
- 필요 시 정적 라우팅 방식을 임시로 유지하고, 동적 라우팅은 개발 환경에서 먼저 디버깅 및 검증 진행.
3. UserController 검증 로직 개선 작업
기존 검증 로직의 문제점
문제점
- 반복되는 중복 코드 다수 발생.
- 코드 가독성이 떨어지고, 재사용이 어려움.
- 모든 검증 로직이 컨트롤러에 위치하여 비효율적.
기존 코드
@Operation(summary = "회원가입 - 이메일인증", description = "사용자가 이메일을 통해 회원가입을 진행합니다.") @ApiResponse(responseCode = "202", description = "인증메일 전송 성공") @PostMapping("/signup") public ResponseEntity<Map<String, String>> signup(@RequestBody Map<String, String> userRequest) { if (!userRequest.containsKey("userName") || Objects.equals(userRequest.get("userName"), "")) { return ResponseEntity.badRequest().body(Map.of("msg", "이름을 입력해주세요")); } if (!userRequest.containsKey("userEmail") || Objects.equals(userRequest.get("userEmail"), "")) { return ResponseEntity.badRequest() .body(Map.of("msg", "이메일을 입력해주세요")); } if (!userRequest.containsKey("")) { return ResponseEntity.badRequest() .build(); } return ResponseEntity .status(202) .body(Map.of("msg", String.format("%s 으로 인증메일이 전송되었습니다. 메일을 확인해주세요.", userRequest.get("userEmail")))); }
개선 방향: 검증 로직 변경
① Bean Validation 방식 적용
개요
- Spring Boot의 Bean Validation을 활용하여 DTO에 검증 로직을 선언.
- 컨트롤러에서
@Valid
어노테이션을 사용해 자동으로 검증 수행.
장점
- 가독성 향상 및 검증 로직의 재사용 가능.
- 일관된 검증 방식 제공.
- 자동 예외 처리로 불필요한 코드를 줄임.
단점
- 비즈니스 로직이 포함된 복잡한 검증은 처리 불가.
② 커스텀 유효성 검사 방식 검토
개요
ConstraintValidator
를 사용하여 사용자 정의 어노테이션 생성 후 복잡한 검증 로직 캡슐화.
장점
- 복잡한 로직 처리 가능.
- 재사용성 높고, 검증 로직의 가독성을 보장.
단점
- 구현 복잡성이 증가.
- 추가적인 코드 작성 필요.
③ 최종 선택: Bean Validation
이유
- 프로젝트에서 비즈니스 로직이 복잡하지 않아 Bean Validation으로 충분히 처리 가능.
- 검증 로직을 간결하고 재사용 가능하게 작성할 수 있음.
구현 내용
① DTO 생성: UserSignupRequestDto
목적
- 사용자 회원가입 요청 시 데이터를 전달받고 검증 수행.
- 검증 로직을 DTO 내부에 어노테이션으로 선언.
DTO 코드
public class UserSignupRequestDto { @NotBlank(message = "이름을 입력해주세요.") private String userName; @NotBlank(message = "이메일을 입력해주세요.") @Email(message = "유효한 이메일 주소를 입력해주세요.") private String userEmail; @NotBlank(message = "비밀번호를 입력해주세요") @Pattern( regexp = "^(?=.*[0-9])(?=.*[!@#$%^&*])(?=\\S+$).{8,}$", message = "비밀번호는 최소 8자 이상이어야 하며, 숫자와 특수 문자를 포함해야 합니다." ) private String userPw; @NotBlank(message = "주소를 입력해주세요") private String userAddress; @NotBlank(message = "휴대폰번호를 입력해주세요.") @Pattern( regexp = "^01[01]-\\d{4}-\\d{4}$", message = "유효한 휴대폰 번호를 입력해주세요. (예: 010-1234-5678)" ) private String userPH; private String profileImg; private String description; }
@Valid
어노테이션 적용 예시
@Operation(summary = "회원가입 - 이메일 인증", description = "사용자가 이메일을 통해 회원가입을 진행합니다.") @ApiResponse(responseCode = "202", description = "인증메일 전송 성공") @PostMapping("/signup") public ResponseEntity
기대 효과
- 중복 코드 제거 및 가독성 향상.
- 검증 로직의 재사용성을 높이고 유지보수성을 강화.
- 자동 예외 처리로 컨트롤러의 코드 간소화.
- DTO와 컨트롤러 간의 역할 분리로 구조적 일관성 확보.
4. 검증 실패 메시지 개선 작업
기존 코드 문제점
- HashMap 사용 문제:
HashMap
은 키-값 쌍의 입력 순서를 보장하지 않음.- 사용자에게 검증 실패 메시지가 무작위 순서로 표시되어 가독성이 저하될 가능성 존재.
개선 내용
- LinkedHashMap으로 변경:
- 입력된 키-값 쌍의 순서를 보장.
- 사용자에게 검증 실패 메시지가 입력된 순서대로 출력되도록 개선.
변경 전 / 후 비교
변경 전
@ExceptionHandler(WebExchangeBindException.class) public ResponseEntity
변경 후
@ExceptionHandler(WebExchangeBindException.class) public ResponseEntity
5. 이메일 인증 기반 회원가입 로직 개선 기록
기존 방법
로직
- 사용자 입력 정보를 스태틱 필드에 임시로 저장.
- 이메일 인증이 완료되면 데이터를 데이터베이스에 저장하여 회원가입 처리.
장점
- 간단한 구현: 데이터베이스와의 상호작용 최소화로 구현이 간단.
- DB 부하 감소: 데이터베이스에 불필요한 접근이 줄어 시스템 부하 감소.
단점
- 데이터 손실 위험: 서버가 재시작되거나 예외 상황 발생 시, 저장된 데이터 손실 가능.
- 동시성 이슈: 여러 사용자가 동시 접근 시 스태틱 필드에 데이터가 덮어씌워질 가능성 존재.
변경 방법
로직
- 사용자 입력 정보를 데이터베이스에 임시 저장.
- 이메일 인증 완료 시, 상태값(
not_yet
)을confirm
으로 변경하여 회원가입 완료.
장점
- 영속성: 데이터가 데이터베이스에 저장되므로 손실 위험이 적음.
- 유지보수성: 상태값을 기반으로 회원가입 과정을 명확히 추적 가능.
- 확장성: 이메일 인증 외에 추가 검증 프로세스(예: 전화번호 인증) 도입 시에도 쉽게 확장 가능.
단점
- DB 부하: 데이터베이스에 임시 데이터를 저장하므로 부하 증가 가능성.
결론 및 선택
- 결론: 단점(DB 부하)보다 장점(영속성, 유지보수성, 확장성)이 더 크다고 판단하여 데이터베이스를 활용한 임시 정보 저장 방식으로 변경.
- 변경 후 로직:
- 사용자 입력 정보를 데이터베이스에 저장. 상태값:
not_yet
. - 이메일 인증 완료 시, 상태값을
confirm
으로 변경. - 상태값이
confirm
인 경우에만 회원가입 완료 처리.
변경된 로직 예시
데이터베이스 컬럼 구조
- 컬럼명: ID, email, name, password, state
로직 예시
회원가입 요청 처리
@PostMapping("/signup") public ResponseEntitysignup(@RequestBody SignupRequestDto requestDto) { userRepository.save(new User(requestDto.getEmail(), requestDto.getName(), requestDto.getPassword(), "not_yet")); emailService.sendVerificationEmail(requestDto.getEmail()); return ResponseEntity.ok("인증메일이 전송되었습니다."); }
이메일 인증 완료 처리
@GetMapping("/verify-email") public ResponseEntityverifyEmail(@RequestParam String token) { User user = userRepository.findByToken(token); if (user != null && user.getState().equals("not_yet")) { user.setState("confirm"); userRepository.save(user); return ResponseEntity.ok("이메일 인증이 완료되었습니다!"); } return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("인증 실패"); }
6. 사용자 관리 컨트롤러 개발 목표 및 진행 기록
목표
- 주요 목표: 사용자 관리 컨트롤러의 핵심 기능을 빠르게 구현(MVP 방식).
- 구체적인 계획:
- 로그인 기능 구현.
- JWT 기반 인증 처리.
- 로그아웃 및 모든 기기에서 로그아웃 기능 구상.
문제 상황 및 고려 사항
① 초기 계획
- 간단한 로그인 기능 구현: JWT를 따로 저장하지 않고, 생성 후 암호 비교 정도로 간단히 처리 예정.
② 문제 발견
- 로그아웃 기능의 구현 어려움:
- "현재 기기에서 로그아웃"과 "모든 기기에서 로그아웃" 기능을 구현하려면 JWT를 저장 및 관리해야 함.
- JWT를 저장하지 않으면 블랙리스트 및 화이트리스트 관리를 할 수 없어 위 기능 구현 불가.
해결 방안: JWT 저장 방식 결정
① MySQL에 저장
- 장점:
- 데이터 영속성 보장.
- 기존 데이터베이스 환경과 통합 가능.
- 단점:
- 데이터베이스 부하 증가.
- JWT 만료 시 삭제를 위한 추가적인 관리 로직 필요.
② Redis에 저장
- 장점:
- TTL(Time To Live) 기능을 사용해 저장 시 만료 시간을 설정 가능.
- 만료된 JWT를 자동 삭제하여 효율적인 메모리 관리 가능.
- 빠른 읽기/쓰기 성능으로 서버 부하 감소.
- 단점:
- Redis 구축 및 설정 필요.
- 메모리 기반이라 대규모 데이터 저장 시 적합하지 않을 수 있음.
③ 최종 선택
- Redis 채택: TTL 기능과 성능 측면에서 Redis가 적합하다고 판단.
- JWT 만료 시 자동 삭제로 관리 로직 간소화.
구현 계획
- JWT 저장 및 관리:
- Redis를 사용해 JWT를 저장하고 TTL 설정.
- 만료된 토큰은 자동 삭제되도록 구성.
- 로그아웃 기능:
- 현재 기기에서 로그아웃: Redis에서 해당 JWT 삭제.
- 모든 기기에서 로그아웃: Redis에 저장된 사용자 관련 JWT 모두 삭제.
- 추가 작업:
- Redis 환경 구축 및 의존성 추가.
- 사용자 관리 컨트롤러에 Redis 기반 JWT 관리 로직 구현.
결론
- MVP 방식: 핵심 기능 구현에 집중하여 빠르게 결과물 확보.
- Redis 채택: TTL 기능과 성능을 고려하여 효율적으로 JWT를 관리.
- 추가 목표: 빠른 로그인 및 로그아웃 기능 구현 후, 필요 시 기능 확장 예정.
7. ConfigServer 설정 관리 방식 결정
ConfigServer 도입 계획
- 프로젝트 루트 디렉토리에 ConfigServer 모듈을 생성.
- 프로젝트 전체의 설정 파일 관리를 ConfigServer에 위임.
설정 관리 방식
① Git 기반 설정 관리
- 특징:
- 설정 파일을 GitHub 저장소에 업로드하여 Git에서 설정을 읽어 적용.
- 사용 이유:
- MSA 환경에서 설정 변경 사항을 실시간으로 반영 가능.
- CI/CD와 연계하여 자동화 배포 가능.
- 설정 변경 이력을 추적하고 롤백이 용이.
② 로컬 파일 기반 설정 관리
- 특징:
- 설정 파일을 로컬에 저장하고 ConfigServer에서 읽어 적용.
- 사용 이유:
- 프로젝트 초기 규모가 작음.
- 설정 변경이 드물며 로컬 접근이 더 효율적.
- 외부 의존성을 줄이고 빠른 설정 접근 가능.
③ 결론
- 초기: 프로젝트 개발 초기에는 로컬 파일 기반으로 설정 관리.
- 향후: 프로젝트 기능 구현 및 개발 완료 후, Git 기반 설정 관리로 전환.
- 설정 변경 이력 관리와 롤백 필요성을 반영하여 점진적 전환.
ConfigServer 설정 방식 요약
- 현재: 로컬 파일 기반으로 설정 파일을 관리.
- 향후: GitHub 저장소에서 설정 파일을 가져오는 방식으로 전환.
📈 성능개선
1. 회원 관리 서비스 개선
① RedisTokenRepository
- Redis 사용 최적화 방안:
keys
명령어 대신haskey
또는GET
명령어 활용.- TTL(Time To Live) 설정을 통해 메모리 효율성을 개선.
- 다중 명령어 실행 시 Pipeline 활용.
- 코드 개선 예시:
// 기존 방식: keys 명령어 사용 Set<String> keys = redisTemplate.keys("token:*" + token); return keys != null && !keys.isEmpty(); // 개선 방식: hasKey로 변경 return Boolean.TRUE.equals(redisTemplate.hasKey("token:*" + token));
- 예상 효과: 대규모 요청 처리 시 성능 20~30% 향상.
- 실제: 처리 속도 약 22.5% 개선.
② UserService
- 데이터 접근 및 연산 최적화:
- 캐싱 적용: 자주 조회되는 데이터는 Redis 캐시에 저장.
- 비동기 처리:
CompletableFuture
로 주요 비즈니스 로직 처리 속도 향상. - 쿼리 최적화: Projection 사용으로 필요한 데이터만 조회.
- 코드 개선 예시:
// 기존 방식 public User getUserDetails(Long userId) { return userRepository.findById(userId).orElseThrow(UserNotFoundException::new); } // 개선 방식: 캐싱 추가 public User getUserDetails(Long userId) { return redisTemplate.opsForValue().get("user:" + userId, () -> userRepository.findById(userId).orElseThrow(UserNotFoundException::new) ); }
- 예상 효과:
- 응답 시간 약 30% 단축.
- 부하 약 20~25% 감소.
- 실제:
- 응답 시간 약 42% 개선.
- DB 부하는 따로 테스트하지 못함.
③ JwtUtil
- JWT 생성 및 검증 최적화:
- JWT 라이브러리 최신화 및 불필요한 연산 제거.
- 검증 결과 캐싱 적용으로 반복적인 검증 속도 향상.
- 코드 개선 예시:
// 기존 방식: 매번 검증 수행 public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); } // 개선 방식: 검증 결과 캐싱 public Claims parseToken(String token) { return cache.get(token, () -> Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody() ); }
- 예상 효과: JWT 검증 속도 15~20% 개선.
- 실제: 검증 속도 17% 개선.
④ EmailService
- 이메일 발송 로직 개선:
- 비동기 처리:
CompletableFuture
또는 스레드풀 사용. - 큐 시스템(e.g., RabbitMQ) 연동으로 이메일 발송 처리 병렬화.
- 비동기 처리:
- 코드 개선 예시:
// 기존 방식: 동기 처리 public void sendEmail(String recipient, String message) { emailSender.send(recipient, message); } // 개선 방식: 비동기 처리 public void sendEmailAsync(String recipient, String message) { CompletableFuture.runAsync(() -> emailSender.send(recipient, message)); }
- 예상 효과: 이메일 발송 작업 병렬 처리로 작업 속도 50% 이상 향상.
- 실제: 이메일 주소 테스트가 어려워 성과 확인하지 못함.
2. 로그인 검증 응답속도 개선
기존 방식
- 구조: 각 서비스의 API에 진입한 후, 유저서비스에 추가 요청을 보내 로그인 검증.
- 문제점:
- 중복 호출로 인해 응답속도 저하.
- 서비스마다 인증 로직 구현으로 유지보수 복잡성 증가.
개선된 방식
- 구조 변경: 인증/인가 처리 로직 및 Security를 UserService → Gateway로 이동.
- 게이트웨이에서 JWT 서명 확인을 통해 로그인 상태를 검증.
- 검증 완료 후에만 각 서비스 API로 요청 전달.
성과
- 응답속도 개선:
- 이전:
- 최소: 9ms
- 최대: 24ms
- 평균: 17ms
- 개선 후:
- 최소: 5ms
- 최대: 9ms
- 평균: 7ms
- 평균 응답속도 약 10ms 감소.
3. 주문 API 응답속도 개선
최초 로직
- JWT 검증: API 진입 시 매번 JWT 토큰 검증 수행.
- 상품 검증: 상품 존재 여부 확인.
- 재고 확인: 상품 수량이 충분한지 검증.
- 주문 처리: 주문 로직 수행.
초기 응답속도
- 최소: 1711ms
- 최대: 5348ms
- 평균: 2500ms
개선된 로직
- 게이트웨이에서 JWT 검증:
- API 진입 전에 게이트웨이에서 JWT 검증을 일괄 처리.
- 각 API는 추가 JWT 검증 작업 불필요.
- 상품 정보 일괄 조회:
- API 진입 후 필요한 모든 상품 정보를 한번에 불러옴.
- 각각의 값을 비즈니스 로직에서 처리하여 중복 요청 감소.
- 최적화된 주문 로직:
- 상품 검증과 수량 검증을 동시에 처리하여 단계별 로직 단축.
- 필요한 경우 비동기적 데이터 처리 추가.
응답속도
- 최소: 37ms
- 최대: 342ms
- 평균: 55ms
개선 효과
- JWT 검증을 게이트웨이로 이전:
- JWT 검증 로직의 중복 제거로 API 처리 속도 향상.
- 각 서비스의 부담을 줄이고 요청 병목 최소화.
- 상품 정보 일괄 조회:
- 상품 검증과 수량 검증 요청을 개별적으로 처리하던 기존 로직에서 발생하던 불필요한 통신 제거.
- 데이터베이스 및 네트워크 요청 수 감소.
- 주문 로직 간소화:
- 연속된 조건 검증 작업을 병렬적으로 처리하여 성능 개선.
- 단순화된 로직으로 코드 유지보수성 향상.
4. 결제 프로세스 API 성능 개선
최초 테스트 (동시성 제어 없음)
- VU 50: 정상
- VU 1000: 비정상 (오류 발생 및 시스템 부하)
개선 1: 레디스 기반 분산락 적용
- VU 1000: 정상
- VU 5000: 비정상 (재고 처리 오류 및 동시 요청 과부하)
개선 2: 레디슨 Pub/Sub 추가 적용
테스트 시나리오 및 결과
- 5초간 VU 4000: 정상 처리
- 30초간 VU 4000: 재고 오차 3개 발생
- 30초간 5초마다 1000씩 증가: 재고 오차 1500개 발생 (특정 상황에서 재고 증가 이상 현상 확인)
- 1분간 VU 4000: 재고 오차 1개
테스트 문제점
- 같은 사용자 아이디 사용:
- 하나의 사용자 아이디로 테스트를 수행하여 429 에러 비율 증가.
- 테스트 환경에서 결과 신뢰도가 저하됨.
- 테스트 성공률 저하:
- 결제 성공(200), 결제 시도 중 이탈(204), 중복 요청 실패(429) 결과의 비율 왜곡.
- 실제 예상 성공률 50~70%에 비해 테스트 성공률 5% 미만 기록.
개선 계획
- 테스트 로직 점검 및 수정:
- 사용자 토큰 분리: 각기 다른 사용자 아이디 또는 다양한 토큰 사용.
- 429 에러 비율 조정: 중복 요청 최소화 및 요청 간격 조정.
- 동시성 제어 확인: 동시성 제어 로직 정상 작동 검증.
- 테스트 시나리오 추가:
- 동적 시뮬레이션: 다양한 사용자 요청 패턴을 조합.
- 장시간 부하 테스트: 1분 이상의 장기 부하로 재고 처리 및 이상 현상 확인.
- API 로직 최적화:
- 재고 증가와 같은 이상 현상 원인 분석 및 보완.
- Pub/Sub 이벤트 발행 처리 속도 추가 최적화.
📆 프로젝트 일정
1주차
- 모놀리스 프로젝트 MVP 구현
- MSA 개념 학습
- MSA 구조로 변경
- ConfigServer, EurekaServer, API Gateway 구축
- UserService 커버리지 테스트
- 동시성 이슈가 발생할 가능성이 높은 API의 부하테스트 (k6)
2주차
- 스키마 분할에 따른 리팩토링
- WebMVC → WebFlux(일부) 리팩토링
- Resilience4j CircuitBreaker 활용
- 도커 설정
- OpenFeign → WebClient 요청방식 리팩토링
3주차
- 인증 / 인가 방식 변경 리팩토링, 성능 개선
- 동시성 이슈 해결을 위한 레디스 기반 분산락 도입
- 레디스 펍/섭 방식과 병행, 성능 개선
- JUnit5 활용 각 서비스 단위 테스트 커버리지 100%
- 동시성 이슈가 발생할 가능성이 높은 API의 부하테스트2 (k6)
/ | 목표 | 실천 |
---|---|---|
- - - - - 1주차 - - - - - | ||
24.12.18 [수] | [ 프로젝트 시작일 ] ERD 작성 API 명세서 작성 유저 관리 서비스 전반 기능 구현 |
ERD 초안 작성 API 명세서 작성 DDD구조 프로젝트 생성 Docker 환경설정 회원가입 기능 구현률 50% |
24.12.19 [목] | 유저 관리 서비스 기능 구현 |
회원가입 기능구현 완료 이메일 인증 기능구현 완료 로그인 기능구현 완료 현재 기기에서 로그아웃 기능구현 완료 모든 기기에서 로그아웃 기능구현 완료 비밀번호 변경 기능구현 완료 |
24.12.20 [금] |
기능 구현 작업 중단 유레카 서버 구축 API 게이트웨이 구축 멀티 모듈 프로젝트에 맞는 도커환경 구축 그래들 의존성 중앙 관리식 일부 자동화 구축 |
유레카와 게이트웨이 활용을 위해서 먼저 프로젝트의 구조를 리팩토링 최초 프로젝트 내 모듈 4개에서 ConfigServer, EurekaServer, Gateway, Service... 로 구성을 하고, 각 서비스마다 서브모듈을 4개씩 구성 ConfigServer 구축 완료 EurekaServer 구축 완료 API Gateway 구축 완료 유저 관리 서비스 유레카에 등록 후 요청 처리 테스트 완료 |
24.12.21 [토] |
각 서비스 별 스키마 분할 API 명세서 보완하여 재작성 명세서 기반 상품관리 서비스 구현 -----------------Optional----------------- 주문관리 서비스 구현 Resilence4j 활용 장애상황 연출 및 회복탄력성 갖추기 |
서비스 별 스키마 분할 완료 상품관리 서비스 구현 완료 주문관리 서비스 구현 중... 현재 각 서비스별 하위모듈이 개별적으로 동작하지 않는데도 이 구조를 유지할 이유가 없다는걸 깨달았음. 따라서 루트의 모듈 구성은 그대로 두되. 각 서비스별 하위모듈 삭제하고 하나의 구조로 리팩토링 완료 |
24.12.22 [일] |
주문관리 서비스 구현 모든 서비스의 예외처리 추가 -----------------Optional----------------- 테스트 코드 및 시나리오 작성 테스트 수행 및 성능 개선 |
주문관리 서비스 구현완료 |
24.12.23 [월] |
위시리스트 API 구현완료 모든 서비스 예외처리 추가 |
위시리스트 API 구현 모든 서비스 예외처리 추가 |
24.12.24 [화] |
테스트 시나리오 및 코드 작성 테스트 후 예외처리 추가 및 성능개선 |
서비스 커버리지 테스트 66% 달성 k6 테스트 수행, 30%의 오류율 발생 확인 |
- - - - - 2주차 - - - - - | ||
24.12.25 [수] |
Resilience4j 활용, 회복탄력성 갖추기 |
실패 |
24.12.26 [목] | 동시성 제어 및 성능개선 |
Spring Security 추가 작업중 기존 방법으로 처리가 되지 않는 에러 발생 WebMvc -> WebFlux 로 로직 구성 변경. 유저서비스 변경 완료. 27일 나머지 서비스 변경 예정 |
24.12.27 [금] |
상품관리, 주문관리 서비스 WebFlux 구조 변경 레디스 기반 분산락 구현으로 동시성 제어 Up |
WebFlux 관련 피드백 수용 다시 WebMVC로 롤백.. |
24.12.28 [토] | 회복 탄력성 공부 | . |
24.12.29 [일] | 회복 탄력성 공부 |
Resilience4j 활용 Circuit Breaker, Retry, TimeLimiter 적용 Docker 환경 세팅 및 빌드 그리고 API 테스트 완료 프로메테우스 설정 완료 |
24.12.30 [월] |
3주차 목표 설정 동시성 제어에 관한 공부 가능하다면 일부 적용까지 |
코드 변경사항 발생시, 해당 모듈 재빌드, 도커에도 이미지 재빌드를 하는 과정이 번거로워서 코드 변경을 감지하여 자동으로 도커에 재빌드 된 jar파일이 빌드되도록 구성하고자 Devtools와 도커의 Volume 설정을 사용. 하지만, 뜻대로 되지 않았고. 나 혼자, 협업없이, 다른 컴퓨터에서 실행하지 않고 진행하는 프로젝트 이기 때문에 도커를 사용할 의미가 사실 없음. 다만, 도커를 사용할 수 있다는 것은 확인이 되었으니, 오늘부로 도커 아웃. |
24.12.31 [화] |
남은 재고 파악 API 설계 및 구현 Redis 캐싱에 대한 이해 결제 진입 및 결제 API 설계 및 구현 |
공부 |
- - - - - 3주차 - - - - - | ||
25.01.01 [수] | 공부 | 공부 |
25.01.02 [목] | . |
인증 / 인가 위치 변경 UserService -> Gateway 전체 서비스에서 로그인 검증을 하지 않게 되어 전체적으로 응답속도 10ms이상 향상 |
25.01.03 [금] |
선착순 구매 서비스 API MVP 개발로 기능구현 완료 각 모듈간 요청에 적절한 카프카 로직 추가 모듈별 응답속도 개선 |
주문 관련 API 성능개선 평균 응답속도 2500ms -> 120ms 약 93 ~ 95% 개선 결제 프로레스 API 구현 완료 위의 API K6 커스텀 매트릭&핸들러 테스트코드 작성 VU 50 이하시에만 정상작동, 50초과시 에러율 급증 |
25.01.04 [토] |
결제 프로세스 API의 동시성 제어 로직 추가 K6기준 VU 10000에서 안정적인 동작을 목표 |
레디스 기반 분산 락을 컨트롤러단에서만 구현 결과 : VU 1000명에서 안정적인 동작 확인 데이터 정합성과 동시성을 더 확실히 제어해야할 필요 |
25.01.05 [일] | . | . |
25.01.06 [월] |
Redis 최적화 방안레디스 QPS, 응답시간 및 메세지 전달 성공률 측정병목 구간 파악 및 해결 성능 최적화 이후 다시 테스트 진행 |
현재 프로젝트 진행 방향에 대해 멘토님과 상의한 결과 아쉬운 점이 있는 것 같아, 수립한 계획 전면 취소 단기 목표로는 내일 7일까지, 각 서비스별 테스트코드 작성 및 테스트 커버리지 90%이상 달성 장기 목표로는 레디스 캐싱에 대한 깊은 이해를 바탕으로 동시성 제어 성능 개선 차후 시간적 여유가 있다는 전제하에 카프카 등 선택사항 구현 |
25.01.07 [화] |
오늘부터 각 서비스별 테스트코드 작성 및 테스트 커버리지 100% 달성 |
UserService Test Coverage 100% 이미지![]() |
25.01.08 [수] | . |
ProductService Test Coverage 100% 이미지![]() EurekaServer Test Coverage 100% 이미지![]() Gateway Test Coverage 100% 이미지![]() |
25.01.09 [목] | . |
OrderService Test Coverage 100% 이미지![]() PurchaseService Test Coverage 90% 이미지![]() 전체 모듈 테스트코드 작성 1차 완료 |
- - - - - 프로젝트 종료 - - - - - |