2015년 9월 22일 화요일

OkHttp 이해하기 - 2

Headers는 헤더 정보를 name과 value로 담고 있습니다. Request에 의해 사용됩니다.
Headers도 Builder에 의해 편하게 생성될 수 있습니다. Builder는 헤더들을 ArrayList에 name과 value를 순서대로 넣어서 저장하고 있습니다. i번째에 name이 저장되어 있다면 i+1번째에 value가 저장되어 있는 것이지요.
private final List<String> namesAndValues = new ArrayList<>(20);
헤더를 넣기위한 함수를 두 가지를 제공합니다. addLenient와 add입니다. addLenient는 헤더에 대한 검증없이 무조건 저장합니다. 외부로부터 온 헤더거나 캐시되어 있던걸 다시 꺼내오는 경우에 적합한 함수입니다. 이러한 경우는 이미 검증이 된 헤더로 볼 수 있으니까요.
addLenient를 살펴보겠습니다. 헤더는 "name: value"의 형태를 띄고 있지요. 그러니 제일 먼저 첫번째 문자 이후에서 ":"의 유무를 파악합니다. ":"가 있으면 이를 기준으로 둘로 나누어 name과 value로 저장합니다. 만약 ":"가 없으면 ":"가 혹시 첫번째 문자인지 확인합니다. 이 경우는 초기 SPDY에서 발생할 수 있는 상황입니다. 이 경우 name은 비어있는 스트링으로 하고 ":"를 제외한 나머지를 value로 저장합니다. ":"이 전혀 없는 경우라면 그냥 name은 비어있는 스트링으로 하고 주어진 값을 그대로 value로 저장합니다.
Builder addLenient(String line) {
  int index = line.indexOf(":", 1);
  if (index != -1) {
    return addLenient(line.substring(0, index), line.substring(index + 1));
  } else if (line.startsWith(":")) {
    // Work around empty header names and header names that start with a
    // colon (created by old broken SPDY versions of the response cache).
    return addLenient("", line.substring(1)); // Empty header name.
  } else {
    return addLenient("", line); // No header name.
  }
}

앞에서 말했듯이 name을 먼저 저장하고 그 바로 뒤에 value를 저장합니다. value의 경우 trim을 해서 저장하는 것에 유의하세요.
Builder addLenient(String name, String value) {
  namesAndValues.add(name);
  namesAndValues.add(value.trim());
  return this;
}

이제 add함수를 살펴보겠습니다. 이 경우는 ":"가 반드시 있는지 확인합니다. 없으면 IllegalArgumentException을 내보냅니다. 여기서는 name에 trim을 하는 것에 유의하세요.
public Builder add(String line) {
  int index = line.indexOf(":");
  if (index == -1) {
    throw new IllegalArgumentException("Unexpected header: " + line);
  }
  return add(line.substring(0, index).trim(), line.substring(index + 1));
}

name과 value는 null이 아니어야 합니다. 또한 name의 경우는 길이가 0이 아니어야 하죠. 그렇기 때문에 앞에서 보았듯이 name은 저장을 위해 값을 넘겨줄 때 trim을 한 값을 넘겨주고 value는 나중에 최종적으로 저장할 때 trim을 해서 저장하는 형태를 취하고 있는 것입니다. 또한 name과 value 둘다 null value('\0')를 포함하고 있어서는 안됩니다.
public Builder add(String name, String value) {
  if (name == null) throw new IllegalArgumentException("name == null");
  if (value == null) throw new IllegalArgumentException("value == null");
  if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
    throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
  }
  return addLenient(name, value);
}

특정 name의 헤더를 지우는 함수를 제공하고 있습니다. 앞에서 name과 value가 어떻게 저장되어 있는지 설명했죠? 그에 따라 찾아낸 name의 위치와 바로 그 다음의 위치도 같이 지우면 name과 value가 다 지워지게 됩니다. for문을 보면 2씩 증가시켜가면서 찾는 것을 볼 수 있습니다.
public Builder removeAll(String name) {
  for (int i = 0; i < namesAndValues.size(); i += 2) {
    if (name.equalsIgnoreCase(namesAndValues.get(i))) {
      namesAndValues.remove(i); // name
      namesAndValues.remove(i); // value
      i -= 2;
    }
  }
  return this;
}

위의 add와 addLenient는 name과 value가 하나의 스트링으로 되어 있는 값을 받아서 이를 둘로 나누어서 처리합니다. 만약 name과 value가 이미 나누어져 있는 값을 저장하고 싶으면 아래의 set함수를 사용합니다. 간단하네요. 먼저 name이 있으면 지우기 위해 removeAll을 먼저 호출해 주고 나고 name과 value를 저장합니다.
public Builder set(String name, String value) {
  removeAll(name);
  add(name, value);
  return this;
}

헤더의 name으로 value를 찾는 함수도 제공해주고 있습니다. for문을 2씩 증가시켜가면서 매칭되는 name의 위치를 찾은 후 그 다음 위치의 값을 리턴해 줍니다.
public String get(String name) {
  for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
    if (name.equalsIgnoreCase(namesAndValues.get(i))) {
      return namesAndValues.get(i + 1);
    }
  }
  return null;
}

마지막으로 build를 호출하면 이제까지 저장된 헤더들을 Headers에 저장하고 이를 리턴해줍니다.
public Headers build() {
  return new Headers(this);
}

Headers는 name과 value를 String 배열에 저장합니다. 따라서 Builder를 생성자에서 받을 때 builder의 nameAndValues를 배열로 바꾸어서 저장합니다.
private Headers(Builder builder) {
  this.namesAndValues = builder.namesAndValues.toArray(new String[builder.namesAndValues.size()]);
}
Headers는 내부에 저장된 헤더를 참조하기 위한 다양한 함수들을 제공합니다. 이중 간단한 것은 제외하고 몇가지만 살펴보겠습니다.
헤더의 name들만을 모아서 리턴해주는 names()함수가 있습니다. 찾은 name을 TreeSet에 저장하고 값을 변경할 수 없도록하기 위해 Collections.unmodifiableSet으로 리턴합니다.
public Set<String> names() {
  TreeSet<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
  for (int i = 0, size = size(); i < size; i++) {
    result.add(name(i));
  }
  return Collections.unmodifiableSet(result);
}
특정 헤더 name의 value들 만을 리턴해주는 values 함수가 있습니다. 파라메터로 넘어온 name에 매치되는 위치를 찾아서 그 위치의 value를 List<String>에 저장하고 찾은 값이 있으면 Collections.unmodifiableList를, 없으면 Collections.<String>emptyList를 리턴합니다. 여기서도 value를 변경하지 못하도록 하기 위해 Collections의 함수들을 사용합니다.
public List<String> values(String name) {
  List<String> result = null;
  for (int i = 0, size = size(); i < size; i++) {
    if (name.equalsIgnoreCase(name(i))) {
      if (result == null) result = new ArrayList<>(2);
      result.add(value(i));
    }
  }
  return result != null
      ? Collections.unmodifiableList(result)
      : Collections.<String>emptyList();
}

Header를 다시 Builder로 변환하려면 String 배열을 List로 변환해 주어야 합니다. 이를 위해 Collections.addAll 함수를 사용합니다.
public Builder newBuilder() {
  Builder result = new Builder();
  Collections.addAll(result.namesAndValues, namesAndValues);
  return result;
}

names()와 values()의 혼합이라 할 수 있는 toMultimap() 함수도 제공됩니다. LinkedHashMap에 key를 name으로 해서 이에 속한 value들을 List<String>으로 저장합니다. 이 경우는 modify가 가능하게 리턴하는군요. 왜일까요?
public Map<String, List<String>> toMultimap() {
  Map<String, List<String>> result = new LinkedHashMap<String, List<String>>();
  for (int i = 0, size = size(); i < size; i++) {
    String name = name(i);
    List<String> values = result.get(name);
    if (values == null) {
      values = new ArrayList<>(2);
      result.put(name, values);
    }
    values.add(value(i));
  }
  return result;
}

위 함수들에 추가해서 두 개의 편리한 함수를 제공합니다. 헤더를 Map의 형태로 가지고 있으면 이를 Headers로 변환해주는 of(Map<String, String> headers) 함수와 여러개의 String 헤더를 한번 호출로 Headers로 변환해주는 of(String... namesAndValues)입니다. 이 두 함수의 구현 자체는 특별할 것이 없으므로 구현에 대한 설명은 생략하겠습니다.

앞에서 보았던 Request의 Builder와 패턴이 거의 똑같습니다. 이정도면 Builder를 어떻게 만들고 어떻게 사용하면 되는지도 쉽게 이해할 수 있겠네요.

댓글 없음:

댓글 쓰기

Building asynchronous views in SwiftUI 정리

Handling loading states within SwiftUI views self loading views View model 사용하기 Combine을 사용한 AnyPublisher Making SwiftUI views refreshable r...