최종 프로젝트 4

이상민·2024년 10월 30일
0

우리 프로젝트는 암호화폐 거래와 구독 관리를 지원하는 웹 애플리케이션으로, 사용자 인증, 거래 및 지갑 관리, 구독 청구, 실시간 시세 스트리밍 기능을 포함하고 있다. 나는 배치 모듈 기능 구현을 함께 담당했지만, 프로젝트 전반적인 구조와 기능 이해와 프로젝트 진행사항에 대해서 스스로 더 명확하게 인지하기 위해서 주요 기능별 코드 예시를 포함하여 프로젝트 내용을 다시 정리하는 시간을 가졌다.

흐름 요약

먼저 우리 프로젝트의 흐름을 대략적으로 요약해보면

  1. 사용자 인증:
    사용자가 AuthController를 통해 로그인하면 JwtUtil로 JWT 토큰이 발급된다. 이 토큰은 프로젝트 전반에서 권한 관리를 위해 사용된다.

  2. 지갑 관리:
    사용자는 UserServiceWalletService를 통해 본인의 잔액을 확인하고 거래를 요청할 수 있다.

  3. 구독 및 청구:
    사용자는 SubscriptionsService에서 암호화폐 구독을 설정하며, 구독은 배치 작업을 통해 주기적으로 청구된다.

  4. 거래 처리:
    사용자가 TradeService를 통해 거래를 요청하면 매매가 실행되고, 실시간 가격 정보를 반영하여 지갑 잔액이 변동된다.

  5. 실시간 시세 데이터 업데이트: FinnhubWebSocketClient에서 WebSocket을 통해 시세 데이터가 스트리밍되고, 이를 사용해 거래와 지갑을 최신 상태로 유지한다.

주요 모듈 및 세부 코드 설명

1. 인증 및 권한 관리 모듈

  • 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();
}

2. 사용자 및 지갑 관리 모듈

이 모듈에서는 사용자의 정보와 지갑 정보를 조회하고, 거래 후 잔액을 관리한다.

  • 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);
}

3. 거래 처리 모듈

사용자의 거래 요청을 처리하고, 거래 내역을 저장하는 모듈이다.

  • 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()));
}

4. 구독 청구 처리 모듈

이 모듈은 사용자가 구독한 암호화폐의 가격 변동에 따라 청구 내역을 생성하고 업데이트하는 기능을 제공한다.

  • 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);
}

5. 배치 작업 모듈 - 구독 청구 처리

배치 작업을 통해 주기적으로 사용자 구독 청구를 처리하는 모듈이다. 일정 간격으로 트리거되며, 전체 구독 청구 내역을 업데이트한다.

  • 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()));
    });
}

6. 실시간 시세 스트리밍 모듈

암호화폐 시세를 실시간으로 스트리밍하여 지갑 잔액과 거래 시스템에 반영하는 기능을 제공한다.

  • FinnhubWebSocketClient.connectAndStreamData 메서드:
    WebSocket을 통해 외부 API와 연결하여 실시간 시세 데이터를 가져오는 메서드이다. 성공적으로 연결되면 비트코인(BTC) 시세 데이터를 구독한다.

전체 동작 흐름 요약

  1. 사용자 인증: AuthControllerJwtUtil이 사용자의 인증을 관리한다. 로그인 시 JWT 토큰을 발급하여 이후 세션을 유지하며, 이를 통해 사용자 권한을 관리한다.

  2. 지갑 및 거래 관리: UserServiceWalletService는 사용자의 지갑 정보를 조회하고 업데이트하며, 거래 요청에 따라 잔액을 갱신한다. 거래 요청은 TradeService에서 처리되며, 유형에 따라 구매 또는 판매로 구분된다.

  3. 구독 및 청구 관리: 사용자가 구독을 설정하면 SubscriptionsService가 구독 데이터를 저장하며, 주기적으로 배치 작업(SubscriptionBillingBatch)이 실행되어 구독 청구 내역을 업데이트한다.

  4. 실시간 시세 반영: WebSocket을 통해 수신한 시세 데이터가 지갑과 거래 시스템에 반영되며, 이를 통해 최신 시세에 맞춘 거래가 가능해진다.

이렇게 프로젝트의 흐름을 파악하고 후에 CI/CD 과정 진행 중 CI에서는 테스트 코드가 필수적이기 때문에 테스트 코드를 작성하였다.





할일

이제 배치 모듈의 성능을 향상시키고 추가적인 테스트를 진행해서 partitioning 멀티스레드를 도입해 볼 예정이다

profile
안녕하세요

0개의 댓글