Github Icon Github Wiki

📌 프로젝트 소개 🛠️ 기술스택과 선택이유 🤔 기술적 의사결정 🔍 트러블슈팅 ⚡성능개선 📆 프로젝트 일정 🔙 돌아가기

📌 프로젝트 소개


MSA 기반 선착순 구매 프로젝트

대규모 트래픽 가능성을 염두에 둔 e-Commerce 기반의 웹 서비스

사용자는 회원가입 이후 정보확인, 장바구니, 위시리스트, 구매와 같은 기능을 경험할 수 있고
특정 시간에 한정된 수량의 상품을 선착순으로 결제하고 구매할 수 있는 서비스를 사용가능한 서비스입니다.

프로젝트 기간: 24.12.18 ~ 25.01.09

개발인원 : 개인프로젝트 (1명)

관련 도메인

배달의 민족, 무신사, 29cm 등 e-Commerce

주차별 구현 목표

    1주차

  1. 프로젝트 기획
  2. DB 설계
  3. API 설계
  4. 프로젝트 구조 설계
  5. 개발 환경 구축 ERD 설계 및 docker-compose 설치
  6. 유저 및 상품 관리를 위한 데이터 셋 구현
  7. PostMan을 활용한 테스트 시나리오 작성

    2주차

  1. Docker를 활용한 로컬 개발 환경 구축
  2. 모노리스 서비스의 마이크로 서비스화
  3. API Gateway 생성, 장애 상황 연출 후 회복 탄력성 갖추기

    3주차

  1. 쇼핑몰 목업 사이트 구축을 위한 API 설계
  2. 자동화 테스트 툴 구축
  3. Redis를 활용한 대규모 주문 처리 기술 이해

    4주차

  1. 목업 사이트 내 실제 구매, 재고 관리 기능 등 구현
  2. 인수 조건에 따른 결제 프로세스 구현
  3. Non Blocking 형태로 API Gateway 전환

🛠️ 기술스택과 선택이유


백엔드 기술 스택

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> signup(@RequestBody Map 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 내부에 어노테이션으로 선언.

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> signup(@Valid @RequestBody UserSignupRequestDto userRequest) {
  return ResponseEntity
  .status(202)
  .body(Map.of(
  "msg",
  String.format("%s 으로 인증메일이 전송되었습니다. 메일을 확인해주세요.", userRequest.getUserEmail())));
}
          
      

기대 효과

  • 중복 코드 제거 및 가독성 향상.
  • 검증 로직의 재사용성을 높이고 유지보수성을 강화.
  • 자동 예외 처리로 컨트롤러의 코드 간소화.
  • DTO와 컨트롤러 간의 역할 분리로 구조적 일관성 확보.

4. 검증 실패 메시지 개선 작업


기존 코드 문제점

HashMap 사용 문제

  • HashMap은 키-값 쌍의 입력 순서를 보장하지 않음.
  • 사용자에게 검증 실패 메시지가 무작위 순서로 표시되어 가독성이 저하될 가능성 존재.

개선 내용

LinkedHashMap으로 변경

  • 입력된 키-값 쌍의 순서를 보장.
  • 사용자에게 검증 실패 메시지가 입력된 순서대로 출력되도록 개선.

변경 전 / 후 비교

변경 전

            
@ExceptionHandler(WebExchangeBindException.class)
public ResponseEntity> handleValidationExceptions(WebExchangeBindException ex) {
    Map errors = new HashMap<>();
    ex.getFieldErrors().forEach(error -> {
        errors.put(error.getField(), error.getDefaultMessage());
    });
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
            
        

변경 후

            
@ExceptionHandler(WebExchangeBindException.class)
public ResponseEntity> handleValidationExceptions(WebExchangeBindException ex) {
    Map errors = new LinkedHashMap<>(); // LinkedHashMap 사용
    ex.getBindingResult().getFieldErrors().forEach(error -> {
        errors.put(error.getField(), error.getDefaultMessage());
    });
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
            
        

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. 주문 관리 서비스의 유저 검증 로직에 대한 고민


문제 상황

현상

  • 현재 로직:
    1. 주문 서비스에서 상품 주문 시 유저 서비스를 통해 유저 검증.
    2. 유저 서비스는 자체 스키마와 통신하여 처리 결과를 반환.
    3. 결과를 주문 서비스로 전달하여 유저 검증을 완료.
  • 의문:
    • 이 방식이 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에서 검증 로직은 주로 로그아웃 시 사용되었으므로, 로그인 검증 로직에 대한 부정 조건이 빠져 있었음.

해결 과정

  1. 조건문 수정: 로그인 검증 조건에 부정 연산자(!) 추가:
    if (!로그인이 되었다면?) {
        // "로그인이 필요한 서비스입니다" 메시지 반환
    }
            
  2. 결과 확인: 조건문 수정 후, 로그인 여부를 올바르게 판단하여 정상 처리.

교훈

  1. 조건문 작성 시 항상 재확인:
    • 로그인, 검증, 부정 조건 등은 헷갈리기 쉬우므로 철저히 검토 필요.
    • 특히, 유사한 로직(UserService의 로그아웃 검증 등)이 영향을 줄 수 있음.
  2. 디버깅의 중요성: 로그 확인과 디버깅 과정을 통해, 로직 오류를 점진적으로 좁혀가는 과정이 중요.

결론

  • 문제 해결: 조건문 수정으로 로그인 검증 로직 정상 작동.
  • 추후 방지: 코드 리뷰 및 테스트 코드 작성으로 조건문 오류 재발 방지.

2. 동적 라우팅 문제 분석 및 해결 방안


문제 상황

  • 현상:
    • 게이트웨이의 라우팅이 유레카 서버에 등록된 서비스 이름을 기준으로 동적 라우팅되도록 설정.
    • 도커 환경에서 빌드 시 동적 서비스 이름을 읽어오지 못하는 문제 발생.
  • 임시 해결 방법:
    • 게이트웨이의 application.yml 파일을 수정하여 정적 라우팅 방식으로 설정 → 문제 해결.
    • 하지만 동적 라우팅의 장점을 잃게 됨.

발생 가능한 원인

  1. 도커 네트워크 문제:
    • 도커 컨테이너 간 네트워크 연결이 제대로 설정되지 않음.
    • 도커에서 기본적으로 사용하는 Bridge 네트워크가 유레카의 서비스 디스커버리를 방해할 가능성.
  2. 유레카 서버와의 통신 문제:
    • 게이트웨이가 유레카 서버와 통신하여 등록된 서비스 정보를 가져오는 과정에서 실패.
    • 도커 내부의 호스트 이름(예: localhost)이 맞지 않거나 IP 주소가 잘못된 경우.
  3. 도커 환경 변수 누락:
    • 도커에서 환경 변수(예: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE)가 제대로 전달되지 않아 유레카 서버를 인식하지 못함.
  4. 게이트웨이 설정 문제:
    • 게이트웨이가 유레카 서버에서 서비스를 가져오는 로직이 제대로 동작하지 않음.
    • 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
            
  • 유레카 서버에 등록된 서비스 이름이 올바르게 사용되고 있는지 확인:
    • 유레카 서버에 등록된 서비스 이름은 대문자로 변환되므로 게이트웨이가 이를 인식할 수 있도록 확인 필요.
    • 예: userserviceUSERSERVICE

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> signup(@Valid @RequestBody UserSignupRequestDto userRequest) {
    return ResponseEntity
        .status(202)
        .body(Map.of(
            "msg",
            String.format("%s 으로 인증메일이 전송되었습니다. 메일을 확인해주세요.", userRequest.getUserEmail())
        ));
}

기대 효과

  • 중복 코드 제거 및 가독성 향상.
  • 검증 로직의 재사용성을 높이고 유지보수성을 강화.
  • 자동 예외 처리로 컨트롤러의 코드 간소화.
  • DTO와 컨트롤러 간의 역할 분리로 구조적 일관성 확보.

4. 검증 실패 메시지 개선 작업


기존 코드 문제점

  • HashMap 사용 문제:
    • HashMap은 키-값 쌍의 입력 순서를 보장하지 않음.
    • 사용자에게 검증 실패 메시지가 무작위 순서로 표시되어 가독성이 저하될 가능성 존재.

개선 내용

  • LinkedHashMap으로 변경:
    • 입력된 키-값 쌍의 순서를 보장.
    • 사용자에게 검증 실패 메시지가 입력된 순서대로 출력되도록 개선.

변경 전 / 후 비교

변경 전

@ExceptionHandler(WebExchangeBindException.class)
public ResponseEntity> handleValidationExceptions(WebExchangeBindException ex) {
    Map errors = new HashMap<>();
    ex.getFieldErrors().forEach(error -> {
        errors.put(error.getField(), error.getDefaultMessage());
    });
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}

변경 후

@ExceptionHandler(WebExchangeBindException.class)
public ResponseEntity> handleValidationExceptions(WebExchangeBindException ex) {
    Map errors = new LinkedHashMap<>(); // LinkedHashMap 사용
    ex.getBindingResult().getFieldErrors().forEach(error -> {
        errors.put(error.getField(), error.getDefaultMessage());
    });
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}

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 만료 시 자동 삭제로 관리 로직 간소화.

구현 계획

  1. JWT 저장 및 관리:
    • Redis를 사용해 JWT를 저장하고 TTL 설정.
    • 만료된 토큰은 자동 삭제되도록 구성.
  2. 로그아웃 기능:
    • 현재 기기에서 로그아웃: Redis에서 해당 JWT 삭제.
    • 모든 기기에서 로그아웃: Redis에 저장된 사용자 관련 JWT 모두 삭제.
  3. 추가 작업:
    • 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 응답속도 개선

최초 로직

  1. JWT 검증: API 진입 시 매번 JWT 토큰 검증 수행.
  2. 상품 검증: 상품 존재 여부 확인.
  3. 재고 확인: 상품 수량이 충분한지 검증.
  4. 주문 처리: 주문 로직 수행.

초기 응답속도

  • 최소: 1711ms
  • 최대: 5348ms
  • 평균: 2500ms

개선된 로직

  1. 게이트웨이에서 JWT 검증:
    • API 진입 전에 게이트웨이에서 JWT 검증을 일괄 처리.
    • 각 API는 추가 JWT 검증 작업 불필요.
  2. 상품 정보 일괄 조회:
    • API 진입 후 필요한 모든 상품 정보를 한번에 불러옴.
    • 각각의 값을 비즈니스 로직에서 처리하여 중복 요청 감소.
  3. 최적화된 주문 로직:
    • 상품 검증과 수량 검증을 동시에 처리하여 단계별 로직 단축.
    • 필요한 경우 비동기적 데이터 처리 추가.

응답속도

  • 최소: 37ms
  • 최대: 342ms
  • 평균: 55ms

개선 효과

  1. JWT 검증을 게이트웨이로 이전:
    • JWT 검증 로직의 중복 제거로 API 처리 속도 향상.
    • 각 서비스의 부담을 줄이고 요청 병목 최소화.
  2. 상품 정보 일괄 조회:
    • 상품 검증과 수량 검증 요청을 개별적으로 처리하던 기존 로직에서 발생하던 불필요한 통신 제거.
    • 데이터베이스 및 네트워크 요청 수 감소.
  3. 주문 로직 간소화:
    • 연속된 조건 검증 작업을 병렬적으로 처리하여 성능 개선.
    • 단순화된 로직으로 코드 유지보수성 향상.

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개

테스트 문제점

  1. 같은 사용자 아이디 사용:
    • 하나의 사용자 아이디로 테스트를 수행하여 429 에러 비율 증가.
    • 테스트 환경에서 결과 신뢰도가 저하됨.
  2. 테스트 성공률 저하:
    • 결제 성공(200), 결제 시도 중 이탈(204), 중복 요청 실패(429) 결과의 비율 왜곡.
    • 실제 예상 성공률 50~70%에 비해 테스트 성공률 5% 미만 기록.

개선 계획

  1. 테스트 로직 점검 및 수정:
    • 사용자 토큰 분리: 각기 다른 사용자 아이디 또는 다양한 토큰 사용.
    • 429 에러 비율 조정: 중복 요청 최소화 및 요청 간격 조정.
    • 동시성 제어 확인: 동시성 제어 로직 정상 작동 검증.
  2. 테스트 시나리오 추가:
    • 동적 시뮬레이션: 다양한 사용자 요청 패턴을 조합.
    • 장시간 부하 테스트: 1분 이상의 장기 부하로 재고 처리 및 이상 현상 확인.
  3. 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% 이미지 테스트 커버리지 100%
25.01.08 [수] .
ProductService Test Coverage 100% 이미지 테스트 커버리지 100%
EurekaServer Test Coverage 100% 이미지 테스트 커버리지 100%
Gateway Test Coverage 100% 이미지 테스트 커버리지 100%
25.01.09 [목] .
OrderService Test Coverage 100% 이미지 테스트 커버리지 100%
PurchaseService Test Coverage 90% 이미지 테스트 커버리지 90%

전체 모듈 테스트코드 작성 1차 완료
- - - - - 프로젝트 종료 - - - - -