각종 후기/우아한테크코스

[우아한 테크코스 3기] LEVEL 2 회고 (120일차)

제이온 (Jayon) 2021. 6. 1.

안녕하세요? 제이온입니다.

 

오늘은 오프라인으로 우테코를 진행했고, 저만의 토이 프로젝트를 구현하는 데 많은 시간을 투자했습니다.

 

 

업비트를 이용한 코인 자동 매매 프로그램 제작 동기 및 구조 설명

저는 우테코에 들어오기 전에 키움증권 api를 이용하여 파이썬 언어로 주식 자동 매매 프로그램을 만든 적이 있습니다. 다만, 우테코 합격 이후에는 Java 공부를 하였고 점점 미완성된 프로젝트에 손이 가지 않았습니다. 결정적으로는 키움증권 api 문서가 불친절하기도 하고, 파이썬을 사용하고 싶지 않았습니다.

 

그러던 어느 날, 우연히 유튜브에서 업비트를 이용한 코인 자동 매매 프로그램을 접했고, 공식 문서가 상당히 친절한 것을 확인하였습니다. Node, Python, Ruby, Java에 대한 예시 코드를 주고 있으며, 해당 기능이 어떤 역할을 하며, 요청과 응답 모델이 잘 나와 있었습니다.

 

이번 2달 이상 스프링을 열심히 공부하였고, 배운 내용을 적용하자는 차원에서 저만의 코인 자동 매매 프로그램 토이 프로젝트를 구현하기로 마음먹었습니다. 그래서 업비트 홈페이지에서 open api를 발급받고, 구조를 어떻게 짜야할지 고민하였습니다.

 

우선, 기존에는 프론트와 서버 간의 통신을 구현하는 프로그램만 만들었지만, 이번에는 서버와 서버 간의 통신을 구현해 주어야하므로 굉장히 어색했습니다. 이 부분은 마크의 도움을 얻어서 구조를 그려낼 수 있었습니다.

 

 

https://woowabros.github.io/experience/2021/02/05/pilot-project-siyoung.html

 

 

위와 같이 컨트롤러 단에서 클라이언트의 요청을 외부 API로 보낸 후, 외부 API의 응답을 다시 가공하여 서비스에게 보내는 것입니다. 결론적으로는 우리가 항상 하던 MVC 패턴에서 api 패키지정도가 추가될 뿐이었죠.

 

만약, 외부 api에서 받은 데이터를 가공 없이 그대로 클라이언트에게 응답해도 된다면 서비스까지 가지 않고, 가공해야 한다면 서비스까지 가야하는 것이죠. 그리고 서비스에서도 DB의 필요성이 생긴다면 DAO 계층을 추가하고, 필요하지 않다면 임의의 비즈니스 로직만 취하는 것입니다.

 

 

업비트를 이용한 코인 자동 매매 프로그램 구현 - 전체 계좌 조회

오늘은 업비트 api의 첫 번째 기능인 전체 계좌 조회를 구현하였습니다. 전반적인 코드 자체는 다 주어져 있었습니다.

 

 

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.util.UUID;

public class GetAccounts {

    public static void main(String[] args) {
        String accessKey = System.getenv("UPBIT_OPEN_API_ACCESS_KEY");
        String secretKey = System.getenv("UPBIT_OPEN_API_SECRET_KEY");
        String serverUrl = System.getenv("UPBIT_OPEN_API_SERVER_URL");

        Algorithm algorithm = Algorithm.HMAC256(secretKey);
        String jwtToken = JWT.create()
                .withClaim("access_key", accessKey)
                .withClaim("nonce", UUID.randomUUID().toString())
                .sign(algorithm);

        String authenticationToken = "Bearer " + jwtToken;

        try {
            HttpClient client = HttpClientBuilder.create().build();
            HttpGet request = new HttpGet(serverUrl + "/v1/accounts");
            request.setHeader("Content-Type", "application/json");
            request.addHeader("Authorization", authenticationToken);

            HttpResponse response = client.execute(request);
            HttpEntity entity = response.getEntity();

            System.out.println(EntityUtils.toString(entity, "UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

 

 

코드는 다음과 같은데, 처음 보는 라이브러리도 꽤 보였습니다. 우선, Algorithm은 무엇인지는 잘모르겠으나 특정 키 값을 해시함수를 통해 해싱하는 것으로 파악하였습니다. jwtToken을 만드는 과정은 이전 우테코 미션에서 열심히 응용했던 것이라 어렵지 않았습니다. 중요한 것은 그 다음부터입니다.

 

서버와 서버 간의 통신을 한다는 소리는 결국 한 쪽은 클라이언트가 되어야 한다는 뜻입니다. 우리는 업비트 api에게 요청을 보내야하므로 우리의 서버가 클라이언트가 됩니다. 그래서 HttpClient, HttpGet, HttpResponse, HttpEntity를 사용하여 api 통신 이후 응답을 얻어올 수 있습니다. 그리고 EntityUtils를 통해 최종적인 응답을 String으로 받아오는 것이죠.

 

 

 

 

예제 응답은 위와 같으며, 자세한 응답 모델은 아래와 같습니다.

 

 

 

 

여기서 중요한 점은 해당 응답 필드가 스네이크 케이스라는 것입니다. 저의 프로젝트 코드는 카멜 케이스인 것과는 대조적이죠. 그래서 이 부분을 직렬화 및 역직렬화하기 위해 굉장히 굉장히 고생했습니다.

 

 

우선, 저는 직렬화를 위해서 gson이 아닌 jackson에서 제공하는 ObjectMapper를 사용하였습니다. 

 

 

    private AllAccountResponses requestAllAccounts(CloseableHttpClient client, HttpGet request) {
        try {
            final CloseableHttpResponse response = client.execute(request);
            final HttpEntity entity = response.getEntity();
            final String result = EntityUtils.toString(entity, "UTF-8");
            final ObjectMapper mapper = new ObjectMapper();
            final List<AllAccountResponse> allAccountResponses = mapper
                .readValue(result, mapper.getTypeFactory().constructCollectionType(List.class, AllAccountResponse.class));
            return new AllAccountResponses(allAccountResponses);
        } catch (IOException e) {
            e.printStackTrace();
            throw new InvalidIOException("라이브러리 예외가 발생하였습니다.");
        }
    }

 

 

해당 코드에서 result는 json 목록이 담긴 형태의 문자열이 됩니다. 그래서 List<AllAccountResponse>로 매핑해 주는 메소드를 찾아야했고, 그 메소드는 위와 같았습니다. 자세한 원리는 모르겠으나 mapper.getTypeFactory().constructCollectionType()를 사용하면 가능하다고 합니다. 그러면 역직렬화할 1단계는 끝났습니다.

 

이제, 스네이크 케이스인 문자열을 카멜 케이스인 문자열에 매핑을 해 줘야 합니다. 이것은 'PropertyNamingStrategies.SnakeCaseStrategy'을 사용하면 됩니다. 참고로, PropertyNamingStrategy는 레거시 코드이므로 2.12 버전부터는 복수 형태의 Strategies를 쓰시길 바랍니다.

 

 

@NoArgsConstructor
@AllArgsConstructor
@Getter
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class AllAccountResponse {

    private String currency;
    private String balance;
    private String locked;
    private String avgBuyPrice;
    private boolean avgBuyPriceModified;
    private String unitCurrency;
}

 

 

위와 같이 모든 Dto에 @JsonNaming을 붙이는 방법도 있으나 이건 귀찮습니다. 그래서 yml 설정 파일에 한 번에 설정해 줄 수 있습니다.

 

 

spring:
  jackson:
    property-naming-strategy: 'com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy'

 

 

다만 따옴표를 반드시 붙여줘야 합니다. 이 부분은 항상 붙여야 하는 것은 아닌데, 해당 문자열만으로 클래스를 찾을 수 있으면 안붙이고 찾을 수 없다면 붙여줘야 합니다.

 

 

 

 

예를 들어, org.h2.Driver는 해당 문자열 만으로 클래스를 찾을 수 있습니다. 실제로 'CTRL + 클릭'을 해 보면 클래스 이동을 하는 것을 알 수 있죠. 다만, 위의 PropertyNamingStrategies 경로는 yml이 인식을 못하므로 따옴표를 붙여줘야 합니다.

 

 

다음으로, @Value를 통한 설정 파일에서의 값 가져오기입니다. 

 

 

@Component
public class JwtTokenProvider {

    @Value("${security.access-key}")
    private String accessKey;

    @Value("${security.secret-key}")
    private String secretKey;

    public String createToken() {
        final Algorithm algorithm = Algorithm.HMAC256(secretKey);
        return JWT.create()
            .withClaim("access_key", accessKey)
            .withClaim("nonce", UUID.randomUUID().toString())
            .sign(algorithm);
    }
}

 

 

위의 코드처럼 yml 설정 파일에 있는 값을 @Value를 통해 가져올 수 있습니다. 저는 처음에 @Value가 붙은 변수를 static으로 선언하였는데, 이 상황에서는 항상 null 값이 나왔습니다. 이유를 찾으려고 구글링을 했는데 생각보다 이유가 허무했습니다. 그냥 @Value 자체가 static 변수에 대해서는 지원을 안한다고 합니다 ㅎㅎ..

 

 

https://www.baeldung.com/spring-inject-static-field

 

 

그 외에, @Component가 아닌 @Configuration과 @Bean을 통한 빈 주입, JwtTokenProvider이 존재 이유를 이해하는 등 배웠던 내용을 복습하는 유익한 경험이었다고 생각합니다. 앞으로도 스프링 개념을 정리하면서 해당 토이 프로젝트를 진행하려고 합니다.

 

 

배포 환경에서의 Swagger fetch 오류

로컬에서는 가능하나, 배포 환경에서 Swagger 테스트가 작동하지 않는 오류가 발생했습니다. cors 오류인가 싶어서 별의별 설정을 하였으나 성과가 없었습니다. 그러던중 다니가 이 문제를 해결해 주었습니다.

 

이유는 간단합니다. Swagger의 host가 실제 배포 주소가 아니었기때문이죠.

 

 

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
            .host("jayon-subway.r-e.kr")
            .select()
            .apis(RequestHandlerSelectors.basePackage("wooteco.subway"))
            .paths(PathSelectors.ant("/**"))
            .build()
            .securityContexts(Lists.newArrayList(securityContext()))
            .securitySchemes(Lists.newArrayList(apiKey()));
    }

 

 

여기서 host()를 설정하지 않으면, 기본값(?)인 'app'이 자동으로 들어가는 것으로 추정되는데, 이렇게 하면 올바른 api 요청이 되지 않으므로 에러가 발생합니다. 따라서 저의 도메인 주소를 적어줘야 합니다. 이후에는 에러가 말끔히 해결되었습니다.

 

 

정리

무엇이든 직접 해보는 것이 중요하다는 사실을 오늘 절실히 깨달았습니다. 강의나 책을 통해 개념을 습득하는 것도 중요하지만, 제가 생각하기에 가장 빨리 성장할 수 있는 길은 '일단 해보기'라고 생각합니다. 많은 것을 경험해보면서 얕은 지식을 쌓아 놓고, 그 빈틈을 차후에 강의나 책을 통해 채워가는 것이죠. 그리고 배운 내용을 제가 좋아하는 주제에 접목하니까 오랜 시간 개발해도 재미가 있었고, 역경이 있어도 포기하지 않고 이겨낼 수 있었던 것 같습니다. 앞으로도 야생형과 학자형 방식을 합친 학습을 이어나가려고 합니다.

댓글

추천 글