battery
Android를 위한 쉬운 웹 API 호출
박준규
스포카
• 한솔넥스지 2009 - 2012
• StyleShare 2012
• 넥슨 2012 - 2015
• 스포카 2015 - 현재
• https://github.com/segfault87
• https://facebook.com/segfault87
발표자 소개
battery
batteries
included
• Java (Android)를 위한 Web API 클라이언트
• MIT 라이센스
• BETA™
• API가 바뀔 수 있습니다
• 문서화도 안 되어 있습니다
• 홈페이지도 아직 없습니다
• 하지만 돌아갑니다 프로덕션에서 사용중…
디자인 목표
• 보일러플레이트의 최소화
• 요청 객체와 응답 객체를 나누지 않는다
• 특정 구조를 강제하지 않는다
• 데이터 모델은 POJO + Annotation
• 노출될 필요가 없는 것은 최대한 숨긴다
• 속도보다는 유연함이 우선
https://github.com/spoqa/battery
사용법
• cd your-project

git clone https://github.com/spoqa/battery.git
• compile project(':battery')
• Jcenter, Maven central에는 첫 정식 릴리즈와 함께 올라갑니다.
• 그 땐 아마 이렇게 하시면 될듯
• compile ‘com.spoqa:battery:1.+’
사용법
@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();
}
});
}
}
@RpcObject
• API 객체에 대한 메타데이터
• API 객체를 정의하기 위해서 별도의 상위 객체를 extend할 필요 없음
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 스타일
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 생성
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
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;
}
엔티티 바디
HTTP 요청
@RpcObject(
method=HttpRequest.Methods.POST,
uri=“/signin”,
requestSerializer=JsonCodec.class
)
public class SignInClass { … }
엔티티 바디
엔티티 serializer는 아래처럼 지정할 수 있다.
default는 UrlEncodedFormEncoder.class
HTTP 요청
엔티티 바디
Multipart entity는 추후 지원 예정
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 어노테이션을 사용
HTTP 응답
@Response(required=true) public boolean result;
@Response
응답에 필수적인 필드는 required를 true로 설정
이 경우 누락시 예외 발생
@Response public int foo;
@Response public Integer bar;
Boxed primitive 사용 가능
HTTP 응답
{“user”: {“nickname”: “kyu”,
“email”: “kyu@spoqa.com”},
‘activities’: []}
@Response(“user.nickname”) public String nickname;
@Response(“user.email”) public String email;
@Response public List<UserActivity> activities;
@Response
Subobject 참조
HTTP 응답
@RpcObject(…)
public class FooObject {
private int id;
@Response public void setId(int id) { this.id = id; }
}
@Response
Setter methods
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
HTTP 응답
@Response
• 그럼 모든 응답 필드에 대해 @Response를 붙여야 하나요?
• 아닙니다.
• 최상단 객체에만 붙여주면 됩니다.
• 하위 객체의 경우 필요하다면 (필드명 override, required=true) 붙
일 수 있습니다.
• 요청과 응답 필드들이 한 클래스 안에 섞이는 것이 싫은데요?
HTTP 응답
@ResponseObject
@RpcObject(…)
public class FooObject {
…
public static class FooResponse {
public int id;
public String name;
}
@ResponseObject public FooResponse response;
}
HTTP 응답
• 응답은 JSON만 지원하나요?
• 현재로선 그렇습니다.
• 다른 포맷도 추가할 계획이 있습니다. (XML, …)
• (일단은) 모듈러하게 구현되어 있습니다.
• ObjectBuilder.registerDeserializer(new JsonCodec());
• ObjectBuilder.registerDeserializer(new YourCodec());
필드 네이밍 자동 변환
{ “user_id”: 1,
“display_name”: “kyu”}
@RpcObject(
localName=CamelCaseTransformer.class,
remoteName=UnderscoreNameTransformer.class,
…)
public class BarObject {
@Response public long userId;
@Response public String displayName;
}
응답 뿐만 아니라 요청 필드에도 일률적으로 적용됨
커스텀 필드 타입
@Response public Date createdAt;
Date 자료형을 알아먹게 만들려면
RpcContext ctx = …;
ctx.registerTypeAdapter(new Rfc1123DateAdapter());
기본 포함된 Date adapter의 종류
• Iso8601DateAdapter
• Rfc1123DateAdapter
• TimestampDateAdapter
열거형 enumeration
public enum Currency {
KRW, USD, EUR, JPY
}
@RpcObject(…)
public class PurchaseObject {
@RequestBody public Currency currency;
@Response public List<Currency> acceptableCurrencies;
}
전역 설정 오버라이드
@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);
요청 전처리
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);
}
});
응답 검증 response validation
{ “result”: true,
“message”: “request successful”
“data”: { … }
}
API 레벨의 오류를 예외로 변환
위 응답을 POJO로 다음과 같이 정의할 수 있습니다.
public class CommonResponse {
public boolean result;
public String message;
}
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
응답 검증 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);
}
}
});
응답 검증 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();
}
});
에러 코드 일반화
{ “result”: 0,
“message”: “successful”,
“data”: { … } }
{ “result”: 100,
“message”: “’cause you used it so wrong” }
{ “result”: 101,
“message”: “sh*t happened” }
일반적으로 API 에러는 identifier를 가진다.
에러 코드 일반화
public @interface ErrorCode {
public String[] value();
}
일반적으로 API 에러는 identifier를 가진다.
에러 코드 일반화
에러 코드 일반화
• @ErrorCode 걸린 모든 예외 클래스들을 검색하여 map에
추가
• Java reflection의 도움으로
• 이렇게 하면 선언만으로 등록이 가능
• ResponseValidator에서 해당 응답의 에러 코드를
map에서 검색
• 발견한다면 예외 instantiate 후 throw
에러 코드 일반화
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 { … }
}
});
전역 예외 핸들링
• 호출과 관계없이 특정 종류의 예외 발생시 호출
• 예를 들어 세션 만료 에러가 발생시 SharedPreferences
에서 세션 정보 삭제 후 다시 로그인 페이지로 돌아가야 한
다거나…
• 핸들러 내에서 현재 UI 컨텍스트를 참조해야 한다면
invokeAsync()에 세번째 인자로 현재 Context를 전
달
• ctx.invokeAsync(req, onResponse, TestActivity.this);
전역 예외 핸들링
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());
전역 예외 핸들링
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()
전역 예외 핸들링
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;
}
});
}
RxJava와 함께 쓰기
Observable<FooObject> ob = ctx.invokeObservable(this, new FooObject(…));
ob.subscribe(new Action1<FooObject>() {
@Override
public void call(FooObject fooObject) {
…
}
});
Kotlin에서 쓰기
• 객체 정의는 Java로 해야 됩니다.
• Java와 Kotlin은 reflection 구현이 달라서 호환이 안됨…
AndroidRpcContext(context).invokeAsync(
FooObject(),
object: OnResponse<FooObject> {
override fun onResponse(response: FooObject?) {
…
}
override fun onFailure(why: Throwable?) {
…
}
})
Proguard와 함께 쓰기
• Reflection에 의존하기 때문에 @RPCObject에는 사용 불
가능
• 필드명, 메소드명을 dynamically lookup하기 때문…
• -keep @com.spoqa.battery.annotations.RpcObject public class *
• 하위 객체가 있을 경우 안 됨
• 가장 좋은 방법은 객체를 특정 패키지에 몰아넣고 적용
• -keep public class com.your_app.objects.*
추후 개발 계획
• 성능개선
• field, method lookup은 고비용
• 현재는 cache를 둬서 반복되는 객체의 lookup을 최소화
하고 있음
• deterministic한 부분은 컴파일타임에서 최적화 가능
• annotation processor
추후 개발 계획
• Multipart encoder 구현
• 파일 업로드
• XML decoder 구현
• 문서화
• 테스트 스위트
• (정식) 릴리즈
버그 리포트, PR은 언제나 환영합니다!
https://github.com/spoqa/battery
개발자	 구합니다	 
kyu@spoqa.com

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

  • 1.
    battery Android를 위한 쉬운웹 API 호출 박준규 스포카
  • 2.
    • 한솔넥스지 2009- 2012 • StyleShare 2012 • 넥슨 2012 - 2015 • 스포카 2015 - 현재 • https://github.com/segfault87 • https://facebook.com/segfault87 발표자 소개
  • 3.
  • 4.
  • 5.
    • Java (Android)를위한 Web API 클라이언트 • MIT 라이센스 • BETA™ • API가 바뀔 수 있습니다 • 문서화도 안 되어 있습니다 • 홈페이지도 아직 없습니다 • 하지만 돌아갑니다 프로덕션에서 사용중…
  • 6.
    디자인 목표 • 보일러플레이트의최소화 • 요청 객체와 응답 객체를 나누지 않는다 • 특정 구조를 강제하지 않는다 • 데이터 모델은 POJO + Annotation • 노출될 필요가 없는 것은 최대한 숨긴다 • 속도보다는 유연함이 우선
  • 7.
  • 8.
    사용법 • cd your-project
 gitclone https://github.com/spoqa/battery.git • compile project(':battery') • Jcenter, Maven central에는 첫 정식 릴리즈와 함께 올라갑니다. • 그 땐 아마 이렇게 하시면 될듯 • compile ‘com.spoqa:battery:1.+’
  • 9.
    사용법 @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(); } }); } }
  • 10.
    @RpcObject • API 객체에대한 메타데이터 • API 객체를 정의하기 위해서 별도의 상위 객체를 extend할 필요 없음
  • 11.
    HTTP 요청 @RpcObject( method=HttpRequest.Methods.GET, uri=“/view_post/%1$s/%2$d” ) public classViewPostObject { public ViewPostObject(String userId, long postId) { this.userId = userId; this.postId = postId; } @UriPath(1) public String userId; @UriPath(2) public long postId; } REST 스타일
  • 12.
    HTTP 요청 @RpcObject( method=HttpRequest.Methods.GET, uri=“/list” ) public classListObject { 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 생성
  • 13.
    HTTP 요청 @RpcObject(uri=“/details”) public classDetailsObject { public ListObject(List<Integer> id) { this.id = id; } @QueryString public List<Integer> id; } 쿼리 스트링 http://foobar.local:5000/details?id=1&id=2&id=3
  • 14.
    HTTP 요청 @RpcObject( method=HttpRequest.Methods.POST, uri=“/signin” ) public classSignInObject { public SignInObject(String id, String password) { this.id = id; this.password = password; } @RequestBody public String id; @RequestBody public String password; } 엔티티 바디
  • 15.
    HTTP 요청 @RpcObject( method=HttpRequest.Methods.POST, uri=“/signin”, requestSerializer=JsonCodec.class ) public classSignInClass { … } 엔티티 바디 엔티티 serializer는 아래처럼 지정할 수 있다. default는 UrlEncodedFormEncoder.class
  • 16.
    HTTP 요청 엔티티 바디 Multipartentity는 추후 지원 예정
  • 17.
    HTTP 응답 @RpcObject(…) public classProfileObject { … @Response public long id; @Response public String user; @Response(“display_name”) public String nickname; @Response public List<Post> recentPosts; } @Response 어노테이션을 사용
  • 18.
    HTTP 응답 @Response(required=true) publicboolean result; @Response 응답에 필수적인 필드는 required를 true로 설정 이 경우 누락시 예외 발생 @Response public int foo; @Response public Integer bar; Boxed primitive 사용 가능
  • 19.
    HTTP 응답 {“user”: {“nickname”:“kyu”, “email”: “kyu@spoqa.com”}, ‘activities’: []} @Response(“user.nickname”) public String nickname; @Response(“user.email”) public String email; @Response public List<UserActivity> activities; @Response Subobject 참조
  • 20.
    HTTP 응답 @RpcObject(…) public classFooObject { private int id; @Response public void setId(int id) { this.id = id; } } @Response Setter methods
  • 21.
    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
  • 22.
    HTTP 응답 @Response • 그럼모든 응답 필드에 대해 @Response를 붙여야 하나요? • 아닙니다. • 최상단 객체에만 붙여주면 됩니다. • 하위 객체의 경우 필요하다면 (필드명 override, required=true) 붙 일 수 있습니다. • 요청과 응답 필드들이 한 클래스 안에 섞이는 것이 싫은데요?
  • 23.
    HTTP 응답 @ResponseObject @RpcObject(…) public classFooObject { … public static class FooResponse { public int id; public String name; } @ResponseObject public FooResponse response; }
  • 24.
    HTTP 응답 • 응답은JSON만 지원하나요? • 현재로선 그렇습니다. • 다른 포맷도 추가할 계획이 있습니다. (XML, …) • (일단은) 모듈러하게 구현되어 있습니다. • ObjectBuilder.registerDeserializer(new JsonCodec()); • ObjectBuilder.registerDeserializer(new YourCodec());
  • 25.
    필드 네이밍 자동변환 { “user_id”: 1, “display_name”: “kyu”} @RpcObject( localName=CamelCaseTransformer.class, remoteName=UnderscoreNameTransformer.class, …) public class BarObject { @Response public long userId; @Response public String displayName; } 응답 뿐만 아니라 요청 필드에도 일률적으로 적용됨
  • 26.
    커스텀 필드 타입 @Responsepublic Date createdAt; Date 자료형을 알아먹게 만들려면 RpcContext ctx = …; ctx.registerTypeAdapter(new Rfc1123DateAdapter()); 기본 포함된 Date adapter의 종류 • Iso8601DateAdapter • Rfc1123DateAdapter • TimestampDateAdapter
  • 27.
    열거형 enumeration public enumCurrency { KRW, USD, EUR, JPY } @RpcObject(…) public class PurchaseObject { @RequestBody public Currency currency; @Response public List<Currency> acceptableCurrencies; }
  • 28.
    전역 설정 오버라이드 @RpcObject( uri=“http://foobar.local:5000/qux”, requestSerializer=UrlEncodedFormEncoder.class, localName=CamelCaseTransformer.class, remoteName=UnderscoreNameTransformer.class) RpcContextctx = …; ctx.setDefaultUriPrefix(“http://foobar.local:5000”); ctx.setRequestSerializer(new UrlEncodedFormEncoder()); ctx.setFieldNameTransformer(new CamelCaseTransformer.class, new UnderscoreNameTransformer.class);
  • 29.
    요청 전처리 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); } });
  • 30.
    응답 검증 responsevalidation { “result”: true, “message”: “request successful” “data”: { … } } API 레벨의 오류를 예외로 변환 위 응답을 POJO로 다음과 같이 정의할 수 있습니다. public class CommonResponse { public boolean result; public String message; }
  • 31.
    public class SignInResponseextends CommonResponse { public String accessToken; } public class ListResponse extends CommonResponse { public List<Article> data; } public class SignOutResponse extends CommonResponse { // No additional data } 응답 검증 response validation
  • 32.
    응답 검증 responsevalidation 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); } } });
  • 33.
    응답 검증 responsevalidation 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(); } });
  • 34.
    에러 코드 일반화 {“result”: 0, “message”: “successful”, “data”: { … } } { “result”: 100, “message”: “’cause you used it so wrong” } { “result”: 101, “message”: “sh*t happened” } 일반적으로 API 에러는 identifier를 가진다.
  • 35.
    에러 코드 일반화 public@interface ErrorCode { public String[] value(); } 일반적으로 API 에러는 identifier를 가진다.
  • 36.
  • 37.
    에러 코드 일반화 •@ErrorCode 걸린 모든 예외 클래스들을 검색하여 map에 추가 • Java reflection의 도움으로 • 이렇게 하면 선언만으로 등록이 가능 • ResponseValidator에서 해당 응답의 에러 코드를 map에서 검색 • 발견한다면 예외 instantiate 후 throw
  • 38.
    에러 코드 일반화 ctx.invokeAsync(newPayObject(…), new OnResponse<PayObject>() { … @Override public void onFailure(Throwable why) { if (why instanceof CreditCardExpired) { … } else if (why instanceof CreditCardStolen) { … } else if (why instanceof CreditCardWithdrawn) { … } else { … } } });
  • 39.
    전역 예외 핸들링 •호출과 관계없이 특정 종류의 예외 발생시 호출 • 예를 들어 세션 만료 에러가 발생시 SharedPreferences 에서 세션 정보 삭제 후 다시 로그인 페이지로 돌아가야 한 다거나… • 핸들러 내에서 현재 UI 컨텍스트를 참조해야 한다면 invokeAsync()에 세번째 인자로 현재 Context를 전 달 • ctx.invokeAsync(req, onResponse, TestActivity.this);
  • 40.
    전역 예외 핸들링 publicclass 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());
  • 41.
    전역 예외 핸들링 publicclass 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()
  • 42.
    전역 예외 핸들링 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; } }); }
  • 43.
    RxJava와 함께 쓰기 Observable<FooObject>ob = ctx.invokeObservable(this, new FooObject(…)); ob.subscribe(new Action1<FooObject>() { @Override public void call(FooObject fooObject) { … } });
  • 44.
    Kotlin에서 쓰기 • 객체정의는 Java로 해야 됩니다. • Java와 Kotlin은 reflection 구현이 달라서 호환이 안됨… AndroidRpcContext(context).invokeAsync( FooObject(), object: OnResponse<FooObject> { override fun onResponse(response: FooObject?) { … } override fun onFailure(why: Throwable?) { … } })
  • 45.
    Proguard와 함께 쓰기 •Reflection에 의존하기 때문에 @RPCObject에는 사용 불 가능 • 필드명, 메소드명을 dynamically lookup하기 때문… • -keep @com.spoqa.battery.annotations.RpcObject public class * • 하위 객체가 있을 경우 안 됨 • 가장 좋은 방법은 객체를 특정 패키지에 몰아넣고 적용 • -keep public class com.your_app.objects.*
  • 46.
    추후 개발 계획 •성능개선 • field, method lookup은 고비용 • 현재는 cache를 둬서 반복되는 객체의 lookup을 최소화 하고 있음 • deterministic한 부분은 컴파일타임에서 최적화 가능 • annotation processor
  • 47.
    추후 개발 계획 •Multipart encoder 구현 • 파일 업로드 • XML decoder 구현 • 문서화 • 테스트 스위트 • (정식) 릴리즈 버그 리포트, PR은 언제나 환영합니다! https://github.com/spoqa/battery
  • 48.