securities real-time rank
JDK 11
Spring Boot 2.5.3
STS-4-4.16.0.RELEASE
JPA
H2
Redis
Maven
Junit
개발 프레임워크 구성은 아래와 같으며, Maven 스크립트에 의해서 자동으로 로드 및 사용하도록 되어있음
h2database : 2.1.214
redis : 2.7.4
Restful 서버 구성 및 DB 엔티티 관리에 사용
spring-integration-jdbc, spring-boot-integration은 분산 락을 활용하여, 특정 카드 또는 결제 ID의 동시 접근을 막는데 사용
spring-boot-starter-test : 2.7.4.RELEASE
spring-boot-data-jpa: 2.7.4.RELEASE
함수 및 메소드의 모듈이 의도한 대로 작동하는지 검증하기 위해 사용자
junit-jupiter-engine : 5.8.2
assertj-core : 3.22.0
mockito-junit-jupiter : 4.5.1
Lombok : 1.18.24
개발 편의를 위해 사용, Annotation Processor를 활용한 자동 Getter, Setter 기능 및 Builder 생성 시에 사용.
IDE 에서 사용해야 하는 경우에 Project setting 내에 Enable annotation processing 옵션을 켜고 사용 해야 함.
opencsv : 3.7
Sample Csv 파일을 "csvReader"를 통해 읽어 효율적인 데이터 핸들링을 위함
Sample Data내 존재하는 주식 상품 정보 저장
원장성 테이블로 주식 랭킹 프로세스에 주체가 되는 테이블 개발내 invest_agent_vol, ohlcv의 자식 테이블을 가지게 된다.
추후 이력 테이블 필요
컬럼ID
컬럼명
type
len
PK
code
주식 코드
char
32
1
code_name
주식 코드명
char
128
거래성 테이블로 등록만 가능한 테이블 저장한 데이터를 정정하거나 취소하는 등의 행위는 Insert를 통해 상태 관리 해야함. 따라서 별도의 이력 테이블은 존재하지 않는다.
파티션 테이블로 거래 일시 기준으로 파티션을 나눠 관리
컬럼ID
컬럼명
type
len
PK
timestamp
거래 일시
datatime
1
code
주식 코드
char
32
2
window_size
주기
int
foreigh_volume
외국인 보유량
int
isttt_volume
기관 보유량
int
indi_volume
개인 보유량
int
see
본 횟수
int
거래성 테이블로 등록만 가능한 테이블 저장한 데이터를 정정하거나 취소하는 등의 행위는 Insert를 통해 상태 관리 해야함. 따라서 별도의 이력 테이블은 존재하지 않는다.
파티션 테이블로 거래 일시 기준으로 파티션을 나눠 관리
컬럼ID
컬럼명
type
len
PK
timestamp
거래 일시
datatime
1
code
주식 코드
char
32
2
window_size
주기
int
open
오픈가격
int
close
종료가격
int
상태코드
싱태
설명
200
성공
정상 응답
201
성공
정상적으로 생성
400
실패
잘못된 요청
404
실패
리소스를 찾을 수 없음
500
실패
시스템 에러
code
설명
HTTP상태코드
-1
처리 중에 에러가 발생했습니다.
400
-770
순위 조회 시 주제를 선택해주세요.
400
-771
순위는 최대 100건만 조회가 가능합니다.
400
-772
순위 랜덤 변경 중 오류가 발생했습니다.
400
HTTP/1.1 400 Bad Request
{
"response" : null ,
"suceess" : false ,
"error" : {
"code" : -770 ,
"message" : " 순위 조회 시 주제를 선택해주세요."
}
}
이 API는 모든 주제 Top5를 조회하는 API입니다.
요청이 성공하면 응답은 바디에 JSON 객체로 많이 본, 많이 오른, 많이 내린, 많이 보유한 정보를 포함합니다.
Name
Type
Descrption
viewALot
ViewALot[]
많이 본 상세
riseALot
RiseALot[]
많이 오른 상세
dropALot
DropALot[]
많이 내린 상세
volumeHigh
VolumeHigh[]
많이 보유한 상세
ViewALot(JSON), RiseALot(JSON), DropALot(JSON), VolumeHigh(JSON)
Name
Type
Descrption
code
String
상품코드
codeNm
String
상품코드명
rank
BigDecimal
순위
price
BigDecimal
가격
percent
Double
백분율
curl -v -X GET "http://localhost:8080/v1/stock/rank"
HTTP/1.1 200 OK
Content-type: application/json;charset=UTF-8
{
"response" : {
"dropALot" : [
{
"code" : " 024110" ,
"codeName" : " 기업은행" ,
"rank" : 1.0 ,
"price" : 8929.00 ,
"percent" : -7.8600
},
{
"code" : " 010140" ,
"codeName" : " 삼성중공업" ,
"rank" : 2.0 ,
"price" : 5674.00 ,
"percent" : -5.2800
},
{
"code" : " 272210" ,
"codeName" : " 한화시스템" ,
"rank" : 3.0 ,
"price" : 14507.00 ,
"percent" : -5.1900
},
{
"code" : " 030000" ,
"codeName" : " 제일기획" ,
"rank" : 4.0 ,
"price" : 21517.00 ,
"percent" : -3.7300
},
{
"code" : " 003410" ,
"codeName" : " 쌍용C&E" ,
"rank" : 5.0 ,
"price" : 6771.00 ,
"percent" : -3.6900
}
],
"riseALot" : [
{
"code" : " 008560" ,
"codeName" : " 메리츠증권" ,
"rank" : 1.0 ,
"price" : 5309.00 ,
"percent" : 7.6800
},
{
"code" : " 018880" ,
"codeName" : " 한온시스템" ,
"rank" : 2.0 ,
"price" : 11390.00 ,
"percent" : 7.4500
},
{
"code" : " 028670" ,
"codeName" : " 팬오션" ,
"rank" : 3.0 ,
"price" : 5876.00 ,
"percent" : 6.4400
},
{
"code" : " 015760" ,
"codeName" : " 한국전력" ,
"rank" : 4.0 ,
"price" : 22485.00 ,
"percent" : 4.3300
},
{
"code" : " 088980" ,
"codeName" : " 맥쿼리인프라" ,
"rank" : 5.0 ,
"price" : 13331.00 ,
"percent" : 3.7400
}
],
"viewALot" : [
{
"code" : " 010140" ,
"codeName" : " 삼성중공업" ,
"rank" : 1.0 ,
"price" : 5674.00 ,
"percent" : -5.2800
},
{
"code" : " 007070" ,
"codeName" : " GS리테일" ,
"rank" : 2.0 ,
"price" : 24730.00 ,
"percent" : -3.4000
},
{
"code" : " 023530" ,
"codeName" : " 롯데쇼핑" ,
"rank" : 3.0 ,
"price" : 101034.00 ,
"percent" : -0.4600
},
{
"code" : " 086280" ,
"codeName" : " 현대글로비스" ,
"rank" : 4.0 ,
"price" : 177546.00 ,
"percent" : -0.5400
},
{
"code" : " 066570" ,
"codeName" : " LG전자" ,
"rank" : 5.0 ,
"price" : 102187.00 ,
"percent" : 0.1800
}
],
"volumeHigh" : [
{
"code" : " 078930" ,
"codeName" : " GS" ,
"rank" : 1.0 ,
"price" : 43781.00 ,
"percent" : -0.9500
},
{
"code" : " 377300" ,
"codeName" : " 카카오페이" ,
"rank" : 2.0 ,
"price" : 68629.00 ,
"percent" : 0.3300
},
{
"code" : " 036570" ,
"codeName" : " 엔씨소프트" ,
"rank" : 3.0 ,
"price" : 381680.00 ,
"percent" : -0.2200
},
{
"code" : " 003410" ,
"codeName" : " 쌍용C&E" ,
"rank" : 4.0 ,
"price" : 6771.00 ,
"percent" : -3.6900
},
{
"code" : " 034220" ,
"codeName" : " LG디스플레이" ,
"rank" : 5.0 ,
"price" : 15685.00 ,
"percent" : -3.4800
}
]
},
"suceess" : true ,
"error" : null
}
이 API는 주제별 최대 Top100을 조회하는 API입니다.
요청이 성공하면 응답은 바디에 JSON 객체로 많이 본, 많이 오른, 많이 내린, 많이 보유한 중 하나의 상세 정보를 포함합니다.
Name
Type
Descrption
id
int
0 = 많이 본, 1 = 많이 오른, 2 = 많이 내린, 3 = 많이 보유한
paging
int
default = 20, max = 100
Name
Type
Descrption
viewALot
ViewALot[]
많이 본 상세
riseALot
RiseALot[]
많이 오른 상세
dropALot
DropALot[]
많이 내린 상세
volumeHigh
VolumeHigh[]
많이 보유한 상세
ViewALot(JSON), RiseALot(JSON), DropALot(JSON), VolumeHigh(JSON)
Name
Type
Descrption
code
String
상품코드
codeNm
String
상품코드명
rank
BigDecimal
순위
price
BigDecimal
가격
percent
Double
백분율
curl -v -X GET "http://localhost:8080/v1/stock/rank/{id}"
HTTP/1.1 200 OK
Content-type: application/json;charset=UTF-8
{
"response" : {
"riseALot" : [
{
"code" : " 008560" ,
"codeName" : " 메리츠증권" ,
"rank" : 1.0 ,
"price" : 5309.00 ,
"percent" : 7.6800
},
{
"code" : " 018880" ,
"codeName" : " 한온시스템" ,
"rank" : 2.0 ,
"price" : 11390.00 ,
"percent" : 7.4500
},
{
"code" : " 028670" ,
"codeName" : " 팬오션" ,
"rank" : 3.0 ,
"price" : 5876.00 ,
"percent" : 6.4400
},
{
"code" : " 015760" ,
"codeName" : " 한국전력" ,
"rank" : 4.0 ,
"price" : 22485.00 ,
"percent" : 4.3300
},
{
"code" : " 088980" ,
"codeName" : " 맥쿼리인프라" ,
"rank" : 5.0 ,
"price" : 13331.00 ,
"percent" : 3.7400
},
{
"code" : " 316140" ,
"codeName" : " 우리금융지주" ,
"rank" : 6.0 ,
"price" : 12636.00 ,
"percent" : 3.5700
},
{
"code" : " 000060" ,
"codeName" : " 메리츠화재" ,
"rank" : 7.0 ,
"price" : 39066.00 ,
"percent" : 2.4000
},
{
"code" : " 138040" ,
"codeName" : " 메리츠금융지주" ,
"rank" : 8.0 ,
"price" : 30682.00 ,
"percent" : 2.1000
},
{
"code" : " 371460" ,
"codeName" : " TIGER 차이나전기차SOLACTIVE" ,
"rank" : 9.0 ,
"price" : 16941.00 ,
"percent" : 2.0500
},
{
"code" : " 006360" ,
"codeName" : " GS건설" ,
"rank" : 10.0 ,
"price" : 32128.00 ,
"percent" : 1.9900
},
{
"code" : " 047050" ,
"codeName" : " 포스코인터내셔널" ,
"rank" : 11.0 ,
"price" : 25628.00 ,
"percent" : 1.9000
},
{
"code" : " 032640" ,
"codeName" : " LG유플러스" ,
"rank" : 12.0 ,
"price" : 12574.00 ,
"percent" : 1.8100
},
{
"code" : " 000100" ,
"codeName" : " 유한양행" ,
"rank" : 13.0 ,
"price" : 57727.00 ,
"percent" : 1.6300
},
{
"code" : " 071050" ,
"codeName" : " 한국금융지주" ,
"rank" : 14.0 ,
"price" : 62092.00 ,
"percent" : 1.4500
},
{
"code" : " 021240" ,
"codeName" : " 코웨이" ,
"rank" : 15.0 ,
"price" : 65946.00 ,
"percent" : 1.4500
},
{
"code" : " 251270" ,
"codeName" : " 넷마블" ,
"rank" : 16.0 ,
"price" : 68239.00 ,
"percent" : 1.3900
},
{
"code" : " 026960" ,
"codeName" : " 동서" ,
"rank" : 17.0 ,
"price" : 26001.00 ,
"percent" : 1.3600
},
{
"code" : " 012450" ,
"codeName" : " 한화에어로스페이스" ,
"rank" : 18.0 ,
"price" : 72186.00 ,
"percent" : 1.1000
},
{
"code" : " 005830" ,
"codeName" : " DB손해보험" ,
"rank" : 19.0 ,
"price" : 63053.00 ,
"percent" : 1.0400
},
{
"code" : " 052690" ,
"codeName" : " 한전기술" ,
"rank" : 20.0 ,
"price" : 75661.00 ,
"percent" : 1.0100
}
]
},
"suceess" : true ,
"error" : null
}
이 API는 모든 주제를 랜덤하게 변경하는 API입니다.
요청이 성공하면 응답은 바디에 JSON 객체로 많이 본, 많이 오른, 많이 내린, 많이 보유한 정보를 랜덤하게 변경합니다.
Name
Type
Descrption
code
String
코드
message
String
메시지
curl -v -X POST "http://localhost:8080/v1/stock/random"
HTTP/1.1 200 OK
Content-type: application/json;charset=UTF-8
{
"response" : null ,
"suceess" : false ,
"error" : {
"code" : 201 ,
"message" : " success"
}
}
단위 테스트는 "/kakaopaysec/src/test/java/com/kakaopaysec/controller" 폴더 아래에 있는 파일들을 실행
Application Loading 시 데이터 적재
CommandLineRunner 인터페이스를 @Bean 어노테이션을 활용하여 익명 클래스 선언
Application이 정상적으로 구동되면 H2 Database내에 Sample Data를 적재하는 로직 구현
@ Bean
public CommandLineRunner commandLineRunner () {
return (args ) -> {
try {
Optional <InvestAgentVolInfo > investAgentVolInfo ;
CSVReader csvReader = new CSVReader (new FileReader ("/kakaopaysec/src/main/resources/data/SampleData.csv" ));
String [] strArr ;
while ((strArr = csvReader .readNext ()) != null ) {
/**
* 종목정보 save
* 0 = code
* 1 = code_name
*/
itemRepository .save (new ItemInfo (strArr [1 ]
, strArr [2 ]));
/**
* 캔들정보 save
* 0 = code
* 1 = windowSize (default = 1min)
* 2 = timestamp
* 3 = open (default = 초기 금액)
* 4 = close (default = 초기 금액)
*/
ohlcvRepository .save (new OhlcvInfo (strArr [1 ]
, BigDecimal .ONE
, LocalDateTime .now ()
, new BigDecimal (strArr [3 ])
, new BigDecimal (strArr [3 ])));
/**
* 거래주체별거래량 save
* 0 = code
* 1 = windowSize (default = 1min)
* 2 = timestamp
* 3 = foreighVolume (default = Random().nextInt(1000))
* 4 = istttVolume (default = Random().nextInt(1000))
* 5 = indiVolume (default = Random().nextInt(1000))
* 6 = see (default = Random().nextInt(1000))
*/
investAgentVolRepository .save (new InvestAgentVolInfo (strArr [1 ]
, BigDecimal .ONE
, LocalDateTime .now ()
, new BigDecimal (Integer .toString (new Random ().nextInt (1000 )))
, new BigDecimal (Integer .toString (new Random ().nextInt (1000 )))
, new BigDecimal (Integer .toString (new Random ().nextInt (1000 )))
, new BigDecimal (Integer .toString (new Random ().nextInt (1000 )))));
}
} catch (IOException e ) {
e .printStackTrace ();
}
};
}
실시간으로 변경되는 데이터로 인해 발생하는 서능을 고려하여 인-메모리 H2 Database를 통해 가볍고, 관리가 편한 RDBMS 선택
중복값의 제어와 O(log(N)) Select, 실시간 데이터 추가 및 변경을 고려한 Redis Sorted Set 자료구조 사용
@ Autowired
private RedisTemplate <String , String > redisTemplate ;
private ZSetOperations <String , String > zSetOps ;
@ Transactional
public void updateRandomRank () {
List <ItemInfo > listItemInfo = itemRepository .findAll ();
Optional <InvestAgentVolInfo > investAgentVolInfo ;
Optional <OhlcvInfo > ohlcvInfo ;
for (int i = 0 ; i < listItemInfo .size (); i ++) {
ohlcvInfo = ohlcvRepository .findByCode (listItemInfo .get (i ).getCode ());
/**
* 캔들정보 update
* 0 = code
* 1 = windowSize (default = 1min)
* 2 = timestamp
* 3 = open
* 4 = close
*/
if (i %2 == 0 ) {
ohlcvRepository .save (new OhlcvInfo (listItemInfo .get (i ).getCode ()
, BigDecimal .ONE
, LocalDateTime .now ()
, ohlcvInfo .get ().getOpen ()
, ohlcvInfo .get ().getClose ().subtract (new BigDecimal (Integer .toString (new Random ().nextInt (1000 ))))
));
}else {
ohlcvRepository .save (new OhlcvInfo (listItemInfo .get (i ).getCode ()
, BigDecimal .ONE
, LocalDateTime .now ()
, ohlcvInfo .get ().getOpen ()
, ohlcvInfo .get ().getClose ().add (new BigDecimal (Integer .toString (new Random ().nextInt (1000 ))))
));
}
ohlcvInfo = ohlcvRepository .findByCode (listItemInfo .get (i ).getCode ());
/**
* 거래주체별거래량 update
* 0 = code
* 1 = windowSize (default = 1min)
* 2 = timestamp
* 3 = foreighVolume (default = Random().nextInt(10000))
* 4 = istttVolume (default = Random().nextInt(10000))
* 5 = indiVolume (default = Random().nextInt(10000))
* 6 = see (default = Random().nextInt(10000))
*/
investAgentVolRepository .save (new InvestAgentVolInfo (listItemInfo .get (i ).getCode ()
, BigDecimal .ONE
, LocalDateTime .now ()
, new BigDecimal (Integer .toString (new Random ().nextInt (1000 )))
, new BigDecimal (Integer .toString (new Random ().nextInt (1000 )))
, new BigDecimal (Integer .toString (new Random ().nextInt (1000 )))
, new BigDecimal (Integer .toString (new Random ().nextInt (1000 )))
));
investAgentVolInfo = investAgentVolRepository .findByCode (listItemInfo .get (i ).getCode ());
//많이 본
zSetOps .add ("viewALot" , listItemInfo .get (i ).getCode ()
, investAgentVolInfo .get ().getSee ().doubleValue ());
//많이 오른, 많이 내린
zSetOps .add ("volumeOfLot" , listItemInfo .get (i ).getCode ()
, ohlcvInfo .get ().getClose ()
.subtract (ohlcvInfo .get ().getOpen ())
.divide (ohlcvInfo .get ().getOpen (), 4 , RoundingMode .FLOOR )
.multiply (new BigDecimal ("100" )).doubleValue ());
//거래량 많은
zSetOps .add ("volumeHigh" , listItemInfo .get (i ).getCode ()
, investAgentVolInfo .get ().getForeighVolume ()
.add (investAgentVolInfo .get ().getIstttVolume ())
.add (investAgentVolInfo .get ().getIndiVolume ())
.doubleValue ());
}
}
invest_agent_vol, ohlcv 거래성 테이블의 대용량 처리를 위해 Partition Range 생성
create table `invest_agent_vol `
(timestamp datatime not null ,
code char (32 ) not null ,
window_size int ,
foreigh_volume int ,
isttt_volume int ,
indi_volume int ,
see int ,
primary key (timestamp , code)
) engine= innodb default charset= utf8mb4
partition by range(date_format(DATETIME,' %Y%m%d%H' )) (
partition p0 values less than(2022092709 ) engine= innodb,
partition p1 values less than(2022092710 ) engine= innodb,
partition p2 values less than(2022092711 ) engine= innodb,
partition p3 values less than(2022092712 ) engine= innodb,
partition p999 values less than maxvalue engine= innodb);