성능 테스트 설계서 - SeungpilPark/uEngine-bill GitHub Wiki

테스트 준비를 위한 킬빌 서버 API 코드 작성

package org.uengine.garuda.bill.kbapi;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.uengine.garuda.util.HttpUtils;
import org.uengine.garuda.util.JsonUtils;
import sun.misc.BASE64Encoder;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by uengine on 2016. 6. 16..
 */
public class KillbillApi {

    private HttpUtils httpUtils;
    private String host;
    private int port;
    private String user;
    private String password;
    private String apiKey;
    private String apiSecret;

    public HttpUtils getHttpUtils() {
        return httpUtils;
    }

    public void setHttpUtils(HttpUtils httpUtils) {
        this.httpUtils = httpUtils;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getUser() {
        return user;
    }

    public void setUser(String user) {
        this.user = user;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getApiKey() {
        return apiKey;
    }

    public void setApiKey(String apiKey) {
        this.apiKey = apiKey;
    }

    public String getApiSecret() {
        return apiSecret;
    }

    public void setApiSecret(String apiSecret) {
        this.apiSecret = apiSecret;
    }

    public KillbillApi(String host, int port, String user, String password, String apiKey, String apiSecret) {
        this.host = host;
        this.port = port;
        this.user = user;
        this.password = password;
        this.apiKey = apiKey;
        this.apiSecret = apiSecret;

        this.httpUtils = new HttpUtils();
    }

    public void createTenant(String externalKey) {
        String method = "POST";
        String path = "/1.0/kb/tenants";

        Map params = new HashMap();
        params.put("externalKey", externalKey);
        params.put("apiKey", externalKey);
        params.put("apiSecret", externalKey);


        Map headers = new HashMap();
        try {
            this.apiRequest(method, path, JsonUtils.marshal(params), headers);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public Map getTenantByApiKey(String apiKey) {
        String method = "GET";
        String path = "/1.0/kb/tenants";

        Map params = new HashMap();
        params.put("apiKey", apiKey);
        String getQueryString = HttpUtils.createGETQueryString(params);

        Map headers = new HashMap();
        try {
            HttpResponse httpResponse = this.apiRequest(method, path + getQueryString, null, headers);
            if (httpResponse.getStatusLine().getStatusCode() == 200) {
                String s = EntityUtils.toString(httpResponse.getEntity());
                return JsonUtils.unmarshal(s);
            } else {
                return null;
            }
        } catch (IOException ex) {
            ex.printStackTrace();
            return null;
        }
    }

    public void createAccount(String name, String email, String currency, int billCycleDayLocal, String externalKey) {
        String method = "POST";
        String path = "/1.0/kb/accounts";

        Map params = new HashMap();
        params.put("externalKey", externalKey);
        params.put("currency", currency);
        params.put("name", name);
        params.put("email", email);
        params.put("billCycleDayLocal", billCycleDayLocal);


        Map headers = new HashMap();
        try {
            this.apiRequest(method, path, JsonUtils.marshal(params), headers);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public void updateCatalog(String catalogXml) {
        String method = "POST";
        String path = "/1.0/kb/catalog";

        Map headers = new HashMap();
        headers.put("Content-Type", "application/xml");
        try {
            this.apiRequest(method, path, catalogXml, headers);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public void createSubscription(String accountId, String planName) {
        String method = "POST";
        String path = "/1.0/kb/subscriptions";

        Map params = new HashMap();
        params.put("accountId", accountId);
        params.put("planName", planName);

        Map headers = new HashMap();
        try {
            this.apiRequest(method, path, JsonUtils.marshal(params), headers);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public Map getUser(String externalKey){
        String method = "GET";
        String path = "/1.0/kb/accounts";

        Map params = new HashMap();
        params.put("externalKey", externalKey);
        String getQueryString = HttpUtils.createGETQueryString(params);

        Map headers = new HashMap();
        try {
            HttpResponse httpResponse = this.apiRequest(method, path + getQueryString, null, headers);
            if (httpResponse.getStatusLine().getStatusCode() == 200) {
                String s = EntityUtils.toString(httpResponse.getEntity());
                return JsonUtils.unmarshal(s);
            } else {
                return null;
            }
        } catch (IOException ex) {
            ex.printStackTrace();
            return null;
        }
    }

    private HttpResponse apiRequest(String method, String path, String data, Map headers) throws IOException {

        String auth = this.user + ":" + this.password;
        BASE64Encoder encoder = new BASE64Encoder();
        String encode = encoder.encode(auth.getBytes());

        Map mergeHeaders = new HashMap();
        mergeHeaders.put("Authorization", "Basic " + encode);
        mergeHeaders.put("Content-Type", "application/json");
        mergeHeaders.put("Accept", "application/json");
        mergeHeaders.put("X-Killbill-CreatedBy", "OpenBill");
        mergeHeaders.put("X-Killbill-ApiKey", this.apiKey);
        mergeHeaders.put("X-Killbill-ApiSecret", this.apiSecret);

        mergeHeaders.putAll(headers);

        String url = "http://" + this.host + ":" + this.port + path;

        HttpResponse httpResponse = httpUtils.makeRequest(method, url, data, mergeHeaders);
        return httpResponse;
    }
}

대용량 카달로그 전송

대용량 카달로그 업데이트시 소요시간을 측정한다.

대용량 카달로그 생성 코드

package org.uengine.garuda.test;

import org.joda.time.DateTime;
import org.killbill.billing.catalog.*;
import org.killbill.billing.catalog.api.*;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.xmlloader.ValidatingConfig;
import org.killbill.xmlloader.XMLLoader;
import org.killbill.xmlloader.XMLSchemaGenerator;
import org.springframework.util.FileCopyUtils;
import org.uengine.garuda.bill.kbapi.KillbillApi;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URI;
import java.util.*;

/**
 * Created by uengine on 2016. 12. 13..
 */
public class CatalogGenerateTest {
    public static Marshaller marshaller(final Class<?> clazz) throws JAXBException, SAXException, IOException, TransformerException {
        final JAXBContext context = JAXBContext.newInstance(clazz);

        final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        final Marshaller um = context.createMarshaller();

        final Schema schema = factory.newSchema(new StreamSource(XMLSchemaGenerator.xmlSchema(clazz)));
        um.setSchema(schema);

        return um;
    }

    /**
     * 한화 1만원, 달러 10달러 프라이스 리스트
     *
     * @return
     */
    public static DefaultInternationalPrice internationalPrice() {
        DefaultInternationalPrice internationalPrice = new DefaultInternationalPrice();

        DefaultPrice price1 = new DefaultPrice();
        price1.setCurrency(Currency.USD);
        price1.setValue(new BigDecimal(10));

        DefaultPrice price2 = new DefaultPrice();
        price2.setCurrency(Currency.KRW);
        price2.setValue(new BigDecimal(10000));

        DefaultPrice[] prices = new DefaultPrice[]{price1, price2};
        internationalPrice.setPrices(prices);
        return internationalPrice;
    }

    public static void main(String[] args) throws Exception {
        String user = "admin";
        String password = "password";
        String apiKey = "forcs";
        String apiSecret = "forcs";
        String host = "localhost";
        int port = 8080;

        //포시에스 테넌트 가져오기
        KillbillApi api = new KillbillApi(host, port, user, password, apiKey, apiSecret);
        Map tenant = api.getTenantByApiKey("forcs");

        //포시에스 테넌트에 카달로그 생성(기본 상품 4개 보유 + 서드파티 상품 1만개 등록)
        XMLLoader xmlLoader = new XMLLoader();
        StandaloneCatalog catalog = xmlLoader.getObjectFromUri(new URI("catalog/FORCS.xml"), StandaloneCatalog.class);

        catalog.getCatalogName();
        Collection<Product> currentProducts = catalog.getCurrentProducts();
        Collection<Plan> currentPlans = catalog.getCurrentPlans();
        List<Product> currentProductsList = new ArrayList<Product>(currentProducts);
        List<Plan> currentPlansList = new ArrayList<Plan>(currentPlans);

        DefaultPriceListSet priceLists = catalog.getPriceLists();
        DefaultPriceList defaultPricelist = priceLists.getDefaultPricelist();
        Collection<Plan> plans = defaultPricelist.getPlans();
        List<Plan> plansList = new ArrayList<Plan>(plans);


        String subProductName = "ADD_ON_";
        int subProductCount = 20000;
        for (int i = 0; i < subProductCount; i++) {
            //신규 프로덕트 이름
            String productName = subProductName + i;

            //신규 프로덕트
            Product product = new DefaultProduct(productName, ProductCategory.STANDALONE);

            //신규 플랜
            String planName = subProductName + i + "_PLAN";

            //finalPhase 를 지정한다.
            DefaultPlanPhase finalPhase = new DefaultPlanPhase();

            //finalPhase 의 phaseType
            finalPhase.setPhaseType(PhaseType.EVERGREEN);

            //finalPhase 의 Duration
            finalPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.UNLIMITED));

            //finalPhase 의 recurring
            DefaultRecurring recurring = new DefaultRecurring();
            recurring.setBillingPeriod(BillingPeriod.MONTHLY);
            recurring.setRecurringPrice(internationalPrice());
            finalPhase.setRecurring(recurring);

            //플랜 생성
            DefaultPlan defaultPlan = new DefaultPlan();
            defaultPlan.setProduct(product);
            defaultPlan.setFinalPhase(finalPhase);
            defaultPlan.setName(planName);

            //생성한 프로덕트, 플랜, 디폴트 프라이스 리스트 등록
            currentProductsList.add(product);
            currentPlansList.add(defaultPlan);
            plansList.add(new DefaultPlan().setName(planName));
        }

        //카탈로그에 프로덕트 리스트, 플랜리스트, 프라이스 리스트 오버라이드
        currentProducts = new HashSet<Product>(currentProductsList);
        currentPlans = new HashSet<Plan>(currentPlansList);
        defaultPricelist.setPlans(new HashSet<Plan>(plansList));

        catalog.setProducts(currentProducts);
        catalog.setPlans(currentPlans);

        //xml 변환
        Marshaller m = marshaller(StandaloneCatalog.class);
        File file = new File("/Users/uengine/IdeaProjects/OpenBill/openbill-test/src/main/resources/catalog/override.xml");
        m.marshal(catalog, file);

        //킬빌에 xml 전송
        String catalogXml = FileCopyUtils.copyToString(new FileReader(file));
        api.updateCatalog(catalogXml);
        //포시에스 테넌트에 유저(구매자) 100만명 등록

    }
}

카달로그 버전 중첩과 성능

데일리마다 한번씩 대용량 카달로그가 업데이트 된다는 가정하에, 카달로그 버젼을 업데이트 해가며 서브스크립션 차지 계산 및 인보이스 생성 능력을 테스트함.

서브스크립션 생성 코드

package org.uengine.garuda.test;

import org.uengine.garuda.bill.kbapi.KillbillApi;

import java.util.Map;
import java.util.Random;

/**
 * Created by uengine on 2016. 12. 13..
 */
public class SubscriptionGenerateTest {

    public static void main(String[] args) throws Exception {

        //서브스크립션 1만유저 * 2개 = 2만개 생성
        //1명당 기본 1개 + 추가상품중 하나 구독.
        //기본은 basic-monthly, 추가상품은 ADD_ON_랜덤숫자_PLAN

        int userNumbers = 100;
        int thredsNumbers = 50;

        for (int i = 40; i < thredsNumbers; i++) {
            int startCount = i * userNumbers + 1;
            int endCount = startCount + userNumbers;

            CreateSubscription test = new CreateSubscription(startCount, endCount);
            test.start();
        }
    }

    public static class CreateSubscription extends Thread {
        int startCount;
        int endCount;

        public CreateSubscription(int startCount, int endCount) {
            this.startCount = startCount;
            this.endCount = endCount;
        }

        public void run() {
            String user = "admin";
            String password = "password";
            String apiKey = "forcs";
            String apiSecret = "forcs";
            String host = "localhost";
            int port = 8080;

            int max = 20000;
            Random rand = new Random();
            int addonIndex = rand.nextInt(max);

            KillbillApi api = new KillbillApi(host, port, user, password, apiKey, apiSecret);
            for (int c = startCount; c < endCount; c++) {
                String userName = "test-user-" + c;
                String externalKey = userName;
                Map userMap = api.getUser(externalKey);
                if(userMap != null && userMap.containsKey("accountId")){
                    String accountId = (String) userMap.get("accountId");
                    String planName = "ADD_ON_" + addonIndex + "_PLAN";

                    api.createSubscription(accountId, planName);

                    String basicPlanName = "basic-monthly";
                    api.createSubscription(accountId, basicPlanName);
                }
            }
        }
    }
}

쓰레드별(스트레스 테스트) 성능 측정

사용자 생성 api 호출을 다음과 같이 돌렸을 때 성능을 측정한다.

사용자 생성 코드

package org.uengine.garuda.test;

import org.killbill.billing.catalog.*;
import org.killbill.billing.catalog.api.*;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.xmlloader.XMLLoader;
import org.killbill.xmlloader.XMLSchemaGenerator;
import org.springframework.util.FileCopyUtils;
import org.uengine.garuda.bill.kbapi.KillbillApi;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URI;
import java.util.*;

/**
 * Created by uengine on 2016. 12. 13..
 */
public class AccountGenerateTest {

    public static void main(String[] args) throws Exception {

        LogOff.logOff();

        //포시에스 테넌트에 유저(구매자) 1만명 등록
        //100명씩 100개 쓰레드로 접속.
        int userNumbers = 100;
        int thredsNumbers = 100;

        for (int i = 80; i < thredsNumbers; i++) {
            int startCount = i * userNumbers + 1;
            int endCount = startCount + userNumbers;

            CreateAcount test = new CreateAcount(startCount, endCount);
            test.start();
        }
    }

    public static class CreateAcount extends Thread {
        int startCount;
        int endCount;

        public CreateAcount(int startCount, int endCount) {
            this.startCount = startCount;
            this.endCount = endCount;
        }

        public void run() {
            String user = "admin";
            String password = "password";
            String apiKey = "forcs";
            String apiSecret = "forcs";
            String host = "localhost";
            int port = 8080;

            KillbillApi api = new KillbillApi(host, port, user, password, apiKey, apiSecret);
            for (int c = startCount; c < endCount; c++) {
                String name = "test-user-" + c;
                String email = name + "@gmail.com";
                String currency = "USD";
                String externalKey = name;
                int billCycleDayLocal = 5;
                api.createAccount(name, email, currency, billCycleDayLocal, externalKey);
            }
        }
    }
}

⚠️ **GitHub.com Fallback** ⚠️