8. 사용법
• cd your-project
git clone 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();
}
});
}
}
12. 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 생성
13. 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
17. 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 어노테이션을 사용
18. HTTP 응답
@Response(required=true) public boolean 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 class FooObject {
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) 붙
일 수 있습니다.
• 요청과 응답 필드들이 한 클래스 안에 섞이는 것이 싫은데요?
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. 커스텀 필드 타입
@Response public Date createdAt;
Date 자료형을 알아먹게 만들려면
RpcContext ctx = …;
ctx.registerTypeAdapter(new Rfc1123DateAdapter());
기본 포함된 Date adapter의 종류
• Iso8601DateAdapter
• Rfc1123DateAdapter
• TimestampDateAdapter
27. 열거형 enumeration
public enum Currency {
KRW, USD, EUR, JPY
}
@RpcObject(…)
public class PurchaseObject {
@RequestBody public Currency currency;
@Response public List<Currency> acceptableCurrencies;
}
30. 응답 검증 response validation
{ “result”: true,
“message”: “request successful”
“data”: { … }
}
API 레벨의 오류를 예외로 변환
위 응답을 POJO로 다음과 같이 정의할 수 있습니다.
public class CommonResponse {
public boolean result;
public String message;
}
31. 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
32. 응답 검증 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);
}
}
});
33. 응답 검증 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();
}
});
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를 가진다.
37. 에러 코드 일반화
• @ErrorCode 걸린 모든 예외 클래스들을 검색하여 map에
추가
• Java reflection의 도움으로
• 이렇게 하면 선언만으로 등록이 가능
• ResponseValidator에서 해당 응답의 에러 코드를
map에서 검색
• 발견한다면 예외 instantiate 후 throw
38. 에러 코드 일반화
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 { … }
}
});
39. 전역 예외 핸들링
• 호출과 관계없이 특정 종류의 예외 발생시 호출
• 예를 들어 세션 만료 에러가 발생시 SharedPreferences
에서 세션 정보 삭제 후 다시 로그인 페이지로 돌아가야 한
다거나…
• 핸들러 내에서 현재 UI 컨텍스트를 참조해야 한다면
invokeAsync()에 세번째 인자로 현재 Context를 전
달
• ctx.invokeAsync(req, onResponse, TestActivity.this);
40. 전역 예외 핸들링
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());
41. 전역 예외 핸들링
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()
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