FineAnts 프로젝트 회고

2023. 11. 6. 21:32회고

코드스쿼드 부트 캠프에서 진행한 자유 프로젝트인 FineAnts 프로젝트에 대한 회고록을 작성하고자 합니다. 회고록에는 프로젝트의 정보와 프로젝트를 하면서 잘한점, 아쉬웠던 점, 어려웠던 점 등에 대한 내용들을 작성하였습니다.

 

프로젝트 정보

기간 : 2023년 10월 10일 ~ 2023년 11월 03일

팀원 구성 : 프론트 엔드 (3명), 백엔드 (1명, 본인)

특이사항 : 기간은 4주이지만 그 이후에도 1~2달정도 계속 프로젝트를 진행할 예정

주제 : 주식 포트폴리오를 관리할 수 있는 애플리케이션으로써 사용자는 포트폴리오에 종목을 추가하고 관리할 수 있는 애플리케이션입니다. 사용자는 포트폴리오에 종목에 대한 매입이력을 추가하여 종목의 현재 시세에 따른 손익을 확인할 수 있고 포트폴리오가 특정한 손익금액에 도달하는 경우 메일을 통하여 알림을 받을 수 있도록 합니다.

 

해당 페이지는 저희 애플리케이션에서 가장 중요한 기능 페이지입니다.

 

 

기술스택

  • Spring Boot 2.7.16
  • Spring Data JPA, QueryDSL
  • Spring WebSocket, STOMP
  • Spring mail
  • JJWT
  • MySQL, Redis

 

Open API

  • Kakao, Naver, Google OAuth
  • 한국 투자 증권 Kis

 

인프라 환경

 

 

1. 잘했거나 배웠던 점

1.1 AWS CodeDeploy 사용

이전까지의 프로젝트에서 사용하던 기존 배포 방식은 Github Action를 이용하여 특정한 하나의 EC2 인스턴스에 SSH 원격 접속하여 배포하는 방식을 사용하였습니다. 하지만 이 방식의 단점은 EC2 인스턴스가 중지되어 IP 주소가 변경되거나 다른 EC2 인스턴스로 배포해야 하는 경우 EC2 인스턴스 IP 주소 및 액세스 키를 저장하고 있는 시크릿 정보를 변경해야 한다는 단점이 있습니다. 또한 만약에 여러개의 EC2 인스턴스로 배포해야 한다면 시크릿 환경 변수에 각각의 액세스 키 ID와 시크릿 키값을 추가해야 합니다.

 

위 그림을 보면 Github Action을 이용하여 EC2 인스턴스에 SSH 원격 접속을 수행한 다음에 Docker Hub에 푸시한 스프링 이미지를 가져와 실행하는 방식임을 알 수 있습니다.

 

저는 특정한 EC2 인스턴스에 직접 접속하는 방식에 불편함을 느꼈고 마스터의 조언으로 AWS CodeDeploy를 사용하는 것을 조언받았습니다. AWS CodeDeploy를 이용하면 빌드된 Jar 파일을 S3에 zip 파일 형태로 저장하고 CodeDeploy는 S3에 저장된 zip 파일을 EC2 인스턴스에 압축 해제하여 배포 과정을 수행합니다. CodeDeploy를 사용하였을 때 느꼈던 장점은 실행중인 EC2 인스턴스가 리소스 절약을 위해 도중에 중지되고 다시 시작되었을 때 IP 주소가 변경되었더라도 별도의 EC2 인스턴스의 IP 주소를 신경쓰지 않아도 된다는 점입니다. 그리고 CodeDeploy를 사용하면 특정한 그룹을 대상으로 배포과정을 할 수 있기 때문에 개발 서버와 릴리즈 서버를 구분하여 배포할 수 있습니다.

 

위 그림과 같이 AWS CodeDeploy를 사용하여 개선한 방식은 Github Action은 스프링 애플리케이션을 빌드하고 압축하여 S3에 저장합니다. 그리고 AWS CodeDeploy에게 배포를 요청합니다. AWS CodeDeploy는 S3에 저장된 zip 파일을 가져와 압축해제 한다음에 선택한 EC2 인스턴스에 배포하는 과정을 수행합니다.

 

1.2 한국투자 증권 open api 연결

이번 프로젝트에서 핵심이었던 것은 한국투자 증권 open api 서버에 연결하여 현재 주식 시세를 요청하고 받아오는 기능이었습니다. 증권 서버에 값을 요청하고 받아오기 위해서 open api 공식 문서를 참고하여 필요한 요청 헤더 값과 바디값을 확인하고 보내는 과정을 코드로 구현하는 과정이 필요하였습니다. 기존까지는 Google, Naver와 같은 소셜 사이트에 소셜 로그인을 수행하기 위한 OAuth 로그인만 다루다가 외부의 open api를 다루는 기회를 가져서 좋았습니다. open api와 통신하기 위해서 중요했던 점은 공식 문서를 기반으로 올바른 프로퍼티값을 명세하여 전달하는 것이었습니다. 저 같은 경우 secretkey라는 프로퍼티를 이용하여 웹소켓 접속키를 발급 받아 볼 수 있었는데 현재 주식 시세를 요청하는 경우에는 secretkey값을 appsecret 프로퍼티로 전달해야 했습니다. 이를 인지하지 못하고 secretkey 프로퍼티 값으로 전달하여 값을 받아오지 못한적이 있었습니다. 따라서 이번 프로젝트의 한국투자 증권 open api와 통신해봤던 경험을 계기로 다른 open api와 통신하는 것도 수월할 것이라고 생각했습니다.

 

 

1.3  CompleteFuture를 이용한 비동기적 프로그래밍

서버의 기능중에 5초에 한번씩 포트폴리오의 상세 정보를 현재 주식 시세를 기반으로 실시간으로 포트폴리오의 정보를 갱신하는 스케줄링 메소드가 존재하였습니다. 이 메소드는 포트폴리오에 대한 정보를 저장하고 있는 컬렉션을 순회하여 현재 주식 시세를 기반으로 포트폴리오의 상세 정보를 갱신하여 구독중인 채널에 전파하는 메소드입니다. 즉, 해당 스케줄링 메소드를 다시 정리하면 다음과 같습니다.

  • 클라이언트가 구독중인 포트폴리오 컬렉션을 순회합니다.
  • 데이터베이스에 요청하여 포트폴리오 등록번호를 기반으로 포트폴리오 정보와 포트폴리오에 있는 종목 정보들을 쿼리합니다.
  • 입력으로 전달한 주식 현재 시세를 기반으로 포트폴리오에 대한 손익 금액과 같은 정보들을 계산합니다.
  • 갱신된 포트폴리오 상세 정보를 구독중인 클라이언트에게 전파하여 클라이언트는 실시간으로 갱신되는 포트폴리오 상세 정보를 응답받습니다.

하지만 각각의 포트폴리오가 다른 포트폴리오와는 관계가 없기 때문에 A라는 포트폴리오가 다른 B라는 포트폴리오의 결과를 기다릴 필요가 없습니다. 그저 A라는 포트폴리오는 비동적으로 수행하여 해당하는 구독 채널에 전파하기만 하면 됩니다. 따라서 저는 CompleteFuture를 이용하여 다음과 같이 개선할 수 있습니다.

@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void publishPortfolioDetail() {
    List<CompletableFuture<PortfolioHoldingsResponse>> futures = portfolioSubscriptionManager.values()
        .parallelStream()
        .filter(this::hasAllCurrentPrice)
        .map(PortfolioSubscription::getPortfolioId)
        .map(portfolioId -> CompletableFuture.supplyAsync(
                () -> portfolioStockService.readMyPortfolioStocks(portfolioId), portfolioDetailExecutor)
            .exceptionally(e -> {
                log.info(e.getMessage(), e);
                return null;
            }))
        .collect(Collectors.toList());

    futures.parallelStream()
        .map(CompletableFuture::join)
        .forEach(response -> messagingTemplate.convertAndSend(
            String.format(SUBSCRIBE_PORTFOLIO_HOLDING_FORMAT, response.getPortfolioId()), response));
}

 

위 코드에서 List<CompletableFuture<PortfolioHoldingsResponse>> 타입의 futures 변수를 받아서 별도의 스트림으로 수행한 이유는 만약 하나의 병렬 스트림으로 수행하였다면 스트림의 지연 연산 특성에 의하여 동기적으로 수행하기 때문입니다. 그래서 별도로 실행하도록 하였습니다. 그리고 2번째 병렬 스트림 호출에서 join 메소드를 호출하여 각각의 스레드들은 계산이 종료되면 각자 구독중인 채널에 브로드캐스팅하게 됩니다. 위와 같이 코드를 개선하여 단순히 반복문을 이용하여 수행하는 것보다 비동기적으로 프로그래밍하여 실행 속도를 개선할 수 있었습니다.

 

1.4 주식 현재가 시세 재사용

클라이언트에게 실시간으로 주식 현재가 시세를 전달하는데 고려했었던 것은 주식 현재가 시세를 어떻게 하면 재사용할까 였습니다. 예를 들어 2개의 포트폴리오가 존재하고 모두 서버에 STOMP 방식의 웹 소켓 연결이 된 상태라고 가정합니다. 두 포트폴리오 모두 "삼성전자"라는 종목을 등록된 상태라고 가정합니다. 이때 서버에서는 두 클라이언트에게 삼성전자 종목에 대한 현재가 시세를 응답해야 합니다. 그런데 한국투자 증권 open api에 두번 요청하여 동일한 값의 삼성전자 종목의 현재 시세값을 응답하는 것은 매우 비효율적이라고 생각하였습니다. 

 

다음 그림과 같이 /pub/portfolio/1 url 주소에 리퀘스트 바디에 종목 코드를 같이 전달하며 종목 코드에 대한 현재 주식 시세를 요청한다면 일반적으로 메시지 핸들러(SimpAnnotationMethod MessageHandler)는 한국 투자증권 open api(kis open api) 서버에 종목 코드를 전달하며 종목 코드에 대한 현재 주식 시세를 가져올 것입니다. 하지만 동일한 종목 코드를 요청하는 포트폴리오가 여러개 존재한다면 매우 비효율적일 것입니다. 즉, 정리하면 다음과 같은 문제를 가질 것입니다.

  • 각각의 구독 및 발행을 요청한 포트폴리오에서 각각에 open api 서버에 현재 주식 시세를 요청하는 것은 매우 비효율적인 문제입니다.

 

따라서 위와 같은 문제로 인하여 저는 다음과 같이 설계하고 구현하였습니다.

 

 

  1. KisService는 5초마다 스케줄링 메소드를 실행하여 KisClient에게 요청하여 kis open api 서버로부터 특정 종목들의 현재 주식 시세를 가져옵니다.
  2. 현재 주식 시세를 받은 KisService는 currentpriceManager에게 요청하여 갱신된 현재 주식 시세들을 전달합니다.
  3. 전달받은 CurrentPriceManager 객체는 Redis에 값들을 저장합니다.
  4. SimpAnnotationMethodMessageHandler는 직접 kis open api에 요청할 필요없이 KisService에 요청하여 처리할 수 있습니다.

위와 같이 설계했을때의 느꼈던 장점은 메시지 핸들러가 더이상 kis open api에 각각 동일한 종목을 요청할 필요 없이 KisService 객체를 통하여 값을 가져올 수 있었습니다.  

 

 

1.5 주식 용어 정리

이번 FineAnts 프로젝트를 지원하게 된 동기는 평소 주식에 대한 관심이 있어서 였습니다. 그런데 막상 프로젝트를 진행하니 비슷하면서도 다른 용어들이 많이 사용하게 됨을 느꼈습니다. 대표적인 예시로 평가금액, 투자금액 등이 있었습니다. 평가금액은 종목의 현재가 * 개수를 의미하며, 투자 금액은 종목 매입 평균가 * 개수를 의미합니다. 그래서 이번 프로젝트를 하면서 잘했다고 생각했던 점은 프로젝트에서 사용하는 용어들을 표로 정리하고 해당 값을 계산하기 위한 공식들을 정리하였다는 점이 잘했다고 생각하였습니다. 

 

 

위와 같이 표로 개념들을 정리하니 코드로 구현할때도 많은 도움이 되었습니다.

 

2. 아쉬웠던 점

2.1 엘라스틱 서치(Elastic Search)

FineAnts 애플리케이션 기능중 종목 검색 기능이 존재하였습니다. 사용자에게 실시간 검색을 제공하기 위해서는 단순 REST API가 아닌 엘라스틱 서치를 이용한 실시간 검색을 제공하는 것이 더 적절하다고 생각하였습니다. 그래서 엘라스틱 서치에 대해서 학습하고 구현하는 방법에 대해서 학습하였습니다. 그런데 막상 로컬 개발 환경에서는 잘 동작하였지만 배포 환경(EC2 t2.micro)에서 엘라스틱 서치 컨테이너를 도커 컨테이너에서 실행했을때는 작도하지 않았습니다. 원인은 엘라스틱 서치, Redis, Spring 컨테이너를 한번에 시작하여 메모리 부족으로 실행되지 않았습니다. 결국에는 엘라스틱 서치를 포기하고 단순 REST API 검색으로 구현했던 기억이 납니다. 추후에 배포 환경에서 엘라스틱 서치를 제공하는 방법을 찾아야 겠다고 생각하였습니다.

 

2.2 Jmeter를 이용한 성능 테스트

마스터와의 미팅에서 FineAnts 애플리케이션의 컨셉과 목적을 설명하던중 해당 애플리케이션은 실시간성이 중요하다고 하여 나중에 기회가 된다면 Jmeter나 nGrinder를 사용하여 성능 테스트를 해볼 것을 조언하였습니다. 아쉬웠던 점은 Jmeter를 사용한적이 없고 프로젝트기한에 대한 시간도 부족하여 성능 테스트는 하지 못한것이었습니다. 그래도 다행히 해당 프로젝트는 자유 프로젝트이기 때문에 1~2달 정도 계속하게 되어 회고록을 작성하는 주에 성능 테스트를 해볼 예정입니다.

 

2.3 보안에 대한 인증 및 인가 부분에 대한 미흡

서버 개발 구현이 본인 한명뿐이었꼬 1차 기한은 한달이었기 때문에 인가 부분에 대해서 미흡한 점이 많았습니다. 대표적으로 사용자가 다른 사용자의 포트폴리오를 조회할 수 있는 부분이 있습니다. 이 부분 역시 1차 기한이 종료된 이후 계속되는 프로젝트에서 보완해야 겠다고 생각하였습니다.

 

 

3. 어려웠던 점

3.1 배당금 정보 초기화

데이터베이스에 종목과 종목에 대한 배당금 정보(배당 지급일, 주당 배당금)들을 초기화 해야 했습니다. 종목에 대한 정보는 정보데이터시스템에서 csv 파일로 편리하게 가져올 수 있었지만 배당금에 대한 정보는 연간 배당금에 대한 정보밖에 찾지 못하였습니다. 제가 원했던 것은 분기별 배당까지 상세하게 나타내는 배당금 정보였습니다. 그렇게 찾아보던중 세이브로라는 포털에서 배당 정보 데이터를 찾을 수 있었습니다.

https://seibro.or.kr/websquare/control.jsp?w2xPath=/IPORTAL/user/company/BIP_CNTS01041V.xml&menuNo=285

 

SEIBro

 

seibro.or.kr

 

3.2 서버 개발자의 부족

프로젝트 정보 부분에서도 명시했다 시피 이번 프로젝트에서는 프론트 엔드 개발자가 3명이고 백엔드 개발자가 1명(본인)이었기 때문에 한달이라는 길다면 길고, 짧다면 짧은 시간동안 많은 기능을 구현하지는 못한다고 생각하였습니다. 아무래도 4주에서 첫주는 애플리케이션을 기획하고 요구사항 분석 등과 같은 문서들을 작성하는데 시간을 대부분 보내게 되었습니다. 또한 웹소켓과 STOMP를 이용한 실시간 연결 기능을 구현해본적이 없었기 때문에 이러한 기술들을 학습하는데도 많은 시간이 소요되었습니다. 하지만 프로젝트 시작 초기부터 한명이서 많은 기능들을 구현할 수는 없다고 예상할 수 있기 때문에 FineAnts 애플리케이션에서 필요한 최소한의 기능들(소셜 로그인, 포트폴리오 목록)을 구현하고 가장 핵심적인 페이지의 기능들을 구현하면 해당 프로젝트의 MVP(Minimum VIable Product)를 만족할 수 있다고 생각하였습니다. 그래서 다행히 4주동안 서버 개발자가 부족함에도 불구하고 MVP에 요구되는 기능들은 어느정도 완성될 수 있었습니다.

FineAnts 애플리케이션의 MVP를 요구하는 페이지

 

 

References

github : https://github.com/fine-ants/backend

 

 

 

'회고' 카테고리의 다른 글

사과마켓 프로젝트 회고  (0) 2023.10.09
TodoList 프로젝트 회고  (0) 2023.07.22