gkac 2015 apr. - battery, 안드로이드를 위한 쉬운 웹 api 호출
Post on 18-Jul-2015
425 Views
Preview:
TRANSCRIPT
batteryAndroid를 위한 쉬운 웹 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
• 노출될 필요가 없는 것은 최대한 숨긴다
• 속도보다는 유연함이 우선
사용법
• cd your-projectgit 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
top related