Skip to content

Machrie/InsureConnect

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Insure Connect

SpingBoot를 활용한 보험 중개 서비스

🖥️ 프로젝트 소개

보험 설계사와 보험 가입자를 연결해주는 웹 서비스

🧑 참여 인원

  • 황인수
    • 사용자, 관리자 관련 페이지 및 OAuth2.0 및 JWT관련 파트 제작
  • 이한얼
    • 설계사, 리뷰 관련 페이지 및 ModelMapper관련 파트 제작

🎬 프로젝트 기획 동기

  1. 보험에 대해 무지하여 자신에게 필요한 보험의 카테고리에 대해 잘 모르는 사람이 많음
  2. 기존 보험 설계는 다양한 설계사를 비교할 수 없어 설계사 선택에 제약 사항이 많음
  3. 다양한 보험 회사에서 제공하는 보험 설계 서비스를 한 곳에서 볼 수 있는 서비스가 부재함
  • 가입자 입장에서 자신이 필요한 보험이 무엇인지 손쉽게 파악하고 인기 설계사를 골라서 상담할 수 있는 중립적인 플랫폼에 대한 필요성이 대두되어 해당 프로젝트 기획

🎯 프로젝트 목표 및 기대 효과

  • Chat GPT와의 대화를 통해 자신에게 맞는 보험 카테고리를 알 수 있음
  • 카테고리를 통해 보험 가입자가 자신에게 맞는 다양한 설계사를 확인 및 상담 요청할 수 있음
  • 상담을 마친 후 남긴 리뷰를 통해 다음 가입자에게 설계사에 대한 정보를 남길 수 있음

📆 개발 기간

  • 2024.01.02 ~ 2024.01.25

⚙️ 개발 환경

  • Language : Java 17
  • IDE : IntelliJ
  • Framework : SpringBoot 3.2.1
  • Featured Library : OAuth 2.0 + JWT 0.9.1 + ModelMapper 3.1.1
  • Servlet Engine : Apache Tomcat 10.1.17
  • Database : Postgresql 16
  • ORM : Hibernate 6.4.1 + JPA 3.2.1
  • Web : HTML(+thymeleaf) + JavaScript(+JQuery) + CSS(+bootstrap)

🌳 ERD

earl - public

🧐 프로젝트 특징

🛠️ 주요 기능

  • 보험 가입자
  1. Chat GPT를 활용하여 가입하고자 하는 보험 카테고리 확인
  2. 보험 카테고리를 활용하여 설계사 검색 및 설계사 홍보글 확인
  3. 자신에게 맞는 보험 설계사와 연결
  4. 상담 후 설계사에 대한 리뷰 작성
  • 보험 설계사
  1. 자신을 나타낼 수 있는 홍보글 작성
  2. 가입자가 첨부한 AI 채팅 내역 확인 후 상담

1. 메인 페이지

screencapture-localhost-8080-2024-01-25-16_39_00

  1. 인기 상담사 확인
  2. 최근 리뷰 확인

2. 관리 페이지

스크린샷 2024-01-25 오후 3 54 16 스크린샷 2024-01-25 오후 2 39 48 스크린샷 2024-01-25 오후 2 08 55
  1. 설계사 신청 내역 확인 및 등록
  2. 유저 관리
  3. 리뷰 관리
  4. 카테고리 관리

3. 설계사 관리 페이지

스크린샷 2024-01-25 오후 7 16 32
  1. 요청 상담자 AI채팅 내역 확인
  2. 홍보글 관리

4. 마이 페이지

스크린샷 2024-01-25 오후 3 43 01
  1. 내 정보 수정
  2. 내가 쓴 리뷰 관리
  3. 회원 탈퇴

5. AI 채팅

스크린샷 2024-01-25 오후 3 48 14
  1. Chat GPT 기반 보험 정보 획득

6. 보험 설계사 홍보 페이지

스크린샷 2024-01-25 오후 3 38 53 스크린샷 2024-01-25 오후 3 52 10
  1. 가입자의 상황에 맞는 설계사 확인 및 상담 신청
  2. 상담사 카카오톡 채널 연결

7. 리뷰 페이지

스크린샷 2024-01-25 오후 3 38 12
  1. 전체 리뷰 확인

⚠️ 문제 발생 및 해결

1. 인기 설계사 선정 및 호출 문제

  • 문제 파악
    • 인기 설계사 선정
      • 단순 리뷰 점수의 합으로 나열하기에는 최근에 시작한 설계사가 불리한 면이 있음
      • 리뷰 평균 점수로 나열하기에는 보험 가입자의 피드백을 수정받아 더 좋은 서비스를 제공해도 과거의 낮은 점수의 리뷰가 발목을 잡을 수 있음
    • 인기 설계사 계산 함수 호출
      • 메인 페이지에 접속할 때마다 인기 설계사 계산 함수 호출시 서버에 과부하가 걸릴 가능성이 있음
  • 문제 해결
    • 인기 설계사 선정
      • 최근 1년 리뷰만 반영 후 평균 점수로 나열 -> 최근의 리뷰와 1년 전 리뷰가 같은 가치를 갖는 것은 2번 문제점을 해결하지 못함
      • 기간별로 가중치를 두어 최근의 리뷰가 더 높은 점수를 받도록 조정 -> 리뷰 100개 평균 점수 4.8인 설계사보다 리뷰 1개 평균 점수 5.0인 설계사가 더 높은 점수를 받는 문제 발생
      • 기간별로 가중치를 두고 평균 점수를 구한 후 전체 리뷰 수에 대한 점수 추가 부여 -> 최근 리뷰에 대한 가치 상승 및 평균의 함정 해결 가능
    • 인기 설계사 계산 함수 호출
      • 리뷰가 추가될 때마다 점수 계산 -> 실시간으로 점수 반영이 가능하나 리뷰 작성 사용자 수가 많을 경우 서버 과부하 문제 해결 불가
      • 최소 1시간마다 한번씩 점수 계산 -> 실시간 점수 반영이 불가능하나 한 상담사가 1시간에 많은 상담을 진행하지 못한다는 상황을 고려하여 점수 실시간 반영보다 과부하 방지가 더 큰 이점이 있다고 판단
  • 해결 과정
    • 인기 설계사 선정
      • 기간에 따른 선형적 가중치 값 활용 -> 단순하여 합리적인 점수 부여가 불가능하다고 판단
      • 가중치 값으로 망각 곡선의 망각률 활용 -> 상담에 대한 기억(리뷰)은 인간의 기억과 유사할 것이라 판단
        • 망각곡선
        • (1달(58점)+3달(44점)+6달(33점)+1년(20점))*리뷰 점수/전체 리뷰수+리뷰 1개당(30점)
          public int getRecommendRating(){
              int score = 0;
              if(review != null && !review.isEmpty()){
                  int cnt = 0;
                  for(Review reviewScore : review){
                      Timestamp before = reviewScore.getWrite();
                      Timestamp now = new Timestamp(System.currentTimeMillis());
                      Instant beforeIns = before.toInstant();
                      Instant nowIns = now.toInstant();
                      Duration duration = Duration.between(beforeIns, nowIns);
                      long day = Math.abs(duration.toDays());
                      if(day > 365) continue; //1년 이상 0점
                      else if(day > 180){
                          score+=reviewScore.getRate()*20; //6개월~1년 20점
                          cnt++;
                      }
                      else if(day > 90){
                          score+=reviewScore.getRate()*33; //3개월~6개월 33점
                          cnt++;
                      }
                      else if(day > 30){
                          score+=reviewScore.getRate()*44; //1개월~3개월 30점
                          cnt++;
                      }
                      else{
                          score+=reviewScore.getRate()*58; //1개월 이하 58점
                          cnt++;
                      }
                  }
                  score = (score/cnt)+(cnt*30); //가중치 점수 평균 + 리뷰 1개당 30점점
              }
              return score;
          }
    • 인기 설계사 계산 함수 호출
      • 최소 1시간 마다 계산한 내용을 DB에 저장 후 호출 -> 짧은 시간에 발생하는 동일한 계산을 방지하여 서버 과부하 방지
        • public RecommendDto recommendPlanner(){
              List<PlannerDto> plannerDtos = new ArrayList<>();
              RecommendPlanner recommendPlanner = recommendPlannerRepository.findFirstByOrderByTimeDesc(); //가장 최근에 저장된 인기 설계사 리스트 호출
              Timestamp time = null;
              if(recommendPlanner != null){ //저장된 내용이 있는 경우 1시간 이내의 데이터를 호출
                  time = recommendPlanner.getTime();
                  Timestamp now = new Timestamp(System.currentTimeMillis());
                  Instant beforeIns = time.toInstant();
                  Instant nowIns = now.toInstant();
                  Duration duration = Duration.between(beforeIns, nowIns);
                  long hour = Math.abs(duration.toHours());
                  if(hour < 1){
                      String [] s = recommendPlanner.getList().split(",");
                      for(int i = 0; i < 5; i++){ //최대 5개까지 호출
                          if(s[i].equals("A")) break; //A는 설계사 정보가 없는 경우
                          Planner planner = plannerRepository.findById(Long.parseLong(s[i])).orElseThrow(IllegalArgumentException::new);
                          plannerDtos.add(modelMapper.map(planner, PlannerDto.class));
                      }
                  }
                  else recommendPlanner = null;
              }
              if(recommendPlanner == null) { //저장된 내용이 없는 경우나 저장된지 1시간이 초과한 경우 새로운 계산 후 DB에 저장
                  List<Planner> planners = plannerRepository.findAllPermitPlanner();
                  List<Recommend> recommends = new ArrayList<>();
                  for (Planner planner : planners) {
                      recommends.add(new Recommend(planner.getReview().size(), planner.getRecommendRating(), modelMapper.map(planner, PlannerDto.class)));
                  }
                  Collections.sort(recommends);
                  String s = "";
                  for (Recommend recommend : recommends) {
                      plannerDtos.add(recommend.getPlannerDto());
                      s+=recommend.getPlannerDto().getId();
                      s+=","; //설계사 ID간 ,를 활용하여 데이터 분리
                  }
                  s+="A,A,A,A,A"; //설계사가 0명인 경우를 대비하여 항상 빈 데이터를 5개 추가
                  time = new Timestamp(System.currentTimeMillis());
                  RecommendPlanner created = RecommendPlanner.builder()
                          .time(time)
                          .list(s)
                          .build();
                  recommendPlannerRepository.save(created);
              }
              RecommendDto recommendDto = RecommendDto.builder()
                      .list(plannerDtos)
                      .time(time)
                      .build();
              return recommendDto;
          }

2. JWT 및 지연로딩 관련 문제

  • 문제 파악
    • JWT 사용 시 LazyInitializationException 발생.
  • 문제 해결
    • ModelMapper 수정
      • Converter com.example.InsureConnect.Config.AppConfig$$Lambda$1365/0x000000b8017ef260@389a7e23 failed to convert org.hibernate.collection.spi.PersistentBag to java.util.List.
        • modelMapper.createTypeMap(User.class, UserDto.class)
                  .addMappings(mapping -> {
                      mapping.map(src -> src.getPlanner().getId(), UserDto::setPlannerId);
                      mapping.using(convertReviewToLongList()).map(User::getReview, UserDto::setReviewId);
                      mapping.using(convertChatRoomToLongList()).map(User::getChatRoom, UserDto::setChatRoomId);
                  });
                  
          private Converter<List<ChatRoom>, List<Long>> convertChatRoomToLongList() {
          return context -> context.getSource()
                      .stream()
                      .map(ChatRoom::getId)
                      .collect(Collectors.toList());
          
          }
          private Converter<List<Review>, List<Long>> convertReviewToLongList() {
          return context -> context.getSource()
                      .stream()
                      .map(Review::getId)
                      .collect(Collectors.toList());
          }
      • ModelMapper Config에서 convertChatRoomToLongList,convertReviewToLongList에서 오류 발생을 확인, User,UserDto에서 Review,ChatRoom을 사용하는 코드가 없기때문에 매핑에서 제외하니 정상적으로 작동.
        • modelMapper.createTypeMap(User.class, UserDto.class)
                  .addMappings(mapping -> {
                      mapping.map(src -> src.getPlanner().getId(), UserDto::setPlannerId);
                      mapping.skip(User::getReview, UserDto::setReviewId);
                      mapping.skip(User::getChatRoom, UserDto::setChatRoomId);
                  });
    • 즉시로딩으로 변경
      • 현재는 User,UserDto에서 Review,ChatRoom을 사용하지 않지만 추후 기능 추가시 필요할수도 있기때문에 ModelMapper 수정을 Rollback, 즉시로딩 사용
      • 정상적으로 작동됨을 확인했으나 즉시로딩 사용 시 필요하지 않은 엔티티도 함께 조회, 예상치 못한 SQL 발생등 성능상의 문제가 존재하기때문에 즉시로딩 사용방안은 폐기.
    • UserService 수정
      • 지연로딩이 아니라 즉시로딩 + 변경 전 modelmaaper사용시 정상적으로 작동하는것으로 보아 modelmapper 설정 변경은 정상적인 해결방법이 아님을 인지
      • JWT 도입이후부터 오류가 발생하였기 때문에 TokenService를 확인 -> userService.findById를 호출
        • public String createNewAccessToken(String refreshToken) {
          if(!tokenProvider.validToken(refreshToken)) {
              throw new IllegalArgumentException("Unexpected token");
          }
          
          UUID userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
          User user = modelMapper.map(userService.findById(userId), User.class);
          
          return tokenProvider.generateToken(user, Duration.ofHours(2));
          }
          
          public UserDto findById(UUID id){
          Optional<User> byId = userRepository.findById(id);
          return byId.map(user -> modelMapper.map(user, UserDto.class))
                  .orElseThrow(() -> new IllegalArgumentException("User not found with id: " + id));
          }
      • findById메서드에서 planner 혹은 review,chatroom 설정이 없으니 프록시가 전부 생성되어야하지만 chatRoom과 review에서 LazyInitializationException: failed to lazily initialize a collection of role: could not initialize proxy - no Session라는 예외가 발생하고 planner만 정상적으로 프록시 생성이 진행.
      • findById메서드가 트랜잭션을 열어주면, Planner,Review,ChatRoom 조회가 같은 트랜잭션 범위에서 이루어져야하지만 planner는 프록시 초기화에 성공, review,chatroom은 실패하였기때문에 별개의 트랜잭션에서 진행된다는 것을 확인 후 userService를 다시 한번 검토하니 @Transactional 어노테이션을 사용하지않아 범위가 지정되지않았다는 것을 인지. @Transactional 어노테이션을 추가하여 트랜잭션 범위를 설정하니 정상적으로 작동하였다.
        • @RequiredArgsConstructor
          @Service
          @Transactional(readOnly = true)
          public class UserService {
          
          private final UserRepository userRepository;
          private final ModelMapper modelMapper;
          
          public UserDto findById(UUID id){
              Optional<User> byId = userRepository.findById(id);
              return byId.map(user -> modelMapper.map(user, UserDto.class))
                      .orElseThrow(() -> new IllegalArgumentException("User not found with id: " + id));
          }
          .....

📖 배운점

  • 황인수
    • 늘 사용하기만 했던 웹 서비스와 서버를 직접 구현해보며 보이지 않는 부분에서 조회나 검증 등 처리해야할 내용이 많다는 것을 알게 됨
    • 인기 설계사 계산 함수를 구현하며 평소 단순하게 생각했던 정보에 대해 좀 더 깊고 합리적인 고민을 하는 계기가 됨
    • 다양한 기업에서 다양한 API를 제공한다는 것을 알게 되었고 이를 직접 사용해보며 더 많은 기능을 사용자에게 제공할 수 있음을 알게 됨
    • OAuth2.0 + JWT를 직접 구현해보며 사용자 정보를 가져오는 방법, 세션 방식과 토큰 방식의 차이점에 대해 배움
    • 학부 시절 Spring을 접한 경험이 없었으나 취업 준비를 하며 다양한 기업에서 Spring에 대한 지식을 요구한다는 것을 알게 되었고 이에 충족하고자 학습 및 실습하는 과정을 통해 늘 새로운 것을 접하고 활용하는 개발자의 태도에 대해 배움
  • 이한얼
    • 문제가 발생했을 때 그 원인을 정확하게 파악하는 것이 제일 중요하다. 이번 JWT 지연 로딩 문제의 경우에도 원인을 정확하게 알았다면 시간을 아끼고 정확한 조치를 취할 수 있었을 것이다.
    • 기반이 얼마나 중요한지 깨달음. 복잡한 문제에 대한 해결은 항상 기초 지식에서 출발한다. 프로그래밍, 데이터베이스, 백엔드 시스템 등의 기본 개념을 잘 이해하는 것은 문제 해결에 필수적이라는 것을 다시 한번 상기하게 됨.
    • 복잡한 문제의 시작은 종종 사소한 곳에서 비롯된다. 작은 설정 오류, 미처 고려하지 못한 부분이 큰 문제의 근본이 될 수 있다.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Java 55.1%
  • HTML 44.9%