우리 프로젝트는 암호화폐 거래와 구독 관리를 지원하는 웹 애플리케이션으로, 사용자 인증, 거래 및 지갑 관리, 구독 청구, 실시간 시세 스트리밍 기능을 포함하고 있다. 나는 배치 모듈 기능 구현을 함께 담당했지만, 프로젝트 전반적인 구조와 기능 이해와 프로젝트 진행사항에 대해서 스스로 더 명확하게 인지하기 위해서 주요 기능별 코드 예시를 포함하여 프로젝트 내용을 다시 정리하는 시간을 가졌다.
먼저 우리 프로젝트의 흐름을 대략적으로 요약해보면
사용자 인증:
사용자가 AuthController
를 통해 로그인하면 JwtUtil
로 JWT 토큰이 발급된다. 이 토큰은 프로젝트 전반에서 권한 관리를 위해 사용된다.
지갑 관리:
사용자는 UserService
와 WalletService
를 통해 본인의 잔액을 확인하고 거래를 요청할 수 있다.
구독 및 청구:
사용자는 SubscriptionsService
에서 암호화폐 구독을 설정하며, 구독은 배치 작업을 통해 주기적으로 청구된다.
거래 처리:
사용자가 TradeService
를 통해 거래를 요청하면 매매가 실행되고, 실시간 가격 정보를 반영하여 지갑 잔액이 변동된다.
실시간 시세 데이터 업데이트: FinnhubWebSocketClient
에서 WebSocket을 통해 시세 데이터가 스트리밍되고, 이를 사용해 거래와 지갑을 최신 상태로 유지한다.
AuthService는 사용자 인증 및 권한 부여 기능을 담당하며, 사용자 로그인 시 JWT 토큰을 발급하여 사용자 세션을 관리한다.
AuthService.authenticate
메서드:
사용자의 인증 정보를 검증하여 유효한 사용자일 경우 JWT 토큰을 발급한다. 이 토큰은 요청마다 첨부되어 인증 상태를 유지하며, 미리 설정된 SECRET_KEY
로 서명된다.
public String authenticate(String username, String password) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new InvalidRequestException("Invalid credentials"));
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new InvalidRequestException("Invalid credentials");
}
return jwtUtil.generateToken(user);
}
JwtUtil.generateToken
메서드:public String generateToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
return Jwts.builder()
.setClaims(claims)
.setSubject(user.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
이 모듈에서는 사용자의 정보와 지갑 정보를 조회하고, 거래 후 잔액을 관리한다.
UserService.getUserById
메서드:public UserDto getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
return new UserDto(user.getUsername(), user.getEmail(), user.getWalletList());
}
WalletService.updateWalletBalance
메서드:public void updateWalletBalance(User user, String cryptoSymbol, double amount, long newBalance) {
Wallet wallet = user.getWalletList().stream()
.filter(w -> w.getCryptoSymbol().equals(cryptoSymbol))
.findFirst()
.orElseThrow(() -> new RuntimeException("지갑을 찾을 수 없습니다."));
wallet.setAmount(amount);
wallet.setCash(newBalance);
walletRepository.save(wallet);
}
사용자의 거래 요청을 처리하고, 거래 내역을 저장하는 모듈이다.
TradeService.postTrade
메서드:public TradeResponseDto postTrade(AuthUser authUser, long cryptoId, TradeRequestDto tradeRequestDto) {
User user = userRepository.findById(authUser.getId())
.orElseThrow(() -> new InvalidRequestException("no such user"));
Crypto crypto = cryptoRepository.findById(cryptoId)
.orElseThrow(() -> new NullPointerException("no such crypto"));
Long price = cryptoWebService.getCryptoValueAsLong(crypto.getSymbol(), DateTimeUtil.getCurrentDate(), DateTimeUtil.getCurrentTime());
Wallet wallet = walletRepository.findByUserIdAndCryptoSymbol(user.getId(), crypto.getSymbol());
if (tradeRequestDto.getTradeType().equals(TradeType.Authority.BUY)) {
if (wallet.getCash() < price * tradeRequestDto.getAmount()) {
throw new InvalidRequestException("잔액이 부족합니다.");
}
wallet.update(wallet.getAmount() + tradeRequestDto.getAmount(), wallet.getCash() - (long) (price * tradeRequestDto.getAmount()), price);
} else if (tradeRequestDto.getTradeType().equals(TradeType.Authority.SELL)) {
if (wallet.getAmount() < tradeRequestDto.getAmount()) {
throw new InvalidRequestException("암호화폐 수량이 부족합니다.");
}
wallet.update(wallet.getAmount() - tradeRequestDto.getAmount(), wallet.getCash() + (long) (price * tradeRequestDto.getAmount()), price);
}
Trade trade = new Trade(user, crypto, tradeRequestDto.getTradeType(), tradeRequestDto.getTradeFor(), tradeRequestDto.getAmount(), price, (long) (price * tradeRequestDto.getAmount()), user.getId());
tradeRepository.save(trade);
return new TradeResponseDto(crypto.getSymbol(), tradeRequestDto.getAmount(), tradeRequestDto.getTradeType(), (long) (price * tradeRequestDto.getAmount()));
}
이 모듈은 사용자가 구독한 암호화폐의 가격 변동에 따라 청구 내역을 생성하고 업데이트하는 기능을 제공한다.
SubscriptionsService.processSubscription
메서드:public void processSubscription(User user, String cryptoSymbol, double amount) {
Subscriptions subscription = subscriptionsRepository.findByUserAndCryptoSymbol(user.getId(), cryptoSymbol)
.orElseThrow(() -> new ResourceNotFoundException("Subscription not found"));
long price = cryptoWebService.getCryptoValueAsLong(cryptoSymbol, DateTimeUtil.getCurrentDate(), DateTimeUtil.getCurrentTime());
long totalCost = (long) (amount * price);
subscription.setNowPrice(price);
subscription.setCryptoAmount(amount);
Billing billing = new Billing(subscription, totalCost);
billingRepository.save(billing);
}
배치 작업을 통해 주기적으로 사용자 구독 청구를 처리하는 모듈이다. 일정 간격으로 트리거되며, 전체 구독 청구 내역을 업데이트한다.
SubscriptionBillingBatch.runBillingJob
메서드:@Scheduled(cron = "0 0 0 * * ?")
public void runBillingJob() {
List<User> users = userRepository.findAll();
users.forEach(user -> {
List<Subscriptions> subscriptions = subscriptionsRepository.findByUser(user);
subscriptions.forEach(subscription -> processSubscription(user, subscription.getCrypto().getSymbol(), subscription.getCryptoAmount()));
});
}
암호화폐 시세를 실시간으로 스트리밍하여 지갑 잔액과 거래 시스템에 반영하는 기능을 제공한다.
FinnhubWebSocketClient.connectAndStreamData
메서드:사용자 인증: AuthController
와 JwtUtil
이 사용자의 인증을 관리한다. 로그인 시 JWT 토큰을 발급하여 이후 세션을 유지하며, 이를 통해 사용자 권한을 관리한다.
지갑 및 거래 관리: UserService
와 WalletService
는 사용자의 지갑 정보를 조회하고 업데이트하며, 거래 요청에 따라 잔액을 갱신한다. 거래 요청은 TradeService
에서 처리되며, 유형에 따라 구매 또는 판매로 구분된다.
구독 및 청구 관리: 사용자가 구독을 설정하면 SubscriptionsService
가 구독 데이터를 저장하며, 주기적으로 배치 작업(SubscriptionBillingBatch)
이 실행되어 구독 청구 내역을 업데이트한다.
실시간 시세 반영: WebSocket을 통해 수신한 시세 데이터가 지갑과 거래 시스템에 반영되며, 이를 통해 최신 시세에 맞춘 거래가 가능해진다.
이렇게 프로젝트의 흐름을 파악하고 후에 CI/CD 과정 진행 중 CI에서는 테스트 코드가 필수적이기 때문에 테스트 코드를 작성하였다.
이제 배치 모듈의 성능을 향상시키고 추가적인 테스트를 진행해서 partitioning 멀티스레드를 도입해 볼 예정이다