(Spring) 취향 기반 향수 추천 서비스 - 8. 세번째 기능 (타인이 보는 나의 향수)

김준석·2023년 3월 27일
1

향수 추천 서비스

목록 보기
9/21

목표

이전 글까지 카카오 Oauth, Jwt 발행, 로그아웃에 관한 글을 썼다.
세번째 기능은 타인이 보는 나의 향수이다. 이 기능은 로그인을 완료한 사용자가 나한테 잘 어울릴만한 향수를 추천해줘! 라는 링크를 복사하여 상대방한테 공유하고, 상대방은 해당 링크에 접속해 첫번째 기능과 유사한 질문을 수행한다. 해당 결과는 로그인을 완료한 나의 DB에 저장되고, 결과를 분석해준다.
기능적으로는 별거 없지만, 기능의 사용을 위해 로그인 서비스가 필요하기 때문에 개발 기간이 꽤 오래 걸렸다.

요구사항

  1. 첫번째 기능(질문을 통한 향수 추천)에 사용한 api를 그대로 사용한다.
  2. 추천된 향수 리스트중 Random으로 1개의 향수를 선택한다.
  3. 추천인과 comment를 받아, 여러 사람이 추천했을 경우, 해당 리스트를 반환한다.
  4. 추천된 향수가 하나도 없을 경우 RecommendNotFoundException을 발생시킨다.
  5. 가장 많이 추천된 향기를 Count하여 Response한다.
  6. 가장 많이 추천된 향수를 Count하여 Response한다.

Domain

  • Recommendation.java
@Entity(name = "recommend")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Recommendation {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Perfume perfume;

    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;

    @Column(nullable = false, length = 40)
    private String recommender;

    @Column(length = 500)
    private String comment;

    private String scentAnswer;

    @Builder
    public Recommendation(Long id, Member member, Perfume perfume, String recommender, String comment, String scentAnswer) {
        this.id = id;
        this.member = member;
        this.perfume = perfume;
        this.recommender = recommender;
        this.comment = comment;
        this.scentAnswer = scentAnswer;
    }
}
  • Perfume과 Member를 fetchType.Lazy로 ManyToOne하였다.
    원래 처음에는 Perfume을 OneToMany 양방향 맵핑으로 하여 PerfumeList를 모두 응답하려 했으나 다음과 같은 이유로 ManyToOne으로 결정했다.
    1. 많은 사용자가 나의 향수를 추천해준다면 , 그리고 추천된 향수 List의 Size가 커지면 사용자의 시각 면에서, 서버의 성능 면에서 좋지 않을 것 같다.
    2. 결과 분석을 할텐데, 한명이 추천한 결과에 여러 향수가 추천되면 신뢰성에 Issue가 발생한다.
    3. UI를 보는 사용자가 불편함을 느낄 것 같다.

이런 고려사항을 토대로 하나의 향수만을 노출하기로 결정했고, 그래서 ManyToOne 단방향 맵핑을 사용하였다.

DTO

  • PerfumeAnalyzeResponse.java
@Getter
public class PerfumeAnalyzeResponse {
    private Long countNumber;
    private String perfumeName;

    @Builder
    public PerfumeAnalyzeResponse(Long countNumber, String perfumeName) {
        this.countNumber = countNumber;
        this.perfumeName = perfumeName;
    }
}

많이 추천된 향수를 Count와 함께 Response해주기 위한 DTO이다.

  • ScentAnalyzeResponse.java
@Getter
public class ScentAnalyzeResponse {

    private String scent;

    private Long count;

    @Builder
    public ScentAnalyzeResponse(String scent, Long count){
        this.scent = scent;
        this.count = count;
    }
}

가장 많이 추천된 향을 Count와 함께 Response해주기 위한 DTO이다.

  • RecommendRequestDto.java
@Getter
public class RecommendRequestDto {

    private String genderAnswer;
    private String moodAnswer;
    private String scentAnswer;
    private String seasonAnswer;
    private String styleAnswer;

    private String recommender;
    private String comment;

    public RecommendRequestDto() {
    }

    public RecommendRequestDto(String recommender, String comment, String genderAnswer, String moodAnswer, String scentAnswer, String seasonAnswer, String styleAnswer) {
        this.recommender = recommender;
        this.comment = comment;
        this.genderAnswer = genderAnswer;
        this.moodAnswer = moodAnswer;
        this.scentAnswer = scentAnswer;
        this.seasonAnswer = seasonAnswer;
        this.styleAnswer = styleAnswer;
    }
}

앞 설문 dto랑 유사한데, recommender와 comment가 추가되었다.

  • RecommendResponseDto.java
@Getter
public class RecommendResponseDto {

    private Long id;
    
    private List<Recommendation> recommendationList;

    public RecommendResponseDto() {
    }

    @Builder
    public RecommendResponseDto(Long id, List<Recommendation> recommendationList) {
        this.id = id;
        this.recommendationList = recommendationList;
    }
    
}

로그인을 한 사용자의 id와, 추천된 향수 List를 Response해준다.

Repository

RecommendRepository.java

@Repository
public interface RecommendRepository extends JpaRepository<Recommendation, Long> {

    @Query("SELECT r FROM recommend r JOIN FETCH r.perfume WHERE r.member.id = :memberId")
    List<Recommendation> findByMemberId(@Param("memberId") Long memberId);

    Optional<Recommendation> findById(Long memberId);
}

findByMemberId는 Recommend엔티티와 Perfume엔티티를 조인하고, memberId를 배개변수로 전달된 id와 일치하는 Recommendation 엔티티를 검색한다. 또한, JOIN FETCH 구문을 통해 연관된 perfume 엔티티를 로드한다.

Service

RecommendationService.java

@Service
public class RecommendationService {

    private final RecommendRepository recommendRepository;

    private final SurveyService surveyService;

    private final MemberService memberService;

    private final PerfumeService perfumeService;


    public RecommendationService(RecommendRepository recommendRepository, SurveyService surveyService,
                                 MemberService memberService,
                                 PerfumeService perfumeService) {
        this.recommendRepository = recommendRepository;
        this.surveyService = surveyService;
        this.memberService = memberService;
        this.perfumeService = perfumeService;
    }

    private SurveyRequestDto createSurveyResponseDto(RecommendRequestDto recommendRequestDto) {
        SurveyRequestDto surveyRequestDto = SurveyRequestDto.builder()
                .genderAnswer(recommendRequestDto.getGenderAnswer())
                .moodAnswer(recommendRequestDto.getMoodAnswer())
                .scentAnswer(recommendRequestDto.getScentAnswer())
                .seasonAnswer(recommendRequestDto.getSeasonAnswer())
                .styleAnswer(recommendRequestDto.getStyleAnswer())
                .build();
        return surveyRequestDto;
    }

    public Recommendation recommendByOtherGuest(Long id, RecommendRequestDto recommendRequestDto) {
        Recommendation recommendation = Recommendation.builder()
                .member(memberService.findMemberById(id))
                .perfume(findPerfumeBySurvey(recommendRequestDto))
                .recommender(recommendRequestDto.getRecommender())
                .comment(recommendRequestDto.getComment())
                .scentAnswer(recommendRequestDto.getScentAnswer())
                .build();
        recommendRepository.save(recommendation);
        return recommendation;
    }

    private Perfume findPerfumeBySurvey(RecommendRequestDto recommendRequestDto) {
        List<Perfume> surveyResultList = surveyService.showPerfumeListBySurvey(createSurveyResponseDto(recommendRequestDto));
        int randomNumber = createRandomPerfumeFromList(surveyResultList);
        return perfumeService.findPerfumeById(surveyResultList.get(randomNumber).getId());
    }

    private int createRandomPerfumeFromList(List<Perfume> surveyResultList) {
        Random random = new Random();
        return random.nextInt(surveyResultList.size());
    }

    @Transactional
    public RecommendResponseDto showRecommendedPerfume(Long id) {
        Long memberId = memberService.findMemberById(id).getId();

        RecommendResponseDto recommendResponseDto = RecommendResponseDto.builder()
                .id(memberId)
                .recommendationList(recommendRepository.findByMemberId(memberId))
                .build();
        return recommendResponseDto;
    }

    @Transactional
    public void deleteRecommendedData(){
        recommendRepository.deleteAll();
    }
}
  • recommendByOtherGuest()는 타인이 추천해준 향수 결과를 Recommendation 엔티티에 저장하는 메서드이다.
  • findPerfumeBySurvey()는 타인이 추천해중 향수 리스트들 중 Random으로 하나를 추출하여 해당 결과를 반환해주는 메서드이다.
  • createRandomPerfumeFromList()는 향수 리트의 사이즈중 랜덤 값을 추출해주는 메서드이다.
  • showRecoomendedPerfume()은 추천인들을 List에 담아 Response해주는 메서드이다.

PerfumeAnalyze.java

@Service
public class PerfumeAnalyze {
    private final RecommendRepository recommendRepository;

    public PerfumeAnalyze(RecommendRepository recommendRepository) {

        this.recommendRepository = recommendRepository;
    }

    private List<String> extractRecommendedPerfume(Long memberId) { //추천된 향수 리스트에서 향수 id를 추출해 List 생성
        List<Recommendation> recommendationList = recommendRepository.findByMemberId(memberId);
        List<String> perfumeList = new ArrayList<>();
        for (Recommendation recommendation : recommendationList) {
            perfumeList.add(recommendation.getPerfume().getPerfumeName());
        }
        return perfumeList;
    }

    public PerfumeAnalyzeResponse filterMostRecommendedPerfumeName(Long memberId) {
        List<String> perfumeNameList = extractRecommendedPerfume(memberId);
        Long maxCount = 0L;
        String perfumeName = "";

        for (int i = 0; i < perfumeNameList.size(); i++) {
            Long count = countPerfume(perfumeNameList, i);
            if (count > maxCount) {
                perfumeName = perfumeNameList.get(i);
                maxCount = count;
            }
        }
        AnalyzeUtil.isCountingNumberExist(maxCount);
        return PerfumeAnalyzeResponse.builder()
                .perfumeName(perfumeName)
                .countNumber(maxCount).build();
    }

    private Long countPerfume(List<String> perfumeNameList, int i) {
        return perfumeNameList.stream().filter(x -> perfumeNameList.get(i).matches(x)).count();
    }
}
  • extractRecommendedPerfume()은 추천된 향수 리스트들 중 향수 이름을 추출하여 반환하는 메서드이다.
    filterMostRecommendedPerfumeName()은 가장 많이 추천된 향수 이름을 Response해주는 메서드이다. depth가 2라 추후 리팩토링 할 예정이다.
  • countPerfume()은 같은 이름의 향수들을 찾아 count해주는 메서드이다.

ScentAnalyze.java

@Service
public class ScentAnalyze {
    private final RecommendRepository recommendRepository;

    public ScentAnalyze(RecommendRepository recommendRepository) {
        this.recommendRepository = recommendRepository;
    }

    private List<String> extractScentAnswer(Long memberId) {
        List<Recommendation> recommendationList = recommendRepository.findByMemberId(memberId);
        List<String> perfumeList = new ArrayList<>();
        for (Recommendation recommendation : recommendationList) {
            perfumeList.add(recommendation.getScentAnswer());
        }
        return perfumeList;
    }

    public ScentAnalyzeResponse filterMostRecommendedScent(Long memberId) {
        List<String> scentList = extractScentAnswer(memberId);
        Long maxCount = 0L;
        String scentAnswer = "";

        for (int i = 0; i < scentList.size(); i++) {
            Long count = countScent(scentList, i);
            if (count > maxCount) {
                scentAnswer = scentList.get(i);
                maxCount = count;
            }
        }
        AnalyzeUtil.isCountingNumberExist(maxCount);
        return ScentAnalyzeResponse.builder()
                .scent(scentAnswer)
                .count(maxCount)
                .build();
    }

    private Long countScent(List<String> scentList, int i) {
        return scentList.stream().filter(x -> scentList.get(i).matches(x)).count();
    }
}

위에 코드와 로직은 동일하다. 따라서 추상화가 필요해보인다. 추후 수정할 예정이다. ㅜ_ㅜ

AnalyzeUtil.java

@Service
public class AnalyzeUtil {

    public static void isCountingNumberExist(Long maxCount) {
        if (maxCount < 1) {
            throw new RecommendNotFoundException();
        }
    }
}

추천된 것이 있는지 확인해주는 메서드이다. Recommend 로직들은 이미 의존관계가 많고, static이 성능에 영향을 미칠 규모가 아니라고 판단하여 static으로 설계하였다.

AnalyzeService.java

@Service
public class AnalyzeService {

    private final PerfumeAnalyze perfumeAnalyze;
    private final ScentAnalyze scentAnalyze;

    public AnalyzeService(PerfumeAnalyze perfumeAnalyze, ScentAnalyze scentAnalyze) {
        this.perfumeAnalyze = perfumeAnalyze;
        this.scentAnalyze = scentAnalyze;
    }

    public Map<String, Long> showAnalyzedData(Long memberId) {
        Map<String, Long> analyzedData = new HashMap<>();
        PerfumeAnalyzeResponse perfumeAnalyzeResponse = perfumeAnalyze.filterMostRecommendedPerfumeName(memberId);
        ScentAnalyzeResponse scentAnalyzeResponse = scentAnalyze.filterMostRecommendedScent(memberId);

        analyzedData.put(perfumeAnalyzeResponse.getPerfumeName(), perfumeAnalyzeResponse.getCountNumber());
        analyzedData.put(scentAnalyzeResponse.getScent(), scentAnalyzeResponse.getCount());

        return analyzedData;
    }
}

showAnalyzedData()는 분석된 (향수,향) 데이터를 Map에 담아 Response해주는 메서드이다.

테스트 결과!

세번째 기능을 완료하고, 프론트&백엔드 각각 서버를 배포하여 통신했고, 아직 UI를 다듬어야겠지만, 테스트 결과는 성공적이었다!

이제 끝인가요?

다음 글부터는 배포 과정을 기록하려고 한다.

깃허브

profile
기록하면서 성장하기!

0개의 댓글