tellusboutyourself
[Final Project] 기능 3 <내 생각 확인하기> 백엔드 구현 본문
데이터엔지니어링 30기 Final Project에서 '백엔드' 파트를 담당하게 되었습니다.
저희가 개발 중인 서비스는 크게 4개의 기능을 갖고 있는데, 이 중 저는 기능3/기능4의 백엔드를 구현하였습니다.
이 게시물에서 설명하고자 하는 기능은 <기능3: 내 생각 확인하기> 입니다.
사용툴 : MariaDB, Intellij Community, SpringBoot
기능 설명
위와 같은 페이지를 구현하고자 합니다.
(해당 페이지는 실제 프론트엔드가 적용되어 구현된 화면이 아닌 figma를 사용해 임시적으로 그려놓은 기획화면입니다.)
예비창업자들을 위한 '내 생각 확인하기 '서비스입니다.
예비창업자들이 특정 업종의 매출과 요인의 상관관계를 확인할 수 있도록 도와줍니다.
해당 서비스 이용 절차는 아래와 같습니다.
1. 사용자는 창업하고자 하는 '업종'을 선택합니다.
업종 옵션 : [ "한식음식점", "분식전문점", "호프-간이주점", "일반교습학원", "제과점", "커피-음료", "슈퍼마켓", "편의점", "청과상", "의약품", "화장품", "예술학원", "미용실", "세탁소", "일반의류", "전자상거래업", "일식음식점", "양식음식점", "패스트푸드점", "미곡판매", "반찬가게", "문구", "화초", "육류판매", "서적", "스포츠 강습", "가방", "시계및귀금속", "여관", "중식음식점", "인테리어", "치킨전문점", "당구장", "자동차수리", "애완동물", "철물점", "노래방", "핸드폰", "컴퓨터및주변장치판매", "치과의원", "가구", "운동/경기용품", "일반의원", "피부관리실", "가전제품수리", "가전제품", "스포츠클럽", "외국어학원", "조명용품", "골프연습장", "의료기기", "PC방", "네일숍", "신발", "안경", "한의원", "섬유제품", "자전거 및 기타운송장비", "자동차미용", "수산물판매", "부동산중개업", "완구", "고시원" ]
2. 해당 업종의 매출에 큰 영향을 미칠 것으로 예상되는 요인을 선택합니다.
요인 옵션 : [ "월_평균_소득_금액", "지출_총금액", "식료품_지출_총금액", "의류_신발_지출_총금액", "생활용품_지출_총금액", "의료비_지출_총금액", "교통_지출_총금액", "여가_지출_총금액", "문화_지출_총금액", "교육_지출_총금액", "유흥_지출_총금액", "총_유동인구_수", "남성_유동인구_수", "여성_유동인구_수", "연령대_10_유동인구_수", "연령대_20_유동인구_수", "연령대_30_유동인구_수", "연령대_40_유동인구_수", "연령대_50_유동인구_수", "연령대_60_이상_유동인구_수", "시간대_00_06_유동인구_수", "시간대_06_11_유동인구_수", "시간대_11_14_유동인구_수", "시간대_14_17_유동인구_수", "시간대_17_21_유동인구_수", "시간대_21_24_유동인구_수", "월요일_유동인구_수", "화요일_유동인구_수", "수요일_유동인구_수", "목요일_유동인구_수", "금요일_유동인구_수", "토요일_유동인구_수", "일요일_유동인구_수", "총_상주인구_수", "남성_상주인구_수", "여성_상주인구_수", "연령대_10_상주인구_수", "연령대_20_상주인구_수", "연령대_30_상주인구_수", "연령대_40_상주인구_수", "연령대_50_상주인구_수", "연령대_60_이상_상주인구_수", "남성연령대_10_상주인구_수", "남성연령대_20_상주인구_수", "남성연령대_30_상주인구_수", "남성연령대_40_상주인구_수", "남성연령대_50_상주인구_수", "남성연령대_60_이상_상주인구_수", "여성연령대_10_상주인구_수", "여성연령대_20_상주인구_수", "여성연령대_30_상주인구_수", "여성연령대_40_상주인구_수", "여성연령대_50_상주인구_수", "여성연령대_60_이상_상주인구_수", "총_가구_수", "비_아파트_가구_수", "아파트_단지_수", "아파트_면적_66_제곱미터_미만_세대_수", "아파트_면적_66_제곱미터_세대_수", "아파트_면적_99_제곱미터_세대_수", "아파트_면적_132_제곱미터_세대_수", "아파트_면적_165_제곱미터_세대_수", "아파트_가격_1_억_미만_세대_수", "아파트_가격_1_억_세대_수", "아파트_가격_2_억_세대_수", "아파트_가격_3_억_세대_수", "아파트_가격_4_억_세대_수", "아파트_가격_5_억_세대_수", "아파트_가격_6_억_이상_세대_수", "아파트_평균_면적", "아파트_평균_시가", "총_직장_인구_수", "남성_직장_인구_수", "여성_직장_인구_수", "연령대_10_직장_인구_수", "연령대_20_직장_인구_수", "연령대_30_직장_인구_수", "연령대_40_직장_인구_수", "연령대_50_직장_인구_수", "연령대_60_이상_직장_인구_수", "남성연령대_10_직장_인구_수", "남성연령대_20_직장_인구_수", "남성연령대_30_직장_인구_수", "남성연령대_40_직장_인구_수", "남성연령대_50_직장_인구_수", "남성연령대_60_이상_직장_인구_수", "여성연령대_10_직장_인구_수" ]
3. '높으면/낮으면'을 선택합니다. 해당 업종의 매출액과 요인이 양의 상관관계일지, 음의 상관관계일지 선택할 수 있는 옵션입니다.
4. '확인하기' 버튼을 누르면, check-your-thoughts-002 페이지와 같이 상관계수를 통해 해당 가정이 통계적으로 유의미한지 확인해볼 수 있습니다.
ex. 한식음식점 매출에 남성연령대_30_직장_인구_수가 미치는 영향을 파악하고 싶은 사용자.
가정 설정 : {한식음식점}은 {남성연령대_30_직장_인구_수}가 {높으면} 매출이 높을 것이다.
데이터베이스 설정
1. industry_correlation_ranking
갖고 있는 전체 데이터의 양이 매우 방대하기에, 이를 사용하기 편리하도록 위와 같은 간단한 테이블( industry_correlation_ranking )로 축소했습니다.
업종별로 각 요인들과의 상관관계를 도출해 상관계수 값을 갖는 새로운 컬럼을 추가하고, 각 업종 내에서의 요인들 별 상관관계 높은 순으로 순위를 매겼습니다.
이 테이블을 기반으로 상황에 맞게 필요한 정보들을 가져올 것입니다.
2. checking_thoughts
사용자가 선택한 옵션을 저장하는 테이블입니다.
선택한 세 가지 옵션의 값과 userid가 저장됩니다.
(userid 설정 기능은 아직 구현하지 않았기에, 임의적으로 1로 데이터가 저장되도록 설정했습니다. 모든 기능이 구현되면 수정할 예정.)
API 명세서
프론트엔드 담당 팀원과의 원활한 소통을 위해 작성한 API 명세서입니다. 기능3을 구현하기 위해 작성한 내용은 아래와 같습니다.
먼저, 옵션을 제공하기 위해 '업종'과 '요인'리스트를 반환하는 api를 각각 하나씩 만들 것입니다. (GET)
사용자가 선택한 옵션을 저장하고, 이에 해당하는 정보들(서비스 코드, 상관계수, 순위 등)을 가져올 것입니다. 즉, industry_correlation_ranking 테이블에서, service_industry_name, factor 컬럼의 값과 사용자 선택 옵션의 값이 일치하는 행을 도출한다고 보면 됩니다.
백엔드 코드
- Controller
CheckYourThoughtsController
package com.springboot.fp_ml_web.controller;
import com.springboot.fp_ml_web.data.entity.CheckingThought;
import com.springboot.fp_ml_web.data.entity.IndustryCorrelationRanking;
import com.springboot.fp_ml_web.dto.CheckYourThoughtsRequest;
import com.springboot.fp_ml_web.service.CheckingThoughtService;
import com.springboot.fp_ml_web.service.IndustryCorrelationRankingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class CheckYourThoughtsController {
@Autowired
private IndustryCorrelationRankingService industryCorrelationRankingService;
@Autowired
private CheckingThoughtService checkingThoughtService;
@GetMapping("/industries")
public List<String> getIndustries() {
return industryCorrelationRankingService.getDistinctServiceIndustryNames();
}
@GetMapping("/factors")
public List<String> getFactors() {
return industryCorrelationRankingService.getDistinctFactors();
}
@PostMapping("/check-your-thoughts")
public ResponseEntity<List<IndustryCorrelationRanking>> checkYourThoughts(@RequestBody CheckYourThoughtsRequest request) {
String industry = request.getIndustry();
String factor = request.getFactor();
String condition = request.getCondition();
CheckingThought checkingThought = new CheckingThought();
checkingThought.setUserId(1); // 사용자의 아이디를 설정해야 합니다. 예시로 1로 설정했습니다.
checkingThought.setService_industry_name(industry); // 인자를 제공해야 합니다.
checkingThought.setReasons_columns(factor);
checkingThought.setUp_and_down(condition);
checkingThoughtService.saveCheckingThought(checkingThought);
List<IndustryCorrelationRanking> results;
if ("높으면".equals(condition)) {
results = industryCorrelationRankingService.getByServiceIndustryNameAndFactor(industry, factor);
} else {
results = industryCorrelationRankingService.getByServiceIndustryNameAndFactor(industry, factor);
}
return ResponseEntity.ok(results);
}
}
- Dto
CheckYourThoughtsRequest
package com.springboot.fp_ml_web.dto;
public class CheckYourThoughtsRequest {
private String industry;
private String factor;
private String condition;
// Getters and setters
public String getIndustry() {
return industry;
}
public void setIndustry(String industry) {
this.industry = industry;
}
public String getFactor() {
return factor;
}
public void setFactor(String factor) {
this.factor = factor;
}
public String getCondition() {
return condition;
}
public void setCondition(String condition) {
this.condition = condition;
}
}
- Entity
CheckingThought
package com.springboot.fp_ml_web.data.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class CheckingThought {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int checkingThought_id;
private int userId;
private String service_industry_name;
private String reasons_columns ;
private String up_and_down;
}
IndustryCorrelationRanking
package com.springboot.fp_ml_web.data.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Column;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class IndustryCorrelationRanking {
@Id
@Column(name = "service_industry_code")
private String serviceIndustryCode;
@Column(name = "service_industry_name")
private String serviceIndustryName;
@Column(name = "factor")
private String factor;
@Column(name = "correlation_coefficient")
private String correlationCoefficient;
@Column(name = "rank")
private int rank;
}
- Repository
CheckingThoughtRepository
package com.springboot.fp_ml_web.data.repository;
import com.springboot.fp_ml_web.data.entity.CheckingThought;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
public interface CheckingThoughtRepository extends JpaRepository<CheckingThought, Integer> {
}
IndustryCorrelationRankingRepository
package com.springboot.fp_ml_web.data.repository;
import com.springboot.fp_ml_web.data.entity.IndustryCorrelationRanking;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface IndustryCorrelationRankingRepository extends JpaRepository<IndustryCorrelationRanking, String> {
@Query("SELECT DISTINCT i.serviceIndustryName FROM IndustryCorrelationRanking i")
List<String> findDistinctServiceIndustryNames();
@Query("SELECT DISTINCT i.factor FROM IndustryCorrelationRanking i")
List<String> findDistinctFactors();
List<IndustryCorrelationRanking> findByServiceIndustryNameAndFactor(String serviceIndustryName, String factor);
}
- Service
CheckingThoughtService
package com.springboot.fp_ml_web.service;
import com.springboot.fp_ml_web.data.entity.CheckingThought;
import com.springboot.fp_ml_web.data.repository.CheckingThoughtRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CheckingThoughtService {
@Autowired
private CheckingThoughtRepository repository;
public CheckingThought saveCheckingThought(CheckingThought checkingThought) {
return repository.save(checkingThought);
}
}
API 테스트
백엔드는 구현하고자 하는 기능이 잘 작동하는지 화면을 통해 가시적으로 확인하는 것이 불가능합니다.
(프론트 작업이 아직 진행되지 않았기 때문입니다.)
따라서 API 테스트를 통해 올바른 정보들이 response에 잘 담겨져 나오는지 확인해야 합니다.
postman이 가장 대중적인 api테스트 툴인듯 하나 저는 swaggerui를 통해 테스트를 진행했습니다.
/api/industries
-> 업종 목록이 정상적으로 출력됩니다.
/api/factors
-> 요인 목록이 정상적으로 출력됩니다.
/api/check-your-thoughts
사용자가 입력한 세 개의 옵션을 request란에 입력합니다.
입력 내용이 db(checking_thought)에 정상적으로 들어와 저장됩니다.
상관관계 확인을 위해 ‘industry_correlation_ranking’으로부터 상관관계를 포함한 데이터들을 가져옵니다.
request가 api명세서와 동일한 형식으로 출력됩니다.
이제, 해당 api 를 기반으로 프론트 작업을 진행해 웹페이지를 구현할 수 있습니다
