본문 바로가기

Web_Project

[프로젝트 브로셔] Giftipie 프로젝트 상세설명

기프티파이에서 정말 원하는 선물을 주고 받아요!

원하는 선물을 받고 싶을 때, 한 사람을 통해 받기에는 부담되는 가격의 선물을 받고 싶을 때가 있습니다. 이때 선물 펀딩을 등록하여 지인들에게 링크를 공유하고, 지인들이 원하는 금액만큼 후원할 수 있는 서비스입니다.

 

🎁 Giftipie 바로가기

🖥️ Front-End Github

💻 Back-End Github

 

🏗️ 서비스 아키텍처

✅ 전체 아키텍처

Front-End(좌측) &  Back-End(우측)

 📌 기술적 의사결정

Redux Toolkit :
상태 관리와 관련하여 초기 설정에 필요한 보일러플레이트와 불필요한 반복 작업을 최소한 할 수 있는 상태 관리툴로 미들웨어를 쉽게 통합할 수 있어 비동기 작업 및 다양한 확장 기능을 쉽게 추가할 수 있어서 선택하였습니다.
GitHub Actions :
GitHub와의 통합이 용이하며 비교적 설정이 간단하고, 빠른 배포와 프로젝트의 규모가 작은 경우 유리하기 때문에 해당 기술을 선택하였습니다.
Docker :
독립적인 환경을 구성하고, 개발 환경과 운영 환경 간의 일관성을 유지하며 컨테이너 기반의 배포로 가볍게 배포할 수 있기 때문에 해당 기술을 선택하였습니다.
Blue-Green :
사용자에게 영향을 주지 않으면서 신규 버전을 안전하게 테스트하고 점진적으로 전환할 수 있으며, blue-green 두 환경이 독립적이기 때문에 새 버전의 오류가 기존 시스템에 영향을 미치지 않는 이점으로 해당 기술을 선택하였습니다.
Nginx :
한정된 예산을 사용하는 상황에서 하나의 EC2 인스턴스로 서버를 구축하였기 때문에, nginx의 리버스 프록시 기능을 통해 한대의 서버로 무중단배포를 구현하였습니다.
SSE (Server-Sent Events) :
서버에서 클라이언트로의 메세지 전달만 필요했기 때문에 단방향 통신 기술인 SSE가 가장 적합한 기술이라 판단하여 선택하였습니다.
Social Login(Kakao & Google) :
펀딩 후원에 참여하기 위해 다수가 접근할 수 있는 점을 고려하여, 사용자의 접근성에 중점을 둔 Social Login(Kakao & Google) 기능 구현을 선택하였습니다.
Spring Security :
인증되지 않은 불특정 다수가 접근할 수 있는 점을 고려하여, 개인정보 보안성에 중점을 둔 Spring Security 기반의 로그인 기능 구현을 선택하였습니다.
Kakaopay Online Payment API:
원하는 펀딩에 후원을 진행하고, 후원 결제 내역을 수집하기 위해 Kakaopay 온라인 결제 기능 구현을 선택하였습니다.
Redis :
사용자들에게 빈번하게 보여지는 정보들은 캐시를 적용하여 처리하면 성능 개선을 할 수 있을 것이라고 생각하였고, 추후 사용자가 늘어남에 따라 동시성 문제도 발생할 수 있다고 생각하여 이를 제어할 수 있는 기능을 제공하는 해당 기술을 선택하였습니다.
Meta tag 크롤링 :
사용자가 이미지를 직접 등록하는 것보다는 링크를 입력해서 자동으로 해당 페이지의 링크나 사진을 가져오는게 편할 것이라고 생각하여 기술을 선택하였습니다.
Prometheus, Grafana, Node Exporter, Slack :
한정된 예산 상황에 맞춰 낮은 성능의 서버로 개발을 진행한 상태라, 유저테스트 시 서버 상태를 모니터링하여 스케일업을 대비하기 위해 기술을 선택하였습니다.
Google Analytics :
유저테스트 시에 유저들이 어느 페이지에서 오래 머물렀는지, 어느 페이지에서 몇번 클릭했는지, 접속 환경, 조회수 등에 대한 정보를 얻을 수 있어 유저 경험 보완에 도움이 될 것으로 판단되어 기술을 선택하였습니다.

 

🔧 트러블 슈팅

1️⃣[FE] redux-persist 적용 시 발생한 오류

🔥문제상황 :

로그인 상태를 유지하기 위해 redux-persist를 적용하다가 Uncaught TypeError: baseReducer is not a function 오류와 non-serializable 오류가 발생했다.

🙄원인파악 :

첫번째는 주어진 코드에서 "baseReducer"라는 변수 또는 함수가 기대한 형식이 아니라는 오류 메시지였고, 두번째는 redux의 serializable action을 유지하는 원칙에 어긋나는 비직렬화가 가능하지 않은 값이 action에 포함되어 있다는 오류 메시지였다.

👍 해결방법 :

첫번째 오류 해결: redux-persist를 사용할 때는 configureStore에 전달되는 reducer는 함수여야 한다. slice를 reducer로 변경했다. 또 rootReducer 객체를 함수로 변환해야 한다. store.js에서 combineReducers 함수를 사용했다.

 

두번째 오류 해결: store.js 파일에서 serializableCheck를 false로 지정했다. 이것은 특별한 상황에서만 사용해야 하며, 주의가 필요한 설정이다.

 

2️⃣ [FE] 소셜 관련 API 연결 시 404 페이지가 보여지는 현상

🔥 문제상황 : 

라우터로 페이지를 구성할 때 지정한 페이지를 제외한 모든 경로는 404 페이지가 되도록 만들었다. 소셜 관련 API 연결 시 클라이언트에서 서버로 인증을 요청하고 기다리는 동안 보여지는 페이지는 지정한 페이지가 아니었기 때문에 404 페이지를 띄우게 된다.

🙄 원인파악 :

사용자가 인증을 기다리는 동안 404 페이지 대신에 보여줄 새로운 페이지가 필요했다.

👍 해결방법 :

React에서 로딩 상태를 시각적으로 나타내는 데 사용되는 react-spinners라는 라이브러리를 사용해서 새로운 로딩 페이지를 만들었다. 사용자에게 인증 작업이 진행 중이라는 것을 보여줄 수 있었다.

 

3️⃣ [BE] blue-green 무중단 배포 시 50% 확률로 502 Bad Gate Way

🔥 문제상황 : 

CI-CD를 통해 자동으로 배포가 이루어질 때, 포트 전환이 되면서 50%확률로 502 Bad Gateway 페이지가 나왔다.

🙄 원인파악 :

로드밸런스 대상그룹의 헬스체크의 딜레이 문제로 서버 안정화가 되지 않았을때 새로운 컨테이너로 호스팅을 연결해서 나타난 문제

👍 해결방법 :

배포 스크립트에 AWS CLI에 접근하는 코드를 추가하고 대상그룹의 상태검사 체크 여부를 확인하는 코드를 추가하고, health check 딜레이를 감안하여 30의 sleep을 주고, 대상그룹의 상태검사편집을 통해 딜레이를 최소화하여 502 문제를 해결하였다.

 

4️⃣ [BE] SSE Emitter 연결이 1분마다 끊기는 현상

🔥 문제상황 :

로그인에 성공하여 자동으로 서버와 클라이언트가 SSE 연결이 되는데, 백엔드 코드 상에서는 Time Out 시간을 1시간으로 설정했지만, 1분이 지나면 자동으로 계속 연결이 끊기는 상황이 발생

🙄 원인파악 :

개발자 도구 콘솔에 ERR_HTTP2_PROTOCOL_ERROR 200 에러 코드가 찍혀있는 것으로 보아 코드상의 문제가 아니라 서버 환경 설정에 문제가 있는 것으로 추론. Nginx 사용시 HTTP 프로토콜 버전이 자동으로 1.0으로 설정되어 SSE 연결이 유지 불가능함을 확인.

👍 해결방법 :

  1. Nginx와 로드밸런서를 같이 사용 중이면 로드밸런서에서 자동으로 HTTP/2 를 기본으로 설정해주기 때문에 별도의 Nginx 설정이 필요없었음.
  2. AWS ALB의 ‘트래픽 구성 - 유휴 제한 시간’을 4000초 (1시간 이상)으로 설정하여 코드 상의 1시간 Time Out 설정이 동작할 수 있도록함.

5️⃣ [BE] 만료된 Access token 내 Claim 추출 불가 건

🔥 문제상황 :

만료된 access token에서 claim을 추출할 경우, ServletException이 지속적으로 발생함

이에 클라이언트가 만료된 access token으로 API를 요청할 경우,

유저를 식별하는 email을 추출하지 못해 아래 로직을 수행하지 못함

  • claim 내 email로 DB 내 refresh token 유효 여부를 판별할 수 없음
  • claim 내 email로 새로운 access token 재발급이 불가함

🙄 원인파악 :

보안상의 이유로 인해, 만료된 access token에서 claim을 추출할 수 없음을 파악함

👍 해결방법 :

로그인 시 발급된 access token을 DB에 저장함

이에 클라이언트가 만료된 access token으로 API를 요청할 경우,

만료된 access token으로 유저를 식별해 아래 로직을 수행함

  • 식별된 유저의 refresh token으로 DB 내 refresh token 유효 여부를 판별함
  • 식별된 유저의 email로 새로운 access token을 재발급함
 

[Project | 트러블 슈팅] 만료된 Access token 내 Claim 추출 불가 건

📌 진행 목차 1. 배경 사항 2-1. 문제 발생 2-2. 문제 파악 및 해결 2-3. 해결 결과 3. 진행 후 느낀 점 💡 1. 배경 사항 1) 진행 배경 •기존의 경우, access token이 담긴 cookie를 클라이언트에 반환하는

jisulee-shsf.tistory.com

 

6️⃣ [BE] Redis 날짜/시간 데이터 직렬화, 역직렬화 문제

🔥 문제상황 : 

날짜 데이터를 직렬화/역직렬화 하는 과정에서 LocalDateType을 지원하지 않는다는 오류 발생

2024-02-06T11:42:21.857+09:00 ERROR 13956 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Java 8 date/time type `java.time.LocalDate` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.giftforyoube.funding.dto.FundingResponseDto["endDate"])] with root cause

🙄 원인파악 :

**java.time.LocalDate**와 같은 Java 8 날짜/시간 타입을 기본적으로 지원하지 않는 ObjectMapper 설정 때문에 발생하였습니다.

👍 해결방법 :

처음에는Jackson2JsonRedisSerializer를 사용해서 날짜 데이터를 직렬화/역직렬화 할 수있게 ObjectMapper에 JavaTimeModule을 추가해 설정을 했습니다.

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(redisConnectionFactory);

    Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new JavaTimeModule());
    objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 
																			ObjectMapper.DefaultTyping.NON_FINAL, 
																			JsonTypeInfo.As.WRAPPER_ARRAY);
    serializer.setObjectMapper(objectMapper);  //스프링부트 3.0 이상부터 deprecated  

    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(serializer);

    return template;
}

하지만 ObjectMapper의 setObjectMapper가 스프링부트 3.0 이상부터 deprecated 되었다고 하여 커스텀한 serializer를 만들어서 해결하기로 했습니다.

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
     RedisTemplate<String, Object> template = new RedisTemplate<>();
     template.setConnectionFactory(redisConnectionFactory);
     // Key Serializer
     template.setKeySerializer(new StringRedisSerializer());
     // Value Serializer
     template.setValueSerializer(customRedisSerializer());
     return template;
 }

@Bean
public RedisSerializer<Object> customRedisSerializer() {
			ObjectMapper objectMapper = new ObjectMapper();
      objectMapper.registerModule(new JavaTimeModule()); // JavaTimeModule 등록
      objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 
																				ObjectMapper.DefaultTyping.NON_FINAL,
																				JsonTypeInfo.As.PROPERTY);

      return new RedisSerializer<Object>() {
          @Override
          public byte[] serialize(Object t) throws SerializationException {
              try {
                  return objectMapper.writeValueAsBytes(t);
              } catch (Exception e) {
                  throw new SerializationException("Error serializing object to JSON", e);
              }
          }

          @Override
          public Object deserialize(byte[] bytes) throws SerializationException {
              if (bytes == null || bytes.length == 0) {
                  return null;
              }
              try {
                  return objectMapper.readValue(bytes, Object.class);
              } catch (Exception e) {
                  throw new SerializationException("Error deserializing object from JSON", e);
              }
          }
      };
  }

이렇게 커스텀한 serializer설정을 사용하여 Redis에 저장되는 객체들이 JavaTimeModule을 통해 java.time 패키지 타입을 올바르게 처리할 수 있어 날짜/시간 타입의 직렬화/역직렬화 문제를 해결할 수 있었습니다.

🔎 주요기능

 회원가입 / 로그인

📌 일반 회원가입 / 로그인(Spring Security)

유효성 검증과 약관 동의를 포함된 회원가입 기능을 구현했습니다. 개인정보&nbsp;보안성에 중점을 둔 Spring Security 기반의 로그인 기능을 구현했습니다.

 

📌 일반 회원가입 시 이메일 인증

실제 사용 중인 이메일인지 인증 메일을 발송하고, 인증 코드를 발급하여 메일을 인증할 수 있습니다.

📌 Social Login(Kakao, Google)

사용자의 접근성에 중점을 둔 Social Login(Kakao & Google) 기능을 구현했습니다.

액세스 토큰과 리프레시 토큰을 사용해 stateless한 통신 환경에서 인증 및 권한을 부여합니다

 

 펀딩 등록

상품 정보가 있는 링크 를 입력하여 이미지를 등록할 수 있습니다. 이미지 등록 후 나머지 정보들을 모두 입력하여 펀딩을 등록할 수 있습니다.

 

 펀딩 조회

로그인 시 홈 화면에서 내 펀딩 정보를 볼 수 있고, 링크복사, 수정이 가능하고 사진을 클릭하면 해당 펀딩페이지로 이동이 가능합니다.
최근 펀딩 구경하기 - 펀딩더보기 를 눌러 최근 순으로 다른 유저들의 펀딩을 볼 수 있습니다. 전체, 진행중, 완료 탭을 눌러 상태별로 확인이 가능합니다.

 

펀딩 수정, 삭제

펀딩 상세페이지에서 &lsquo;수정하기&rsquo; 버튼을 눌러 펀딩 내용을 수정할 수 있습니다. 수정 페이지에서 펀딩 삭제, 펀딩 종료 또한 할 수 있습니다. 펀딩 수정, 삭제, 종료는 본인 게시글만 할 수 있습니다.

 링크 복사 기능

링크 복사 버튼을 눌러 펀딩 게시글을 다른 사람에게 공유 하여 후원을 유도할 수 있습니다.

 Kakaopay를 통한 후원(결제) 기능

원하는 펀딩에 후원을 진행하고, 후원 결제 내역을 수집하기 위해 Kakaopay 온라인 결제 기능을 구현했습니다.

 

 Giftipie에서 함께한 선물 - 통계 기능

펀딩에 참여한 총 인원, 목표 금액 달성으로 펀딩이 종료되어 선물을 받은 인원, Giftipie에서 이루어진 펀딩의 총 금액을 계산하여 페이지에 보여줍니다.

 

 실시간 1:1 문의기능

채널톡을 연동 하여, 페이지 우측 하단의 아이콘을 누르면 Giftipie의 개발자들과 1:1 문의 를 할 수 있습니다.

 

 실시간 알림 기능

📌 사이트 내 실시간 알림 기능

페이지 상단에 실시간 알림을 띄워줍니다. 실시간 알림은 후원발생, 펀딩성공, 펀딩마감 시에 동작합니다.
우측 상단의 종모양 버튼을 누르면 알림 목록 페이지로 이동되어 받았던 알림메시지들을 확인 할 수 있습니다. 해당 알림메시지를 누르면 관련 페이지로 이동 합니다. 원하는 메시지를 선택하여 삭제 할 수 있고, 읽은 메시지를 한번에 삭제 할 수도 있습니다.

 

📌 알림 이메일 발송

회원 가입시에 이메일 수신 동의에 체크 했다면 실시간 알림 발생 시 사이트에 접속 중이 아니더라도, 이메일로 알림 이메일을 받아볼 수 있습니다.

 

💗 팀원 소개 & 팀원 역할

👥 팀원 역할

역할 이름 분담
FE 👑 류지현 홈, 회원가입, 로그인, 404, 로딩스피너 페이지 생성, 소셜 로그인, 이메일 검증, 실시간 알림, 결제 기능 구현
FE 이현진 펀딩 상세, 생성, 수정, 결제페이지 반응형(모바일/웹) UI/UX 적용 및 펀딩 CRUD(펀딩 등록, 조회, 수정, 삭제) 기능 구현
BE 👑 고훈 펀딩 CRUD, SWAGGER, S3 이미지 업로드 기능, 펀딩 리스트 (전체, 진행중, 완료된 펀딩) 페이지네이션 기능 및 무한스크롤 기능 구현, 구글 애널리틱스 연동
BE 현민영 CI-CD, AWS 서버 관리, Nginx, SSL, 모니터링 구현, 펀딩 수정/삭제, 내 펀딩 조회, 실시간 알림기능(SSE), 알림 이메일 발송, 회원가입 시 이메일 검증 메일 기능, 테스트코드 작성(NotificationService), UT 총무
BE 김도현 펀딩 CRUD, Redis 캐시 적용 및 캐시 무효화, 메타 태그 크롤링을 통한 링크 상품 이미지 등록, Scheduler를 이용한 펀딩 자동 갱신, 펀딩 통계(Summary) 계산, D-Day 및 목표 금액 달성율 계산, Redisson 락 적용(링크 상품 등록, 펀딩 생성/수정/삭제)
BE 이지수 회원가입, 로그인(Spring Security), 로그아웃, 회원탈퇴 기능 구현 / Spring Security 기반 인증 및 인가 기능 구현 / Social Login(Kakao & Google) 기능 구현 / Access & Refresh token 사용 로직 설계 및 기능 구현 / Kakaopay 온라인 결제 API & Kakaopay 데모 연동을 통한 결제 기능(준비 & 승인) 구현 / Global Exception Handler를 사용한 전역 예외 처리 기능 구현 / Github Organization 생성 및 관리
Design 윤다인 디자인 담당

 

💟 로고