네임스페이스를 사용한 멀티테넌시 구현

Namespaces API를 사용하면 NamespaceManager 패키지를 사용하여 web.xml의 각 테넌트에 네임스페이스 문자열을 선택하여 애플리케이션에서 멀티테넌시를 쉽게 사용 설정할 수 있습니다.

현재 네임스페이스 설정

NamespaceManager를 사용하여 네임스페이스를 가져오고 설정하고 확인할 수 있습니다. 네임스페이스 관리자는 네임스페이스 사용 API에 현재 네임스페이스를 설정할 수 있습니다. web.xml을 사용해 미리 현재 네임스페이스를 설정하면 datastore 및 memcache가 자동으로 그 네임스페이스를 사용합니다.

대부분의 App Engine 개발자는 Google Workspace(이전의 G Suite) 도메인을 현재 네임스페이스로 사용합니다. Google Workspace를 사용하면 소유한 모든 도메인에 앱을 배포할 수 있어 이 메커니즘으로 도메인마다 다른 네임스페이스를 쉽게 구성할 수 있습니다. 그런 다음 별도의 네임스페이스를 사용해 여러 도메인의 데이터를 분리하면 됩니다. 자세한 내용은 커스텀 도메인 매핑을 참조하세요.

다음 코드 샘플은 URL 매핑에 사용된 Google Workspace 도메인으로 현재 네임스페이스를 설정하는 방법을 보여줍니다. 특히, 이 문자열은 동일한 Google Workspace 도메인을 통해 매핑된 모든 URL에 똑같이 사용할 수 있습니다.

서블릿 메서드를 호출하기 전에 서블릿 필터 인터페이스를 사용하여 자바에서 네임스페이스를 설정할 수 있습니다. 다음의 간단한 예시는 Google Workspace 도메인을 현재 네임스페이스로 사용하는 방법을 보여줍니다.

// Filter to set the Google Apps domain as the namespace.
public class NamespaceFilter implements Filter {

  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    // Make sure set() is only called if the current namespace is not already set.
    if (NamespaceManager.get() == null) {
      // If your app is hosted on appspot, this will be empty. Otherwise it will be the domain
      // the app is hosted on.
      NamespaceManager.set(NamespaceManager.getGoogleAppsNamespace());
    }
    chain.doFilter(req, res); // Pass request back down the filter chain
  }

네임스페이스 필터는 web.xml 파일에서 구성해야 합니다. 필터 항목이 여러 개인 경우, 설정할 첫 번째 네임스페이스가 사용됩니다.

다음 코드 샘플은 web.xml에서 네임스페이스 필터를 구성하는 방법을 보여줍니다.

<!-- Configure the namespace filter. -->
<filter>
    <filter-name>NamespaceFilter</filter-name>
    <filter-class>com.example.appengine.NamespaceFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>NamespaceFilter</filter-name>
    <url-pattern>/sign</url-pattern>
</filter-mapping>

web.xml 파일과 서블릿에 대한 URL 경로 매핑에 대한 일반 내용은 배포 설명자: web.xml을 참조하세요.

다음과 같이 try/finally 패턴을 사용하여 임시 작업의 새 네임스페이스를 설정한 후 작업이 완료되면 원래 네임스페이스를 재설정할 수도 있습니다.

// Set the namepace temporarily to "abc"
String oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  //      ... perform operation using current namespace ...
} finally {
  NamespaceManager.set(oldNamespace);
}

namespace 값을 지정하지 않으면 네임스페이스가 빈 문자열로 설정됩니다. namespace 문자열은 임의로 지정되지만 최대 100자로 제한되며 영숫자 문자, 마침표, 밑줄, 하이픈을 사용할 수 있습니다. 보다 명확히 설명하자면 네임스페이스 문자열이 정규 표현식 [0-9A-Za-z._-]{0,100}과 일치해야 합니다.

규칙에 따라 '_'(밑줄)로 시작하는 모든 네임스페이스는 시스템 용도로 예약되어 있습니다. 이 시스템 네임스페이스 규칙은 강제 적용되지는 않지만 준수하지 않을 경우 예기치 않은 부작용이 발생할 가능성이 높습니다.

데이터 유출 방지

일반적인 다중 테넌트 지원 앱과 관련된 위험 중 하나는 바로 네임스페이스에서의 데이터 유출 위험입니다. 의도치 않은 데이터 유출은 다음을 비롯한 다양한 소스에서 발생할 수 있습니다.

  • 네임스페이스를 아직 지원하지 않는 App Engine API와 함께 네임스페이스를 사용하는 경우입니다. 예를 들어, Blobstore는 네임스페이스를 지원하지 않습니다. Blobstore에서 네임스페이스를 사용하는 경우, 최종 사용자 요청에 대한 Blobstore 쿼리 또는 신뢰할 수 없는 소스의 Blobstore 키 사용을 방지해야 합니다.
  • 네임스페이스에 구획 스키마를 제공하지 않고 URL Fetch 또는 기타 메커니즘을 통해 Memcache 및 Datastore 대신 외부 스토리지 매체를 사용하는 경우입니다.
  • 사용자의 이메일 도메인을 사용해 네임스페이스를 설정하는 경우입니다. 도메인의 모든 이메일 주소가 네임스페이스에 액세스하면 안 되는 것이 일반적입니다. 또한 이메일 도메인을 사용하면 사용자가 로그인할 때까지 애플리케이션에서 네임스페이스를 사용하지 못합니다.

네임스페이스 배포

다음 섹션에서는 다른 App Engine 도구 및 API를 사용해 네임스페이스를 배포하는 방법을 설명합니다.

사용자 단위로 네임스페이스 만들기

일부 애플리케이션에서는 사용자별로 네임스페이스를 만들어야 합니다. 로그인한 사용자의 사용자 수준에서 데이터를 분류하려면 해당 사용자의 고유한 영구 ID를 반환하는 User.getUserId()를 사용해 보세요. 다음 코드 샘플에서는 이를 위해 Users API를 사용하는 방법을 보여줍니다.

if (com.google.appengine.api.NamespaceManager.get() == null) {
  // Assuming there is a logged in user.
  namespace = UserServiceFactory.getUserService().getCurrentUser().getUserId();
  NamespaceManager.set(namespace);
}

일반적으로 사용자 단위로 네임스페이스를 만드는 앱은 사용자마다 다른 방문 페이지를 제공합니다. 이러한 경우 애플리케이션에서 사용자에게 표시할 방문 페이지를 지정하는 URL 스키마를 제공해야 합니다.

Datastore에서 네임스페이스 사용

기본적으로 Datastore에서는 Datastore 요청에 네임스페이스 관리자의 현재 네임스페이스 설정을 사용합니다. API는 Key 또는 Query 객체가 생성되면 현재 네임스페이스를 해당 객체에 적용합니다. 따라서 애플리케이션이 Key 또는 Query 객체를 직렬화 형식으로 저장하는 경우 해당 직렬화 형식으로 네임스페이스가 보존되므로 유의해야 합니다.

직렬화를 해제한 KeyQuery 객체를 사용할 때는 정상적으로 동작하는지 확인해야 합니다. 다른 스토리지 메커니즘을 사용하지 않고 Datastore(put/query/get)를 사용하는 대부분의 간단한 애플리케이션은 Datastore API를 호출하기 전에 현재 네임스페이스를 설정해 예상대로 작동합니다.

QueryKey 객체는 네임스페이스와 관련하여 다음과 같은 고유한 동작을 보여줍니다.

  • 명시적 네임스페이스를 설정하지 않으면 QueryKey 객체가 생성될 때 현재 네임스페이스를 상속합니다.
  • 애플리케이션이 상위 항목에서 새 Key를 만들 때 새 Key가 상위 항목의 네임스페이스를 상속합니다.
  • Key 또는 Query의 네임스페이스를 명시적으로 설정하는 자바용 API는 없습니다.
다음 코드 예시에서는 현재 네임스페이스의 수를 늘리는 SomeRequest 요청 핸들러와 임의로 이름이 지정되는 Counter Datastore 항목의 -global- 네임스페이스를 보여줍니다.

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws java.io.IOException {

    // Update the count for the current namespace.
    updateCount("request");

    // Update the count for the "-global-" namespace.
    String namespace = NamespaceManager.get();
    try {
      // "-global-" is namespace reserved by the application.
      NamespaceManager.set("-global-");
      updateCount("request");
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are now updated.");
  }

Memcache에서 네임스페이스 사용

기본적으로 Memcache는 Memcache 요청에 네임스페이스 관리자의 현재 네임스페이스를 사용합니다. 일반적으로 Memcache에서는 네임스페이스를 명시적으로 설정할 필요가 없으며 이를 설정할 경우 예기치 않은 버그를 초래할 수 있습니다.

하지만 Memcache에서 네임스페이스를 명시적으로 설정해야 하는 특별한 상황도 있습니다. 예를 들어 애플리케이션에 모든 네임스페이스에서 공유하는 공통된 데이터(국가 코드가 포함된 테이블 등)가 있을 수 있습니다.

다음 코드는 Memcache에서 네임스페이스를 명시적으로 설정하는 방법을 보여줍니다.

기본적으로 자바용 Memcache API는 MemcacheService에서 현재 네임스페이스의 네임스페이스 관리자를 쿼리합니다. getMemcacheService(Namespace)를 사용하여 Memcache를 생성할 때 네임스페이스를 명시적으로 지정할 수도 있습니다. 대부분의 애플리케이션에서는 네임스페이스를 명시적으로 지정할 필요가 없습니다.

다음 코드 샘플은 네임스페이스 관리자에서 현재 네임스페이스를 사용하는 Memcache를 만드는 방법을 보여줍니다.

// Create a MemcacheService that uses the current namespace by
// calling NamespaceManager.get() for every access.
MemcacheService current = MemcacheServiceFactory.getMemcacheService();

// stores value in namespace "abc"
oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  current.put("key", value); // stores value in namespace “abc”
} finally {
  NamespaceManager.set(oldNamespace);
}

이 코드 샘플에서는 Memcache 서비스를 만들 때 네임스페이스를 명시적으로 지정합니다.

// Create a MemcacheService that uses the namespace "abc".
MemcacheService explicit = MemcacheServiceFactory.getMemcacheService("abc");
explicit.put("key", value); // stores value in namespace "abc"

태스크 큐에 네임스페이스 사용

기본적으로 내보내기 대기열에서는 작업이 생성될 당시의 네임스페이스 관리자 설정에 따라 현재 네임스페이스를 사용합니다. 일반적으로 태스크 큐에서는 네임스페이스를 명시적으로 설정할 필요가 없으며 이를 설정할 경우 예기치 않은 버그를 초래할 수 있습니다.

작업 이름이 모든 네임스페이스에서 공유됩니다. 네임스페이스를 다르게 사용해도 동일한 이름의 작업을 2개 만들 수는 없습니다. 여러 네임스페이스에 동일한 작업 이름을 사용하려면 각 네임스페이스를 작업 이름에 추가하면 됩니다.

새 태스크가 태스크 큐 add() 메서드를 호출하면 태스크 큐가 네임스페이스 관리자에서 현재 네임스페이스와 Google Workspace 도메인(해당하는 경우)을 복사합니다. 태스크가 실행되면 현재 네임스페이스 및 Google Workspace 네임스페이스가 복원됩니다.

원래 요청에 현재 네임스페이스가 설정되어 있지 않으면(즉, get()에서 null을 반환하는 경우) 태스크 큐에서 실행된 태스크의 ""에 네임스페이스를 설정합니다.

하지만 모든 네임스페이스에서 작동하는 태스크의 네임스페이스를 명시적으로 설정해야 하는 특별한 상황도 있습니다. 예를 들어 모든 네임스페이스에서 사용 통계를 집계하는 작업을 만들 수 있습니다. 그러면 작업의 네임스페이스를 명시적으로 설정할 수 있습니다. 다음 코드 샘플은 태스크 큐에 명시적으로 네임스페이스를 설정하는 방법을 보여줍니다.

먼저 Counter Datastore 항목의 수를 늘리는 태스크 큐 핸들러를 만듭니다.

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

// called from Task Queue
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
  String[] countName = req.getParameterValues("countName");
  if (countName.length != 1) {
    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    return;
  }
  updateCount(countName[0]);
}

그런 다음 서블릿을 사용해 작업을 만듭니다.

public class SomeRequestServlet extends HttpServlet {

  // Handler for URL get requests.
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

    // Increment the count for the current namespace asynchronously.
    QueueFactory.getDefaultQueue()
        .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    // Increment the global count and set the
    // namespace locally.  The namespace is
    // transferred to the invoked request and
    // executed asynchronously.
    String namespace = NamespaceManager.get();
    try {
      NamespaceManager.set("-global-");
      QueueFactory.getDefaultQueue()
          .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are being updated.");
  }
}

Blobstore에 네임스페이스 사용

Blobstore는 네임스페이스로 분할되지 않습니다. Blobstore에서 네임스페이스를 보존하려면 네임스페이스를 인식하는 스토리지 매체(현재 Memcache, Datastore, 태스크 큐만 가능)를 통해 Blobstore에 액세스해야 합니다. 예를 들어 blob의 Key가 Datastore 항목에 저장된 경우 네임스페이스를 인식하는 Datastore Key 또는 Query를 사용하여 여기에 액세스할 수 있습니다.

애플리케이션에서 네임스페이스 인식 스토리지에 저장된 키를 통해 Blobstore에 액세스하는 경우 Blobstore 자체를 네임스페이스에 따라 분할할 필요는 없습니다. 애플리케이션에서 다음과 같이 네임스페이스 간 blob 유출을 방지해야 합니다.

  • 최종 사용자 요청에 대해 com.google.appengine.api.blobstore.BlobInfoFactory를 사용하지 않습니다. 관리 요청(모든 애플리케이션 blob에 대한 보고서 생성 등)에 BlobInfo 쿼리를 사용할 수 있지만 모든 BlobInfo 레코드가 네임스페이스별로 분류되지는 않기 때문에 최종 사용자 요청에 이를 사용할 경우 데이터가 유출될 수 있습니다.
  • 신뢰할 수 없는 소스의 Blobstore 키를 사용하지 않아야 합니다.

Datastore 쿼리의 네임스페이스 설정

Google Cloud 콘솔에서 Datastore 쿼리의 네임스페이스를 설정할 수 있습니다.

기본값을 사용하지 않으려면 드롭다운에서 사용할 네임스페이스를 선택합니다.

일괄 로더에서 네임스페이스 사용

일괄 로더에서는 사용할 네임스페이스를 지정할 수 있는 --namespace=NAMESPACE 플래그를 지원합니다. 각 네임스페이스는 별도로 처리되며 모든 네임스페이스에 액세스하려면 해당 네임스페이스를 반복해야 합니다.

Index의 새 인스턴스는 이를 만들 때 사용된 SearchService의 네임스페이스를 상속합니다. 색인에 대한 참조를 만들면 네임스페이스를 변경할 수 없습니다. 네임스페이스를 사용하여 색인을 만들기 전에 SearchService에서 네임스페이스를 설정하는 방법에는 두 가지가 있습니다.

  • 기본적으로 새 SearchService는 현재 네임스페이스를 사용합니다. 서비스를 만들기 전에 현재 네임스페이스를 설정할 수 있습니다.
// Set the current namespace to "aSpace"
NamespaceManager.set("aSpace");
// Create a SearchService with the namespace "aSpace"
SearchService searchService = SearchServiceFactory.getSearchService();
// Create an IndexSpec
IndexSpec indexSpec = IndexSpec.newBuilder().setName("myIndex").build();
// Create an Index with the namespace "aSpace"
Index index = searchService.getIndex(indexSpec);

서비스를 만들 때 SearchServiceConfig에서 네임스페이스를 지정할 수 있습니다.

// Create a SearchServiceConfig, specifying the namespace "anotherSpace"
SearchServiceConfig config =
    SearchServiceConfig.newBuilder().setNamespace("anotherSpace").build();
// Create a SearchService with the namespace "anotherSpace"
searchService = SearchServiceFactory.getSearchService(config);
// Create an IndexSpec
indexSpec = IndexSpec.newBuilder().setName("myindex").build();
// Create an Index with the namespace "anotherSpace"
index = searchService.getIndex(indexSpec);