├── .gitignore ├── README.md ├── bitcoin-api ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── org │ │ └── bitcoin │ │ └── api │ │ ├── BitcoinApiApplication.kt │ │ ├── controller │ │ ├── CoinSymbolController.kt │ │ └── dto │ │ │ ├── AddCoinSymbolResponse.kt │ │ │ └── AddCoinsymbolRequest.kt │ │ └── service │ │ ├── CoinSymbolService.kt │ │ └── dto │ │ └── OrderBookDepthRequest.kt │ └── resources │ └── application.yml ├── bitcoin-consumer ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── org │ │ └── bitcoin │ │ └── consumer │ │ ├── ConsumerApplication.kt │ │ ├── bithumb │ │ ├── BithumbConsumer.kt │ │ └── BithumbService.kt │ │ ├── config │ │ └── StompWebsocketConfig.kt │ │ ├── dto │ │ ├── BitumbOrderbookResponseDTO.kt │ │ └── UpbitOrderBookResponse.kt │ │ └── upbit │ │ ├── UpbitConsumer.kt │ │ └── UpbitService.kt │ └── resources │ └── application.yml ├── bitcoin-domain ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── org │ └── bitcoin │ └── domain │ ├── bithumb │ ├── request │ │ ├── BithumbSocket.kt │ │ └── CoinSymbol.kt │ └── response │ │ ├── BithumbOrderBookDepth.kt │ │ ├── BithumbTicker.kt │ │ └── BitumbOrderbook.kt │ ├── type │ └── ExchangeType.kt │ └── upbit │ ├── OrderBookResponse.kt │ ├── SocketRequest.kt │ └── TickerResponse.kt ├── bitcoin-infrastructure ├── bitcoin-external-data │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── kotlin │ │ │ └── org │ │ │ └── bitcoin │ │ │ └── external │ │ │ ├── ExternalApplication.kt │ │ │ └── bithumb │ │ │ ├── path │ │ │ └── BitcoinWebsocketPath.kt │ │ │ └── webflux │ │ │ ├── config │ │ │ └── WebClientConfig.kt │ │ │ └── fetcher │ │ │ └── BithumbFetcher.kt │ │ └── test │ │ └── kotlin │ │ └── org │ │ └── bitcoin │ │ └── external │ │ ├── ExternalApplicationTests.kt │ │ └── fetcher │ │ └── WebClientFetcherTest.kt ├── bitcoin-infrastructure-jpa │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── kotlin │ │ └── org │ │ │ └── bitcoin │ │ │ └── infrastructure │ │ │ └── jpa │ │ │ ├── JpaApplication.kt │ │ │ └── bithumb │ │ │ ├── entity │ │ │ ├── JpaCoinSymbol.kt │ │ │ ├── JpaCoinSymbolRepository.kt │ │ │ ├── JpaOrderBookAsks.kt │ │ │ ├── JpaOrderBookAsksRepository.kt │ │ │ ├── JpaOrderBookBids.kt │ │ │ ├── JpaOrderBookBidsRepository.kt │ │ │ ├── JpaOverBook.kt │ │ │ └── JpaOverBookRepository.kt │ │ │ └── service │ │ │ └── CoinSymbolRepository.kt │ │ └── resources │ │ └── application-jpa.yml ├── bitcoin-infrastructure-kafka │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── resources │ │ └── application-kafka.yml └── bitcoin-redis │ ├── bitcoin-redis-publish │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── org │ │ └── bitcoin │ │ └── redispublish │ │ ├── config │ │ └── RedisPublishConfig.kt │ │ └── publish │ │ └── RedisPublishService.kt │ └── bitcoin-redis-subscribe │ ├── build.gradle.kts │ └── src │ └── main │ └── kotlin │ └── org │ └── bitcoin │ └── redissubscribe │ ├── config │ └── RedisSubscribeConfig.kt │ ├── listener │ └── RedisSubscribeListener.kt │ ├── scheduler │ └── ChannelSubscribeScheduler.kt │ └── service │ ├── BithumbChannelSubscribeService.kt │ ├── KobitChannelSubscribeService.kt │ └── UpbitChannelSubscribeService.kt ├── bitcoin-producer ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── org │ │ └── bitcoin │ │ └── producer │ │ ├── ProducerApplication.kt │ │ ├── polling │ │ ├── reader │ │ │ └── BithumbReader.kt │ │ └── scheduler │ │ │ └── BithumbScheduler.kt │ │ └── socket │ │ └── BithumbSocketPublish.kt │ └── resources │ └── application.yml ├── bitcoin-websocket ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── org │ │ └── bitcoin │ │ └── websocket │ │ ├── BitcoinWebsocketApplication.kt │ │ ├── bithumb │ │ ├── handler │ │ │ └── BithumbSocketHandler.kt │ │ └── service │ │ │ └── BithumbService.kt │ │ └── upbit │ │ ├── handler │ │ └── UpbitSocketHandler.kt │ │ └── service │ │ └── UpbitService.kt │ └── resources │ └── application.yml ├── build.gradle.kts ├── db ├── init.d │ └── init.sql └── mysql-dockerfile ├── docker-compose.yml ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | gradle 40 | build 41 | 42 | kafka 43 | zookeeper -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kafka-bitcoin-stream 2 | 실시간 비트코인 변동데이터를 통한 카프카 적용사례 구현 ( 빗썸, 업비트, 코빗 ) 3 | 4 | part 1 블로그 포스팅 5 | https://joecp17.tistory.com/78 6 | -------------------------------------------------------------------------------- /bitcoin-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | val jar: Jar by tasks 4 | val bootJar: BootJar by tasks 5 | 6 | bootJar.enabled = true 7 | jar.enabled = false 8 | 9 | apply(plugin = "org.springframework.boot") 10 | 11 | dependencies { 12 | implementation(project(":bitcoin-infrastructure:bitcoin-infrastructure-jpa")) 13 | implementation(project(":bitcoin-infrastructure:bitcoin-external-data")) 14 | implementation(project(":bitcoin-domain")) 15 | 16 | // spring boot 17 | implementation("org.springframework.boot:spring-boot-starter-web") 18 | 19 | } -------------------------------------------------------------------------------- /bitcoin-api/src/main/kotlin/org/bitcoin/api/BitcoinApiApplication.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.api 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication(scanBasePackages = ["org.bitcoin"]) 7 | class BitcoinApiApplication 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } 12 | -------------------------------------------------------------------------------- /bitcoin-api/src/main/kotlin/org/bitcoin/api/controller/CoinSymbolController.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.api.controller 2 | 3 | import org.bitcoin.api.controller.dto.AddCoinSymbolResponse 4 | import org.bitcoin.api.controller.dto.AddCoinsymbolRequest 5 | import org.bitcoin.api.service.CoinSymbolService 6 | import org.springframework.web.bind.annotation.PostMapping 7 | import org.springframework.web.bind.annotation.RequestBody 8 | import org.springframework.web.bind.annotation.RequestMapping 9 | import org.springframework.web.bind.annotation.RestController 10 | 11 | @RestController 12 | @RequestMapping("/api/v1") 13 | class CoinSymbolController( 14 | val coinSymbolService: CoinSymbolService 15 | ) { 16 | 17 | @PostMapping("/add/coin-symbol") 18 | fun addCoinSymbol(@RequestBody symbol: AddCoinsymbolRequest): AddCoinSymbolResponse { 19 | return coinSymbolService.addCoinSymbol(symbol) 20 | } 21 | } -------------------------------------------------------------------------------- /bitcoin-api/src/main/kotlin/org/bitcoin/api/controller/dto/AddCoinSymbolResponse.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.api.controller.dto 2 | 3 | import org.bitcoin.domain.bithumb.request.CoinSymbol 4 | import org.bitcoin.domain.type.ExchangeType 5 | 6 | data class AddCoinSymbolResponse( 7 | val id: Long, 8 | val symbol: String, 9 | var channel: String = "", 10 | val exchange: ExchangeType, // bithumb, upbit, kobit 11 | val createdDate: String, 12 | val updatedDate: String 13 | ) { 14 | companion object { 15 | fun of(coinSymbol: CoinSymbol): AddCoinSymbolResponse { 16 | return AddCoinSymbolResponse( 17 | id = coinSymbol.id!!, 18 | symbol = coinSymbol.symbol, 19 | channel = coinSymbol.channel, 20 | exchange = coinSymbol.exchange, 21 | createdDate = coinSymbol.createdDate.toString(), 22 | updatedDate = coinSymbol.updatedDate.toString() 23 | ) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /bitcoin-api/src/main/kotlin/org/bitcoin/api/controller/dto/AddCoinsymbolRequest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.api.controller.dto 2 | 3 | import org.bitcoin.domain.bithumb.request.CoinSymbol 4 | import org.bitcoin.domain.type.ExchangeType 5 | import org.bitcoin.infrastructure.jpa.bithumb.entity.JpaCoinSymbol 6 | import java.time.LocalDateTime 7 | 8 | data class AddCoinsymbolRequest( 9 | val symbol: String, 10 | val exchange: String // bithumb, upbit, kobit 11 | ) { 12 | private val topicPath = "/symbol/" 13 | 14 | fun convertToCoinSymbol(): CoinSymbol { 15 | return CoinSymbol( 16 | symbol = this.symbol, 17 | channel = topicPath.plus(this.exchange).plus("/").plus(this.symbol), 18 | exchange = ExchangeType.valueOf(this.exchange), 19 | createdDate = LocalDateTime.now(), 20 | updatedDate = LocalDateTime.now() 21 | ) 22 | } 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /bitcoin-api/src/main/kotlin/org/bitcoin/api/service/CoinSymbolService.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.api.service 2 | 3 | import org.bitcoin.api.controller.dto.AddCoinSymbolResponse 4 | import org.bitcoin.api.controller.dto.AddCoinsymbolRequest 5 | import org.bitcoin.domain.bithumb.request.CoinSymbol 6 | import org.bitcoin.infrastructure.jpa.bithumb.service.CoinSymbolRepository 7 | import org.springframework.stereotype.Service 8 | import org.springframework.transaction.annotation.Transactional 9 | 10 | @Service 11 | @Transactional(readOnly = true) 12 | class CoinSymbolService( 13 | private val coinSymbolRepository: CoinSymbolRepository 14 | ) { 15 | 16 | @Transactional 17 | fun addCoinSymbol(symbol: AddCoinsymbolRequest): AddCoinSymbolResponse { 18 | saveBitcoinSymbol(symbol.convertToCoinSymbol()).let { coinSymbol -> 19 | return AddCoinSymbolResponse.of(coinSymbol) 20 | } 21 | } 22 | 23 | private fun saveBitcoinSymbol(bitcoinSymbol: CoinSymbol): CoinSymbol { 24 | return coinSymbolRepository.save(bitcoinSymbol) 25 | } 26 | } -------------------------------------------------------------------------------- /bitcoin-api/src/main/kotlin/org/bitcoin/api/service/dto/OrderBookDepthRequest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.api.service.dto 2 | 3 | data class OrderBookDepthRequest( 4 | val type: String = "orderbookdepth", 5 | val symbols: List 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /bitcoin-api/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | include: jpa, kafka -------------------------------------------------------------------------------- /bitcoin-consumer/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | val jar: Jar by tasks 4 | val bootJar: BootJar by tasks 5 | 6 | bootJar.enabled = true 7 | jar.enabled = false 8 | 9 | apply(plugin = "org.springframework.boot") 10 | 11 | dependencies { 12 | implementation(project(":bitcoin-infrastructure:bitcoin-infrastructure-kafka")) 13 | implementation(project(":bitcoin-infrastructure:bitcoin-infrastructure-jpa")) 14 | implementation(project(":bitcoin-domain")) 15 | 16 | implementation("org.springframework.boot:spring-boot-starter-web") 17 | 18 | // websocket 19 | implementation ("org.springframework.boot:spring-boot-starter-websocket") 20 | implementation("org.webjars:sockjs-client:1.1.2") 21 | implementation("org.webjars:stomp-websocket:2.3.3-1") 22 | } -------------------------------------------------------------------------------- /bitcoin-consumer/src/main/kotlin/org/bitcoin/consumer/ConsumerApplication.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.consumer 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication(scanBasePackages = ["org.bitcoin"]) 7 | class ConsumerApplication 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } 12 | -------------------------------------------------------------------------------- /bitcoin-consumer/src/main/kotlin/org/bitcoin/consumer/bithumb/BithumbConsumer.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.consumer.bithumb 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.bitcoin.domain.bithumb.response.BithumbTicker 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.kafka.annotation.KafkaListener 7 | 8 | @Configuration 9 | class BithumbConsumer( 10 | val objectMapper: ObjectMapper, 11 | val bithumbService: BithumbService 12 | ) { 13 | 14 | // TODO : 웹소켓을 연동하여 데이터 보내기 15 | 16 | @KafkaListener(topics = ["bithumb"], groupId = "bitcoin") 17 | fun getBitumbOrderBookData(message: String) { 18 | val deserializeData = 19 | objectMapper.deserialize(message, BithumbTicker::class.java) 20 | println(deserializeData) 21 | bithumbService.sendMessageToUser(deserializeData) 22 | } 23 | 24 | @KafkaListener(topics = ["bithumb-stream"], groupId = "bitcoin") 25 | fun getBitumbOrderBookStreamData(message: String) { 26 | val deserializeData = 27 | objectMapper.deserialize(message, BithumbTicker::class.java) 28 | println(deserializeData) 29 | bithumbService.sendMessageToUser(deserializeData) 30 | } 31 | 32 | fun ObjectMapper.deserialize(data: String, clazz: Class): T = readValue(data, clazz) 33 | 34 | } -------------------------------------------------------------------------------- /bitcoin-consumer/src/main/kotlin/org/bitcoin/consumer/bithumb/BithumbService.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.consumer.bithumb 2 | 3 | import org.bitcoin.consumer.dto.BitumbOrderbookResponseDTO 4 | import org.bitcoin.domain.bithumb.response.BithumbTicker 5 | import org.bitcoin.infrastructure.jpa.bithumb.entity.* 6 | import org.springframework.messaging.simp.SimpMessageSendingOperations 7 | import org.springframework.stereotype.Service 8 | import org.springframework.transaction.annotation.Transactional 9 | 10 | @Service 11 | @Transactional(readOnly = true) 12 | class BithumbService( 13 | val orderBookAsksRepository: JpaOrderBookAsksRepository, 14 | val orderBookBidsRepository: JpaOrderBookBidsRepository, 15 | val overBookRepository: JpaOverBookRepository, 16 | val simpMessageSendingOperations: SimpMessageSendingOperations 17 | ) { 18 | 19 | fun sendMessageToUser(response: BithumbTicker) { 20 | simpMessageSendingOperations.convertAndSend("/topic/bithumb", response) 21 | } 22 | 23 | @Transactional 24 | fun saveOrderBookData(response: BitumbOrderbookResponseDTO) { 25 | val findOrderBook = findOrderBookByOrderCurrency(response.data.orderCurrency) 26 | 27 | findOrderBook?.run { 28 | saveOrderBookAsks(response, findOrderBook) 29 | saveOrderBookBids(response, findOrderBook) 30 | 31 | } ?: run { 32 | val overBook = saveOverBook(response) 33 | saveOrderBookAsks(response, overBook) 34 | saveOrderBookBids(response, overBook) 35 | } 36 | } 37 | 38 | private fun saveOverBook(response: BitumbOrderbookResponseDTO): JpaOverBook { 39 | return toOverBookEntityBy(response).let { overBook -> 40 | overBookRepository.save(overBook) 41 | } 42 | } 43 | 44 | private fun saveOrderBookAsks(response: BitumbOrderbookResponseDTO, overBook: JpaOverBook) { 45 | response.data.asks.forEach { ask -> 46 | toAsksEntityBy(ask.quantity, ask.price, overBook).let { overbookAsks -> 47 | orderBookAsksRepository.save(overbookAsks) 48 | } 49 | } 50 | } 51 | 52 | private fun saveOrderBookBids(response: BitumbOrderbookResponseDTO, overBook: JpaOverBook) { 53 | response.data.bids.forEach { bids -> 54 | toBidsEntityBy(bids.quantity, bids.price, overBook).let { bidsEntity -> 55 | orderBookBidsRepository.save(bidsEntity) 56 | } 57 | } 58 | } 59 | 60 | private fun toOverBookEntityBy(response: BitumbOrderbookResponseDTO): JpaOverBook = 61 | response.data.toOverBookEntity() 62 | 63 | 64 | private fun toAsksEntityBy(quantity: String, price: String, overBook: JpaOverBook): JpaOrderBookAsks = 65 | JpaOrderBookAsks(quantity = quantity, 66 | price = price, 67 | overBookId = overBook) 68 | 69 | 70 | private fun toBidsEntityBy(quantity: String, price: String, overBook: JpaOverBook): JpaOrderBookBids = 71 | JpaOrderBookBids(quantity = quantity, 72 | price = price, 73 | overBookId = overBook) 74 | 75 | 76 | 77 | // 값이 없을 경우 save 처리를 하기 위해 ?로 설정 78 | private fun findOrderBookByOrderCurrency(symbol: String): JpaOverBook? = 79 | overBookRepository.findByOrderCurrency(symbol) 80 | 81 | } -------------------------------------------------------------------------------- /bitcoin-consumer/src/main/kotlin/org/bitcoin/consumer/config/StompWebsocketConfig.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.consumer.config 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | class StompWebsocketConfig: WebSocketMessageBrokerConfigurer { 12 | 13 | override fun registerStompEndpoints(registry: StompEndpointRegistry) { 14 | registry.addEndpoint("/ws").setAllowedOriginPatterns("*") 15 | } 16 | 17 | override fun configureMessageBroker(registry: MessageBrokerRegistry) { 18 | registry.enableSimpleBroker("/queue", "/topic") 19 | registry.setApplicationDestinationPrefixes("/app") 20 | } 21 | } -------------------------------------------------------------------------------- /bitcoin-consumer/src/main/kotlin/org/bitcoin/consumer/dto/BitumbOrderbookResponseDTO.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.consumer.dto 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | import org.bitcoin.infrastructure.jpa.bithumb.entity.JpaOrderBookAsks 5 | import org.bitcoin.infrastructure.jpa.bithumb.entity.JpaOverBook 6 | import java.time.LocalDateTime 7 | 8 | data class BitumbOrderbookResponseDTO( 9 | val status: String, 10 | val data: BithumbOrderbookDataDTO 11 | ) { 12 | data class BithumbOrderbookDataDTO( 13 | val timestamp: String, 14 | @JsonProperty("payment_currency") 15 | val paymentCurrency: String, // 주문통화 ( 코인 ) 16 | @JsonProperty("order_currency") 17 | val orderCurrency: String, // 결제 통화 ( 마켓 ) 18 | val bids: List, // 매수 요청 내역 19 | val asks: List // 매도 요청 내역 20 | ) { 21 | 22 | data class BithumbOrderbookBidsDTO( 23 | val quantity: String, // 수량 24 | val price: String // 가격 25 | ) 26 | 27 | data class BithumbOrderbookAsksDTO( 28 | val quantity: String, 29 | val price: String 30 | ) { 31 | companion object { 32 | fun toEntity(asks: BithumbOrderbookAsksDTO, overBook: JpaOverBook): JpaOrderBookAsks { 33 | return JpaOrderBookAsks( 34 | quantity = asks.quantity, 35 | price = asks.price, 36 | overBookId = overBook, 37 | createdDate = LocalDateTime.now(), 38 | updatedDate = LocalDateTime.now() 39 | ) 40 | } 41 | } 42 | } 43 | fun toOverBookEntity(): JpaOverBook { 44 | return JpaOverBook( 45 | timestamp = this.timestamp, 46 | paymentCurrency = this.paymentCurrency, 47 | orderCurrency = this.orderCurrency, 48 | createdDate = LocalDateTime.now(), 49 | updatedDate = LocalDateTime.now() 50 | ) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /bitcoin-consumer/src/main/kotlin/org/bitcoin/consumer/dto/UpbitOrderBookResponse.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.consumer.dto 2 | 3 | data class UpbitOrderBookResponse ( 4 | val type: String, 5 | val code: String, 6 | val timestamp: Long, 7 | val totalAskSize: Double, 8 | val totalBidSize: Double, 9 | val orderbook_units: List 10 | ) { 11 | data class OrderbookUnit ( 12 | val askPrice: Double, 13 | val bidPrice: Double, 14 | val askSize: Double, 15 | val bidSize: Double 16 | ) { 17 | } 18 | } -------------------------------------------------------------------------------- /bitcoin-consumer/src/main/kotlin/org/bitcoin/consumer/upbit/UpbitConsumer.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.consumer.upbit 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.bitcoin.domain.upbit.TickerResponse 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.kafka.annotation.KafkaListener 7 | 8 | @Configuration 9 | class UpbitConsumer( 10 | val objectMapper: ObjectMapper, 11 | val upbitService: UpbitService 12 | ) { 13 | 14 | @KafkaListener(topics = ["upbit-stream"], groupId = "bitcoin") 15 | fun getUpbitOrderBookData(message: String) { 16 | val deserializeData = 17 | objectMapper.deserialize(message, TickerResponse::class.java) 18 | println(deserializeData) 19 | upbitService.sendMessageToUser(deserializeData) 20 | } 21 | 22 | 23 | fun ObjectMapper.deserialize(data: String, clazz: Class): T = readValue(data, clazz) 24 | } -------------------------------------------------------------------------------- /bitcoin-consumer/src/main/kotlin/org/bitcoin/consumer/upbit/UpbitService.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.consumer.upbit 2 | 3 | import org.bitcoin.domain.upbit.TickerResponse 4 | import org.bitcoin.infrastructure.jpa.bithumb.entity.JpaOrderBookAsksRepository 5 | import org.bitcoin.infrastructure.jpa.bithumb.entity.JpaOrderBookBidsRepository 6 | import org.bitcoin.infrastructure.jpa.bithumb.entity.JpaOverBookRepository 7 | import org.springframework.messaging.simp.SimpMessageSendingOperations 8 | import org.springframework.stereotype.Service 9 | import org.springframework.transaction.annotation.Transactional 10 | 11 | @Service 12 | @Transactional 13 | class UpbitService( 14 | val orderBookAsksRepository: JpaOrderBookAsksRepository, 15 | val orderBookBidsRepository: JpaOrderBookBidsRepository, 16 | val overBookRepository: JpaOverBookRepository, 17 | val simpMessageSendingOperations: SimpMessageSendingOperations 18 | ) { 19 | fun sendMessageToUser(response: TickerResponse) { 20 | simpMessageSendingOperations.convertAndSend("/topic/upbit", response) 21 | } 22 | } -------------------------------------------------------------------------------- /bitcoin-consumer/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | include: jpa, kafka 4 | 5 | server: 6 | port: 8082 -------------------------------------------------------------------------------- /bitcoin-domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | val jar: Jar by tasks 4 | val bootJar: BootJar by tasks 5 | 6 | bootJar.enabled = true 7 | jar.enabled = false 8 | 9 | dependencies { 10 | } -------------------------------------------------------------------------------- /bitcoin-domain/src/main/kotlin/org/bitcoin/domain/bithumb/request/BithumbSocket.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.domain.bithumb.request 2 | 3 | // 변경호가에 대한 Request 요청 4 | data class BithumbSocket( 5 | val type: String, 6 | val symbols: List, // ex) ["BTC_KRW", "ETH_KRW"] 7 | val tickTypes: List // ex) ["1H", "24H"] 8 | ) { 9 | 10 | companion object { 11 | fun createOrderBookDepthRequest(symbols: List): BithumbSocket { 12 | return BithumbSocket( 13 | type = "orderbookdepth", 14 | symbols = addKRW(symbols), 15 | tickTypes = listOf("1H", "24H") 16 | ) 17 | } 18 | 19 | fun createTickerRequest(symbols: List): BithumbSocket { 20 | return BithumbSocket( 21 | type = "ticker", 22 | symbols = addKRW(symbols), 23 | tickTypes = listOf("1H", "24H") 24 | ) 25 | } 26 | 27 | private fun addKRW(symbols: List): List { 28 | return symbols.map { "${it.symbol}_KRW" } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /bitcoin-domain/src/main/kotlin/org/bitcoin/domain/bithumb/request/CoinSymbol.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.domain.bithumb.request 2 | 3 | import org.bitcoin.domain.type.ExchangeType 4 | import java.time.LocalDateTime 5 | 6 | data class CoinSymbol( 7 | var id: Long? = null, 8 | 9 | var symbol: String, 10 | 11 | var channel: String, 12 | 13 | var exchange: ExchangeType, // bithumb, upbit, kobit 14 | 15 | var createdDate: LocalDateTime, 16 | 17 | var updatedDate: LocalDateTime, 18 | ) -------------------------------------------------------------------------------- /bitcoin-domain/src/main/kotlin/org/bitcoin/domain/bithumb/response/BithumbOrderBookDepth.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.domain.bithumb.response 2 | 3 | // 변경호가에 대한 Response 응답 4 | // 참고 :https://apidocs.bithumb.com/reference/%EB%B9%97%EC%8D%B8-%EA%B1%B0%EB%9E%98%EC%86%8C-%EC%A0%95%EB%B3%B4-%EC%88%98%EC%8B%A0 5 | data class BithumbOrderBookDepth ( 6 | val type: String, 7 | val content: ContentDTO 8 | ) { 9 | 10 | data class ContentDTO ( 11 | val datetime: String, 12 | val list: MutableList 13 | ) 14 | 15 | data class listItem( 16 | val total: String, 17 | val orderType: String, 18 | val quantity: String, 19 | val price: String, 20 | val symbol: String 21 | ) 22 | } -------------------------------------------------------------------------------- /bitcoin-domain/src/main/kotlin/org/bitcoin/domain/bithumb/response/BithumbTicker.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.domain.bithumb.response 2 | 3 | data class BithumbTicker( 4 | val type: String, 5 | val content: TickerResponse 6 | ) { 7 | 8 | data class TickerResponse( 9 | val volumePower: String, 10 | val chgAmt: String, 11 | val chgRate: String, 12 | val prevClosePrice: String, 13 | val buyVolume: String, 14 | val sellVolume: String, 15 | val volume: String, 16 | val value: String, 17 | val highPrice: String, 18 | val lowPrice: String, 19 | val closePrice: String, 20 | val openPrice: String, 21 | val time: String, 22 | val date: String, 23 | val tickType: String, 24 | val symbol: String 25 | ) { 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /bitcoin-domain/src/main/kotlin/org/bitcoin/domain/bithumb/response/BitumbOrderbook.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.domain.bithumb.response 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | data class BitumbOrderbook( 6 | val status: String, 7 | val data: bithumbOrderbookDataDTO 8 | ) { 9 | data class bithumbOrderbookDataDTO( 10 | val timestamp: String, 11 | @JsonProperty("payment_currency") 12 | val paymentCurrency: String, // 주문통화 ( 코인 ) 13 | @JsonProperty("order_currency") 14 | val orderCurrency: String, // 결제 통화 ( 마켓 ) 15 | val bids: List, // 매수 요청 내역 16 | val asks: List // 매도 요청 내역 17 | ) { 18 | 19 | data class bithumbOrderbookBidsDTO( 20 | val quantity: String, // 수량 21 | val price: String // 가격 22 | ) 23 | 24 | data class bithumbOrderbookAsksDTO( 25 | val quantity: String, 26 | val price: String 27 | ) 28 | } 29 | } -------------------------------------------------------------------------------- /bitcoin-domain/src/main/kotlin/org/bitcoin/domain/type/ExchangeType.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.domain.type 2 | 3 | enum class ExchangeType( 4 | val exchange: String 5 | ) { 6 | BITHUMB("bithumb"), 7 | BITHUMB_STREAM("bithumb-stream"), 8 | UPBIT("upbit"), 9 | UPBIT_STREAM("upbit-stream"), 10 | KOBIT("kobit"), 11 | NONE("none") 12 | } -------------------------------------------------------------------------------- /bitcoin-domain/src/main/kotlin/org/bitcoin/domain/upbit/OrderBookResponse.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.domain.upbit 2 | 3 | data class OrderBookResponse ( 4 | val type: String, 5 | val code: String, 6 | val timestamp: Long, 7 | val total_ask_size: Double, 8 | val total_bid_size: Double, 9 | val orderbook_units: List 10 | ) { 11 | data class OrderbookUnit ( 12 | val ask_price: Double, 13 | val bid_price: Double, 14 | val ask_size: Double, 15 | val bid_size: Double 16 | ) { 17 | } 18 | } -------------------------------------------------------------------------------- /bitcoin-domain/src/main/kotlin/org/bitcoin/domain/upbit/SocketRequest.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.domain.upbit 2 | 3 | data class SocketRequest( 4 | val ticket: Ticket, 5 | val type: Type, 6 | val format: Format 7 | ) { 8 | data class Ticket ( 9 | val ticket: String = "test example" 10 | ) { 11 | } 12 | 13 | data class Type ( 14 | val type: String, 15 | val codes: List 16 | ) { 17 | } 18 | 19 | data class Format ( 20 | val format: String = "DEFAULT" 21 | ) { 22 | } 23 | 24 | companion object { 25 | fun createRequest(codes: List): SocketRequest { 26 | return SocketRequest( 27 | ticket = Ticket(), 28 | type = Type("orderbook" ,addKRW(codes)), 29 | format = Format("DEFAULT") 30 | ) 31 | } 32 | 33 | private fun addKRW(codes: List): List { 34 | return codes.map { code -> "KRW-$code" } 35 | } 36 | 37 | fun createBufferRequest(type: String, codes: List): ByteArray { 38 | val requestValue = StringBuilder().apply { 39 | append("[{\"ticket\":\"test example\"},{\"type\":$type,\"codes\":[") 40 | codes.forEachIndexed { index, code -> 41 | append("\"KRW-$code\"") 42 | if (index != codes.size - 1) { 43 | append(",") 44 | } 45 | } 46 | append("]},{\"format\":\"DEFAULT\"}]") 47 | } 48 | 49 | return requestValue.toString().toByteArray() 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bitcoin-domain/src/main/kotlin/org/bitcoin/domain/upbit/TickerResponse.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.domain.upbit 2 | 3 | data class TickerResponse( 4 | val type: String, 5 | val code: String, 6 | val opening_price: Long, 7 | val high_price: Long, 8 | val low_price: Long, 9 | val prev_closing_price: Long, 10 | val acc_trade_price: Long, 11 | val change: String, 12 | val change_price: Double, 13 | val signed_change_price: Double, 14 | val change_rate: Double, 15 | val signed_change_rate: Double, 16 | val ask_bid: String, 17 | val trade_volume: Double, 18 | val acc_trade_volume: Double, 19 | val trade_date: String, 20 | val trade_time: String, 21 | val trade_timestamp: Long, 22 | val acc_ask_volume: Double, 23 | val acc_bid_volume: Double, 24 | val highest_52_week_price: Double, 25 | val highest_52_week_date: String, 26 | val lowest_52_week_price: Double, 27 | val lowest_52_week_date: String, 28 | val market_state: String, 29 | val is_trading_suspended: Boolean, 30 | val delisting_date: String?, 31 | val market_warning: String, 32 | val timestamp: Long, 33 | val acc_trade_price_24h: Double, 34 | val acc_trade_volume_24h: Double, 35 | val stream_type: String 36 | ) { 37 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-external-data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | val jar: Jar by tasks 4 | val bootJar: BootJar by tasks 5 | 6 | bootJar.enabled = true 7 | jar.enabled = false 8 | 9 | apply(plugin = "org.springframework.boot") 10 | 11 | dependencies { 12 | implementation(project(":bitcoin-domain")) 13 | implementation ("org.springframework.boot:spring-boot-starter-webflux") 14 | implementation("org.springframework.boot:spring-boot-starter-web") 15 | 16 | // websocket 17 | implementation ("org.springframework.boot:spring-boot-starter-websocket") 18 | 19 | // DNS Resorver 20 | implementation ("io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64") 21 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-external-data/src/main/kotlin/org/bitcoin/external/ExternalApplication.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.external 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class ExternalApplication 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } 12 | -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-external-data/src/main/kotlin/org/bitcoin/external/bithumb/path/BitcoinWebsocketPath.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.external.bithumb.path 2 | 3 | class BitcoinWebsocketPath { 4 | 5 | companion object { 6 | const val bithumb: String = "wss://pubwss.bithumb.com/pub/ws" 7 | 8 | const val upbit: String = "wss://api.upbit.com/websocket/v1" 9 | } 10 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-external-data/src/main/kotlin/org/bitcoin/external/bithumb/webflux/config/WebClientConfig.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.external.bithumb.webflux.config 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.http.HttpHeaders 6 | import org.springframework.http.MediaType 7 | import org.springframework.web.reactive.function.client.WebClient 8 | 9 | @Configuration 10 | class WebClientConfig { 11 | 12 | /** 13 | * @description: bithumb 호가정보 조회 14 | */ 15 | @Bean 16 | fun bitumbWebClient(): WebClient { 17 | return WebClient.builder() 18 | .baseUrl("https://api.bithumb.com/public/orderbook/") 19 | .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 20 | .build() 21 | } 22 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-external-data/src/main/kotlin/org/bitcoin/external/bithumb/webflux/fetcher/BithumbFetcher.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.external.bithumb.webflux.fetcher 2 | 3 | import org.bitcoin.domain.bithumb.response.BitumbOrderbook 4 | import org.springframework.beans.factory.annotation.Qualifier 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.web.reactive.function.client.WebClient 7 | 8 | @Configuration 9 | class BithumbFetcher( 10 | 11 | @Qualifier("bitumbWebClient") 12 | val bitumbWebClient: WebClient 13 | ) { 14 | 15 | fun getBitumbOrderbook(code: String): BitumbOrderbook { 16 | return bitumbWebClient.get() 17 | .uri{builder -> builder.path("/$code").build()} 18 | .retrieve() 19 | .bodyToMono(BitumbOrderbook::class.java) 20 | .block()!! 21 | } 22 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-external-data/src/test/kotlin/org/bitcoin/external/ExternalApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.external 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.springframework.boot.test.context.SpringBootTest 5 | 6 | @SpringBootTest 7 | class ExternalApplicationTests { 8 | 9 | @Test 10 | fun contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-external-data/src/test/kotlin/org/bitcoin/external/fetcher/WebClientFetcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.kafka.scheduledata.fetcher 2 | 3 | import org.assertj.core.api.Assertions 4 | import org.bitcoin.external.ExternalApplication 5 | import org.bitcoin.external.bithumb.webflux.fetcher.BithumbFetcher 6 | import org.junit.jupiter.api.BeforeEach 7 | import org.junit.jupiter.api.DisplayName 8 | import org.junit.jupiter.api.Test 9 | import org.springframework.beans.factory.annotation.Autowired 10 | import org.springframework.boot.test.context.SpringBootTest 11 | import org.springframework.http.HttpHeaders 12 | import org.springframework.http.MediaType 13 | import org.springframework.web.reactive.function.client.WebClient 14 | 15 | /** 16 | * 참조한 링크 : https://minkukjo.github.io/framework/2020/06/28/JUnit-23/ 17 | * 생성자 주입 시, 클래스에 @Autowired 를 붙여주게되면 선언되어있는 constructor가 자동으로 주입되게 된다. 18 | */ 19 | @SpringBootTest(classes = [ExternalApplication::class, BithumbFetcher::class]) 20 | @DisplayName("WebClientFetcher 테스트") 21 | class WebClientFetcherTest @Autowired constructor( 22 | var webClient: WebClient, 23 | var bithumbFetcher: BithumbFetcher 24 | ) { 25 | 26 | lateinit var code: String 27 | 28 | @BeforeEach 29 | fun setUp() { 30 | code = "BTC" 31 | 32 | webClient = WebClient.builder() 33 | .baseUrl("https://api.bithumb.com/public/orderbook/") 34 | .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 35 | .build() 36 | 37 | bithumbFetcher = BithumbFetcher(webClient) 38 | } 39 | 40 | @Test 41 | @DisplayName("빗썸 API 호출 테스트") 42 | fun bithumbFetcherTest() { 43 | // when & then 44 | val bitumbOrderbook = bithumbFetcher.getBitumbOrderbook(code) 45 | Assertions.assertThat(bitumbOrderbook.status).isEqualTo("0000") 46 | } 47 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | import java.util.regex.Pattern.compile 3 | 4 | val jar: Jar by tasks 5 | val bootJar: BootJar by tasks 6 | 7 | bootJar.enabled = true 8 | jar.enabled = false 9 | 10 | apply(plugin = "org.springframework.boot") 11 | 12 | dependencies { 13 | implementation(project(":bitcoin-domain")) 14 | 15 | //spring boot 16 | api("org.springframework.boot:spring-boot-starter-data-jpa") 17 | 18 | //DB connect 19 | runtimeOnly("com.h2database:h2") 20 | runtimeOnly("mysql:mysql-connector-java") 21 | 22 | // entity 23 | allOpen { 24 | annotation("javax.persistence.Entity") 25 | annotation("javax.persistence.MappedSuperclass") 26 | annotation("javax.persistence.Embeddable") 27 | } 28 | compile("javax.xml.bind:jaxb-api:2.3.0") 29 | 30 | 31 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/src/main/kotlin/org/bitcoin/infrastructure/jpa/JpaApplication.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.infrastructure.jpa 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | import org.springframework.context.annotation.ComponentScan 6 | 7 | @SpringBootApplication 8 | class JpaApplication 9 | 10 | fun main(args: Array) { 11 | runApplication(*args) 12 | } 13 | -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/src/main/kotlin/org/bitcoin/infrastructure/jpa/bithumb/entity/JpaCoinSymbol.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.infrastructure.jpa.bithumb.entity 2 | 3 | import org.bitcoin.domain.bithumb.request.CoinSymbol 4 | import org.bitcoin.domain.type.ExchangeType 5 | import java.time.LocalDateTime 6 | import javax.persistence.* 7 | 8 | @Entity 9 | @Table(name = "coin_symbol") 10 | class JpaCoinSymbol( 11 | symbol: String? = "", 12 | channelTopic: String? = "", 13 | exchange: ExchangeType = ExchangeType.NONE, // bithumb, upbit, kobit 14 | createdDate: LocalDateTime = LocalDateTime.now(), 15 | updatedDate: LocalDateTime = LocalDateTime.now() 16 | ) { 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | @Column(name = "coin_symbol_id", nullable = false) 20 | var id: Long = 0 21 | protected set 22 | 23 | @Column(name = "coin_symbol", length = 10, nullable = false) 24 | var symbol: String = symbol!! 25 | protected set 26 | 27 | @Column(name = "channel_topic", length = 50, nullable = false) 28 | var channelTopic: String = channelTopic!! 29 | protected set 30 | 31 | @Column(name = "exchange", length = 10, nullable = false) 32 | @Enumerated(EnumType.STRING) 33 | var exchange: ExchangeType = exchange 34 | protected set 35 | 36 | @Column(name = "created_date", nullable = false) 37 | var createdDate: LocalDateTime = createdDate 38 | protected set 39 | 40 | @Column(name = "updated_date", nullable = false) 41 | var updatedDate: LocalDateTime = updatedDate 42 | protected set 43 | 44 | 45 | fun mapToCoinSymbolDTO(): CoinSymbol { 46 | return CoinSymbol( 47 | id = this.id, 48 | symbol = this.symbol, 49 | channel = this.channelTopic, 50 | exchange = this.exchange, 51 | createdDate = this.createdDate, 52 | updatedDate = this.updatedDate 53 | ) 54 | } 55 | 56 | companion object { 57 | fun toEntity(coinSymbol: CoinSymbol): JpaCoinSymbol { 58 | return JpaCoinSymbol( 59 | symbol = coinSymbol.symbol, 60 | channelTopic = coinSymbol.channel, 61 | exchange = coinSymbol.exchange, 62 | createdDate = coinSymbol.createdDate, 63 | updatedDate = coinSymbol.updatedDate 64 | ) 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/src/main/kotlin/org/bitcoin/infrastructure/jpa/bithumb/entity/JpaCoinSymbolRepository.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.infrastructure.jpa.bithumb.entity 2 | 3 | import org.bitcoin.domain.type.ExchangeType 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | import org.springframework.stereotype.Repository 6 | 7 | @Repository 8 | interface JpaCoinSymbolRepository: JpaRepository { 9 | 10 | fun findBySymbol(symbol: String): JpaCoinSymbol 11 | 12 | fun findAllByExchange(exchange: ExchangeType): List 13 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/src/main/kotlin/org/bitcoin/infrastructure/jpa/bithumb/entity/JpaOrderBookAsks.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.infrastructure.jpa.bithumb.entity 2 | 3 | import java.time.LocalDateTime 4 | import javax.persistence.* 5 | 6 | @Entity 7 | @Table(name = "order_book_asks") 8 | class JpaOrderBookAsks( 9 | quantity: String = "", 10 | price: String = "", 11 | overBookId: JpaOverBook? = JpaOverBook(), 12 | createdDate: LocalDateTime = LocalDateTime.now(), 13 | updatedDate: LocalDateTime = LocalDateTime.now() 14 | ) { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | @Column(name = "order_book_asks_id", nullable = false) 19 | var id: Long = 0 20 | protected set 21 | 22 | @Column(name = "quantity", length = 30, nullable = false) 23 | var quantity: String = quantity 24 | protected set 25 | 26 | @Column(name = "price", length = 40, nullable = false) 27 | var price: String = price 28 | protected set 29 | 30 | @ManyToOne(targetEntity = JpaOverBook::class, fetch = FetchType.LAZY) 31 | @JoinColumn(name = "over_book_id") 32 | var overBookId: JpaOverBook = overBookId!! 33 | protected set 34 | 35 | @Column(name = "created_date", nullable = false) 36 | var createdDate: LocalDateTime = createdDate 37 | protected set 38 | 39 | @Column(name = "updated_date", nullable = false) 40 | var updatedDate: LocalDateTime = updatedDate 41 | protected set 42 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/src/main/kotlin/org/bitcoin/infrastructure/jpa/bithumb/entity/JpaOrderBookAsksRepository.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.infrastructure.jpa.bithumb.entity 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | interface JpaOrderBookAsksRepository: JpaRepository { 6 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/src/main/kotlin/org/bitcoin/infrastructure/jpa/bithumb/entity/JpaOrderBookBids.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.infrastructure.jpa.bithumb.entity 2 | 3 | import java.time.LocalDateTime 4 | import javax.persistence.* 5 | 6 | @Entity 7 | @Table(name = "order_book_bids") 8 | class JpaOrderBookBids( 9 | quantity: String = "", 10 | price: String = "", 11 | overBookId: JpaOverBook? = JpaOverBook(), 12 | createdDate: LocalDateTime = LocalDateTime.now(), 13 | updatedDate: LocalDateTime = LocalDateTime.now() 14 | ) { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | @Column(name = "order_book_bids_id", nullable = false) 19 | var id: Long = 0 20 | protected set 21 | 22 | @Column(name = "quantity", length = 30, nullable = false) 23 | var quantity: String = quantity 24 | protected set 25 | 26 | @Column(name = "price", length = 40, nullable = false) 27 | var price: String = price 28 | protected set 29 | 30 | @ManyToOne(targetEntity = JpaOverBook::class, fetch = FetchType.LAZY) 31 | @JoinColumn(name = "over_book_id") 32 | var overBookId: JpaOverBook = overBookId!! 33 | protected set 34 | 35 | @Column(name = "created_date", nullable = false) 36 | var createdDate: LocalDateTime = createdDate 37 | protected set 38 | 39 | @Column(name = "updated_date", nullable = false) 40 | var updatedDate: LocalDateTime = updatedDate 41 | protected set 42 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/src/main/kotlin/org/bitcoin/infrastructure/jpa/bithumb/entity/JpaOrderBookBidsRepository.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.infrastructure.jpa.bithumb.entity 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | interface JpaOrderBookBidsRepository: JpaRepository { 6 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/src/main/kotlin/org/bitcoin/infrastructure/jpa/bithumb/entity/JpaOverBook.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.infrastructure.jpa.bithumb.entity 2 | 3 | import java.time.LocalDateTime 4 | import javax.persistence.* 5 | 6 | @Entity 7 | @Table(name = "over_book") 8 | class JpaOverBook( 9 | timestamp: String = "", 10 | paymentCurrency: String = "", 11 | orderCurrency: String = "", 12 | createdDate: LocalDateTime = LocalDateTime.now(), 13 | updatedDate: LocalDateTime = LocalDateTime.now() 14 | ) { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | @Column(name = "over_book_id", nullable = false) 19 | var id: Long = 0 20 | protected set 21 | 22 | @Column(name = "time_stamp", length = 50, nullable = false) 23 | var timestamp: String = timestamp 24 | protected set 25 | 26 | @Column(name = "payment_currency", length = 50, nullable = false) 27 | var paymentCurrency: String = paymentCurrency 28 | protected set 29 | 30 | @Column(name = "order_currency", length = 50, nullable = false) 31 | var orderCurrency: String = orderCurrency 32 | protected set 33 | 34 | @Column(name = "created_date", nullable = false) 35 | var createdDate: LocalDateTime = createdDate 36 | protected set 37 | 38 | @Column(name = "updated_date", nullable = false) 39 | var updatedDate: LocalDateTime = updatedDate 40 | protected set 41 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/src/main/kotlin/org/bitcoin/infrastructure/jpa/bithumb/entity/JpaOverBookRepository.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.infrastructure.jpa.bithumb.entity 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | interface JpaOverBookRepository: JpaRepository { 6 | 7 | fun findByOrderCurrency(symbol: String): JpaOverBook? 8 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/src/main/kotlin/org/bitcoin/infrastructure/jpa/bithumb/service/CoinSymbolRepository.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.infrastructure.jpa.bithumb.service 2 | 3 | import org.bitcoin.infrastructure.jpa.bithumb.entity.JpaCoinSymbolRepository 4 | import org.bitcoin.domain.bithumb.request.CoinSymbol 5 | import org.bitcoin.domain.type.ExchangeType 6 | import org.bitcoin.infrastructure.jpa.bithumb.entity.JpaCoinSymbol 7 | import org.springframework.stereotype.Repository 8 | import org.springframework.transaction.annotation.Transactional 9 | 10 | @Repository 11 | @Transactional(readOnly = true) 12 | class CoinSymbolRepository( 13 | private val coinSymbolRepository: JpaCoinSymbolRepository 14 | ) { 15 | fun findBySymbolName(symbol: String): CoinSymbol { 16 | val symbolEntity = coinSymbolRepository.findBySymbol(symbol) 17 | 18 | return symbolEntity.mapToCoinSymbolDTO() 19 | } 20 | fun findByExchange(exchange: ExchangeType): List { 21 | val symbolEntities = coinSymbolRepository.findAllByExchange(exchange) 22 | return symbolEntities.map { it.mapToCoinSymbolDTO() } 23 | } 24 | 25 | fun findAll(): List { 26 | val symbolEntities = coinSymbolRepository.findAll() 27 | return symbolEntities.map { it.mapToCoinSymbolDTO() } 28 | } 29 | 30 | @Transactional 31 | fun save(coinSymbol: CoinSymbol): CoinSymbol { 32 | val jpaCoinSymbol = coinSymbolRepository.save(JpaCoinSymbol.toEntity(coinSymbol)) 33 | 34 | return jpaCoinSymbol.mapToCoinSymbolDTO() 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-jpa/src/main/resources/application-jpa.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | driver-class-name: com.mysql.cj.jdbc.Driver 4 | hikari: 5 | username: root 6 | jdbc-url: jdbc:mysql://localhost:3307/coin?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimeZone=Asia/Seoul 7 | 8 | jpa: 9 | database-platform: org.hibernate.dialect.MySQL5Dialect 10 | properties: 11 | hibernate: 12 | format_sql: true 13 | show_sql: true 14 | hibernate: 15 | ddl-auto: update 16 | -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-kafka/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | val jar: Jar by tasks 4 | val bootJar: BootJar by tasks 5 | 6 | bootJar.enabled = true 7 | jar.enabled = false 8 | 9 | apply(plugin = "org.springframework.boot") 10 | 11 | dependencies { 12 | implementation(project(":bitcoin-domain")) 13 | api("org.springframework.kafka:spring-kafka") 14 | implementation("org.springframework.boot:spring-boot-starter-validation") // kafka 에서 필요 15 | 16 | api("org.springframework.kafka:spring-kafka-test") 17 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-infrastructure-kafka/src/main/resources/application-kafka.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | kafka: 3 | producer: 4 | bootstrap-servers: localhost:9092, localhost:9093, localhost:9094 5 | key-serializer: org.apache.kafka.common.serialization.StringSerializer 6 | value-serializer: org.apache.kafka.common.serialization.StringSerializer 7 | # StringSerializer : producer의 경우 값을 보내는 입장이기에, 해당 값을 직렬화하여 보낸다. 8 | streams: 9 | replication-factor: 3 10 | consumer: 11 | bootstrap-servers: localhost:9092, localhost:9093, localhost:9094 12 | group-id: bitcoin # Unique string that identifies the consumer group to which this consumer belongs. 13 | auto-offset-reset: earliest # What to do when there is no initial offset in Kafka or if the current offset no longer exists on the server. 14 | key-deserializer: org.apache.kafka.common.serialization.StringDeserializer 15 | value-deserializer: org.apache.kafka.common.serialization.StringDeserializer 16 | # StringDeserializer : consumer의 경우 값을 받는 입장이기에, 해당 값을 역직렬화하여 받는다. 17 | streams: 18 | replication-factor: 3 -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-redis/bitcoin-redis-publish/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | val jar: Jar by tasks 4 | val bootJar: BootJar by tasks 5 | 6 | bootJar.enabled = true 7 | jar.enabled = false 8 | 9 | dependencies { 10 | implementation(project(":bitcoin-domain")) 11 | 12 | implementation("org.springframework.boot:spring-boot-starter-data-redis") 13 | 14 | implementation("com.fasterxml.jackson.core:jackson-databind:2.15.1") 15 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-redis/bitcoin-redis-publish/src/main/kotlin/org/bitcoin/redispublish/config/RedisPublishConfig.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.redispublish.config 2 | 3 | import org.springframework.beans.factory.annotation.Value 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory 7 | import org.springframework.data.redis.core.RedisTemplate 8 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer 9 | import org.springframework.data.redis.serializer.StringRedisSerializer 10 | 11 | @Configuration 12 | class RedisPublishConfig( 13 | @Value("\${spring.redis.host}") 14 | val host: String, 15 | 16 | @Value("\${spring.redis.port}") 17 | val port: Int, 18 | ) { 19 | 20 | @Bean 21 | fun redisConnectionFactory(): LettuceConnectionFactory { 22 | return LettuceConnectionFactory(host, port) 23 | } 24 | 25 | @Bean 26 | fun redisTemplate(): RedisTemplate { 27 | val redisTemplate = RedisTemplate() 28 | redisTemplate.setConnectionFactory(redisConnectionFactory()) 29 | redisTemplate.setEnableTransactionSupport(true) 30 | redisTemplate.keySerializer = StringRedisSerializer() 31 | redisTemplate.valueSerializer = Jackson2JsonRedisSerializer(Any::class.java) 32 | redisTemplate.hashKeySerializer = StringRedisSerializer() 33 | redisTemplate.hashValueSerializer = Jackson2JsonRedisSerializer(Any::class.java) 34 | return redisTemplate 35 | } 36 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-redis/bitcoin-redis-publish/src/main/kotlin/org/bitcoin/redispublish/publish/RedisPublishService.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.redispublish.publish 2 | 3 | import org.springframework.data.redis.core.RedisTemplate 4 | import org.springframework.stereotype.Service 5 | 6 | @Service 7 | class RedisPublishService( 8 | private val redisTemplate: RedisTemplate 9 | ) { 10 | 11 | fun publish(channel: String, message: Any) { 12 | redisTemplate.convertAndSend(channel, message) 13 | } 14 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-redis/bitcoin-redis-subscribe/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | val jar: Jar by tasks 4 | val bootJar: BootJar by tasks 5 | 6 | bootJar.enabled = true 7 | jar.enabled = false 8 | 9 | dependencies { 10 | implementation(project(":bitcoin-domain")) 11 | implementation(project(":bitcoin-infrastructure:bitcoin-infrastructure-jpa")) 12 | 13 | implementation("org.springframework.boot:spring-boot-starter-data-redis") 14 | 15 | implementation("com.fasterxml.jackson.core:jackson-databind:2.15.1") 16 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-redis/bitcoin-redis-subscribe/src/main/kotlin/org/bitcoin/redissubscribe/config/RedisSubscribeConfig.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.redissubscribe.config 2 | 3 | import org.springframework.beans.factory.annotation.Value 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory 7 | import org.springframework.data.redis.listener.RedisMessageListenerContainer 8 | 9 | @Configuration 10 | class RedisSubscribeConfig( 11 | @Value("\${spring.redis.host}") 12 | val host: String, 13 | 14 | @Value("\${spring.redis.port}") 15 | val port: Int, 16 | ) { 17 | 18 | @Bean 19 | fun redisConnectionFactory(): LettuceConnectionFactory { 20 | return LettuceConnectionFactory(host, port) 21 | } 22 | 23 | @Bean 24 | fun redisMessageListenerContainer( 25 | ): RedisMessageListenerContainer { 26 | val container = RedisMessageListenerContainer() 27 | container.setConnectionFactory(redisConnectionFactory()) 28 | return container 29 | } 30 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-redis/bitcoin-redis-subscribe/src/main/kotlin/org/bitcoin/redissubscribe/listener/RedisSubscribeListener.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.redissubscribe.listener 2 | 3 | import org.bitcoin.redissubscribe.service.BithumbChannelSubscribeService 4 | import org.bitcoin.redissubscribe.service.KobitChannelSubscribeService 5 | import org.bitcoin.redissubscribe.service.UpbitChannelSubscribeService 6 | import org.springframework.data.redis.listener.ChannelTopic 7 | import org.springframework.data.redis.listener.RedisMessageListenerContainer 8 | import org.springframework.stereotype.Component 9 | import org.springframework.transaction.annotation.Transactional 10 | 11 | @Component 12 | @Transactional 13 | class RedisSubscribeListener( 14 | private val bithumbChannelSubscribeService: BithumbChannelSubscribeService, 15 | private val upbitChannelSubscribeService: UpbitChannelSubscribeService, 16 | private val kobitChannelSubscribeService: KobitChannelSubscribeService, 17 | private val redisMessageListenerContainer: RedisMessageListenerContainer 18 | ) { 19 | 20 | fun subscribeBithumb(topic: ChannelTopic) { 21 | redisMessageListenerContainer 22 | .addMessageListener(bithumbChannelSubscribeService, topic) 23 | } 24 | 25 | fun subscribeUpbit(channelTopic: ChannelTopic) { 26 | redisMessageListenerContainer 27 | .addMessageListener(upbitChannelSubscribeService, channelTopic) 28 | } 29 | 30 | fun subscribeKobit(channelTopic: ChannelTopic) { 31 | redisMessageListenerContainer 32 | .addMessageListener(kobitChannelSubscribeService, channelTopic) 33 | } 34 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-redis/bitcoin-redis-subscribe/src/main/kotlin/org/bitcoin/redissubscribe/scheduler/ChannelSubscribeScheduler.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.redissubscribe.scheduler 2 | 3 | import org.bitcoin.domain.bithumb.request.CoinSymbol 4 | import org.bitcoin.domain.type.ExchangeType 5 | import org.bitcoin.infrastructure.jpa.bithumb.service.CoinSymbolRepository 6 | import org.bitcoin.redissubscribe.listener.RedisSubscribeListener 7 | import org.springframework.data.redis.listener.ChannelTopic 8 | import org.springframework.scheduling.annotation.Scheduled 9 | import org.springframework.stereotype.Component 10 | import org.springframework.transaction.annotation.Transactional 11 | 12 | @Component 13 | class ChannelSubscribeScheduler( 14 | private val coinSymbolRepository: CoinSymbolRepository, 15 | private val redisSubscribeListener: RedisSubscribeListener 16 | ) { 17 | 18 | /** 19 | * @description : symbol를 읽어온 후, 10초마다 해당 symbolTopic을 구독한다. (bithumb, upbit, kobit) 20 | */ 21 | 22 | @Transactional 23 | @Scheduled(cron = "3 * * * * *") 24 | fun subscribeRedisTopic() { 25 | val symbols = coinSymbolRepository.findAll() 26 | 27 | symbols.forEach { coinSymbol -> 28 | processingSubscribeSymbol(coinSymbol) 29 | } 30 | } 31 | 32 | 33 | private fun processingSubscribeSymbol(coinSymbol: CoinSymbol) { 34 | when (coinSymbol.exchange) { 35 | ExchangeType.BITHUMB -> { 36 | redisSubscribeListener.subscribeBithumb(ChannelTopic(coinSymbol.channel)) 37 | } 38 | ExchangeType.UPBIT -> { 39 | redisSubscribeListener.subscribeUpbit(ChannelTopic(coinSymbol.channel)) 40 | } 41 | ExchangeType.KOBIT -> { 42 | redisSubscribeListener.subscribeKobit(ChannelTopic(coinSymbol.channel)) 43 | } 44 | 45 | else -> { 46 | throw InternalError("Not support exchange type") 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-redis/bitcoin-redis-subscribe/src/main/kotlin/org/bitcoin/redissubscribe/service/BithumbChannelSubscribeService.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.redissubscribe.service 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.bitcoin.domain.bithumb.response.BithumbTicker 5 | import org.springframework.context.ApplicationEventPublisher 6 | import org.springframework.data.redis.connection.Message 7 | import org.springframework.data.redis.connection.MessageListener 8 | import org.springframework.stereotype.Service 9 | 10 | @Service 11 | class BithumbChannelSubscribeService( 12 | private val objectMapper: ObjectMapper, 13 | private val applicationEventPublisher: ApplicationEventPublisher 14 | ): MessageListener { 15 | override fun onMessage(message: Message, pattern: ByteArray?) { 16 | println("[BITHUMB] message : ${String(message.body)}") 17 | val response = objectMapper.readValue(message.body, BithumbTicker::class.java) 18 | 19 | applicationEventPublisher.publishEvent(response) 20 | } 21 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-redis/bitcoin-redis-subscribe/src/main/kotlin/org/bitcoin/redissubscribe/service/KobitChannelSubscribeService.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.redissubscribe.service 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.springframework.context.ApplicationEventPublisher 5 | import org.springframework.data.redis.connection.Message 6 | import org.springframework.data.redis.connection.MessageListener 7 | import org.springframework.stereotype.Service 8 | 9 | @Service 10 | class KobitChannelSubscribeService( 11 | private val objectMapper: ObjectMapper, 12 | private val applicationEventPublisher: ApplicationEventPublisher 13 | ): MessageListener { 14 | override fun onMessage(message: Message, pattern: ByteArray?) { 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /bitcoin-infrastructure/bitcoin-redis/bitcoin-redis-subscribe/src/main/kotlin/org/bitcoin/redissubscribe/service/UpbitChannelSubscribeService.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.redissubscribe.service 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.bitcoin.domain.upbit.OrderBookResponse 5 | import org.bitcoin.domain.upbit.TickerResponse 6 | import org.springframework.context.ApplicationEventPublisher 7 | import org.springframework.data.redis.connection.Message 8 | import org.springframework.data.redis.connection.MessageListener 9 | import org.springframework.stereotype.Service 10 | 11 | @Service 12 | class UpbitChannelSubscribeService( 13 | private val objectMapper: ObjectMapper, 14 | private val applicationEventPublisher: ApplicationEventPublisher 15 | ): MessageListener { 16 | override fun onMessage(message: Message, pattern: ByteArray?) { 17 | println("[UPBIT] message : ${String(message.body)}") 18 | val response = objectMapper.readValue(message.body, TickerResponse::class.java) 19 | 20 | applicationEventPublisher.publishEvent(response) 21 | } 22 | } -------------------------------------------------------------------------------- /bitcoin-producer/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | val jar: Jar by tasks 4 | val bootJar: BootJar by tasks 5 | 6 | bootJar.enabled = true 7 | jar.enabled = false 8 | 9 | apply(plugin = "org.springframework.boot") 10 | 11 | dependencies { 12 | implementation(project(":bitcoin-infrastructure:bitcoin-infrastructure-kafka")) 13 | implementation(project(":bitcoin-infrastructure:bitcoin-external-data")) 14 | implementation(project(":bitcoin-infrastructure:bitcoin-infrastructure-jpa")) 15 | implementation(project(":bitcoin-infrastructure:bitcoin-redis:bitcoin-redis-subscribe")) 16 | 17 | implementation(project(":bitcoin-domain")) 18 | 19 | 20 | // spring boot 21 | implementation("org.springframework.boot:spring-boot-starter-web") 22 | } -------------------------------------------------------------------------------- /bitcoin-producer/src/main/kotlin/org/bitcoin/producer/ProducerApplication.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.producer 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | import org.springframework.scheduling.annotation.EnableScheduling 6 | 7 | @EnableScheduling 8 | @SpringBootApplication(scanBasePackages = ["org.bitcoin"]) 9 | class ProducerApplication 10 | 11 | fun main(args: Array) { 12 | runApplication(*args) 13 | } 14 | -------------------------------------------------------------------------------- /bitcoin-producer/src/main/kotlin/org/bitcoin/producer/polling/reader/BithumbReader.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.producer.polling.reader 2 | 3 | import org.bitcoin.domain.bithumb.request.CoinSymbol 4 | import org.bitcoin.domain.bithumb.response.BitumbOrderbook 5 | import org.bitcoin.external.bithumb.webflux.fetcher.BithumbFetcher 6 | import org.bitcoin.infrastructure.jpa.bithumb.service.CoinSymbolRepository 7 | import org.springframework.stereotype.Service 8 | import org.springframework.transaction.annotation.Transactional 9 | 10 | @Service 11 | @Transactional(readOnly = true) 12 | class BithumbReader( 13 | val bithumbFetcher: BithumbFetcher, 14 | val coinSymbolRepository: CoinSymbolRepository 15 | ) { 16 | fun getBitcoinSymbolDataBySavedSymbolList(): List = 17 | getAllBitcoinSymbol().stream() 18 | .map { bitcoinSymbol -> getBitumbOrderbookData(bitcoinSymbol.symbol) } 19 | .toList() 20 | 21 | private fun getAllBitcoinSymbol(): List { 22 | return coinSymbolRepository.findAll() 23 | } 24 | 25 | private fun getBitumbOrderbookData(code: String): BitumbOrderbook { 26 | return bithumbFetcher.getBitumbOrderbook(code) 27 | } 28 | } -------------------------------------------------------------------------------- /bitcoin-producer/src/main/kotlin/org/bitcoin/producer/polling/scheduler/BithumbScheduler.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.producer.polling.scheduler 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.bitcoin.producer.polling.reader.BithumbReader 5 | import org.springframework.kafka.core.KafkaTemplate 6 | import org.springframework.stereotype.Component 7 | 8 | @Component 9 | class BithumbScheduler( 10 | val bithumbReader: BithumbReader, 11 | val kafkaTemplate: KafkaTemplate, 12 | val objectMapper: ObjectMapper 13 | ) { 14 | 15 | // TODO : 현재는 사용 안함 추후 서킷브레이커 적용 후, 살릴 예정 16 | 17 | // @Transactional 18 | // @Scheduled(cron = "3 * * * * *") 19 | // fun getBitumbOrderbookData() { 20 | // val bitcoinSymbolDataBySavedSymbolList = 21 | // bithumbReader.getBitcoinSymbolDataBySavedSymbolList() 22 | // 23 | // bitcoinSymbolDataBySavedSymbolList.forEach {response -> 24 | // kafkaTemplate.send(TopicType.BITHUMB.topicName, objectMapper.serialize(response)) 25 | // } 26 | // } 27 | 28 | fun ObjectMapper.serialize(data: T): String = writeValueAsString(data) 29 | } -------------------------------------------------------------------------------- /bitcoin-producer/src/main/kotlin/org/bitcoin/producer/socket/BithumbSocketPublish.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.producer.socket 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.bitcoin.domain.bithumb.response.BithumbTicker 5 | import org.bitcoin.domain.type.ExchangeType 6 | import org.bitcoin.domain.upbit.TickerResponse 7 | import org.springframework.context.event.EventListener 8 | import org.springframework.kafka.core.KafkaTemplate 9 | import org.springframework.stereotype.Service 10 | 11 | @Service 12 | class BithumbSocketPublish( 13 | private val kafkaTemplate: KafkaTemplate, 14 | private val objectMapper: ObjectMapper 15 | ) { 16 | 17 | @EventListener(BithumbTicker::class) 18 | fun listenBithumbStreamSocketListener(tickerResponse: BithumbTicker) { 19 | println("[BITHUMB] Receive OrderBookDepthResponse: $tickerResponse") 20 | kafkaTemplate.send(ExchangeType.BITHUMB_STREAM.exchange, objectMapper.serialize(tickerResponse)) 21 | } 22 | 23 | @EventListener(TickerResponse::class) 24 | fun listenUpbitStreamSocketListener(tickerResponse: TickerResponse) { 25 | println("[UPBIT] Receive OrderBookResponse: $tickerResponse") 26 | kafkaTemplate.send(ExchangeType.UPBIT_STREAM.exchange, objectMapper.serialize(tickerResponse)) 27 | } 28 | 29 | fun ObjectMapper.serialize(data: T): String = writeValueAsString(data) 30 | } -------------------------------------------------------------------------------- /bitcoin-producer/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | include: jpa, kafka 4 | redis: 5 | host: localhost 6 | port: 6479 7 | 8 | server: 9 | port: 8081 -------------------------------------------------------------------------------- /bitcoin-websocket/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | val jar: Jar by tasks 4 | val bootJar: BootJar by tasks 5 | 6 | bootJar.enabled = true 7 | jar.enabled = false 8 | 9 | apply(plugin = "org.springframework.boot") 10 | 11 | dependencies { 12 | implementation(project(":bitcoin-infrastructure:bitcoin-infrastructure-jpa")) 13 | implementation(project(":bitcoin-infrastructure:bitcoin-external-data")) 14 | implementation(project(":bitcoin-infrastructure:bitcoin-redis:bitcoin-redis-publish")) 15 | implementation(project(":bitcoin-domain")) 16 | 17 | // spring boot 18 | implementation("org.springframework.boot:spring-boot-starter-web") 19 | 20 | // websocket 21 | implementation ("org.springframework.boot:spring-boot-starter-websocket") 22 | 23 | } -------------------------------------------------------------------------------- /bitcoin-websocket/src/main/kotlin/org/bitcoin/websocket/BitcoinWebsocketApplication.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.websocket 2 | 3 | import org.bitcoin.websocket.bithumb.service.BithumbService 4 | import org.bitcoin.websocket.upbit.service.UpbitService 5 | import org.springframework.boot.autoconfigure.SpringBootApplication 6 | import org.springframework.boot.runApplication 7 | 8 | @SpringBootApplication(scanBasePackages = ["org.bitcoin"]) 9 | class BitcoinWebsocketApplication 10 | 11 | fun main(args: Array) { 12 | val ctx = runApplication(*args) 13 | val bithumb = ctx.getBean(BithumbService::class.java) 14 | val upbit = ctx.getBean(UpbitService::class.java) 15 | bithumb.getOrderBootDepth() 16 | upbit.getTickerDepth() 17 | } 18 | -------------------------------------------------------------------------------- /bitcoin-websocket/src/main/kotlin/org/bitcoin/websocket/bithumb/handler/BithumbSocketHandler.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.websocket.bithumb.handler 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.bitcoin.domain.bithumb.request.CoinSymbol 5 | import org.bitcoin.domain.bithumb.request.BithumbSocket 6 | import org.bitcoin.domain.bithumb.response.BithumbTicker 7 | import org.bitcoin.domain.type.ExchangeType 8 | import org.bitcoin.infrastructure.jpa.bithumb.service.CoinSymbolRepository 9 | import org.bitcoin.redispublish.publish.RedisPublishService 10 | import org.springframework.stereotype.Component 11 | import org.springframework.web.socket.CloseStatus 12 | import org.springframework.web.socket.TextMessage 13 | import org.springframework.web.socket.WebSocketSession 14 | import org.springframework.web.socket.handler.TextWebSocketHandler 15 | import java.util.concurrent.CountDownLatch 16 | 17 | @Component 18 | class BithumbSocketHandler( 19 | private val objectMapper: ObjectMapper, 20 | private val bithumbRepository: CoinSymbolRepository, 21 | private val redisPublishService: RedisPublishService 22 | ): TextWebSocketHandler() { 23 | 24 | // 클라이언트가 접속을 종료할 때까지 대기하는 CountDownLatch 25 | private val closeLatch = CountDownLatch(1) 26 | 27 | // 클라이언트가 접속을 종료할 경우 발생하는 이벤트 28 | override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) { 29 | println("[BITHUMB] Connection closed: ${status.code} - ${status.reason}") 30 | session.close() 31 | closeLatch.countDown() 32 | } 33 | 34 | // 클라이언트가 접속을 성공할 경우 발생하는 이벤트 35 | override fun afterConnectionEstablished(session: WebSocketSession) { 36 | println("[BITHUMB] Got Connect : ${session.id}") 37 | val depthRequest = BithumbSocket.createTickerRequest(findAllByExchange(ExchangeType.BITHUMB)) 38 | session.sendMessage(TextMessage(objectMapper.writeValueAsString(depthRequest))) 39 | } 40 | 41 | // 클라이언트가 메시지를 보낼 경우 발생하는 이벤트 42 | override fun handleTextMessage(session: WebSocketSession, message: TextMessage) { 43 | println("[BITHUMB] Got Message : ${message.payload}") 44 | 45 | try { 46 | val readTree = objectMapper.readTree(message.payload) 47 | 48 | if (!readTree.has("status")) { 49 | val response = 50 | objectMapper.readValue(message.payload, BithumbTicker::class.java) 51 | 52 | redisPublishService.publish( 53 | getChannelInSymbols(findAllByExchange(ExchangeType.BITHUMB)), 54 | response 55 | ) 56 | } 57 | } catch (e: Exception) { 58 | e.printStackTrace() 59 | } 60 | } 61 | 62 | private fun findAllByExchange(exchage: ExchangeType): List { 63 | return bithumbRepository.findByExchange(exchage) 64 | } 65 | 66 | private fun getChannelInSymbols(symbols: List): String { 67 | return symbols[0].channel 68 | } 69 | } -------------------------------------------------------------------------------- /bitcoin-websocket/src/main/kotlin/org/bitcoin/websocket/bithumb/service/BithumbService.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.websocket.bithumb.service 2 | 3 | import org.bitcoin.external.bithumb.path.BitcoinWebsocketPath 4 | import org.bitcoin.websocket.bithumb.handler.BithumbSocketHandler 5 | import org.springframework.stereotype.Service 6 | import org.springframework.web.socket.client.WebSocketConnectionManager 7 | import org.springframework.web.socket.client.standard.StandardWebSocketClient 8 | 9 | @Service 10 | class BithumbService( 11 | private val bithumbSocketHandler: BithumbSocketHandler 12 | ) { 13 | /** 14 | * @description : 비트코인 웹소켓을 통해 실시간 주문 체결 내역을 가져온다. 15 | * @param request : 요청 객체 16 | */ 17 | fun getOrderBootDepth() { 18 | val client = StandardWebSocketClient() 19 | try { 20 | client.taskExecutor!!.execute { 21 | val manager = WebSocketConnectionManager( 22 | client, 23 | bithumbSocketHandler, 24 | BitcoinWebsocketPath.bithumb 25 | ) 26 | manager.start() 27 | println("connection to : ${BitcoinWebsocketPath.bithumb}") 28 | } 29 | } catch (e: Exception) { 30 | e.printStackTrace() 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /bitcoin-websocket/src/main/kotlin/org/bitcoin/websocket/upbit/handler/UpbitSocketHandler.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.websocket.upbit.handler 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.bitcoin.domain.bithumb.request.CoinSymbol 5 | import org.bitcoin.domain.type.ExchangeType 6 | import org.bitcoin.domain.upbit.OrderBookResponse 7 | import org.bitcoin.domain.upbit.SocketRequest 8 | import org.bitcoin.domain.upbit.TickerResponse 9 | import org.bitcoin.infrastructure.jpa.bithumb.service.CoinSymbolRepository 10 | import org.bitcoin.redispublish.publish.RedisPublishService 11 | import org.springframework.stereotype.Component 12 | import org.springframework.web.socket.BinaryMessage 13 | import org.springframework.web.socket.CloseStatus 14 | import org.springframework.web.socket.WebSocketSession 15 | import org.springframework.web.socket.handler.BinaryWebSocketHandler 16 | import java.nio.ByteBuffer 17 | import java.util.concurrent.CountDownLatch 18 | 19 | @Component 20 | class UpbitSocketHandler( 21 | private val objectMapper: ObjectMapper, 22 | private val symbolRepository: CoinSymbolRepository, 23 | private val redisPublishService: RedisPublishService 24 | ): BinaryWebSocketHandler() { 25 | 26 | private val closeLatch = CountDownLatch(1) 27 | 28 | override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) { 29 | println("[UPBIT] Connection closed: ${status.code} - ${status.reason}") 30 | session.close() 31 | closeLatch.countDown() 32 | } 33 | 34 | override fun afterConnectionEstablished(session: WebSocketSession) { 35 | println("[UPBIT] Got Connect : ${session.id}") 36 | 37 | val coinList = 38 | findAllByExchange(ExchangeType.UPBIT).stream().map { coin -> coin.symbol }.toList() 39 | 40 | val tickerRequest = SocketRequest.createBufferRequest("ticker", coinList) 41 | 42 | session.sendMessage(BinaryMessage(tickerRequest)) 43 | } 44 | 45 | override fun handleBinaryMessage(session: WebSocketSession, message: BinaryMessage) { 46 | println("[UPBIT] Got Message : ${message.payload}") 47 | try { 48 | val decodeBinaryData = decodeBinaryDataToTicker(message.payload) 49 | 50 | if (decodeBinaryData != null) { 51 | redisPublishService.publish( 52 | getChannelInSymbols(findAllByExchange(ExchangeType.UPBIT)), 53 | decodeBinaryData 54 | ) 55 | } 56 | } catch (e: Exception) { 57 | e.printStackTrace() 58 | } 59 | } 60 | 61 | private fun findAllByExchange(exchange: ExchangeType): List { 62 | return symbolRepository.findByExchange(exchange) 63 | } 64 | 65 | private fun getChannelInSymbols(symbols: List): String { 66 | return symbols[0].channel 67 | } 68 | 69 | // TODO : 디코딩 fun 확장함수로 관리하거나, 수정 필요 (현재는 임시로 작성) 70 | private fun decodeBinaryDataToOrderBook(hexData: ByteBuffer): OrderBookResponse? { 71 | val data = hexData.array() 72 | val decodeData = String(data, Charsets.UTF_8) 73 | return objectMapper.readValue(decodeData, OrderBookResponse::class.java) 74 | } 75 | 76 | private fun decodeBinaryDataToTicker(hexData: ByteBuffer): TickerResponse? { 77 | val data = hexData.array() 78 | val decodeData = String(data, Charsets.UTF_8) 79 | return objectMapper.readValue(decodeData, TickerResponse::class.java) 80 | } 81 | } -------------------------------------------------------------------------------- /bitcoin-websocket/src/main/kotlin/org/bitcoin/websocket/upbit/service/UpbitService.kt: -------------------------------------------------------------------------------- 1 | package org.bitcoin.websocket.upbit.service 2 | 3 | import org.bitcoin.external.bithumb.path.BitcoinWebsocketPath 4 | import org.bitcoin.websocket.bithumb.handler.BithumbSocketHandler 5 | import org.bitcoin.websocket.upbit.handler.UpbitSocketHandler 6 | import org.springframework.stereotype.Service 7 | import org.springframework.web.socket.client.WebSocketConnectionManager 8 | import org.springframework.web.socket.client.standard.StandardWebSocketClient 9 | 10 | @Service 11 | class UpbitService( 12 | private val upbitSocketHandler: UpbitSocketHandler 13 | ) { 14 | /** 15 | * @description : 비트코인 웹소켓을 통해 실시간 주문 체결 내역을 가져온다. 16 | * @param request : 요청 객체 17 | */ 18 | fun getTickerDepth() { 19 | val client = StandardWebSocketClient() 20 | try { 21 | client.taskExecutor!!.execute { 22 | val manager = WebSocketConnectionManager( 23 | client, 24 | upbitSocketHandler, 25 | BitcoinWebsocketPath.upbit 26 | ) 27 | manager.start() 28 | println("[UPBIT] connection to : ${BitcoinWebsocketPath.upbit}") 29 | } 30 | } catch (e: Exception) { 31 | e.printStackTrace() 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /bitcoin-websocket/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8084 3 | 4 | spring: 5 | profiles: 6 | include: jpa 7 | redis: 8 | host: localhost 9 | port: 6479 -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id("org.springframework.boot") version "2.6.3" 5 | id("io.spring.dependency-management") version "1.0.11.RELEASE" 6 | kotlin("jvm") version "1.6.10" 7 | kotlin("plugin.spring") version "1.6.10" apply false 8 | kotlin("plugin.jpa") version "1.6.10" apply false 9 | } 10 | 11 | java.sourceCompatibility = JavaVersion.VERSION_11 12 | 13 | allprojects { 14 | group = "org.bitcoin" 15 | version = "1.0-SNAPSHOT" 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | } 21 | 22 | subprojects { 23 | apply(plugin = "java") 24 | apply(plugin = "io.spring.dependency-management") 25 | apply(plugin = "org.springframework.boot") 26 | apply(plugin = "org.jetbrains.kotlin.plugin.spring") 27 | 28 | apply(plugin = "kotlin") 29 | apply(plugin = "kotlin-spring") //all-open 30 | 31 | 32 | dependencies { 33 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin") 34 | developmentOnly("org.springframework.boot:spring-boot-devtools") 35 | 36 | //kotlin 37 | implementation("org.jetbrains.kotlin:kotlin-reflect") 38 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 39 | 40 | //lombok 41 | compileOnly("org.projectlombok:lombok") 42 | annotationProcessor("org.projectlombok:lombok") 43 | 44 | // log 45 | implementation("org.slf4j:slf4j-api:1.7.30") 46 | 47 | // test 48 | testImplementation("org.springframework.boot:spring-boot-starter-test") 49 | } 50 | 51 | dependencyManagement { 52 | imports { 53 | mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) 54 | } 55 | 56 | dependencies { 57 | dependency("net.logstash.logback:logstash-logback-encoder:6.6") 58 | } 59 | } 60 | 61 | tasks.test { 62 | useJUnitPlatform() 63 | } 64 | 65 | tasks.withType { 66 | kotlinOptions { 67 | freeCompilerArgs = listOf("-Xjsr305=strict") 68 | jvmTarget = "11" 69 | } 70 | } 71 | 72 | configurations { 73 | compileOnly { 74 | extendsFrom(configurations.annotationProcessor.get()) 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /db/init.d/init.sql: -------------------------------------------------------------------------------- 1 | create database if not exists coin; 2 | use coin; -------------------------------------------------------------------------------- /db/mysql-dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:8.0.19 2 | 3 | COPY ./init.d /docker-entrypoint-initdb.d 4 | 5 | ENV LANG C.UTF-8 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | db: 5 | image: mysql 6 | platform: linux/x86_64 7 | build: 8 | context: ./db 9 | dockerfile: mysql-dockerfile 10 | environment: 11 | MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' 12 | TZ: 'Asia/Seoul' 13 | ports: 14 | - "3307:3306" 15 | 16 | redis: 17 | container_name: redis 18 | image: redis 19 | ports: 20 | - "6479:6379" 21 | 22 | zookeeper-1: 23 | container_name: zookeeper-1 24 | hostname: zookeeper1 25 | image: wurstmeister/zookeeper:latest 26 | ports: 27 | - "2181:2181" 28 | environment: 29 | ZOOKEEPER_SERVER_ID: 1 30 | ZOOKEEPER_CLIENT_PORT: 2181 31 | ZOOKEEPER_SERVERS: zookeeper1:2888:3888;zookeeper2:2888:3888;zookeeper3:2888:3888 32 | # 왼쪽 port가 리더 승격을 위한, 투표 포트, 오른쪽이 데이터 송수신을 위한 포트 33 | networks: 34 | - kafka-net 35 | 36 | zookeeper-2: 37 | container_name: zookeeper-2 38 | hostname: zookeeper2 39 | image: wurstmeister/zookeeper:latest 40 | ports: 41 | - "2182:2182" 42 | environment: 43 | ZOOKEEPER_SERVER_ID: 2 44 | ZOOKEEPER_CLIENT_PORT: 2182 45 | ZOOKEEPER_SERVERS: zookeeper1:2888:3888;zookeeper2:2888:3888;zookeeper3:2888:3888 46 | networks: 47 | - kafka-net 48 | 49 | zookeeper-3: 50 | container_name: zookeeper-3 51 | hostname: zookeeper3 52 | image: wurstmeister/zookeeper:latest 53 | ports: 54 | - "2183:2183" 55 | environment: 56 | ZOOKEEPER_SERVER_ID: 3 57 | ZOOKEEPER_CLIENT_PORT: 2183 58 | ZOOKEEPER_SERVERS: zookeeper1:2888:3888;zookeeper2:2888:3888;zookeeper3:2888:3888 59 | networks: 60 | - kafka-net 61 | 62 | kafka-1: 63 | container_name: kafka-1 64 | hostname: kafka1 65 | image: wurstmeister/kafka:latest 66 | depends_on: 67 | - zookeeper-1 68 | - zookeeper-2 69 | - zookeeper-3 70 | ports: 71 | - "9092:9092" 72 | - "19092:19092" 73 | environment: 74 | KAFKA_LISTENERS: INSIDE://0.0.0.0:19092,OUTSIDE://0.0.0.0:9092 75 | KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka1:19092,OUTSIDE://localhost:9092 76 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT 77 | KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE 78 | KAFKA_ZOOKEEPER_CONNECT: "zookeeper1:2181,zookeeper2:2182,zookeeper3:2183" 79 | KAFKA_BROKER_ID: 1 80 | KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO" 81 | networks: 82 | - kafka-net 83 | 84 | kafka-2: 85 | container_name: kafka-2 86 | hostname: kafka2 87 | image: wurstmeister/kafka:latest 88 | depends_on: 89 | - zookeeper-1 90 | - zookeeper-2 91 | - zookeeper-3 92 | ports: 93 | - "9093:9093" 94 | - "29092:29092" 95 | environment: 96 | KAFKA_LISTENERS: INSIDE://0.0.0.0:29092,OUTSIDE://0.0.0.0:9093 97 | KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka2:29092,OUTSIDE://localhost:9093 98 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT 99 | KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE 100 | KAFKA_ZOOKEEPER_CONNECT: "zookeeper1:2181,zookeeper2:2182,zookeeper3:2183" 101 | KAFKA_BROKER_ID: 2 102 | KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO" 103 | networks: 104 | - kafka-net 105 | 106 | kafka-3: 107 | container_name: kafka-3 108 | hostname: kafka3 109 | image: wurstmeister/kafka:latest 110 | depends_on: 111 | - zookeeper-1 112 | - zookeeper-2 113 | - zookeeper-3 114 | ports: 115 | - "9094:9094" 116 | - "39092:39092" 117 | environment: 118 | KAFKA_LISTENERS: INSIDE://0.0.0.0:39092,OUTSIDE://0.0.0.0:9094 119 | KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka3:39092,OUTSIDE://localhost:9094 120 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT 121 | KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE 122 | KAFKA_ZOOKEEPER_CONNECT: "zookeeper1:2181,zookeeper2:2182,zookeeper3:2183" 123 | KAFKA_BROKER_ID: 3 124 | KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO" 125 | networks: 126 | - kafka-net 127 | 128 | # Kafka 모니터링 툴 129 | akhq: 130 | image: tchiotludo/akhq:latest 131 | hostname: akhq 132 | depends_on: 133 | - zookeeper-1 134 | - zookeeper-2 135 | - zookeeper-3 136 | - kafka-1 137 | - kafka-2 138 | - kafka-3 139 | environment: 140 | AKHQ_CONFIGURATION: | 141 | akhq: 142 | connections: 143 | kafka: 144 | properties: 145 | bootstrap.servers: kafka1:19092,kafka2:29092,kafka3:39092 146 | ports: 147 | - "8083:8080" 148 | networks: 149 | - kafka-net 150 | 151 | networks: 152 | kafka-net: 153 | external: true -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | rootProject.name = "kafka-bitcoin-stream" 3 | include("bitcoin-api") 4 | include("bitcoin-websocket") 5 | include("bitcoin-producer") 6 | include("bitcoin-consumer") 7 | include("bitcoin-infrastructure") 8 | include("bitcoin-infrastructure:bitcoin-infrastructure-kafka") 9 | include("bitcoin-infrastructure:bitcoin-infrastructure-jpa") 10 | include("bitcoin-infrastructure:bitcoin-external-data") 11 | include("bitcoin-domain") 12 | include("bitcoin-infrastructure:bitcoin-redis") 13 | findProject(":bitcoin-infrastructure:bitcoin-redis")?.name = "bitcoin-redis" 14 | include("bitcoin-infrastructure:bitcoin-redis:bitcoin-redis-publish") 15 | findProject(":bitcoin-infrastructure:bitcoin-redis:bitcoin-redis-publish")?.name = "bitcoin-redis-publish" 16 | include("bitcoin-infrastructure:bitcoin-redis:bitcoin-redis-subscribe") 17 | findProject(":bitcoin-infrastructure:bitcoin-redis:bitcoin-redis-subscribe")?.name = "bitcoin-redis-subscribe" 18 | --------------------------------------------------------------------------------