GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

48
battery Android위한 쉬운 API 호출 박준규 스포카

Transcript of GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

Page 1: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

batteryAndroid를 위한 쉬운 웹 API 호출

박준규 스포카

Page 2: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

• 한솔넥스지 2009 - 2012

• StyleShare 2012

• 넥슨 2012 - 2015

• 스포카 2015 - 현재

• https://github.com/segfault87

• https://facebook.com/segfault87

발표자 소개

Page 3: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

battery

Page 4: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

batteries included

Page 5: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

• Java (Android)를 위한 Web API 클라이언트

• MIT 라이센스

• BETA™

• API가 바뀔 수 있습니다

• 문서화도 안 되어 있습니다

• 홈페이지도 아직 없습니다

• 하지만 돌아갑니다 프로덕션에서 사용중…

Page 6: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

디자인 목표

• 보일러플레이트의 최소화

• 요청 객체와 응답 객체를 나누지 않는다

• 특정 구조를 강제하지 않는다

• 데이터 모델은 POJO + Annotation

• 노출될 필요가 없는 것은 최대한 숨긴다

• 속도보다는 유연함이 우선

Page 7: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

https://github.com/spoqa/battery

Page 8: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

사용법

• cd your-projectgit clone https://github.com/spoqa/battery.git

• compile project(':battery')

• Jcenter, Maven central에는 첫 정식 릴리즈와 함께 올라갑니다.

• 그 땐 아마 이렇게 하시면 될듯

• compile ‘com.spoqa:battery:1.+’

Page 9: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

사용법

@RpcObject(uri=“http://ip.jsontest.com”) public class TestObject { @Response public String ip; }

public class TestActivity extends Activity { … private void test() { AndroidRpcContext context = new AndroidRpcContext(this); context.invokeAsync(new TestObject(), new OnResponse<TestObject>() { @Override public void onResponse(TestObject responseBody) { Log.d(TAG, “Your IP address: “ + responseBody.ip); } @Override public void onFailure(Throwable why) { why.printStackTrace(); } }); } }

Page 10: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

@RpcObject

• API 객체에 대한 메타데이터

• API 객체를 정의하기 위해서 별도의 상위 객체를 extend할 필요 없음

Page 11: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 요청

@RpcObject( method=HttpRequest.Methods.GET, uri=“/view_post/%1$s/%2$d” ) public class ViewPostObject { public ViewPostObject(String userId, long postId) { this.userId = userId; this.postId = postId; }

@UriPath(1) public String userId; @UriPath(2) public long postId; }

REST 스타일

Page 12: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 요청

@RpcObject( method=HttpRequest.Methods.GET, uri=“/list” ) public class ListObject { public ListObject(int offset, int limit) { this.offset = offset; this.limit = limit; }

@QueryString public int offset; @QueryString(“count”) public int limit; // 명시적 이름 설정

}

쿼리 스트링

context.setDefaultUriPrefix(“http://foobar.local:5000”); context.invokeAsync(new ListObject(0, 100), new OnResponse<ListObject>() { … });

http://foobar.local:5000/list?offset=0&count=100 생성

Page 13: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 요청

@RpcObject(uri=“/details”)

public class DetailsObject {

public ListObject(List<Integer> id) {

this.id = id;

}

@QueryString public List<Integer> id;

}

쿼리 스트링

http://foobar.local:5000/details?id=1&id=2&id=3

Page 14: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 요청

@RpcObject( method=HttpRequest.Methods.POST, uri=“/signin” ) public class SignInObject { public SignInObject(String id, String password) { this.id = id; this.password = password; }

@RequestBody public String id; @RequestBody public String password; }

엔티티 바디

Page 15: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 요청

@RpcObject( method=HttpRequest.Methods.POST, uri=“/signin”, requestSerializer=JsonCodec.class ) public class SignInClass { … }

엔티티 바디

엔티티 serializer는 아래처럼 지정할 수 있다.

default는 UrlEncodedFormEncoder.class

Page 16: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 요청

엔티티 바디

Multipart entity는 추후 지원 예정

Page 17: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 응답

@RpcObject(…)

public class ProfileObject {

@Response public long id;

@Response public String user;

@Response(“display_name”) public String nickname;

@Response public List<Post> recentPosts;

}

@Response 어노테이션을 사용

Page 18: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 응답

@Response(required=true) public boolean result;

@Response

응답에 필수적인 필드는 required를 true로 설정 이 경우 누락시 예외 발생

@Response public int foo;

@Response public Integer bar;

Boxed primitive 사용 가능

Page 19: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 응답

{“user”: {“nickname”: “kyu”,

“email”: “[email protected]”},

‘activities’: []}

@Response(“user.nickname”) public String nickname;

@Response(“user.email”) public String email;

@Response public List<UserActivity> activities;

@Response

Subobject 참조

Page 20: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 응답

@RpcObject(…)

public class FooObject {

private int id;

@Response public void setId(int id) { this.id = id; }

}

@Response

Setter methods

Page 21: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 응답

@Response

하위 응답 객체의 생성자

public static class SubObject {

public String foo;

}

public static class SubObject {

public SubObject(String foo) { this.foo = foo; }

public String foo;

}

public static class SubObject {

public SubObject() { this.foo = null; }

public SubObject(String foo) { this.foo = foo; }

public String foo;

}

O

O

X

Page 22: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 응답

@Response

• 그럼 모든 응답 필드에 대해 @Response를 붙여야 하나요?

• 아닙니다.

• 최상단 객체에만 붙여주면 됩니다.

• 하위 객체의 경우 필요하다면 (필드명 override, required=true) 붙일 수 있습니다.

• 요청과 응답 필드들이 한 클래스 안에 섞이는 것이 싫은데요?

Page 23: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 응답

@ResponseObject

@RpcObject(…)

public class FooObject {

public static class FooResponse {

public int id;

public String name;

}

@ResponseObject public FooResponse response;

}

Page 24: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

HTTP 응답

• 응답은 JSON만 지원하나요?

• 현재로선 그렇습니다.

• 다른 포맷도 추가할 계획이 있습니다. (XML, …)

• (일단은) 모듈러하게 구현되어 있습니다.

• ObjectBuilder.registerDeserializer(new JsonCodec());

• ObjectBuilder.registerDeserializer(new YourCodec());

Page 25: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

필드 네이밍 자동 변환

{ “user_id”: 1,

“display_name”: “kyu”}

@RpcObject(

localName=CamelCaseTransformer.class,

remoteName=UnderscoreNameTransformer.class,

…)

public class BarObject {

@Response public long userId;

@Response public String displayName;

}

응답 뿐만 아니라 요청 필드에도 일률적으로 적용됨

Page 26: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

커스텀 필드 타입

@Response public Date createdAt;

Date 자료형을 알아먹게 만들려면

RpcContext ctx = …;

ctx.registerTypeAdapter(new Rfc1123DateAdapter());

기본 포함된 Date adapter의 종류

• Iso8601DateAdapter

• Rfc1123DateAdapter

• TimestampDateAdapter

Page 27: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

열거형 enumeration

public enum Currency {

KRW, USD, EUR, JPY

}

@RpcObject(…)

public class PurchaseObject {

@RequestBody public Currency currency;

@Response public List<Currency> acceptableCurrencies;

}

Page 28: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

전역 설정 오버라이드

@RpcObject(

uri=“http://foobar.local:5000/qux”,

requestSerializer=UrlEncodedFormEncoder.class,

localName=CamelCaseTransformer.class,

remoteName=UnderscoreNameTransformer.class)

RpcContext ctx = …;

ctx.setDefaultUriPrefix(“http://foobar.local:5000”);

ctx.setRequestSerializer(new UrlEncodedFormEncoder());

ctx.setFieldNameTransformer(new CamelCaseTransformer.class,

new UnderscoreNameTransformer.class);

Page 29: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

요청 전처리

RpcContext ctx = …;

ctx.setRequestPreprocessor(new RequestPreprocessor() {

@Override

public void validateContext(Object forWhat) throws ContextException {}

@Override

public void processHttpRequest(HttpRequest req) {

req.putHeader(“X-Application-Id”, BuildConfig.APP_ID);

req.putHeader(“X-Shared-Secret”, BuildConfig.SHARED_SECRET);

}

});

Page 30: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

응답 검증 response validation

{ “result”: true, “message”: “request successful” “data”: { … } }

API 레벨의 오류를 예외로 변환

위 응답을 POJO로 다음과 같이 정의할 수 있습니다.

public class CommonResponse { public boolean result; public String message; }

Page 31: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

public class SignInResponse extends CommonResponse { public String accessToken; }

public class ListResponse extends CommonResponse { public List<Article> data; }

public class SignOutResponse extends CommonResponse { // No additional data }

응답 검증 response validation

Page 32: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

응답 검증 response validation

RpcContext ctx = …;

ctx.setResponseValidator(new ResponseValidator() {

@Override

public void validate(Object object) throws ResponseValidationException {

if (object instanceof CommonResponse) {

CommonResponse response = (CommonResponse) object;

if (!response.result)

throw new ResponseValidationException(response.message);

}

}

});

Page 33: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

응답 검증 response validation

ctx.invokeAsync(new SignInObject(…), new OnResponse<SignInObject>() {

@Override

public void onResponse(SignInObject response) {

// Nothing wrong happened

}

@Override

public void onFailure(Throwable why) {

// ResponseValidator에서 예외 발생시 이 쪽으로 옴

why.printStackTrace();

}

});

Page 34: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

에러 코드 일반화

{ “result”: 0,

“message”: “successful”,

“data”: { … } }

{ “result”: 100,

“message”: “\’cause you used it so wrong” }

{ “result”: 101,

“message”: “sh*t happened” }

일반적으로 API 에러는 identifier를 가진다.

Page 35: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

에러 코드 일반화

public @interface ErrorCode { public String[] value(); }

일반적으로 API 에러는 identifier를 가진다.

Page 36: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

에러 코드 일반화

Page 37: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

에러 코드 일반화

• @ErrorCode 걸린 모든 예외 클래스들을 검색하여 map에 추가

• Java reflection의 도움으로

• 이렇게 하면 선언만으로 등록이 가능

• ResponseValidator에서 해당 응답의 에러 코드를 map에서 검색

• 발견한다면 예외 instantiate 후 throw

Page 38: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

에러 코드 일반화

ctx.invokeAsync(new PayObject(…), new OnResponse<PayObject>() {

@Override

public void onFailure(Throwable why) {

if (why instanceof CreditCardExpired) {

} else if (why instanceof CreditCardStolen) {

} else if (why instanceof CreditCardWithdrawn) {

} else { … }

}

});

Page 39: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

전역 예외 핸들링

• 호출과 관계없이 특정 종류의 예외 발생시 호출

• 예를 들어 세션 만료 에러가 발생시 SharedPreferences에서 세션 정보 삭제 후 다시 로그인 페이지로 돌아가야 한다거나…

• 핸들러 내에서 현재 UI 컨텍스트를 참조해야 한다면 invokeAsync()에 세번째 인자로 현재 Context를 전달

• ctx.invokeAsync(req, onResponse, TestActivity.this);

Page 40: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

전역 예외 핸들링

public class SessionExpiredHandler implements ExceptionHandler<Context> {

@Override

public boolean onException(Context context, Throwable error) {

SharedPreferences prefs = context.getSharedPreferences(…);

prefs.edit().remove(“session_key”).apply();

context.startActivity(new Intent(context, SignInActivity.class));

return true; // 참일 경우 예외를 더 이상 상위로 전달하지 않는다.

}

}

ctx.registerExceptionHandler(SessionExpiredException.class,

new SessionExpiredHandler());

Page 41: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

전역 예외 핸들링

public class CredentialException extends Throwable { … }

public class WrongPasswordException extends CredentialException { … }

public class TooManyRetriesException extends WrongPasswordException { … }

ctx.registerExceptionHandler(Throwable.class, new FooHandler());

ctx.registerExceptionHandler(CredentialException.class, new BarHandler());

ctx.registerExceptionHandler(WrongPasswordException.class, new BazHandler());

ctx.registerExceptionHandler(TooManyRetriesException.class, new QuxHandler());

요청 결과로 TooManyRetriesException 발생 시 핸들러 평가 순서 QuxHandler → BazHandler → BarHandler → FooHandler → OnResponse.onFailure()

Page 42: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

전역 예외 핸들링if (BuildConfig.DEBUG) { ctx.registerExceptionHandler(Throwable.class, new ExceptionHandler<Context>() { @Override public boolean onException(Context context, Throwable error) { StringWriter sw = new StringWriter(); error.printStackTrace(new PrintWriter(sw)); new AlertDialog.Builder(context) .setTitle(“오류 발생”)

.setMessage(sw.toString()) .setPositiveButton(“닫기”, new DialogInterface.OnClickListener() {

@Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }) .show(); return false; } }); }

Page 43: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

RxJava와 함께 쓰기

Observable<FooObject> ob = ctx.invokeObservable(this, new FooObject(…));

ob.subscribe(new Action1<FooObject>() {

@Override

public void call(FooObject fooObject) {

}

});

Page 44: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

Kotlin에서 쓰기

• 객체 정의는 Java로 해야 됩니다.

• Java와 Kotlin은 reflection 구현이 달라서 호환이 안됨…

AndroidRpcContext(context).invokeAsync(

FooObject(),

object: OnResponse<FooObject> {

override fun onResponse(response: FooObject?) {

}

override fun onFailure(why: Throwable?) {

}

})

Page 45: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

Proguard와 함께 쓰기

• Reflection에 의존하기 때문에 @RPCObject에는 사용 불가능

• 필드명, 메소드명을 dynamically lookup하기 때문…

• -keep @com.spoqa.battery.annotations.RpcObject public class *

• 하위 객체가 있을 경우 안 됨

• 가장 좋은 방법은 객체를 특정 패키지에 몰아넣고 적용

• -keep public class com.your_app.objects.*

Page 46: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

추후 개발 계획

• 성능개선

• field, method lookup은 고비용

• 현재는 cache를 둬서 반복되는 객체의 lookup을 최소화하고 있음

• deterministic한 부분은 컴파일타임에서 최적화 가능

• annotation processor

Page 47: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

추후 개발 계획

• Multipart encoder 구현

• 파일 업로드

• XML decoder 구현

• 문서화

• 테스트 스위트

• (정식) 릴리즈

버그 리포트, PR은 언제나 환영합니다! https://github.com/spoqa/battery

Page 48: GKAC 2015 Apr. - Battery, 안드로이드를 위한 쉬운 웹 API 호출

개발자 구합니다 [email protected]