티스토리 뷰
[Spring AI] (1) : Google Gemini 연동 - 뉴스 요약 구현
https://ekeprl.tistory.com/68 [Spring Security - JWT] (6) : 재발급 / 로그아웃 API + Postman 테스트https://ekeprl.tistory.com/67 [Spring Security - JWT] (5) : Refresh Token 구현https://ekeprl.tistory.com/66 [Spring Security - JWT] (4) : 로그
ekeprl.tistory.com
이전 포스팅에이어 이번에는 Yahoo Finance를 연동해 실시간 주가 조회기능을 구현해보려 한다.
1. build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux") // WebClient
}
먼저 WebClient 사용을 위한 의존성을 추가한다.
2. WebClientConfig.kt
@Configuration
class WebClientConfig {
@Bean
fun webClient(): WebClient {
return WebClient.create()
}
}
Config 클래스에서 Bean으로 등록하고, 필요한 곳에서 생성자를 주입받기 위해 작성했다.
3. StockModel 추가
data class StockModel(
val symbol: String,
val price: Double,
val change: Double, // 전일대비 가격
val changePercent: Double // 전일대비 %
)
data클래스를 추가해 주고,
4. StockService 작성
@Service
class StockService(
private val webClient: WebClient = WebClient.create()
) {
private val logger = LoggerFactory.getLogger(NewsService::class.java)
// 단일 종목 조회
fun fetchPrice(symbol: String): StockModel {
val url = "https://query1.finance.yahoo.com/v8/finance/chart/$symbol"
val response = webClient.get()
.uri(url)
.retrieve()
.bodyToMono(Map::class.java)
.block() ?: throw RuntimeException("$symbol 데이터 없음")
logger.info("RAW RESPONSE: $response") // 추가
val meta = ((response["chart"] as Map<*, *>)["result"] as List<*>)
.first()
.let { it as Map<*, *> }["meta"] as Map<*, *>
val price = (meta["regularMarketPrice"] as? Number)?.toDouble() ?: 0.0
val previousClose = (meta["previousClose"] as? Number)?.toDouble() ?: 0.0
val change = price - previousClose
val changePercent = if (previousClose != 0.0) (change / previousClose) * 100 else 0.0
return StockModel(
symbol = symbol,
price = price,
change = Math.round(change * 100) / 100.0,
changePercent = Math.round(changePercent * 100) / 100.0
)
}
// 여러 종목 동시 조회 - CompletableFuture
fun fetchPrices(symbols: List<String>): List<StockModel> {
val futures = symbols.map { symbol ->
CompletableFuture.supplyAsync { fetchPrice(symbol) }
}
CompletableFuture.allOf(*futures.toTypedArray()).join()
return futures.map { it.get() }
}
}
서비스에서는 단일 / 여러 종목을 동시 조회하는 두 가지의 메서드를 작성했다.
(1) 단일 종목 조회 > fetchPrice()
- webClient.get(). uri(url). retrieve() — Yahoo Finance 비공식 엔드포인트 호출한다.
- bodyToMono(Map::class.java) — 응답을 Map 형태로 받는다.
- . block() — 비동기 응답을 동기적으로 기다려서 값 추출한다.
Yahoo Finance의 응답은 chart > result > meta 구조로 중첩되어 있다. 따라서 필요한 값만 꺼내 사용한다.
** 또한 응답에 가격차이/백분율이 없어
change = 현재가 - 전일종가,changePercent = (change / 전일종가) * 100 두 가지를 이용해 계산했다. **
(2) 다중 종목 조회 > fetchPrices()
- symbols.map {... } — 종목 리스트를 순회하면서 각각 CompletableFuture.supplyAsync로 비동기 실행한다.
- 이 시점에서 모든 종목 조회가 동시에 시작된다 (별도 스레드)
- CompletableFuture.allOf(*futures.toTypedArray()). join() — 모든 Future가 끝날 때까지 대기한다.
- futures.map { it.get() } — 각 Future의 결괏값 추출한다.
** 순차로 호출하면 종목 수만큼 누적이 되지만, 병렬실행을 채택하여 가장 느린 요청 하나 기준으로 끝나도록 했다.
5. StockController
@RestController
@RequestMapping("/stock")
class StockController(
private val stockService: StockService
) {
// 단일 조회
@GetMapping("/{symbol}")
fun getPrice(@PathVariable symbol: String): ApiResponse<StockModel> {
return ApiResponse.success(stockService.fetchPrice(symbol))
}
// 다중 조회 - ?symbols=AAPL,BTC-USD,005930.KS
@GetMapping
fun getPrices(@RequestParam symbols: List<String>): ApiResponse<List<StockModel>> {
return ApiResponse.success(stockService.fetchPrices(symbols))
}
}
(1) 단일 조회 > GET방식 /stock/{symbol}
@PathVariable로 URL 경로의 {symbol} 값을 받아 StockService.fetchPrice()에 그대로 넘겨준다.
(2) 다중 조회 > GET방식 /stock? symbols=~~
@RequestParam으로 쿼리 파라미터를 받는데, 타입을 List <String>으로 받으면 Spring이 콤마(,)로 구분된 값을 자동으로
리스트로 변환해 준다.
6.ApiResponse.kt
data class ApiResponse<T>(
val success: Boolean,
val message: String?,
val data: T?,
) {
companion object {
fun <T> success(data: T? = null) = ApiResponse(
success = true,
message = null,
data = data,
)
fun error(message: String) = ApiResponse<Unit>(
success = false,
message = message,
data = null,
)
}
}
단일 / 다중 조회 두 메서드 전부 ApiResponce로 감싸서 반환한다.
따라서 일관된 포맷으로 통일해서 반환하게 작성했다.
7. 포스트맨 실행결과
(0) access Token발급
[Spring Security - JWT] (4) : 로그인 API 및 Postman 테스트
https://ekeprl.tistory.com/65 [Spring Security - JWT] (3) : JwtFilter이전 포스팅을 통해 JWT 토큰의 생성과 검증을 담당하는 JwtUtil을 작성했다.https://ekeprl.tistory.com/64 이번 포스팅은 매 요청마다 JWT 토큰을 검증
ekeprl.tistory.com
(1) 단일 조회


(2) 다중 조회


이상으로 포스팅을 마치겠습니다.
감사합니다.
'Project > Stockly' 카테고리의 다른 글
| [Stockly] (5) : React 프론트엔드 환경 설정 (0) | 2026.07.08 |
|---|---|
| [Spring AI] (4) : 텔레그램 봇 연동 - 목표가 알림 구현 (0) | 2026.07.07 |
| [Spring AI] (3) : 관심 종목(Watchlist) CRUD 구현 (0) | 2026.06.30 |
| [Spring AI] (1) : Google Gemini 연동 - 뉴스 요약 구현 (0) | 2026.05.07 |
- Total
- Today
- Yesterday
- GitHub
- Powershell
- insert
- 서버호스트
- Insert오류
- session저장
- 특일정보
- dbeaver
- Spring Security
- Postman
- CRUD
- Column count doesn't match value count at row 1
- dependencies
- springsecurity
- securityconfig
- JAR매니패스트
- kotlin
- null
- InetAddress
- 의존성
- ubuntu
- spring ai
- gradle
- Git
- jwt
- 백엔드
- 명령줄이 너무 깁니다
- AI
- 게시판
- MariaDB
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |