티스토리 뷰
저는 현재 국내 가상화폐 거래소에 *스테이블 코인 가격 알림 서비스를 운영하고 있습니다.
가상화폐 거래소는 증권시장과 다르게 거래소마다 독자적으로 운영(상장, 거래, 정산 등)하는 방식이라 거래소마다 서로 다른 API를 제공합니다. 문제는 각각의 거래소가 제공하는 Response가 모두 다르기 때문에, 저희 서비스에서 이를 바로 호환하기 어렵다는 점입니다.
*스테이블코인은 달러화 등 기존 화폐에 고정 가치로 발행되는 암호화폐를 말한다.
문제점
@FeignClient(name = "Bithumb", url = "https://api.bithumb.com/public")
public interface BithumbClient {
@GetMapping("/ticker/{symbol}_{currency}")
BithumbTickerResponse getTicker(
@RequestParam("symbol") String symbol,
@RequestParam("currency") String currency
);
}
public record BithumbTickerResponse(
String status,
Data data
) {
}
public record Data(
@JsonProperty("closing_price")
String closingPrice,
@JsonProperty("opening_price")
String openingPrice,
@JsonProperty("min_price")
String minPrice,
@JsonProperty("max_price")
String maxPrice,
@JsonProperty("units_traded")
String unitsTraded
) {
}
@FeignClient(name = "Korbit", url = "https://api.korbit.co.kr")
public interface KorbitClient {
@GetMapping("/v1/ticker/detailed?currency_pair={symbol}_{currency}")
KorbitTickerResponse getTicker(
@RequestParam("symbol") String symbol,
@RequestParam("currency") String currency
);
}
public record KorbitTickerResponse(
String last,
String open,
String low,
String high,
String volume
) {
}
@Slf4j
@Service
@RequiredArgsConstructor
public class StableCoinService {
private final BithumbClient bithumbClient;
private final CoinoneClient coinoneClient;
private final GopaxClient gopaxClient;
private final KorbitClient korbitClient;
public List<StableCoin> saveAll(final BigDecimal exchangeRate) {
...
List<CryptoPair> bithumbCryptoPairs = cryptoPairService.findByCryptoExchange(CryptoExchange.BITHUMB);
for (CryptoPair cryptoPair : bithumbCryptoPairs) {
BithumbTickerResponse ticker = bithumbClient.getTicker(cryptoPair.getCryptoSymbol(), "KRW");
TickerResponse response = ticker.toStableCoinTicker(CryptoExchange.BITHUMB, cryptoPair.getCryptoSymbol());
coins.add(StableCoinMapper.toStableCoin(response, exchangeRate));
}
List<CryptoPair> coinoneCryptoPairs = cryptoPairService.findByCryptoExchange(CryptoExchange.COINONE);
for (CryptoPair cryptoPair : coinoneCryptoPairs) {
BithumbTickerResponse ticker = bithumbClient.getTicker(cryptoPair.getCryptoSymbol(), "KRW");
TickerResponse response = ticker.toStableCoinTicker(CryptoExchange.BITHUMB, cryptoPair.getCryptoSymbol());
coins.add(StableCoinMapper.toStableCoin(response, exchangeRate));
}
...
}
}
이렇게 각각의 다른 거래소 인터페이스를 저희 서비스에 그대로 사용한다면 확장하거나 유지보수 하기 힘든 코드가 작성됩니다.
어댑터 패턴 활용
어댑터 패턴은 호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 호환성을 제공하는 디자인 패턴입니다.
Adaptee(Service) : 외부 시스템(가상화폐 거래소)
Target(Client Interface) : Adapter 가 구현하는 인터페이스.
Adapter : Client 와 Adaptee(Service) 중간에서 호환성이 없는 둘을 연결시켜주는 역할을 담당
Client : 기존 시스템(StableCoinService)을 어댑터를 통해 이용하려는 쪽
public interface CryptoExchangeClient {
String PAYMENT_CURRENCY = "KRW";
TickerResponse getTickers(String cryptoSymbol);
boolean supports(CryptoExchange cryptoExchange);
}
@Service
@Order(2)
@RequiredArgsConstructor
public class BithumbAdapter implements CryptoExchangeClient {
private final BithumbClient bithumbClient;
@Override
public TickerResponse getTickers(final String cryptoSymbol) {
BithumbTickerResponse response = bithumbClient.getTicker(
cryptoSymbol,
CryptoExchangeClient.PAYMENT_CURRENCY
);
return response.toStableCoinTicker(CryptoExchange.BITHUMB, cryptoSymbol);
}
@Override
public boolean supports(final CryptoExchange cryptoExchange) {
return CryptoExchange.BITHUMB == cryptoExchange;
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class StableCoinService {
private final List<CryptoExchangeClient> cryptoExchangeClients;
private final StableCoinRepository repository;
private final CryptoPairService cryptoPairService;
public List<StableCoin> saveAll(final BigDecimal exchangeRate) {
List<StableCoin> coins = new ArrayList<>();
for (CryptoExchangeClient client : cryptoExchangeClients) {
List<CryptoPair> cryptoPairs = cryptoPairService.findByCryptoExchange(findCryptoExchange(client));
for (CryptoPair cryptoPair : cryptoPairs) {
TickerResponse response = client.getTickers(
cryptoPair.getCryptoSymbol().getName()
);
coins.add(StableCoinMapper.toStableCoin(response, exchangeRate));
}
}
repository.saveAll(coins);
return coins;
}
위와 같이 기존 서비스에서 필요로하는 Client Interface(CryptoExchangeClient) 를 생성후 각각의 거래소 클라이언트마다 Client Interface를 상속한 Adapter를 구현해서 기존 서비스 코드에 영향을 주지 않고 어댑터들을 변경하거나 확장할 수 있습니다.
예시
@Service
@Order(1)
@RequiredArgsConstructor
public class UpbitAdapter implements CryptoExchangeClient {
private final UpbitClient upbitClient;
@Override
public TickerResponse getTickers(final String cryptoSymbol) {
UpditTickerResponse response = upbitClient.getTicker(
cryptoSymbol,
CryptoExchangeClient.PAYMENT_CURRENCY
).get(0);
return response.toTickerResponse(CryptoExchange.UPBIT, cryptoSymbol);
}
@Override
public boolean supports(final CryptoExchange cryptoExchange) {
return CryptoExchange.UPBIT == cryptoExchange;
}
}
최근 업비트에서 테더가 추가되는 큰 이슈가 있었습니다.
해당 변경사항을 반영하기 위해 Adapter를 추가함으로써 기존 Service 코드를 수정하지 않고 쉽게 확장할 수 있었습니다.
'spring' 카테고리의 다른 글
Spring Scheduler 동작 방식 (0) | 2024.06.19 |
---|---|
Java 시간 다루기 (1) | 2024.06.19 |
ShedLock 도입기 (0) | 2024.04.11 |
Spring Boot 로그에 바인딩 매개변수가 표시되지 문제 해결 (0) | 2023.10.24 |
FeignClient @FormProperty 트러블 슈팅 (0) | 2023.10.15 |
- Total
- Today
- Yesterday
- 구글 소셜로그인
- Attribute Converter
- ServletContainerInitializer
- BasicBinder
- CreationTimestamp
- ValidateException
- HandlesTypes
- User Scenario
- 구글 OpenID
- feignClient
- WebFlux 의존성
- dto 위치
- @Converter
- 유저 스토리
- JPA SQL Injection
- FormProperty
- @ElementCollection
- java 17
- @FormProperty
- defer-datasource-initialization
- setDateFormat
- 유저 시나리오
- org.springframework:spring-webflux
- CreatedDate
- Spring Boot 3
- DispatcherServletInitializer
- 레이어드 아키텍처
- HTTPInterface
- dto 검증
- entity 검증
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |