Today I Learned. by Rio

InnerClass in Java project 2

|

InnerClass를 조금 더 간단하게 사용하는 방법

public class Customer {

    private String name;
    private int age;
    private String address;
    private Dog dog;

    public static class Dog {
        private String name;
        private int age;
    }
}

위와 같이 내부 클래스 자체를 public static 으로 작성한다.

Customer.Dog dog = new Customer.Dog();

객체 생성은 이와 같이 선언한다. 원래의 코드와 다른게 없어 보이지만 사용하는 측에서 좀더 코드가 깔끔해진다.

여기서 InnerClass 자체를 static import 문을 사용해서 쓰면 아래와 같이 쓸 수 있다. 실제로는 내부 클래스이지만 코드를 읽을 때는 일반 객체를 만든 것과 비슷하게 읽혀서 가독성이 좋다. 테스트 코드를 작성하는 입장에서만 볼 때는, 도메인 단위로 내부 클래스를 만들고 난 후 자연스럽게 아래와 같이 많이 쓰게 된다.

import rio.kim.sample.Customer.Dog;

Dog dog = new Dog();

Test sourceset in gradle project

|

저번에 작성했던 “gradle testset plugin”에서 기본적으로 제공되는 테스트셋인 src/test/java 형태 외의 새로운 테스트 셋을 만드는 방법을 정리했는데, 그 플러그인을 사용하는 경우 한 가지 문제점이 발생한다.

보통 프로젝트 상황에서 main sourceset에 있는 모델 객체를 unit test에서 사용하기 위해 모델 객체의 builder등을 만들어 사용하곤 한다. 이런 경우에 gradle-testsets-plugin 플러그인을 사용하면 src/test/java 내에 있는 builder 또는 테스트를 위해 만든 util등을 사용할 수 없다. 다시말해 내가 새로이 만든 testset에서 src/test/java 내에 있는 코드를 사용할 수 없다는 말이다.(다행히 이 경우에도 build.gradle 내의 dependencies는 상속해서 사용할 수 있다. 또한 main sourceset에 있는 코드는 사용할 수 있다)

여튼, testset 내에서 작성한 코드를 새로 만든 testset에서 사용하기 위해서는 아래와 같이 설정하면 된다. gradle-testsets-plugin를 사용하지 않는다. 예시를 위해 integrationTest 이름을 가진 testset을 만든다고 가정한다.

build.gradle 설정

// gradle sourceset 설정
sourceSets {
    integrationTest {
        java {
            compileClasspath += main.output + test.output
            runtimeClasspath += main.output + test.output
            srcDir file('src/integrationTest/java')
        }
        resources.srcDir file('src/integrationTest/resources')
    }
}

// gradle task 설정
task integrationTest(type: Test) {
    testClassesDirs = sourceSets.integrationTest.output
    classpath = sourceSets.integrationTest.runtimeClasspath
}

// testset을 상속받겠다는 설정
configurations {
    integrationTestCompile.extendsFrom testCompile
    integrationTestRuntime.extendsFrom testRuntime
}

위와 같이 설정하면 src/test/java 내에서 작성한 코드들을 src/integrationTest/java 내에서 import하고 사용할 수 있다.

REST API client library in Java project

|

JAVA 프로젝트에서 REST API를 호출하는 방법은 여러 가지가 있다. 예제를 통해 기록한다. 예제는 bithumb 홈페이지에서 현재 비트코인 시세를 조회하는 open api를 호출하여 json형태의 결과를 가져오는 방법이다.

1. Pure JAVA Code

우선 가장 기본적인 방법은 http connection을 열고 api를 호출한 후 결과를 받아오는 방법이다. 이 방법은 외부 라이브러리를 쓰지 않고 순수한 java라이브러리만 활용한다. 사실 요새 외부 라이브러리들이 엄청 잘 나와 있는 시점에 굳이 이렇게 쓸 일은 거의 없을 것이지만, 기록을 위해.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class CallRestBithumbOpenApi {

    public String callTickerBtc() throws Exception {

        String urlString = "https://api.bithumb.com/public/ticker/btc";
        URL url = new URL(urlString);
        HttpURLConnection con = (HttpURLConnection) url.openConnection();
        con.setRequestMethod("GET");

        InputStreamReader in = new InputStreamReader(con.getInputStream(), "utf-8")
        BufferedReader br = new BufferedReader(in);

        String line;
        StringBuilder sb = new StringBuilder();
        while ((line = br.readLine()) != null) {
            sb.append(line).append("\n");
        }
        br.close();
        return sb.toString();
    }
}

2. RestTemplate

Spring 프로젝트에서는 일반적으로 RestTemplate을 활용한다. RestTemplate은 스프링 기반 프로젝트 구성 하에서 기본적으로 제공한다. 내가 만약 Spring 기반의 프로젝트를 구성하고 있고 별다른 추가 기능 없이 REST Api 호출 및 response 값 활용 정도의 목적이면 여타 다른 라이브러리를 사용하기보단 그냥 이걸 쓰는걸 추천.

import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import java.util.Map;

public class CallRestBithumbOpenApiRestTemplate {

    public Map callTickerBtc() throws Exception {

        // json형태의 return type인 경우 필요한 형태로 response를 재구성할 수 있다.
        // 이 경우는 map형태로 치환하여 받아왔다.
        RestTemplate restTemplate = new RestTemplate();

        String url = "https://api.bithumb.com/public/ticker/btc";
        ResponseEntity<Map> rawResult
                = restTemplate.getForEntity(url, Map.class);
        Map<String, Object> response = rawResult.getBody();
        return response;
    }
}

근데 모든 프로젝트가 스프링은 아니니까, RestTemplate를 쓰기 힘든 경우가 있다. 요것만 쓰기 위해 프로젝트 구성에 Spring dependency를 추가할 필요는 없다. 다른 잘 만들어진 오픈소스 라이브러리들이 있기 때문이다. 종류는 아래와 같다.

3. Rest-Assured

이 라이브러리는 response에 대한 검증에 특화된 특히 테스팅에 많이 쓰이는 라이브러리이다. 그렇다 해도 기본적인 REST API 호출 및 결과값에 대한 핸들링이 기타 다른 라이브러리들에 비해 상당히 편하다고 느꼈다. 아래의 예시는 가장 간단한 방식으로 response value를 얻고자 할 때 사용할 수 있으며 모델링 객체로 변환하거나 전체 response 내에서 원하는 값만 얻고자 할 때에는 jsonPath를 활용하여 사용할 수 있다. 이는 나중에 다시 포스트를 올려볼 예정.

import io.restassured.RestAssured;

import java.util.Map;

public class CallRestBithumbOpenApiRestAssured {

    public Map callTickerBtc() throws Exception {

        String url = "https://api.bithumb.com/public/ticker/btc";
        Map<String, Object> response = RestAssured.get(url).jsonPath().<Map>get("data");
        return response;
    }
}

4. Retrofit

Retrofit은 조금 다른 방식으로 작성된다. 다른 라이브러리와 가장 구별되는 특징은 API를 인터페이스 형태로 관리하여 사용하는 측에서 인터페이스를 호출하는 듯한 느낌을 주게 하는 것이다. 많은 갯수의 API들을 호출해야 하는 프로젝트에서 사용할 때 관리의 용이성 면에서 장점을 가질 수 있으나, 반대로 API를 호출하기 위해 작성하는 코드의 양이 다른 라이브러리를 쓸 때보다 많아진다.

  • Interface 작성
import retrofit2.Call;
import retrofit2.http.GET;

import java.util.Map;

public interface BithumbApiService {

    @GET("/public/ticker/btc")
    Call<Map> callBtc();
}
  • 호출하는 코드 작성
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;

import java.util.Map;

public class CallRestBithumbOpenApiRetrofit {

    public Map callTickerBtc() throws Exception {

        // Retrofit 전체 설정은 처음 한 번만 선언해 주면 된다.
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://api.bithumb.com")
                .addConverterFactory(JacksonConverterFactory.create())
                .build();

        BithumbApiService service = retrofit.create(BithumbApiService.class);
        Response<Map> execute = service.callBtc().execute();
        Map<String, Object> response = execute.body();

        return response;
    }
}

5. Unirest

아주 lightweight한 라이브러리라고 자신을 소개하고 있다. 사용법은 간단하다. 특징은 response를 순수한 JSONObject 형태로 리턴하기가 매우 쉽다는 점. 이렇게 순수한 형태의 JSONObject를 서비스단에서 핸들링할 일이 많으면 이 라이브러리를 썼을 때 이점을 얻을 수 있을 것이다.

import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import org.json.JSONObject;

public class CallRestBithumbOpenApiUnirest {

    public JSONObject callTickerBtc() throws Exception {

        String url = "https://api.bithumb.com/public/ticker/btc";
        HttpResponse<JsonNode> response = Unirest.get(url).asJson();
        return response.getBody().getObject();
    }
}

결론

작성하다보니 get메서드를 기본 예제로 하다보니 post에서 request를 만들어 넘기는 방식에 대한 언급이 좀 없게 되어서 아쉽지만 특히 라이브러리를 사용하는 경우에는 사용법이 그렇게 어렵지 않다. 아래의 사이트를 보면서 쉽게 따라할 수 있다.

이상의 오픈소스 라이브러리를 써보면서 느낀 점은, 라이브러리를 선택할 때 프로젝트의 상황에 따라 적절한 라이브러리가 다 다를것 같다는 생각이다. 개인적으로는 기능도 많고 사용을 많이 해봐서 익숙한 rest-assured를 자주 쓰게 되긴 함.

각 라이브러리 사이트

InnerClass in Java project

|

모델객체를 만들 때 여러 클래스가 중첩되게끔 구성하는 일이 많은데 하위에 있는 클래스가 여러개일 때 그 클래스들을 다 만들어주고나서 클래스 구성을 짜줘야 한다.

예를 들어보자. Customer라는 클래스를 만드는데 그 안에는 Customer의 정보를 담고 있는 필드들이 있고 그 필드 중에는 객체로 관리하게되는 필드도 있을 것이다. 내가 필요한 고객의 정보는 이름, 나이, 주소, 그리고 키우는 강아지의 이름과 나이다. 그러면 Customer의 필드로 개이름, 개나이 이렇게 넣는 것보다 Dog라는 객체를 만들어 그 Dog 안에 개의 이름과 나이를 넣는게 낫다.

이렇게 구조를 짜면 클래스가 두개 생긴다. Customer와 Dog 클래스. 근데 Dog 클래스는 단독으로 쓰이지 않고 Customer안에서만 필요하다고 생각해보자. (쓰다보니 말도안되는 예시를 드는거 같은데 프로젝트에서는 이런일이 빈번하다. 사내보안규정 때문에 그 코드를 가져올 수 없어서 혼자 생각한게 고작 이런 것들 ㅠㅠ) 그러면 불필요하게 Dog 클래스를 외부에 노출시킬 필요가 없다. 단지 Customer 안에서만 쓰이니까. 그러면 이렇게 만들 수 있다.

public class Customer {

    private String name;
    private int age;
    private String address;
    private Dog dog;

    public class Dog {
        private String name;
        private int age;
    }
}

물론 Constructor와 Getter, Setter, toString 등의 기본적인 메서드는 알아서 만들어 주자. 코드가 쓸데없이 길어지는 걸 피하기 위해 여기서는 제외. 그럼 외부에서는 이걸 어떻게 쓰냐.

우선 innerClass의 선언은

Customer customer = new Customer();
Customer.Dog dog = customer.new Dog();

이와 같이 선언한다. 그러면 숨어있는 innerClass를 다른 클래스에서 쓸 수 있다. 아래의 코드를 참고하자.

import org.junit.Test;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;

public class InnerClassTest {

    @Test
    public void innerClassTest() throws Exception {

        Customer customer = new Customer();
        customer.setName("Rio");
        customer.setAge(32);
        customer.setAddress("서울시 송파구");

        Customer.Dog dog = customer.new Dog(); // innerClass 선언부
        dog.setName("부침개");
        dog.setAge(2);
        customer.setDog(dog);

        assertThat(customer.getDog().getAge(), is(2)); // 성공

    }
}

또한 모델 객체가 아닌 경우에, 다시말해 innerClass에 메서드가 있는 경우에도 위에처럼 innerClass에 접근하여 메서드를 호출할 수 있다.

InnerClass인 Dog 클래스 내에 아래와 같은 메서드를 추가한다.

public void printDogInfo() {
    System.out.println("나는 " + name + " 입니다. 내 나이는 " + age + "입니다.");
}

호출부는 다음과 같다. 아까 만든 코드 마지막 부분에 넣으면 된다.

customer.getDog().printDogInfo();

이러면 정상적으로 출력

나는 부침개 입니다. 내 나이는 2입니다.

Gradle testset plugin

|

기본적으로 그래들 프로젝트는 unit test를 src/test/java 형태로 두는데, 이 외에 다른 test set을 구성하고 싶을 때가 있다. 예를 들면 같은 프로젝트 내에 api test를 따로 구성한다거나, ui test를 구성한다거나.

이런 경우에 test를 분리하는 방법에는 여러가지가 있는데 gradle-testsets-plugin 플러그인을 통해서 하는 방법을 알아보자.

build.gradle 설정

plugins {
  id 'org.unbroken-dome.test-sets' version '1.4.2'
}

또는 하위 버전에는 이런 식으로 플러그인을 추가한다.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'org.unbroken-dome.gradle-plugins:gradle-testsets-plugin:1.4.2'
    }
}

apply plugin: 'org.unbroken-dome.test-sets'

testset을 분리하기 위해 아래의 명령도 추가한다.

testSets {
  integrationTest
  integrationOfIntegrationTest { extendsFrom integrationTest }
}

만약 위와같이 쓴다면 testSet을 두개 더 만든거다.

따로 설정하지 않는다면 src/test/java 형태처럼 src/integrationTest/java 거고 상속관계도 저런식으로 처리할수 있다. integrationTest에 있는 놈들을 integrationOfIntegrationTest 쪽에서 쓸수 있다.

testSets 밑에 들어가는 새로만든 testSet은 기본 test를 자동 상속받는다.

만일 integrationTest 에서만 쓰는 외부 라이브러리(ex. rest-assured)가 있다면 아래와 같이 설정한다.

dependencies {
  integrationTestCompile group: 'io.rest-assured', name: 'rest-assured', version: '3.0.3'
}