개발을 하다보면 OpenAPI 에 연결할 필요가 있습니다. 공공 데이터 포털에서 제공하는 공휴일 API 일 수도 있고, Upbit 가상화폐 거래소에서 제공하는 코인 정보일 수도 있습니다.
Java 에서 OpenAPI 를 연결하는 방법은 정말 다양하게 많지만 그 중 하나를 소개하며 어떻게 연결할 수 있을지에 대해서 정리해보겠습니다.
가상화폐 거래소 업비트에서 제공하는 OpenAPI 를 사용해볼 예정입니다.
이 API 는 업비트에서 거래할 수 있는 전체 가상화폐 목록을 JSON
형태로 조회할 수 있습니다.
https://api.upbit.com/v1/market/all
상세 API 스펙은 아래에서 확인할 수 있습니다.
https://docs.upbit.com/reference/%EB%A7%88%EC%BC%93-%EC%BD%94%EB%93%9C-%EC%A1%B0%ED%9A%8C
https://api.upbit.com/v1/market/all
URL 을 직접 호출하기만 하면 됩니다.
queryParam
으로는 유의 종목 조회를 위한 isDetails
파라미터가 있습니다.
총 4개의 필드가 존재합니다.
market
korean_name
english_name
market_warning
Java 에서 OpenAPI 를 통해 데이터를 받기 위해서는 응답DTO 를 먼저 생성해주어야 합니다.
@Getter
public class UpbitMarket {
private String market;
@SerializedName("korean_name")
private String koreanName;
@SerializedName("english_name")
private String englishName;
@SerializedName("market_warning")
private String marketWarning;
}
클래스 명은 UpbitMarket
으로 네이밍을 했습니다.
Java 프로그래밍을 할 땐 대부분 CamelCase
컨벤션을 따라 프로그래밍을 합니다.
하지만 API 에서 제공되는 변수명은 snake_case
로 되어 있습니다.
따라서, Java 어플리케이션에서 CamelCase
로 사용하기 위해서는 @SerializedName
어노테이션을 활용하여 snake_case
를 CamelCase
로 변경해줄 수 있습니다.
참고.
@SerializedName
어노테이션은gson
라이브러리 의존성을 추가해야 사용할 수 있습니다.
implementation 'com.google.code.gson:gson:2.9.0'
v1/market/all
API 에 대한 정보를 enum
으로 관리하고자 합니다. 추후 새로운 OpenAPI 가 추가되었을 때도 이 enum
에 추가할 수 있도록 하기 위함입니다.
@Getter
public enum UpbitRequestType {
MARKET_ALL_V1("v1/market/all", HttpMethod.GET)
;
private String url;
private HttpMethod method;
UpbitRequestType(String url, HttpMethod method) {
this.url = url;
this.method = method;
}
public static String getFullUrl(UpbitRequestType requestType) {
return "https://api.upbit.com/" + requestType.getUrl();
}
}
요청 쿼리를 표현하는 클래스입니다.
url
, method
, body
, param
이 포함되어 있습니다.
@Getter
public class UpbitRequestQuery {
private final String url;
private final HttpMethod method;
private final String body;
private final String param;
@Builder
public UpbitRequestQuery(String url, HttpMethod method, String body, String param) {
assertThat(url).isNotBlank();
assertThat(method).isNotNull();
this.url = url;
this.method = method;
this.body = body;
this.param = param;
}
}
upbitHttpClient.request(...)
를 통해서 응답값을 문자열로 받아올 수 있습니다.
public List<Market> getAllMarkets(MarketClause clause) {
UpbitRequestType requestType = UpbitRequestType.MARKET_ALL_V1;
UpbitRequestQuery query = UpbitRequestQuery.builder()
.url(UpbitRequestType.getFullUrl(requestType))
.body(null)
.param("isDetails=" + clause.isDetails())
.method(HttpMethod.GET)
.build();
String data = upbitHttpClient.request(query);
List<UpbitMarket> result = jsonDeserializer.deserializeAsList(data, UpbitMarket.class);
return marketConverter.convert(result);
}
Market
객체는 domain
내부에서 사용하는 데이터 객체로 다음과 같습니다.
@Getter
public class Market {
private String market;
private String koreanName;
private String englishName;
private String marketWarning;
... // 생성자 설정
}
@Getter
public class MarketClause {
private boolean isDetails;
public MarketClause(boolean isDetails) {
this.isDetails = isDetails;
}
}
그러면 upbitHttpClient.request(...)
는 어떻게 구성되어 있을까요?
authToken
값을 가져와 HttpURLConnection
을 통해 커넥션을 맺고 요청을 하게 됩니다.
public String request(UpbitRequestQuery query) {
String authToken = getAuthToken(query.getParam());
try {
URL url = new URL(query.getUrl());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(query.getMethod().name());
conn.setRequestProperty("Authorization", authToken);
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
if (StringUtils.isNotBlank(query.getBody())) {
OutputStream os = conn.getOutputStream();
os.write(query.getBody().getBytes());
os.flush();
}
return getApiResponse(conn);
} catch (Exception e) {
log.error("failed to request API with auth: {}", e.getMessage());
throw new InvalidUpbitRequestException("failed to request API with auth");
}
}
private String getAuthToken(String query) {
Algorithm algo = Algorithm.HMAC256(secretKey);
if (StringUtils.isBlank(query)) {
return "Bearer " + JWT.create()
.withClaim("access_key", accessKey)
.withClaim("nonce", randomUUID().toString())
.sign(algo);
}
return "Bearer " + JWT.create()
.withClaim("access_key", accessKey)
.withClaim("nonce", randomUUID().toString())
.withClaim("query", query)
.sign(algo);
}
private String getApiResponse(HttpURLConnection conn) {
try {
BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String inputLine;
while ((inputLine = br.readLine()) != null) {
stringBuilder.append(inputLine);
}
br.close();
return stringBuilder.toString();
} catch (Exception e) {
log.error("failed to get api response: {}", e.getMessage(), e);
throw new InvalidUpbitRequestException("failed to get api response");
}
}
여기서 나온 Algorithm
라이브러리는 build.gradle
파일에 아래 코드를 추가해주셔야 합니다.
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
implementation("io.jsonwebtoken:jjwt-jackson:0.11.5")
implementation("com.auth0:java-jwt:4.0.0")
RuntimeException 을 상속받아 만든 별도의 커스텀 예외입니다.
public class InvalidUpbitRequestException extends RuntimeException {
public InvalidUpbitRequestException(String msg) {
super(msg);
}
}
문자열로 받아온 데이터를 기존에 생성해두었던 UpbitMarket
으로 변환하는 과정이 필요합니다.
jsonDeserializer.deserializeAsList
를 활용하면 됩니다.
public List<Market> getAllMarkets(MarketClause clause) {
UpbitRequestType requestType = UpbitRequestType.MARKET_ALL_V1;
UpbitRequestQuery query = UpbitRequestQuery.builder()
.url(UpbitRequestType.getFullUrl(requestType))
.body(null)
.param("isDetails=" + clause.isDetails())
.method(HttpMethod.GET)
.build();
String data = upbitHttpClient.request(query);
List<UpbitMarket> result = jsonDeserializer.deserializeAsList(data, UpbitMarket.class);
return marketConverter.convert(result);
}
@Component
public class JsonDeserializer {
private final GsonUtil gsonUtil;
public JsonDeserializer(GsonUtil gsonUtil) {
this.gsonUtil = gsonUtil;
}
public <T> List<T> deserializeAsList(String data, Class<T> classOfT) {
if (StringUtils.isBlank(data)) {
return Collections.emptyList();
}
Type type = new ListParameterizedType(classOfT);
return gsonUtil.fromJson(data, type);
}
}
이 역시도 gson
라이브러리 의존성이 필요합니다.
이 클래스는 업비트에서 받아온 UpbitMarket
객체를 내부에서 사용할 Market
객체로 변환하는 클래스입니다. MapStruct
라이브러리를 활용해서 변환해도 되고, 직접 변환하는 코드를 작성해도 됩니다.
아래는 MapStruct 라이브러리를 활용한 예시입니다.
@Mapper(config = MapStructConfig.class)
public interface MarketConverter {
@Mapping(source = "market", target = "marketSymbol")
Market convert(UpbitMarket source);
@Mapping(source = "market", target = "marketSymbol")
List<Market> convert(List<UpbitMarket> sources);
}
@MapperConfig(
componentModel = MappingConstants.ComponentModel.SPRING,
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
imports = {SupportValidation.class}
)
public class MapStructConfig {
}
위 과정을 거치면 OpenAPI 에서 데이터를 받아와 내부 어플리케이션에서 활용할 수 있습니다.