๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๋นˆ ๊ตฌ๋ฉ ์ฑ„์šฐ๊ธฐ

[Java] Retrofit2์—์„œ Annotation์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ• ์‚ดํ”ผ๊ธฐ

์ถœ์ฒ˜

 

GitHub - square/retrofit: A type-safe HTTP client for Android and the JVM

A type-safe HTTP client for Android and the JVM. Contribute to square/retrofit development by creating an account on GitHub.

github.com

  • 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/retrofit/src/main/java/retrofit2/http at trunk · square/retrofit

A type-safe HTTP client for Android and the JVM. Contribute to square/retrofit development by creating an account on GitHub.

github.com

 

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 ์š”์ฒญ ๋™์ž‘์„ ์ง„ํ–‰ํ•œ๋‹ค.