- 설치 및 실행 방법 (How to install and run)
- REST API 명세서 (REST API Specification)
- 프로젝트 주요 패키지 설명 (Package description)
- 사용한 주요 오픈소스 목록 (List of used open source libraries)
- 데이터베이스 다이어그램 (Database diagram)
- 개발환경
- Java 1.8 +
- Play Framework 2.7.3 (Java)
- H2 Database (In-Memory)
- sbt
- 문제 해결 전략
- 금융 기관 목록을 저장하는 Bank 테이블, 기관의 월별 지원 금액을 저장하는 Finance 테이블을 정의하고, 두 테이블이 One-To-Many 관계를 가지도록 함. (하나의 Bank는 다수의 Finance를 가진다.)
- 주어진 문제를 해결함에 있어, SQL Native Query를 작성하지 않고, JPA와 ORM, Java Collections, Stream API 등을 이용해 Java 언어 레벨에서 계산하도록 구현.
- 인증을 위해 JWT을 구현하는 부분은 오픈소스 jjwt와 Play Framework의 기능인 Action composition 기능을 적용해 해결.
- 선택 문제(지원금액 예측)에 대해서는 '과거의 데이터가 이후에도 영향을 미칠 것이다'라는 가설을 세우고, 이를 간단하게 구현할 수 있는 시계열(time series)을 적용하기로 결정. 시계열을 Java로 구현한 오픈소스 라이브러리 com.github.signaflo % timeseries % 0.4를 사용하여 해결함.
- 프로그램 시연 영상
- https://www.youtube.com/watch?v=ARBOIFSd17E
- REST client 프로그램 중 하나인 Insomnia를 사용해 API를 테스트 하는 시연 영상
# sbt가 없다면 먼저 설치해주세요.
$ brew install sbt@1
# 프로젝트를 내려받습니다.
$ git clone https://github.com/DaegiKim/play-housing-finance-app.git
# 프로젝트 디렉토리로 이동합니다.
$ cd play-housing-finance-app
# 프로젝트를 실행합니다.
$ sbt run
서비스가 정상적으로 구동 되면 http://localhost:9000 으로 접속이 가능합니다.
API 인증을 위해 JWT(Json Web Token)를 이용해서 Token 기반 API 인증 기능을 개발하고 각 API 호출 시에 HTTP Header 에 발급받은 토큰을 가지고 호출하세요.
signup 계정생성 API: 입력으로 ID, PW 받아 내부 DB 에 계정 저장하고 토큰 생성하여 출력
POST /api/auth/signup HTTP/1.1
Host: localhost:9000
Content-Type: application/json
{
"username":"rmrhtdms",
"password":"1234"
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJtcmh0ZG1zMSIsInBhc3N3b3JkIjoiJDJhJDEwJHlGYnFONmFmTVZnYWxwLmVVaWhLTXVNb2ZlRWljRFRSS21HYlZ2TGdURk5XTWoyc0FKVGxPIiwiaWF0IjoxNTY2MTMxMjM2LCJleHAiOjE1NjYxMzE4MzZ9.xiizdvWWKQDqEmkMiOTZVSFuOGlIrTz12d0HVdtkrec",
"issued_at": "Sun Aug 18 21:27:16 KST 2019",
"expires_in": "Sun Aug 18 21:37:16 KST 2019"
}
signin 로그인 API: 입력으로 생성된 계정 (ID, PW)으로 로그인 요청하면 토큰을 발급한다.
POST /api/auth/signin HTTP/1.1
Host: localhost:9000
Content-Type: application/json
{
"username":"rmrhtdms",
"password":"1234"
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJtcmh0ZG1zIiwicGFzc3dvcmQiOiIkMmEkMTAkNTRMQUNDaThyRDlDc2hjelpLWU1TdTFHSXpqL1BkTVUyZ0kvbmhzcVhDWHhTNXB3RUNRNS4iLCJpYXQiOjE1NjYxMzE4OTQsImV4cCI6MTU2NjEzMjQ5NH0.sjOoNfSEifTAN2bQY3Ot6ds9aBPBKDd2YQOIEQVATIM",
"issued_at": "Sun Aug 18 21:38:14 KST 2019",
"expires_in": "Sun Aug 18 21:48:14 KST 2019"
}
refresh 토큰 재발급 API: 기존에 발급받은 토큰을 Authorization 헤더에 "Bearer Token"으로 입력 요청을 하면 토큰을 재발급한다.
PUT /api/auth/refresh HTTP/1.1
Host: localhost:9000
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJtcmh0ZG1zIiwicGFzc3dvcmQiOiIkMmEkMTAkNTRMQUNDaThyRDlDc2hjelpLWU1TdTFHSXpqL1BkTVUyZ0kvbmhzcVhDWHhTNXB3RUNRNS4iLCJpYXQiOjE1NjYxMzE4OTQsImV4cCI6MTU2NjEzMjQ5NH0.sjOoNfSEifTAN2bQY3Ot6ds9aBPBKDd2YQOIEQVATIM
HTTP/1.1 200 OK
Content-Type: application/json
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJtcmh0ZG1zIiwicGFzc3dvcmQiOiIkMmEkMTAkNTRMQUNDaThyRDlDc2hjelpLWU1TdTFHSXpqL1BkTVUyZ0kvbmhzcVhDWHhTNXB3RUNRNS4iLCJpYXQiOjE1NjYxMzIwMjksImV4cCI6MTU2NjEzMjYyOX0.pydzSiXHvHMfQlFXf59BZWipwV41_3fi_1uHevrkSmQ",
"issued_at": "Sun Aug 18 21:40:29 KST 2019",
"expires_in": "Sun Aug 18 21:50:29 KST 2019"
}
POST /api/finance/init HTTP/1.1
Host: localhost:9000
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJtcmh0ZG1zIiwicGFzc3dvcmQiOiIkMmEkMTAkNTRMQUNDaThyRDlDc2hjelpLWU1TdTFHSXpqL1BkTVUyZ0kvbmhzcVhDWHhTNXB3RUNRNS4iLCJpYXQiOjE1NjYxMzMxMjcsImV4cCI6MTU2NjEzMzcyN30.Bz9vhzhnPiePmWJcyfV4hSvhWonDNEED7tJrXduDEiA
HTTP/1.1 200 OK
Content-Type: application/json
{
"message": "succeed"
}
GET /api/finance/list HTTP/1.1
Host: localhost:9000
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJtcmh0ZG1zIiwicGFzc3dvcmQiOiIkMmEkMTAkTDYxdU9ieTBIRkVkOVNCOWs1US5qdWVINjJkRWFhYlRzN0dLbXdyYS9IWmJBOG8yMWlHY0MiLCJpYXQiOjE1NjYxMzMzMDMsImV4cCI6MTU2NjEzMzkwM30.JX4uAInBUzr0r7Sw8gNat_U6pcYuojfuo3axKy7yetA
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"id": 1,
"bank": "주택도시기금"
},
...
{
"id": 9,
"bank": "기타은행"
}
]
GET /api/finance/summary-by-yearly HTTP/1.1
Host: localhost:9000
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJtcmh0ZG1zIiwicGFzc3dvcmQiOiIkMmEkMTAkTDYxdU9ieTBIRkVkOVNCOWs1US5qdWVINjJkRWFhYlRzN0dLbXdyYS9IWmJBOG8yMWlHY0MiLCJpYXQiOjE1NjYxMzM1MjcsImV4cCI6MTU2NjEzNDEyN30.96lR6PdCT-WArI-JUeG9ioKeOj-hVSO72u2A8SwU-2o
HTTP/1.1 200 OK
Content-Type: application/json
{
"name": "주택금융 공급현황",
"data": [
{
"year": "2005년",
"total_amount": 48016,
"detail_amount": {
"농협은행/수협은행": 1486,
"하나은행": 3122,
"우리은행": 2303,
"국민은행": 13231,
"신한은행": 1815,
"외환은행": 1732,
"주택도시기금": 22247,
"기타은행": 1376,
"한국시티은행": 704
}
},
...
{
"year": "2017년",
"total_amount": 295126,
"detail_amount": {
"농협은행/수협은행": 26969,
"하나은행": 35629,
"우리은행": 38846,
"국민은행": 31480,
"신한은행": 40729,
"외환은행": 0,
"주택도시기금": 85409,
"기타은행": 36057,
"한국시티은행": 7
}
}
]
}
GET /api/finance/maximum-by-yearly HTTP/1.1
Host: localhost:9000
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJtcmh0ZG1zIiwicGFzc3dvcmQiOiIkMmEkMTAkTDYxdU9ieTBIRkVkOVNCOWs1US5qdWVINjJkRWFhYlRzN0dLbXdyYS9IWmJBOG8yMWlHY0MiLCJpYXQiOjE1NjYxMzM2NzcsImV4cCI6MTU2NjEzNDI3N30.xQ2p40oe0OQtiA-q1t_fNJt7cgwCsopz20cW_BKSbrc
HTTP/1.1 200 OK
Content-Type: application/json
{
"year": 2014,
"bank": "주택도시기금"
}
GET /api/finance/max-min-by-yearly HTTP/1.1
Host: localhost:9000
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJtcmh0ZG1zIiwicGFzc3dvcmQiOiIkMmEkMTAkTDYxdU9ieTBIRkVkOVNCOWs1US5qdWVINjJkRWFhYlRzN0dLbXdyYS9IWmJBOG8yMWlHY0MiLCJpYXQiOjE1NjYxMzM2NzcsImV4cCI6MTU2NjEzNDI3N30.xQ2p40oe0OQtiA-q1t_fNJt7cgwCsopz20cW_BKSbrc
{
"bank":"외환은행"
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"bank": "외환은행",
"support_amount": [
{
"year": 2008,
"amount": 78
},
{
"year": 2015,
"amount": 1702
}
]
}
GET /api/finance/forecast HTTP/1.1
Host: localhost:9000
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJtcmh0ZG1zIiwicGFzc3dvcmQiOiIkMmEkMTAkTDYxdU9ieTBIRkVkOVNCOWs1US5qdWVINjJkRWFhYlRzN0dLbXdyYS9IWmJBOG8yMWlHY0MiLCJpYXQiOjE1NjYyODk1NDYsImV4cCI6MTU2NjI5MTM0Nn0.-Fld1qhJJXUuLNjK4jGPMe9DZicgGzIbZGwOW0oejZc
{
"bank":"국민은행",
"month": 2
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"bank": 2,
"year": 2018,
"month": 2,
"amount": 3700
}
Error code | Error Message | HTTP Status Code | Detailed Infomation |
---|---|---|---|
-1 | INVALID_PARAMETER | 400 Bad Request | 잘못된 파라미터로 요청 시 발생 |
-2 | USERNAME_DUPLICATE | 400 Bad Request | 중복된 username으로 가입 시도 시 발생 |
-3 | PASSWORD_NOT_EQUALS | 400 Bad Request | 잘못된 password로 로그인 시도 시 발생 |
-4 | ALREADY_REGISTERED_CSV_FILE | 400 Bad Request | CSV 파일 입력 중복 시도 시 발생 |
-101 | AUTH_TOKEN_INVALID | 401 Unauthorized | 잘못된 JWT 사용 시 발생 |
-102 | AUTH_TOKEN_EXPIRED | 401 Unauthorized | 만료된 JWT 사용 시 발생 |
-103 | AUTH_TOKEN_INVALID_SIGNATURE | 401 Unauthorized | 서명이 올바르지 않은 JWT 사용 시 발생 |
-301 | USERNAME_NOT_FOUND | 404 Not Found | 존재하지 않는 username으로 로그인 시도 시 발생 |
-302 | BANK_NOT_FOUND | 404 Not Found | 존재하지 않는 금융기관 이름을 입력 시 발생 |
-401 | CSV_IO_EXCEPTION | 500 Internal Server Error | CSV 입출력 과정에서 에러 발생 시 |
GET /api/finance/max-min-by-yearly HTTP/1.1
Host: localhost:9000
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJtcmh0ZG1zIiwicGFzc3dvcmQiOiIkMmEkMTAkTDYxdU9ieTBIRkVkOVNCOWs1US5qdWVINjJkRWFhYlRzN0dLbXdyYS9IWmJBOG8yMWlHY0MiLCJpYXQiOjE1NjYyODk5ODMsImV4cCI6MTU2NjI5MTc4M30.sbhlUjiWTpYKGJUAIqO2DNa8nSol3RIfb1YTqloxthM
{
"bank":"없는은행"
}
HTTP/1.1 404 Not Found
Date: Tue, 20 Aug 2019 08:33:16 GMT
Content-Type: application/json
Content-Length: 52
{
"error_message": "BANK_NOT_FOUND",
"error_code": -302
}
$ tree app conf
app
├── ErrorHandler.java -----------------> 에러 발생 시, json 에러 메시지를 출력하기 위한 커스텀 에러 핸들러
├── Filters.java ----------------------> Logging 필터 바인딩을 위한 클래스
├── Module.java -----------------------> 커스텀 DI 바인딩을 위한 모듈 클래스
├── actions
│ └── SecuredAction.java ------------> HTTP Request 헤더를 읽어 JWT를 검증하는 액션 클래스
├── controllers -----------------------> 컨트롤러
│ ├── FinanceController.java
│ ├── HomeController.java
│ └── UserController.java
├── exceptions
│ └── FinanceRuntimeException.java --> 예외 처리를 위한 Runtime Exception
├── filters
│ └── LoggingFilter.java ------------> HTTP Request에 대한 Log를 출력하는 필터 클래스
├── models ----------------------------> 엔터티 모델
│ ├── Bank.java
│ ├── Finance.java
│ └── User.java
├── services --------------------------> 비즈니스 로직이 정의된 인터페이스
│ ├── BankService.java
│ ├── FinanceService.java
│ ├── UserService.java
│ └── impl --------------------------> 비즈니스 로직을 구현한 클래스
│ ├── BankServiceImpl.java
│ ├── FinanceServiceImpl.java
│ └── UserServiceImpl.java
└── views
├── index.scala.html
└── main.scala.html
conf
├── application.conf ------------------> Play Framework 설정 파일
├── data.csv --------------------------> 문제에서 주어진 raw 데이터 파일
├── evolutions
│ └── default
│ └── 1.sql
├── img-db-diagram.png
├── logback.xml -----------------------> Log 설정 파일
└── routes ----------------------------> HTTP 라우팅 정의 파일
# CSV 파일을 다루기 위한 라이브러리
libraryDependencies += "com.opencsv"% "opencsv"% "4.1"
# JWT를 Java로 구현한 라이브러리
libraryDependencies += "io.jsonwebtoken" % "jjwt-api" % "0.10.7"
libraryDependencies += "io.jsonwebtoken" % "jjwt-impl" % "0.10.7"
libraryDependencies += "io.jsonwebtoken" % "jjwt-jackson" % "0.10.7"
# 지원금액 예측을 위한 시계열(time series) 라이브러리
libraryDependencies += "com.github.signaflo" % "timeseries" % "0.4"
# 사용자 암호 인코딩을 위한 bcrypt 라이브러리
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4"
[End Of File]