[Java] OpenAPI 연결해보기

김대니·2023년 1월 15일
0

Java

목록 보기
2/2

들어가며

개발을 하다보면 OpenAPI 에 연결할 필요가 있습니다. 공공 데이터 포털에서 제공하는 공휴일 API 일 수도 있고, Upbit 가상화폐 거래소에서 제공하는 코인 정보일 수도 있습니다.

Java 에서 OpenAPI 를 연결하는 방법은 정말 다양하게 많지만 그 중 하나를 소개하며 어떻게 연결할 수 있을지에 대해서 정리해보겠습니다.

API 소개

가상화폐 거래소 업비트에서 제공하는 OpenAPI 를 사용해볼 예정입니다.

거래 가능한 가상화폐 목록 API

이 API 는 업비트에서 거래할 수 있는 전체 가상화폐 목록을 JSON 형태로 조회할 수 있습니다.

https://api.upbit.com/v1/market/all

API Spec. 확인

상세 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

Response DTO 생성

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 으로 네이밍을 했습니다.

snake_case vs. CamelCase

Java 프로그래밍을 할 땐 대부분 CamelCase 컨벤션을 따라 프로그래밍을 합니다.
하지만 API 에서 제공되는 변수명은 snake_case 로 되어 있습니다.
따라서, Java 어플리케이션에서 CamelCase 로 사용하기 위해서는 @SerializedName 어노테이션을 활용하여 snake_caseCamelCase 로 변경해줄 수 있습니다.

참고. @SerializedName 어노테이션은 gson 라이브러리 의존성을 추가해야 사용할 수 있습니다.

implementation 'com.google.code.gson:gson:2.9.0'

OpenAPI 호출하기

요청 URL 을 Enum 으로 관리하기

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

요청 Query 클래스

요청 쿼리를 표현하는 클래스입니다.
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 객체

Market 객체는 domain 내부에서 사용하는 데이터 객체로 다음과 같습니다.

@Getter
public class Market {
    private String market;
    private String koreanName;
    private String englishName;
    private String marketWarning;
    
    ... // 생성자 설정
}

MarketClause 객체

@Getter
public class MarketClause {
    private boolean isDetails;

    public MarketClause(boolean isDetails) {
        this.isDetails = isDetails;
    }
}

그러면 upbitHttpClient.request(...) 는 어떻게 구성되어 있을까요?

upbitHttpClient

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")

InvalidUpbitRequestException

RuntimeException 을 상속받아 만든 별도의 커스텀 예외입니다.

public class InvalidUpbitRequestException extends RuntimeException {
    public InvalidUpbitRequestException(String msg) {
        super(msg);
    }
}

Deserialization

문자열로 받아온 데이터를 기존에 생성해두었던 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);
}

JsonDeserializer

@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 라이브러리 의존성이 필요합니다.

MarketConverter

이 클래스는 업비트에서 받아온 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 에서 데이터를 받아와 내부 어플리케이션에서 활용할 수 있습니다.

profile
?=!, 물음표를 느낌표로

0개의 댓글