์ถ์ฒ
- ChatGPT
๋ชฉํ
์ปค์คํ Annotation ํ์ฉ๋ฒ์ Retrofit์ ํตํด์ ๋ฐฐ์ด๋ค. ์ด๋ป๊ฒ ๊ธฐ๋ฅ์ ๊ตฌํํ๋์ง ์ดํ๋ค.
์์ธํ Annotation ์ฌ์ฉ ๋ฐ ๊ตฌํ ์ฌํญ๊น์ง๋... ๐ ๋ถ๋ด์ค๋ฌ์์.
Retrofit์์ Annotation์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ ์ฅ์
์ฝ๋์ ๊ฐ๋ ์ฑ ํฅ์
Annotation์ ํตํด ๋ณต์กํ HTTP ์์ฒญ ๋ก์ง์ ์ธํฐํ์ด์ค ์์ ์์ ๊ฐ๋จํ๊ฒ ์ ์ํ ์ ์๋ค.
์ ์ฐ์ฑ
Retrofit์ ๋ค์ํ HTTP ์์ฒญ ๋ฐฉ์๊ณผ ํ๋ผ๋ฏธํฐ ์ ์ก ๋ฐฉ์์ Annotation์ ํตํด ์ ์ฐํ๊ฒ ์ง์ํ๋ค.
์บก์ํ
Annotation์ ์ฌ์ฉํด HTTP ์์ฒญ์ ์ธ๋ถ ์ฌํญ์ ์บก์ํํจ์ผ๋ก์จ ํด๋ผ์ด์ธํธ ์ฝ๋๊ฐ ๋จ์ํด์ง๊ณ ์ ์ง๋ณด์๊ฐ ์ฌ์์ง๋ค.
Retrofit์์ ์ฌ์ฉํ๋ Annotation ์ ์ธ ์ฝ๋๋ค
https://github.com/square/retrofit/tree/trunk/retrofit/src/main/java/retrofit2/http
Retrofit์์ Annotation ์ฒ๋ฆฌ ํ๋ฆ ๋ฐ๋ผ๊ฐ๊ธฐ
ChatGPT์๊ฒ ๋ถํํด์ Retrofit์ ์ฌ์ฉํ๋ ์์ ์ฝ๋๋ฅผ ์ป์๋ค.
0. Retrofit ์์ ์ฝ๋ : GitHub API ํธ์ถ (Kotlin)
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
// 1. API ์ธํฐํ์ด์ค ์ ์
interface GitHubService {
@GET("users/{user}/repos")
fun listRepos(@Path("user") user: String): Call<List<Repo>>
}
// 2. ๋ฐ์ดํฐ ๋ชจ๋ธ ํด๋์ค ์ ์
data class Repo(
val id: Int,
val name: String,
val full_name: String
)
// 3. Retrofit ๊ฐ์ฒด ์์ฑ ๋ฐ ์ฌ์ฉ
fun main() {
// Retrofit ๊ฐ์ฒด ์์ฑ
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/") // GitHub API์ ๊ธฐ๋ณธ URL
.addConverterFactory(GsonConverterFactory.create()) // JSON ์๋ต์ ๊ฐ์ฒด๋ก ๋ณํ
.build()
// GitHubService ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด ์์ฑ
val service = retrofit.create(GitHubService::class.java)
// API ํธ์ถ
val repos = service.listRepos("octocat") // "octocat" ์ฌ์ฉ์์ ์ ์ฅ์ ๋ชฉ๋ก์ ์์ฒญ
repos.enqueue(object : retrofit2.Callback<List<Repo>> {
override fun onResponse(call: Call<List<Repo>>, response: retrofit2.Response<List<Repo>>) {
if (response.isSuccessful) {
// ์ฑ๊ณต ์ ์ ์ฅ์ ๋ชฉ๋ก ์ถ๋ ฅ
response.body()?.forEach { repo ->
println("Repository: ${repo.name}, Full name: ${repo.full_name}")
}
}
}
override fun onFailure(call: Call<List<Repo>>, t: Throwable) {
println("Error: ${t.message}")
}
})
}
์์ ์ค๋ช
1. API ์ธํฐํ์ด์ค ์ ์
- GitHubService ์ธํฐํ์ด์ค๋ @GET Annotation์ ์ฌ์ฉํด GitHub API์ ์ฌ์ฉ์ ์ ์ฅ์ ๋ชฉ๋ก์ ์์ฒญํ๋ ๋ฉ์๋๋ฅด ์ ์ํ๋ค.
- @Path("user")๋ ๋์ ์ผ๋ก URL ๊ฒฝ๋ก์ ์ฌ์ฉ์ ์ด๋ฆ์ ์ฝ์ ํ๋ค.
2. ๋ฐ์ดํฐ ๋ชจ๋ธ ํด๋์ค
- GitHub API์ ์๋ต ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ฒด๋ก ๋งคํํ๊ธฐ ์ํด Repo ๋ฐ์ดํฐ ํด๋์ค๋ฅผ ์ ์ํ๋ค. ์ด ํด๋์ค๋ JSON ์๋ต ํ๋์ ๋งคํ๋๋ค.
3. Retrofit ๊ฐ์ฒด ์์ฑ
- Retrofit.Builder()๋ฅผ ์ฌ์ฉํด Retrofit ์ธ์คํด์ค๋ฅผ ์์ฑํ๊ณ , baseUrl() ๋ฉ์๋๋ก GitHub API์ ๊ธฐ๋ณธ URL์ ์ค์ ํ๋ค.
- GsonConverterFactory๋ฅผ ์ถ๊ฐํด JSON ์๋ต์ ์๋์ผ๋ก Kotlin ๊ฐ์ฒด๋ก ๋ณํํ ์ ์๋ค๋ก ์ค์ ํ๋ค.
4. Retrofit์ create()
- retrofit.create(GitHubService::class.java)๋ฅผ ํธ์ถํด GitHubService ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด๋ฅผ ์์ฑํ๋ค. ์ด ๊ตฌํ์ฒด๋ ๋ฐํ์์ ๋์ ์ผ๋ก ์์ฑ๋ ํ๋ก์ ๊ฐ์ฒด๋ก, listRepos() ํธ์ถ ์ HTTP ์์ฒญ์ ๋ณด๋ธ๋ค.
5. API ํธ์ถ
- service.listRepos("actocat")์ ํธ์ถํด GitHub์ "octocat" ์ฌ์ฉ์์ ์ ์ฅ์ ๋ชฉ๋ก์ ๊ฐ์ ธ์จ๋ค.
- enqueue() ๋ฉ์๋๋ฅผ ์ฌ์ฉํด ๋น๋๊ธฐ ํธ์ถ์ ์คํํ๊ณ , ์๋ต์ด ์ฑ๊ณต์ ์ผ๋ก ๋์ฐฉํ๋ฉด ์ ์ฅ์ ๋ชฉ๋ก์ ์ถ๋ ฅํ๋ค.
1. Retrofit.create()
์์ ์ฝ๋ ๋ถ๋ถ
val service = retrofit.create(GitHubService::class.java)
์์ค ์ฝ๋ ๋ถ๋ถ
@SuppressWarnings("unchecked") // Single-interface proxy creation guarded by parameter safety.
public <T> T create(final Class<T> service) {
validateServiceInterface(service);
return (T)
Proxy.newProxyInstance(
service.getClassLoader(),
new Class<?>[] {service},
new InvocationHandler() {
private final Object[] emptyArgs = new Object[0];
@Override
public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
args = args != null ? args : emptyArgs;
Reflection reflection = Platform.reflection;
return reflection.isDefaultMethod(method)
? reflection.invokeDefaultMethod(method, service, proxy, args)
: loadServiceMethod(service, method).invoke(proxy, args);
}
});
}
์ฃผ์ ํ๋ฆ ๋ถ์
1. validateServiceInterface(service)
- API ์ธํฐํ์ด์ค๊ฐ ์ ์ ํ๊ฒ ์ ์๋์๋์ง ๊ฒ์ฆํ๋ ๋ฉ์๋
2. Proxy.newProxyInstance()
- ํ๋ก์ ํจํด์ ์ด์ฉํด service ์ธํฐํ์ด์ค์ ๋์ ๊ตฌํ์ฒด๋ฅผ ์์ฑํ๊ณ , ํธ์ถ๋๋ ๋ฉ์๋๋ฅผ InvocationHandler๊ฐ ๊ฐ๋ก์ฑ ์ฒ๋ฆฌํ๋ค.
3. loadServiceMethod(service, method)
API ์ธํฐํ์ด์ค์ ๋ฉ์๋์ ์ ์๋ Retrofit Annotation์ ๊ธฐ๋ฐ์ผ๋ก ServiceMethod๋ฅผ ์์ฑํ๋ค. ์ด ServiceMethod๋ HTTP ์์ฒญ์ ์ธ๋ถ ์ฌํญ์ ํฌํจํ๊ณ ์์ผ๋ฉฐ, ํด๋น ๋ฉ์๋๊ฐ ์ค์ ๋ก ์ด๋ป๊ฒ ์์ฒญ๋ ์ง๋ฅผ ์ ์ํ๋ค.
๋์ ์์
interface GitHubService {
@GET("users/{user}/repos")
fun listRepos(@Path("user") user: String): Call<List<Repo>>
}
1. create(GitHubService.class) ํธ์ถ
- create() ๋ฉ์๋๊ฐ ํธ์ถ๋๋ฉด GitHubService ์ธํฐํ์ด์ค์ ๋ํ ํ๋ก์ ๊ฐ์ฒด๊ฐ ์์ฑ๋๋ค.
2. ๋ฉ์๋ ํธ์ถ
- servide.listRepos("octocat")์ ํธ์ถํ๋ฉด, ์ค์ ๋ก๋ ํ๋ก์ ๊ฐ์ฒด๊ฐ ์ด ๋ฉ์๋ ํธ์ถ์ ๊ฐ๋ก์ฑ๊ฒ ๋๋ค.
3. InvocataionHandler.invoke() ์คํ
- ํ๋ก์ ๊ฐ์ฒด๋ listRepos() ๋ฉ์๋ ํธ์ถ์ invoke() ๋ฉ์๋๋ก ์ ๋ฌํ๋ค. ์ด๋, method๋ listRepos() ๋ฉ์๋ ๊ฐ์ฒด๊ฐ ๋๊ณ , args๋ {"octocat"}์ด ๋๋ค.
4. ServiceMethod ์์ฑ ๋ฐ ์คํ
- loadServiceMethod()์์ Annotation(@GET, @Path)์ ๋ถ์ํด HTTP ์์ฒญ์ ์ธ๋ถ ์ฌํญ์ ์ค์ ํ ServiceMethod๋ฅผ ์์ฑํ๋ค.
- ์์ฑ๋ ServiceMethod๋ invoke()๋ฅผ ํตํด ์ค์ ๋ก HTTP ์์ฒญ์ ์๋ฒ๋ก ๋ณด๋ด๊ณ , ์๋ฒ์ ์๋ต์ ๋ฐ์ ์ฒ๋ฆฌํ๋ค.
2. Retrofit.loadService(Class<?> service, Method method)
์์ค ์ฝ๋ ๋ถ๋ถ
ServiceMethod<?> loadServiceMethod(Class<?> service, Method method) {
while (true) {
// Note: Once we are minSdk 24 this whole method can be replaced by computeIfAbsent.
Object lookup = serviceMethodCache.get(method);
if (lookup instanceof ServiceMethod<?>) {
// Happy path: method is already parsed into the model.
return (ServiceMethod<?>) lookup;
}
if (lookup == null) {
// Map does not contain any value. Try to put in a lock for this method. We MUST synchronize
// on the lock before it is visible to others via the map to signal we are doing the work.
Object lock = new Object();
synchronized (lock) {
lookup = serviceMethodCache.putIfAbsent(method, lock);
if (lookup == null) {
// On successful lock insertion, perform the work and update the map before releasing.
// Other threads may be waiting on lock now and will expect the parsed model.
ServiceMethod<Object> result;
try {
result = ServiceMethod.parseAnnotations(this, service, method);
} catch (Throwable e) {
// Remove the lock on failure. Any other locked threads will retry as a result.
serviceMethodCache.remove(method);
throw e;
}
serviceMethodCache.put(method, result);
return result;
}
}
}
// Either the initial lookup or the attempt to put our lock in the map has returned someone
// else's lock. This means they are doing the parsing, and will update the map before
// releasing
// the lock. Once we can take the lock, the map is guaranteed to contain the model or null.
// Note: There's a chance that our effort to put a lock into the map has actually returned a
// finished model instead of a lock. In that case this code will perform a pointless lock and
// redundant lookup in the map of the same instance. This is rare, and ultimately harmless.
synchronized (lookup) {
Object result = serviceMethodCache.get(method);
if (result == null) {
// The other thread failed its parsing. We will retry (and probably also fail).
continue;
}
return (ServiceMethod<?>) result;
}
}
}
์ฃผ์ ํ๋ฆ ๋ถ์
1. serviceMethodCache
- serviceMethodCache์์ ์บ์๋ ServiceMethod ๊ฐ์ฒด๋ฅผ ์ฐพ๋๋ค. ๊ฐ์ฒด๊ฐ ์์ ๊ฒฝ์ฐ ๋ฐ๋ก ๋ฐํํ๋ค.
2. ServiceMethod.parseAnnotations()
- ์บ์๊ฐ ๋น์ด ์์ผ๋ฉด ServiceMethod.parseAnnotations()๋ฅผ ํธ์ถํด Annotation์ ๋ถ์ํ๊ณ ์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์๋ก์ด ServiceMethod ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค.
- ์ด ๊ณผ์ ์์ @GET, @POST, @Query ๋ฑ์ Annotation์ ์ฝ์ด HTTP ์์ฒญ ์ ๋ณด๋ฅผ ์ ์ํ ServiceMethod ๊ฐ์ฒด๋ฅผ ์์ฑํ ํ ์บ์์ ์ ์ฅํ๋ค.
๋์ ์์
1. API ๋ฉ์๋ ํธ์ถ
@GET("users/{user}/repos")
fun listRepos(@Path("user") user: String): Call<List<Repo>>
2. ์ฒซ ๋ฒ์งธ ํธ์ถ
- loadServiceMethod()๊ฐ ํธ์ถ๋๋ฉด serviceMethodCache์์ listRepos() ๋ฉ์๋์ ์บ์๋ ServiceMethod ๊ฐ์ฒด๋ฅผ ์ฐพ๋๋ค
- ์บ์๊ฐ ๋น์ด์์ผ๋ฏ๋ก, serviceMethod.parseAnnotations()๋ฅผ ํธ์ถํด Annotation์ ๋ถ์ํ๊ณ , HTTP ์์ฒญ ์ ๋ณด๋ฅผ ์ ์ํ ServiceMethod ๊ฐ์ฒด๋ฅผ ์บ์์ ์ ์ฅํ๋ค.
3. ๋ ๋ฒ์งธ ํธ์ถ
- listRepos() ๋ฉ์๋๊ฐ ๋ค์ ํธ์ถ๋๋ฉด, loadServiceMethod()๋ ์ด๋ฒ์๋ ์บ์์์ ๋ฐ๋ก ServiceMethod ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค. ๋ฐ๋ผ์ Annotation์ ๋ค์ ๋ถ์ํ ํ์๊ฐ ์๋ค.
3. ServiceMethod.parseAnnotations()
์์ค ์ฝ๋ ๋ถ๋ถ
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Class<?> service, Method method) {
RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, service, method);
Type returnType = method.getGenericReturnType();
if (Utils.hasUnresolvableType(returnType)) {
throw methodError(
method,
"Method return type must not include a type variable or wildcard: %s",
returnType);
}
if (returnType == void.class) {
throw methodError(method, "Service methods cannot return void.");
}
return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}
parseAnnotations() ํ๋ฆ
1. Annotation ํ์ฑ
- API ์ธํฐํ์ด์ค์ ์ ์ธ๋ ๋ฉ์๋์ ํด๋น Annotation์ ๋ถ์ํ๊ณ , ์ด๋ฅผ ํตํด HTTP ์์ฒญ์ ๊ตฌ์ฑ์ ์ ์ํ๋ค. ์ด๋ RequestFactory๊ฐ Annotation์ ํ์ฑํด ์์ฒญ์ ๋ฉ์๋ ํ์ (GET, POST), ๊ฒฝ๋ก, ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ๋ฑ์ ์ ๋ฆฌํ๋ค. -> 4๋ฒ ๋ด์ฉ
2. ๋ฆฌํด ํ์ ๊ฒ์ฆ
- API ๋ฉ์๋๊ฐ ์ฌ๋ฐ๋ฅธ ๋ฆฌํด ํ์ ์ ๊ฐ๊ณ ์๋์ง ํ์ธํ๋ค. ์๋ฅผ ๋ค์ด, ๋ฆฌํด ํ์ ์ ํ์ ๋ณ์๋ ์์ผ๋ ์นด๋๊ฐ ํฌํจ๋ ๊ฒฝ์ฐ, ๊ทธ ํ์ ์ ํด์ํ ์ ์์ผ๋ฏ๋ก ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค. ๋ํ void ๋ฆฌํด ํ์ ์ ํ์ฉ๋์ง ์๋๋ค.
3. HTTP ์์ฒญ ์ฒ๋ฆฌ
- HttpServiceMethod๋ ๊ตฌ์ฒด์ ์ธ HTTP ์์ฒญ ์ฒ๋ฆฌ ๋ฐฉ์์ ์ ์ํ๋ฉฐ, ServiceMethod ํด๋์ค๋ ๊ทธ ๊ธฐ๋ฐ์ ์ ๊ณตํ๋ค. -> 10๋ฒ ๋ด์ฉ
4. RequestFactory.parseAnnotations(retrofit, service, method)
์์ค ์ฝ๋ ๋ถ๋ถ
static RequestFactory parseAnnotations(Retrofit retrofit, Class<?> service, Method method) {
return new Builder(retrofit, service, method).build();
}
๋ถ์
- new Builder(retrofit, service, method) : ์ฌ๊ธฐ์ Builder ๊ฐ์ฒด๊ฐ ์์ฑ๋๋ฉฐ, API ๋ฉ์๋์ ํฌํจ๋ Annotation ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก RequestFactory ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ธฐ ์ํ ์ด๊ธฐ ์ค์ ์ด ์งํ๋๋ค.
- build() : ์ต์ข ์ ์ผ๋ก RequestFactory ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ ๋ถ๋ถ์ด๋ค. ์ด๋ API ๋ฉ์๋์ ๋ํ ๋ชจ๋ ์ ๋ณด๊ฐ ์ค์ ๋ ReqeustFactory๊ฐ ๋ง๋ค์ด์ง๋ค.
5. RequestFactory.Builder(retrofit, service, method), RequestFactory.Builder.build()
์์ค ์ฝ๋ ๋ถ๋ถ
Builder(Retrofit retrofit, Class<?> service, Method method) {
this.retrofit = retrofit;
this.service = service;
this.method = method;
this.methodAnnotations = method.getAnnotations();
this.parameterTypes = method.getGenericParameterTypes();
this.parameterAnnotationsArray = method.getParameterAnnotations();
}
Builder(Retrofit retrofit, Class<?> service, Method method) {
this.retrofit = retrofit;
this.service = service;
this.method = method;
this.methodAnnotations = method.getAnnotations();
this.parameterTypes = method.getGenericParameterTypes();
this.parameterAnnotationsArray = method.getParameterAnnotations();
}
RequestFactory build() {
for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
}
if (httpMethod == null) {
throw methodError(method, "HTTP method annotation is required (e.g., @GET, @POST, etc.).");
}
if (!hasBody) {
if (isMultipart) {
throw methodError(
method,
"Multipart can only be specified on HTTP methods with request body (e.g., @POST).");
}
if (isFormEncoded) {
throw methodError(
method,
"FormUrlEncoded can only be specified on HTTP methods with "
+ "request body (e.g., @POST).");
}
}
int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];
for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {
parameterHandlers[p] =
parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter);
}
if (relativeUrl == null && !gotUrl) {
throw methodError(method, "Missing either @%s URL or @Url parameter.", httpMethod);
}
if (!isFormEncoded && !isMultipart && !hasBody && gotBody) {
throw methodError(method, "Non-body HTTP method cannot contain @Body.");
}
if (isFormEncoded && !gotField) {
throw methodError(method, "Form-encoded method must contain at least one @Field.");
}
if (isMultipart && !gotPart) {
throw methodError(method, "Multipart method must contain at least one @Part.");
}
return new RequestFactory(this);
}
build() ๋ถ์
- ๋ชจ๋ Annotation์ ํ์ฑํ๊ณ ์ต์ข
์ ์ผ๋ก RequestFactory ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค.
- parseMethodAnnotation() ๋ฉ์๋ -> 6๋ฒ ๋ด์ฉ
- parseParameter() ๋ฉ์๋ -> 8๋ฒ ๋ด์ฉ
- ๋ฉ์๋๊ฐ ์ ๋๋ก ๊ตฌ์ฑ๋์ง ์์์ ๊ฒฝ์ฐ, ์๋ฅผ ๋ค์ด HTTP ๋ฉ์๋๊ฐ ๋๋ฝ๋๊ฑฐ๋ URL์ด ์๋ชป๋ ๊ฒฝ์ฐ ์ค๋ฅ๋ฅผ ๋ฐ์์ํจ๋ค.
- ParameterHandler : ๋ฉ์๋ ํ๋ผ๋ฏธํฐ์ ํด๋นํ๋ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํ๋ค. @Query, @Path, @Body์ ๊ฐ์ Annotation์ด ์๋ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ์ญํ ์ ํ๋ค.
6. RequestFactory.Builder.parseMethodAnnotation()
์์ค ์ฝ๋ ๋ถ๋ถ
private void parseMethodAnnotation(Annotation annotation) {
if (annotation instanceof DELETE) {
parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
} else if (annotation instanceof GET) {
parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
} else if (annotation instanceof HEAD) {
parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);
} else if (annotation instanceof PATCH) {
parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);
} else if (annotation instanceof POST) {
parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
} else if (annotation instanceof PUT) {
parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
} else if (annotation instanceof OPTIONS) {
parseHttpMethodAndPath("OPTIONS", ((OPTIONS) annotation).value(), false);
} else if (annotation instanceof HTTP) {
HTTP http = (HTTP) annotation;
parseHttpMethodAndPath(http.method(), http.path(), http.hasBody());
} else if (annotation instanceof retrofit2.http.Headers) {
retrofit2.http.Headers headers = (retrofit2.http.Headers) annotation;
String[] headersToParse = headers.value();
if (headersToParse.length == 0) {
throw methodError(method, "@Headers annotation is empty.");
}
this.headers = parseHeaders(headersToParse, headers.allowUnsafeNonAsciiValues());
} else if (annotation instanceof Multipart) {
if (isFormEncoded) {
throw methodError(method, "Only one encoding annotation is allowed.");
}
isMultipart = true;
} else if (annotation instanceof FormUrlEncoded) {
if (isMultipart) {
throw methodError(method, "Only one encoding annotation is allowed.");
}
isFormEncoded = true;
}
}
๋ถ์
- ์ด ๋ฉ์๋๋ ๋ฉ์๋์ ์ ์ธ๋ HTTP ๋ฉ์๋ Annotation(์: @GET, @POST)๊ณผ ๊ธฐํ Annotation์ ์ฒ๋ฆฌํ๋ค.
- ๊ฐ Annotation์ด ๋ฌด์์ ์๋ฏธํ๋ ์ง ํด์ํ๊ณ , HTTP ๋ฉ์๋๋ ํค๋ ์ ๋ณด, ๋ณธ๋ฌธ ํ์ ๋ฑ์ ์ค์ ํ๋ค. -> 7๋ฒ ๋ด์ฉ
7.RequestFactory.Builder.parseHttpMethodAndPath()
์์ค ์ฝ๋ ๋ถ๋ถ
private void parseHttpMethodAndPath(String httpMethod, String value, boolean hasBody) {
if (this.httpMethod != null) {
throw methodError(
method,
"Only one HTTP method is allowed. Found: %s and %s.",
this.httpMethod,
httpMethod);
}
this.httpMethod = httpMethod;
this.hasBody = hasBody;
if (value.isEmpty()) {
return;
}
// Get the relative URL path and existing query string, if present.
int question = value.indexOf('?');
if (question != -1 && question < value.length() - 1) {
// Ensure the query string does not have any named parameters.
String queryParams = value.substring(question + 1);
Matcher queryParamMatcher = PARAM_URL_REGEX.matcher(queryParams);
if (queryParamMatcher.find()) {
throw methodError(
method,
"URL query string \"%s\" must not have replace block. "
+ "For dynamic query parameters use @Query.",
queryParams);
}
}
this.relativeUrl = value;
this.relativeUrlParamNames = parsePathParameters(value);
}
๋ถ์
- HTTP ๋ฉ์๋์ URL ๊ฒฝ๋ก๋ฅผ ์ฒ๋ฆฌํ๋ค. ์๋ฅผ ๋ค์ด, @GET("users/{user}/repos")์ ๊ฐ์ Annotation์ ํ์ฑํด URL ๊ฒฝ๋ก์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฒ๋ฆฌํ๋ค.
- URL ๊ฒฝ๋ก์ ๋์ ์ผ๋ก ํ๋ผ๋ฏธํฐ๋ฅผ ์ฝ์ ํ ์ ์๋๋ก ๊ฒฝ๋ก ๋งค๊ฐ๋ณ์๋ฅผ ์ค์ ํ๋ค.
8. RequestFactory.Builder.parseParameter()
์์ค ์ฝ๋ ๋ถ๋ถ
private @Nullable ParameterHandler<?> parseParameter(
int p, Type parameterType, @Nullable Annotation[] annotations, boolean allowContinuation) {
ParameterHandler<?> result = null;
if (annotations != null) {
for (Annotation annotation : annotations) {
ParameterHandler<?> annotationAction =
parseParameterAnnotation(p, parameterType, annotations, annotation);
if (annotationAction == null) {
continue;
}
if (result != null) {
throw parameterError(
method, p, "Multiple Retrofit annotations found, only one allowed.");
}
result = annotationAction;
}
}
if (result == null) {
if (allowContinuation) {
try {
if (Utils.getRawType(parameterType) == Continuation.class) {
isKotlinSuspendFunction = true;
return null;
}
} catch (NoClassDefFoundError ignored) {
// Ignored
}
}
throw parameterError(method, p, "No Retrofit annotation found.");
}
return result;
}
๋ถ์
- ๋ฉ์๋ ํ๋ผ๋ฏธํฐ์ ๋ํ Annotatin์ ์ฒ๋ฆฌํ๋ค. ๊ฐ ํ๋ผ๋ฏธํฐ์ ๋ํด ์ ์ธ๋ @Query, @Path, @Filed ๋ฑ Annotation์ ํด์ํด ์ ์ ํ ์์น์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํ๋ค. -> 9๋ฒ ๋ด์ฉ
- ํ๋ผ๋ฏธํฐ์ ๋ณต์์ Retrofit Annotation์ด ์ ์ฉ๋ ์ ์์ผ๋ฉฐ, ๋ง์ฝ ์ฌ๋ฌ ๊ฐ๊ฐ ์ ์ธ๋๋ฉด ์ค๋ฅ๋ฅผ ๋ฐ์์ํจ๋ค.
9.RequestFactory.Builder.parseParameterAnnotation()
์์ค ์ฝ๋ ๋ถ๋ถ
๋๋ฌด ๊ธธ์ด์ ์ค๋ตํ ๋ถ๋ถ์ด ๋ง๋ค. ์ค์ํ ๊ฑด, Annotaion์ instanceof๋ก ๋ถ๊ธฐ์ฒ๋ฆฌํ๊ณ ์๋ค๋ ๊ฒ์ด๋ค.
private ParameterHandler<?> parseParameterAnnotation(
int p, Type type, Annotation[] annotations, Annotation annotation) {
if (annotation instanceof Url) {
validateResolvableType(p, type);
// ...
gotUrl = true;
if (type == HttpUrl.class
|| type == String.class
|| type == URI.class
|| (type instanceof Class && "android.net.Uri".equals(((Class<?>) type).getName()))) {
return new ParameterHandler.RelativeUrl(method, p);
} else {
throw parameterError(
method,
p,
"@Url must be okhttp3.HttpUrl, String, java.net.URI, or android.net.Uri type.");
}
} else if (annotation instanceof Path) {
validateResolvableType(p, type);
// ...
Path path = (Path) annotation;
String name = path.value();
validatePathName(p, name);
Converter<?, String> converter = retrofit.stringConverter(type, annotations);
return new ParameterHandler.Path<>(method, p, name, converter, path.encoded());
} //...
// ...
} else if (annotation instanceof Tag) {
validateResolvableType(p, type);
Class<?> tagType = Utils.getRawType(type);
for (int i = p - 1; i >= 0; i--) {
ParameterHandler<?> otherHandler = parameterHandlers[i];
if (otherHandler instanceof ParameterHandler.Tag
&& ((ParameterHandler.Tag) otherHandler).cls.equals(tagType)) {
throw parameterError(
method,
p,
"@Tag type "
+ tagType.getName()
+ " is duplicate of "
+ Platform.reflection.describeMethodParameter(method, i)
+ " and would always overwrite its value.");
}
}
return new ParameterHandler.Tag<>(tagType);
}
return null; // Not a Retrofit annotation.
}
๊ฐ๋ต์ ์ธ ๋ถ์
- ๋ฉ์๋ ํ๋ผ๋ฏธํฐ์ ์ ์ธ๋ Annotation์ ๋ถ์ํด, ๊ทธ ํ๋ผ๋ฏธํฐ๊ฐ HTTP ์์ฒญ์ ์ด๋ ๋ถ๋ถ์ ํด๋นํ๋์ง ๊ฒฐ์ ํ๋ค. @Path, @Query, @Header, @Body ๋ฑ ๋ค์ํ Annotation์ ์ฒ๋ฆฌํ๋ฉฐ, API ํธ์ถ ์ ์ ์ ํ ์์น์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฐ๊ฒฐํ๋ ์ค์ํ ์ญํ ์ ์ํํ๋ค.
- ParameterHandler๋ฅผ ๋ฐํํ๋ค.
- ParameterHandler๋ ์ถ์ ํด๋์ค์ด๊ณ , ํด๋์ค ๋ด๋ถ์ ParameterHandler์ ์๋ธ ํด๋์ค๋ค์ ์ ์ํ๋ค,
- HTTP ์์ฒญ์ ๊ฐ ํ๋ผ๋ฏธํฐ Annotation์ ์ฒ๋ฆฌํด ๋ด๋นํ๋ ์๋ธ ํด๋์ค๋ค์ด ์๋ค.
10. HttpServiceMethod.parseAnnotations()
์์ค ์ฝ๋ ๋ถ๋ถ
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
// 1. ์ฝ๋ฃจํด ์ฒ๋ฆฌ ์ฌ๋ถ ํ์ธ
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
boolean continuationWantsResponse = false;
boolean continuationBodyNullable = false;
boolean continuationIsUnit = false;
Annotation[] annotations = method.getAnnotations();
Type adapterType;
// 2. ์ฝ๋ฃจํด ์๋ต ํ์
์ฒ๋ฆฌ
if (isKotlinSuspendFunction) {
Type[] parameterTypes = method.getGenericParameterTypes();
Type responseType =
Utils.getParameterLowerBound(
0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
// Unwrap the actual body type from Response<T>.
responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
continuationWantsResponse = true;
} else {
// 3. Kotlin suspend ํจ์์ ๋ํ ๊ฒฝ๊ณ ์ฒ๋ฆฌ
if (getRawType(responseType) == Call.class) {
throw methodError(
method,
"Suspend functions should not return Call, as they already execute asynchronously.\n"
+ "Change its return type to %s",
Utils.getParameterUpperBound(0, (ParameterizedType) responseType));
}
continuationIsUnit = Utils.isUnit(responseType);
// TODO figure out if type is nullable or not
// Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class)
// Find the entry for method
// Determine if return type is nullable or not
}
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
} else {
//4. ๋น์ฝ๋ฃจํด ํจ์ ์ฒ๋ฆฌ
adapterType = method.getGenericReturnType();
}
CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
Type responseType = callAdapter.responseType();
if (responseType == okhttp3.Response.class) {
throw methodError(
method,
"'"
+ getRawType(responseType).getName()
+ "' is not a valid response body type. Did you mean ResponseBody?");
}
// 5. ์๋ต ํ์
์ ํจ์ฑ ๊ฒ์ฌ
if (responseType == Response.class) {
throw methodError(method, "Response must include generic type (e.g., Response<String>)");
}
if (requestFactory.httpMethod.equals("HEAD")
&& !Void.class.equals(responseType)
&& !Utils.isUnit(responseType)) {
throw methodError(method, "HEAD method must use Void or Unit as response type.");
}
// 6. ์๋ต ๋ณํ๊ธฐ ์์ฑ
Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);
okhttp3.Call.Factory callFactory = retrofit.callFactory;
// 7. ๋น์ฝ๋ฃจํด๊ณผ ์ฝ๋ฃจํด ํจ์์ ๋ํ ์ฒ๋ฆฌ
if (!isKotlinSuspendFunction) {
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
} else if (continuationWantsResponse) {
//noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
return (HttpServiceMethod<ResponseT, ReturnT>)
new SuspendForResponse<>(
requestFactory,
callFactory,
responseConverter,
(CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
} else {
//noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
return (HttpServiceMethod<ResponseT, ReturnT>)
new SuspendForBody<>(
requestFactory,
callFactory,
responseConverter,
(CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
continuationBodyNullable,
continuationIsUnit);
}
}
Annotation ๊ด๋ จ ํด๋์ค
CallAdapter
- ๋น๋๊ธฐ ์์ฒญ์ ์๋ต ํ์ ์ ๋ณํํ๋ ์ญํ ์ ํ๋ค. ์๋ฅผ ๋ค์ด, Call<Response<T>>๋ฅผ RxJava์ Observable<T>๋ก ๋ณํํ ์ ์๋ค.
- Annotation์ ํตํด ์ ์๋ API ์๋ต์ ๊ฐ์ฒด๋ก ๋ณํํ๋ค.
CalledAdapter
- ๋น์ฝ๋ฃจํด ํจ์๋ฅผ ์ฒ๋ฆฌํ๋ ํด๋์ค์ด๋ค. ์์ฒญ์ ๋น๋๊ธฐ์ ์ผ๋ก ์คํํ๊ณ , ๊ฒฐ๊ณผ๋ฅผ ์ง์ ๋ ๋ฆฌํด ํ์ ์ผ๋ก ๋ณํํ๋ค.
- Annotation์ผ๋ก ์ ์๋ API ์ธํฐํ์ด์ค์์ ๋ฐํ ํ์ ์ ๋๊ธฐ์ ๋๋ ๋น๋๊ธฐ์ ์ผ๋ก ๋ณํํ๋ ์ญํ ์ ํ๋ค.
์ ๋ฆฌ
Retrofit์ Reflection๊ณผ ํ๋ก์ ํจํด์ ํ์ฉํด Annotation์ ์ด์ฉํด ๋์์ ์คํํ๋ค.
ํ๋ก์ํจํด์ผ๋ก Service ์ธํฐํ์ด์ค์ ํ๋ก์๊ฐ์ฒด๋ฅผ ๊ตฌํํ๊ณ , HTTP ์์ฒญ ๋ฉ์๋ ํธ์ถ์ ํ๋ก์๊ฐ์ฒด๊ฐ ์ด ํธ์ถ์ ๊ฐ๋ก์ฑ์ Annotation์ ๋ถ์ํด HTTP ์์ฒญ ๋์์ ์งํํ๋ค.
'๋น ๊ตฌ๋ฉ ์ฑ์ฐ๊ธฐ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Java] ์ค๋ ๋ ์๋ช ์ฃผ๊ธฐ (1) | 2024.10.01 |
---|---|
[์๋ฃ๊ตฌ์กฐ] ํด์ ์ถฉ๋ Hash Collision (1) | 2024.10.01 |
[Java] PriorityQueue (2) | 2024.09.18 |
[์๋ฃ ๊ตฌ์กฐ]์ด์ง ํ Binary Heap (1) | 2024.09.18 |
[์๋ฃ ๊ตฌ์กฐ][Java] ํ Heap (1) | 2024.09.17 |