■ 서두
올해 5월 즈음 디지털교도소 라는 사적 범죄자 신상공개 사이트를 처음 접했다.
최근 신림 칼부림 사건, 여고사 피살 사건 등등 이유를 불문한 범죄들이 속출 하였으나
정부측은 그 가해자들의 신상을 보호해주자 많은 사람들의 공분을 산 것 을 계기로 위 사이트가 개설 된 것 같았다.
그런 와중 나는 문득 궁금하였다. " 우리내 주변 또는 국내에서는 범죄가 얼마나 발생할까? " 이를 계기로 인터넷을 찾아보았지만 범죄통계와 관련된 PDF 자료나 DB 자료들은 제공해주는 곳이 있되 이를 편하게 볼 수 있는 데이터 시각화 페이지는 이미 한 것으로 추측되었다.
이에 나는 공공으로 제공되는 범죄관련 데이터를 사용해 국내 범조통계 대시보드를 만들기로 결심하였고 오늘 내 계획에 따라 1차 개발이 완료 되어 회고를 진행하려한다.
■ 사용기술
Front | React, Typescript |
Server | FastApi, Python |
Deploy | Docker, CloudType |
데이터 전처리 | Jupyter NoteBook, python |
■ React 채택 의의
1. 리액트는 컴포넌트 기반의 아키텍처로서 코드의 재사용성과 관리 측면에서 우수하여 차트 컴포넌트에 대해 재사용에 용이할 것이라 판단
2. 변경된 부분만 업데이트 및 재랜더링 할 수 있는 가상DOM 특성이 데이터 변환에 따른 값 변화를 유연하게 대처할 것이라 판단.
■ FastAPI 채택 의의
1. swagger 가 별도의 비용소모없이 지원되어 차 후 문서 배포 및 관리에 용이할 것 이라 판단.
2. django 로도 할 수 있었지만 조금더 서버와 클라이언트를 명확하게 구분하여 주고 싶었음
3. 낮은 러닝커브로 인하여 프로젝트에 바로 대입하기에 좋았다.
4. django, flast 에 비해 DB입출력, 전송 등등에서 2배 이상의 퍼포먼스를 보여줌
■ 데이터 전처리
데이터 전처리에는 jupyter NoteBook 과 라이브러리 pdfPlumber 를 사용하였다.
이유는 코드를 단위로 실행할 수 있어서 각 코드에 따른 반환값을 확인하기 쉬웠고 이는 주어진 데이터의 패턴을 파악하기에 용이했다.
데이터는 아래 사이트의 PDF 파일을 가져와 전처리하였다.
https://www.police.go.kr/user/bbs/BD_selectBbsList.do?q_bbsCode=1115&estnColumn2=%EB%B6%84%EA%B8%B0
데이터를 전처리 할 시에 해당 데이터는 특정 패턴을 가지고 있었는데
PdfPlumber로 데이터를 불러 올 시 분류가 대분류라면 0번째 인덱스에 NaN 값이 들어있었고 소분류라면 1번째 인덱스에 NaN 값이 들어 있어서 이를 처리하기 위한 분기를 태워줘야했다.
그리고 데이터에 대한 컬럼이 되어줄 값 들이 0,1 번째 인덱스에 존재하였기 때문에 선 추출 후 Row가 되어줄 값을 추출하기 전에 Pop을 사용하여 제거해줘야했다.
if(table[i][0] != None and table[i][1] != None):
sub_total_name = table[i][0] + " "+ f"({table[i][1]})"
sub_total_죄종 = sub_total_name
# 대분류 헤더
if(table[i][0] != None):
# 0번째 인덱스의 값이 헤더가 되는 경우
main_row_name = table[i][0]
main_죄종 = main_row_name
#소분류 헤더
elif(table[i][0] == None):
# 0번째 인덱스가 None 이며 1번째 인덱스의 값이 헤더가 되는 경우
sub_row_name = table[i][1]
sub_죄종 = sub_row_name
#소계 Rows
if(table[i][0] != None and table[i][1] != None):
table[i].pop(0)
table[i].pop(0)
sub_total_발생건수 = f"{str(table[i][0]).replace(",","")}"
sub_total_검거건수 = f"{str(table[i][1]).replace(",","")}"
sub_total_발생대비검거건수 = table[i][2]
sub_total_검거인원 = f"{str(table[i][3]).replace(",","")}"
sub_total_법인체 = f"{str(table[i][4]).replace(",","")}"
average_list.append([sub_total_죄종,sub_total_발생건수,sub_total_검거건수,sub_total_발생대비검거건수,sub_total_검거인원,sub_total_법인체])
# #대분류
if(table[i][0] != None and table[i][1] == None):
table[i].pop(0)
table[i].pop(0)
main_발생건수 = str(table[i][0]).replace(",","")
main_검거건수 = str(table[i][1]).replace(",","")
main_발생대비검거건수 = table[i][2]
main_검거인원 = str(table[i][3]).replace(",","")
main_법인체 = str(table[i][4]).replace(",","")
main_crime_list.append([main_죄종,main_발생건수,main_검거건수,main_발생대비검거건수,main_검거인원,main_법인체])
#소분류
elif(table[i][0] == None):
table[i].pop(0)
table[i].pop(0)
sub_발생건수 = str(table[i][0]).replace(",","")
sub_검거건수 = str(table[i][1]).replace(",","")
sub_발생대비검거건수 = table[i][2]
sub_검거인원 = str(table[i][3]).replace(",","")
sub_법인체 = str(table[i][4]).replace(",","")
sub_crime_list.append([sub_죄종,sub_발생건수,sub_검거건수,sub_발생대비검거건수,sub_검거인원,sub_법인체])
■ 데이터 전처리 시 아숴웠던 점
필자가 생각하는 생산성이 높은 개발은 재사용성이 높고 코드 가독성이 높은 코드인데 위에 작성한 코드는 각 년도 분기에 따라서 전처리 될 대상의 개수를 range 함수를 통하여 직접 넣어 반복을 태웠고 이는 내가 작성한 코드를 처음 접하거나 나에게 설명을 듣지 않은 사람들은 이해 하기에 어려울 것 이라 생각이 들었다.
이 점은 차 후 공공데이터 포털이 제공해주는 데이터를 처리하며 개선시켜야 될 부분 같다.
전처리 된 파일 예시
■ 서버
우선 FastApi 프로젝트를 시작하기 전에 폴더 구조에 대하여 좋은 자료가 없을까 하여 많이도 찾아봤지만 내 마음에 정말 와닿는 폴더 구조가 없어서 각 기능에 맞는 앱별로 프로젝트를 관리하는 Django 의 프로젝트 구조를 따라갔다.
이는 향 후 공공데이터 포털에서 제공하는 범죄 데이터에 대하여 기능별로 로직을 추가하기에 용이하겠다 판단하였다.
■ 서버에 대한 고찰
이번 서버를 만들면서 고민 두가지를 하였다.
1. 생산성이 높은 서버를 만들 수 있을까
2. 전처리한 데이터를 메타데이터를 불러오는 것으로 처리할 지 아니면 특정 DB에 값을 담을 지
1번에 대해서는 pandas 와 numpy 를 적극적으로 활용하여 받은 인자값에 따라서 유동적으로 데이터를 불러 올 수 있게하였다.
이는 내가 데이터를 직접 전처리하여 데이터의 구조를 파악하고 있기에 가능했던 것 같다. 이 부분은 참 잘한 것 같다 ^^!
2번에 대해서는 메타데이터로 불러오기로 하였다. 이유는 csv 자체가 가벼운 데이터여서 상시로 불러와도 서버에 그렇게 큰 부하를 주지 않을 것이라 판단하였다.
■ 내부구조
이번 프로젝트에서 각 프로그램들의 역할을 명확히 해주려 노력하였다.
MVC 패턴이 Model , View , Controller 의 각 역할을 명확히 부여하여 사용자의 요청을 처리하듯이 나의 프로젝트에서도
MVC 패턴과 유사하게 구성하여 응집도를 낮추려 노력하였다.
그러하여 route 역할 및 controller 역할을 수행하는 router.py를 만들었고
router.py 에서 온 요청을 처리하고 응답을 보내주는 util.py를 만들어 각 역할을 구분해주었다.
router.py
from fastapi import APIRouter
# 사용자 정의 모듈
# import sys
# sys.path('/crime_dash_board_server/branch_crime_app/branch_crime_util.py')
from branch_crime_app import branch_crime_util
router = APIRouter(
prefix="/crime_branch",
tags=["crime_branch"]
)
@router.get("/")
def default_crime_branch():
default_crime_data = branch_crime_util.get_default_crime_branch_data()
return default_crime_data
@router.get("/seleted")
def selected_crime_branch(year:str, branch:int):
default_crime_data = branch_crime_util.get_selected_crime_branch_data(year,branch)
return default_crime_data
@router.get('/categorize')
def categorize_crime_branch(year:str, branch:int, category:str):
categorize_crime_data = branch_crime_util.get_categorize_crime_branch(year=year, branch=branch, category=category)
return categorize_crime_data
util.py
import pandas as pd;
# 페이지 로딩 시 사용될 디폴트 메서드
def get_default_crime_branch_data():
category = ['average','main', 'sub']
dict_data = {}
try:
for i in range(0,3):
csv_data = pd.read_csv(f'./templates/crime_data/branch/2024/{category[i]}/crime_report_branch_1_2024_{category[i]}_crime_data.csv')
df = pd.DataFrame(csv_data)
df = sort_crime_data(df)
df = df.astype(float)
dict_data[category[i]] = df
return dict_data
except Exception:
print(FileNotFoundError)
return "FILE NOT FOUND"
# 페이지 로딩 시 사용될 디폴트 메서드 아규먼트 추가
def get_selected_crime_branch_data(year:str, branch:int ):
category = ['average','main', 'sub']
dict_data = {}
try:
for i in range(0,3):
csv_data = pd.read_csv(f'./templates/crime_data/branch/{year}/{category[i]}/crime_report_branch_{branch}_{year}_{category[i]}_crime_data.csv')
df = pd.DataFrame(csv_data)
df = sort_crime_data(df)
df = df.astype(float)
dict_data[category[i]] = df
return dict_data
except Exception:
print(FileNotFoundError)
return "FILE NOT FOUND"
# 인자 값에 따른 데이터 불러오기 메서드
def get_categorize_crime_branch(year:str, branch:int, category:str):
dict_data = {}
try:
csv_data = pd.read_csv(f'./templates/crime_data/branch/{year}/{category}/crime_report_branch_{branch}_{year}_{category}_crime_data.csv')
df = pd.DataFrame(csv_data)
df = sort_crime_data(df)
df = df.astype(float)
dict_data[f"{category} ({year} {branch}분기)"] = df
return dict_data
except Exception:
print(FileNotFoundError)
return "FILE NOT FOUND"
위 작업을 통하여 나름 코드의 가독성을 높이고 생산적인 로직을 구성한 것 같았다.
■ 아쉬운 점
- 조금은 급하게 FastApi를 도입하려하여 python, fastapi의 많은 기능들을 사용해보진 못한 것 같다.
- 반복되는 코드들이 꽤 있는데 이를 간추려서 공통 요소로 뺐어도 좋았을 것 같다.
■ 프론트
리액트 및 파이썬 으로 프론트앱을 구성할 때에 의의는 타입의 명확성을 부여해주고 위에서 서술한 바와 같이 조금 더 유동적으로 움직일 수 있는 앱을 만들고 그저 명확히 View 의 역할만을 수행하는 웹앱을 만들고 싶었다.
또 웹뷰와 앱뷰를 각자 구성하였는데 이는 정보의 시각화 자료를 제공하는 페이지가 단순이 미디어쿼리에 따른 정렬값만 바꿔준다면 정보전달에 있어서 그 목적성과 거리가 멀어진다 생각하여 웹뷰와 앱뷰를 아에 따로 구성하였다.
■ 아쉬운 점
- 코드 재사용성의 미비
나는 처음 프로젝트를 구성 할 때에 서버부터 만들고 프론트 앱을 만들었다. 이로서 나는 한 차트 컴포넌트로 다양한 데이터를 유동적으로 표현 할 수 있을 줄 알았는데 데이터를 불러왔을 때 기본값으로 들어가 있던 데이터와 현재 사용하고 싶은 데이터의 구조가 달라 매번 똑같은 동작을 수행하지만 데이터만 다른 컴포넌트를 새로 생성해아했다.
가령 총계의 상세데이터와 대분류의 상세데이터는 같은 값을 표현해주지면 컬럼의 개수가 달라서 새로운 컴포넌트를 생성해줘야하는 이슈가 꽤 많이 있었다
■ 마치며
그래도 어떠한 레퍼런스 참고 및 템플릿을 가져다 쓰지도 않고 프로젝트 세팅부터 오늘날까지 참 여러 고민들을 하고 프로젝트를 차곡차곡 만들어온 것 같다.
내가 결코 풀스택 개발자라고 치부할 정도의 실력은 아니지만 프론트와 서버의 업무를 동시에 진행하니 서로 어떻게 하면 더 원활한 통신이 가능할 지에 대한 고민 또는 일련의 고찰들을 참 많이 할 수 있어서 좋았다.
그리고 내가 기획 또는 문서화에는 아주 쥐약이라는 것 도 크게 깨달았다 ...
곧 공공데이터에서 제공해주는 데이터도 어떻게 처리할 것인가 분석하고 앱뷰를 만들었으니 리액트 네이티브를 사용해서 구글스토어와 애플스토어에도 배포해 볼 예정이다.
근자필승이라 했던가, 근면성실하게 임하는 자는 반드시 이기는 법이니 오늘날에 그치지않고 더욱더 고민하고 나아가야겠다.
향 후 에는 좋은 개발자, 좋은 팀이 있는 회사로 이직하여 더 다양한 업무를 배우고 더 성장하고싶다.
끝!