Repository: yorkie-team/yorkie
Pull request #398 (2022-08-29 10:12:22)

Bind project and user with owner field
<!--  Thanks for sending a pull request! -->

**What this PR does / why we need it**:

The development cost is too high to allow multiple people to manage a project
or to develop `Team` concept. So, for now, only one user manages the project,
and let's do migration when implementing `team` in the future.

Perhaps the development steps would be something like this:

- Step 1: Make the `Project` owned by only a `User`
- Step 2: Create `Project`-`User` mapping table `Role`
- Step 3: Create `Team` own the `Project`

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Related to #377

**Special notes for your reviewer**:

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->
```release-note

```

**Additional documentation**:

<!--
This section can be blank if this pull request does not require a release note.

Please use the following format for linking documentation:
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs

```

**Checklist**:
- [x] Added relevant tests or not required
- [x] Didn't break anything


codecov[bot]: # [Codecov](https://codecov.io/gh/yorkie-team/yorkie/pull/398?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yorkie-team) Report
> Merging [#398](https://codecov.io/gh/yorkie-team/yorkie/pull/398?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yorkie-team) (e29236e) into [main](https://codecov.io/gh/yorkie-team/yorkie/commit/109ed36d485c92f123186e5e704a3946ca6c7db6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yorkie-team) (109ed36) will **decrease** coverage by `0.05%`.
> The diff coverage is `26.66%`.

```diff
@@            Coverage Diff             @@
##             main     #398      +/-   ##
==========================================
- Coverage   46.54%   46.48%   -0.06%     
==========================================
  Files          68       68              
  Lines        5844     5862      +18     
==========================================
+ Hits         2720     2725       +5     
- Misses       2828     2840      +12     
- Partials      296      297       +1     
```


| [Impacted Files](https://codecov.io/gh/yorkie-team/yorkie/pull/398?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yorkie-team) | Coverage Δ | |
|---|---|---|
| [api/types/project.go](https://codecov.io/gh/yorkie-team/yorkie/pull/398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yorkie-team#diff-YXBpL3R5cGVzL3Byb2plY3QuZ28=) | `100.00% <ø> (ø)` | |
| [server/backend/database/memory/database.go](https://codecov.io/gh/yorkie-team/yorkie/pull/398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yorkie-team#diff-c2VydmVyL2JhY2tlbmQvZGF0YWJhc2UvbWVtb3J5L2RhdGFiYXNlLmdv) | `43.53% <20.00%> (-0.06%)` | :arrow_down: |
| [server/backend/database/mongo/client.go](https://codecov.io/gh/yorkie-team/yorkie/pull/398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yorkie-team#diff-c2VydmVyL2JhY2tlbmQvZGF0YWJhc2UvbW9uZ28vY2xpZW50Lmdv) | `7.55% <26.66%> (+0.30%)` | :arrow_up: |
| [server/backend/database/project\_info.go](https://codecov.io/gh/yorkie-team/yorkie/pull/398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yorkie-team#diff-c2VydmVyL2JhY2tlbmQvZGF0YWJhc2UvcHJvamVjdF9pbmZvLmdv) | `27.27% <40.00%> (-0.23%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yorkie-team). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yorkie-team)


Patch:
diff --git a/api/types/project.go b/api/types/project.go
index ee14b1fa..9137d789 100644
--- a/api/types/project.go
+++ b/api/types/project.go
@@ -28,6 +28,9 @@ type Project struct {
 	// Name is the name of this project.
 	Name string `json:"name"`
 
+	// Owner is the owner of this project.
+	Owner ID `json:"owner"`
+
 	// AuthWebhookURL is the url of the authorization webhook.
 	AuthWebhookURL string `json:"auth_webhook_url"`
 
diff --git a/server/admin/interceptors/auth.go b/server/admin/interceptors/auth.go
index 5efa0de8..941b190e 100644
--- a/server/admin/interceptors/auth.go
+++ b/server/admin/interceptors/auth.go
@@ -20,22 +20,28 @@ import (
 	"context"
 	"strings"
 
+	grpcmiddleware "github.com/grpc-ecosystem/go-grpc-middleware"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
 	grpcmetadata "google.golang.org/grpc/metadata"
 	grpcstatus "google.golang.org/grpc/status"
 
+	"github.com/yorkie-team/yorkie/api/types"
 	"github.com/yorkie-team/yorkie/server/admin/auth"
+	"github.com/yorkie-team/yorkie/server/backend"
+	"github.com/yorkie-team/yorkie/server/users"
 )
 
 // AuthInterceptor is an interceptor for authentication.
 type AuthInterceptor struct {
+	backend      *backend.Backend
 	tokenManager *auth.TokenManager
 }
 
 // NewAuthInterceptor creates a new instance of AuthInterceptor.
-func NewAuthInterceptor(tokenManager *auth.TokenManager) *AuthInterceptor {
+func NewAuthInterceptor(be *backend.Backend, tokenManager *auth.TokenManager) *AuthInterceptor {
 	return &AuthInterceptor{
+		backend:      be,
 		tokenManager: tokenManager,
 	}
 }
@@ -48,9 +54,15 @@ func (i *AuthInterceptor) Unary() grpc.UnaryServerInterceptor {
 		info *grpc.UnaryServerInfo,
 		handler grpc.UnaryHandler,
 	) (resp interface{}, err error) {
-		if err := i.authenticate(ctx, info.FullMethod); err != nil {
-			return nil, err
+		if isRequiredAuth(info.FullMethod) {
+			user, err := i.authenticate(ctx, info.FullMethod)
+			if err != nil {
+				return nil, err
+			}
+
+			return handler(users.With(ctx, user), req)
 		}
+
 		return handler(ctx, req)
 	}
 }
@@ -63,40 +75,58 @@ func (i *AuthInterceptor) Stream() grpc.StreamServerInterceptor {
 		info *grpc.StreamServerInfo,
 		handler grpc.StreamHandler,
 	) error {
-		if err := i.authenticate(stream.Context(), info.FullMethod); err != nil {
-			return err
+		if isRequiredAuth(info.FullMethod) {
+			ctx := stream.Context()
+			user, err := i.authenticate(ctx, info.FullMethod)
+			if err != nil {
+				return err
+			}
+
+			wrapped := grpcmiddleware.WrapServerStream(stream)
+			wrapped.WrappedContext = users.With(ctx, user)
+			return handler(srv, wrapped)
 		}
+
 		return handler(srv, stream)
 	}
 }
 
+func isRequiredAuth(method string) bool {
+	// NOTE(hackerwins): We don't need to authenticate the request if the
+	// request is from the peer clusters.
+	return method != "/yorkie.v1.AdminService/LogIn" &&
+		method != "/yorkie.v1.AdminService/SignUp" &&
+		strings.HasPrefix(method, "/yorkie.v1.AdminService/")
+}
+
 // authenticate does authenticate the request.
 func (i *AuthInterceptor) authenticate(
 	ctx context.Context,
 	method string,
-) error {
-	// NOTE(hackerwins): We don't need to authenticate the request if the
-	// request is from the peer clusters.
-	if method == "/yorkie.v1.AdminService/LogIn" ||
-		method == "/yorkie.v1.AdminService/SignUp" ||
-		!strings.HasPrefix(method, "/yorkie.v1.AdminService/") {
-		return nil
+) (*types.User, error) {
+	if !isRequiredAuth(method) {
+		return nil, nil
 	}
 
 	data, ok := grpcmetadata.FromIncomingContext(ctx)
 	if !ok {
-		return grpcstatus.Errorf(codes.Unauthenticated, "metadata is not provided")
+		return nil, grpcstatus.Errorf(codes.Unauthenticated, "metadata is not provided")
 	}
 
 	authorization := data["authorization"]
 	if len(authorization) == 0 {
-		return grpcstatus.Errorf(codes.Unauthenticated, "authorization is not provided")
+		return nil, grpcstatus.Errorf(codes.Unauthenticated, "authorization is not provided")
+	}
+
+	claims, err := i.tokenManager.Verify(authorization[0])
+	if err != nil {
+		return nil, grpcstatus.Errorf(codes.Unauthenticated, "authorization is invalid")
 	}
 
-	// TODO: store the userClaims in the `ctx`.
-	if _, err := i.tokenManager.Verify(authorization[0]); err != nil {
-		return grpcstatus.Errorf(codes.Unauthenticated, "authorization is invalid")
+	user, err := users.GetUser(ctx, i.backend, claims.Username)
+	if err != nil {
+		return nil, grpcstatus.Errorf(codes.Unauthenticated, "authorization is invalid")
 	}
 
-	return nil
+	return user, nil
 }
diff --git a/server/admin/server.go b/server/admin/server.go
index fc2fa63f..15bfdc8c 100644
--- a/server/admin/server.go
+++ b/server/admin/server.go
@@ -73,7 +73,7 @@ func NewServer(conf *Config, be *backend.Backend) *Server {
 	)
 
 	loggingInterceptor := grpchelper.NewLoggingInterceptor()
-	authInterceptor := interceptors.NewAuthInterceptor(tokenManager)
+	authInterceptor := interceptors.NewAuthInterceptor(be, tokenManager)
 	defaultInterceptor := interceptors.NewDefaultInterceptor()
 
 	opts := []grpc.ServerOption{
@@ -197,7 +197,8 @@ func (s *Server) CreateProject(
 	ctx context.Context,
 	req *api.CreateProjectRequest,
 ) (*api.CreateProjectResponse, error) {
-	project, err := projects.CreateProject(ctx, s.backend, req.Name)
+	user := users.From(ctx)
+	project, err := projects.CreateProject(ctx, s.backend, user.ID, req.Name)
 	if err != nil {
 		return nil, err
 	}
@@ -216,7 +217,8 @@ func (s *Server) ListProjects(
 	ctx context.Context,
 	req *api.ListProjectsRequest,
 ) (*api.ListProjectsResponse, error) {
-	projectList, err := projects.ListProjects(ctx, s.backend)
+	user := users.From(ctx)
+	projectList, err := projects.ListProjects(ctx, s.backend, user.ID)
 	if err != nil {
 		return nil, err
 	}
@@ -236,7 +238,8 @@ func (s *Server) GetProject(
 	ctx context.Context,
 	req *api.GetProjectRequest,
 ) (*api.GetProjectResponse, error) {
-	project, err := projects.GetProject(ctx, s.backend, req.Name)
+	user := users.From(ctx)
+	project, err := projects.GetProject(ctx, s.backend, user.ID, req.Name)
 	if err != nil {
 		return nil, err
 	}
@@ -263,9 +266,11 @@ func (s *Server) UpdateProject(
 		return nil, err
 	}
 
+	user := users.From(ctx)
 	project, err := projects.UpdateProject(
 		ctx,
 		s.backend,
+		user.ID,
 		types.ID(req.Id),
 		fields,
 	)
@@ -288,7 +293,8 @@ func (s *Server) GetDocument(
 	ctx context.Context,
 	req *api.GetDocumentRequest,
 ) (*api.GetDocumentResponse, error) {
-	project, err := projects.GetProject(ctx, s.backend, req.ProjectName)
+	user := users.From(ctx)
+	project, err := projects.GetProject(ctx, s.backend, user.ID, req.ProjectName)
 	if err != nil {
 		return nil, err
 	}
@@ -318,7 +324,8 @@ func (s *Server) GetSnapshotMeta(
 	ctx context.Context,
 	req *api.GetSnapshotMetaRequest,
 ) (*api.GetSnapshotMetaResponse, error) {
-	project, err := projects.GetProject(ctx, s.backend, req.ProjectName)
+	user := users.From(ctx)
+	project, err := projects.GetProject(ctx, s.backend, user.ID, req.ProjectName)
 	if err != nil {
 		return nil, err
 	}
@@ -350,7 +357,8 @@ func (s *Server) ListDocuments(
 	ctx context.Context,
 	req *api.ListDocumentsRequest,
 ) (*api.ListDocumentsResponse, error) {
-	project, err := projects.GetProject(ctx, s.backend, req.ProjectName)
+	user := users.From(ctx)
+	project, err := projects.GetProject(ctx, s.backend, user.ID, req.ProjectName)
 	if err != nil {
 		return nil, err
 	}
@@ -384,7 +392,8 @@ func (s *Server) SearchDocuments(
 	ctx context.Context,
 	req *api.SearchDocumentsRequest,
 ) (*api.SearchDocumentsResponse, error) {
-	project, err := projects.GetProject(ctx, s.backend, req.ProjectName)
+	user := users.From(ctx)
+	project, err := projects.GetProject(ctx, s.backend, user.ID, req.ProjectName)
 	if err != nil {
 		return nil, err
 	}
@@ -416,7 +425,8 @@ func (s *Server) ListChanges(
 	ctx context.Context,
 	req *api.ListChangesRequest,
 ) (*api.ListChangesResponse, error) {
-	project, err := projects.GetProject(ctx, s.backend, req.ProjectName)
+	user := users.From(ctx)
+	project, err := projects.GetProject(ctx, s.backend, user.ID, req.ProjectName)
 	if err != nil {
 		return nil, err
 	}
diff --git a/server/backend/database/database.go b/server/backend/database/database.go
index faa8bf9d..8cb61402 100644
--- a/server/backend/database/database.go
+++ b/server/backend/database/database.go
@@ -60,15 +60,25 @@ type Database interface {
 	Close() error
 
 	// FindProjectInfoByPublicKey returns a project by public key.
-	FindProjectInfoByPublicKey(ctx context.Context, publicKey string) (*ProjectInfo, error)
+	FindProjectInfoByPublicKey(
+		ctx context.Context,
+		publicKey string,
+	) (*ProjectInfo, error)
 
 	// FindProjectInfoByName returns a project by the given name.
-	FindProjectInfoByName(ctx context.Context, name string) (*ProjectInfo, error)
+	FindProjectInfoByName(
+		ctx context.Context,
+		owner types.ID,
+		name string,
+	) (*ProjectInfo, error)
 
-	// FindProjectInfoByID returns a project by the given id.
+	// FindProjectInfoByID returns a project by the given id. It should not be
+	// used directly by clients because it is not checked if the project is
+	// permitted to be accessed by the admin client.
 	FindProjectInfoByID(ctx context.Context, id types.ID) (*ProjectInfo, error)
 
-	// EnsureDefaultUserAndProject ensures that the default user and project exists.
+	// EnsureDefaultUserAndProject ensures that the default user and project
+	// exists.
 	EnsureDefaultUserAndProject(
 		ctx context.Context,
 		username,
@@ -76,13 +86,22 @@ type Database interface {
 	) (*UserInfo, *ProjectInfo, error)
 
 	// CreateProjectInfo creates a new project.
-	CreateProjectInfo(ctx context.Context, name string) (*ProjectInfo, error)
+	CreateProjectInfo(
+		ctx context.Context,
+		name string,
+		owner types.ID,
+	) (*ProjectInfo, error)
 
 	// ListProjectInfos returns all projects.
-	ListProjectInfos(ctx context.Context) ([]*ProjectInfo, error)
+	ListProjectInfos(ctx context.Context, owner types.ID) ([]*ProjectInfo, error)
 
 	// UpdateProjectInfo updates the project.
-	UpdateProjectInfo(ctx context.Context, id types.ID, fields *types.UpdatableProjectFields) (*ProjectInfo, error)
+	UpdateProjectInfo(
+		ctx context.Context,
+		owner types.ID,
+		id types.ID,
+		fields *types.UpdatableProjectFields,
+	) (*ProjectInfo, error)
 
 	// CreateUserInfo creates a new user.
 	CreateUserInfo(
diff --git a/server/backend/database/memory/database.go b/server/backend/database/memory/database.go
index 7d6ed6fe..54dafe12 100644
--- a/server/backend/database/memory/database.go
+++ b/server/backend/database/memory/database.go
@@ -56,7 +56,10 @@ func (d *DB) Close() error {
 }
 
 // FindProjectInfoByPublicKey returns a project by public key.
-func (d *DB) FindProjectInfoByPublicKey(ctx context.Context, publicKey string) (*database.ProjectInfo, error) {
+func (d *DB) FindProjectInfoByPublicKey(
+	ctx context.Context,
+	publicKey string,
+) (*database.ProjectInfo, error) {
 	txn := d.db.Txn(false)
 	defer txn.Abort()
 
@@ -72,7 +75,11 @@ func (d *DB) FindProjectInfoByPublicKey(ctx context.Context, publicKey string) (
 }
 
 // FindProjectInfoByName returns a project by the given name.
-func (d *DB) FindProjectInfoByName(ctx context.Context, name string) (*database.ProjectInfo, error) {
+func (d *DB) FindProjectInfoByName(
+	ctx context.Context,
+	owner types.ID,
+	name string,
+) (*database.ProjectInfo, error) {
 	txn := d.db.Txn(false)
 	defer txn.Abort()
 
@@ -84,7 +91,12 @@ func (d *DB) FindProjectInfoByName(ctx context.Context, name string) (*database.
 		return nil, fmt.Errorf("%s: %w", name, database.ErrProjectNotFound)
 	}
 
-	return raw.(*database.ProjectInfo).DeepCopy(), nil
+	info := raw.(*database.ProjectInfo).DeepCopy()
+	if info.Owner != owner {
+		return nil, fmt.Errorf("%s: %w", name, database.ErrProjectNotFound)
+	}
+
+	return info, nil
 }
 
 // FindProjectInfoByID returns a project by the given id.
@@ -113,7 +125,7 @@ func (d *DB) EnsureDefaultUserAndProject(
 		return nil, nil, err
 	}
 
-	project, err := d.ensureDefaultProjectInfo(ctx)
+	project, err := d.ensureDefaultProjectInfo(ctx, user.ID)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -155,7 +167,10 @@ func (d *DB) ensureDefaultUserInfo(
 }
 
 // ensureDefaultProjectInfo creates the default project if it does not exist.
-func (d *DB) ensureDefaultProjectInfo(ctx context.Context) (*database.ProjectInfo, error) {
+func (d *DB) ensureDefaultProjectInfo(
+	ctx context.Context,
+	defaultUserID types.ID,
+) (*database.ProjectInfo, error) {
 	txn := d.db.Txn(true)
 	defer txn.Abort()
 
@@ -166,7 +181,7 @@ func (d *DB) ensureDefaultProjectInfo(ctx context.Context) (*database.ProjectInf
 
 	var info *database.ProjectInfo
 	if raw == nil {
-		info = database.NewProjectInfo(database.DefaultProjectName)
+		info = database.NewProjectInfo(database.DefaultProjectName, defaultUserID)
 		info.ID = database.DefaultProjectID
 		if err := txn.Insert(tblProjects, info); err != nil {
 			return nil, err
@@ -180,7 +195,11 @@ func (d *DB) ensureDefaultProjectInfo(ctx context.Context) (*database.ProjectInf
 }
 
 // CreateProjectInfo creates a new project.
-func (d *DB) CreateProjectInfo(ctx context.Context, name string) (*database.ProjectInfo, error) {
+func (d *DB) CreateProjectInfo(
+	ctx context.Context,
+	name string,
+	owner types.ID,
+) (*database.ProjectInfo, error) {
 	txn := d.db.Txn(true)
 	defer txn.Abort()
 
@@ -194,7 +213,7 @@ func (d *DB) CreateProjectInfo(ctx context.Context, name string) (*database.Proj
 		return nil, fmt.Errorf("%s: %w", name, database.ErrProjectAlreadyExists)
 	}
 
-	info := database.NewProjectInfo(name)
+	info := database.NewProjectInfo(name, owner)
 	info.ID = newID()
 	if err := txn.Insert(tblProjects, info); err != nil {
 		return nil, err
@@ -205,22 +224,32 @@ func (d *DB) CreateProjectInfo(ctx context.Context, name string) (*database.Proj
 }
 
 // ListProjectInfos returns all projects.
-func (d *DB) ListProjectInfos(ctx context.Context) ([]*database.ProjectInfo, error) {
+func (d *DB) ListProjectInfos(
+	ctx context.Context,
+	owner types.ID,
+) ([]*database.ProjectInfo, error) {
 	txn := d.db.Txn(false)
 	defer txn.Abort()
 
-	iter, err := txn.Get(tblProjects, "id")
+	defaultProjectID := database.DefaultProjectID
+	iter, err := txn.LowerBound(
+		tblProjects,
+		"owner_id",
+		owner.String(),
+		defaultProjectID.String(),
+	)
 	if err != nil {
 		return nil, err
 	}
 
 	var infos []*database.ProjectInfo
-	for {
-		raw := iter.Next()
-		if raw == nil {
+	for raw := iter.Next(); raw != nil; raw = iter.Next() {
+		info := raw.(*database.ProjectInfo).DeepCopy()
+		if info.Owner != owner {
 			break
 		}
-		infos = append(infos, raw.(*database.ProjectInfo).DeepCopy())
+
+		infos = append(infos, info)
 	}
 
 	return infos, nil
@@ -229,6 +258,7 @@ func (d *DB) ListProjectInfos(ctx context.Context) ([]*database.ProjectInfo, err
 // UpdateProjectInfo updates the given project.
 func (d *DB) UpdateProjectInfo(
 	ctx context.Context,
+	owner types.ID,
 	id types.ID,
 	fields *types.UpdatableProjectFields,
 ) (*database.ProjectInfo, error) {
@@ -244,6 +274,9 @@ func (d *DB) UpdateProjectInfo(
 	}
 
 	info := raw.(*database.ProjectInfo).DeepCopy()
+	if info.Owner != owner {
+		return nil, fmt.Errorf("%s: %w", id, database.ErrProjectNotFound)
+	}
 
 	if fields.Name != nil {
 		existing, err := txn.First(tblProjects, "name", *fields.Name)
diff --git a/server/backend/database/memory/indexes.go b/server/backend/database/memory/indexes.go
index 16f4d6c4..25f23d2b 100644
--- a/server/backend/database/memory/indexes.go
+++ b/server/backend/database/memory/indexes.go
@@ -38,6 +38,16 @@ var schema = &memdb.DBSchema{
 					Unique:  true,
 					Indexer: &memdb.StringFieldIndex{Field: "ID"},
 				},
+				"owner_id": {
+					Name:   "owner_id",
+					Unique: true,
+					Indexer: &memdb.CompoundIndex{
+						Indexes: []memdb.Indexer{
+							&memdb.StringFieldIndex{Field: "Owner"},
+							&memdb.StringFieldIndex{Field: "ID"},
+						},
+					},
+				},
 				"name": {
 					Name:    "name",
 					Unique:  true,
diff --git a/server/backend/database/mongo/client.go b/server/backend/database/mongo/client.go
index a80a6a8c..729c8674 100644
--- a/server/backend/database/mongo/client.go
+++ b/server/backend/database/mongo/client.go
@@ -104,7 +104,7 @@ func (c *Client) EnsureDefaultUserAndProject(
 		return nil, nil, err
 	}
 
-	projectInfo, err := c.ensureDefaultProjectInfo(ctx)
+	projectInfo, err := c.ensureDefaultProjectInfo(ctx, userInfo.ID)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -157,19 +157,27 @@ func (c *Client) ensureDefaultUserInfo(
 }
 
 // ensureDefaultProjectInfo creates the default project info if it does not exist.
-func (c *Client) ensureDefaultProjectInfo(ctx context.Context) (*database.ProjectInfo, error) {
-	candidate := database.NewProjectInfo(database.DefaultProjectName)
+func (c *Client) ensureDefaultProjectInfo(
+	ctx context.Context,
+	defaultUserID types.ID,
+) (*database.ProjectInfo, error) {
+	candidate := database.NewProjectInfo(database.DefaultProjectName, defaultUserID)
 	candidate.ID = database.DefaultProjectID
 	encodedID, err := encodeID(candidate.ID)
 	if err != nil {
 		return nil, err
 	}
+	encodedDefaultUserID, err := encodeID(defaultUserID)
+	if err != nil {
+		return nil, err
+	}
 
 	_, err = c.collection(colProjects).UpdateOne(ctx, bson.M{
 		"_id": encodedID,
 	}, bson.M{
 		"$setOnInsert": bson.M{
 			"name":       candidate.Name,
+			"owner":      encodedDefaultUserID,
 			"public_key": candidate.PublicKey,
 			"secret_key": candidate.SecretKey,
 			"created_at": candidate.CreatedAt,
@@ -195,10 +203,20 @@ func (c *Client) ensureDefaultProjectInfo(ctx context.Context) (*database.Projec
 }
 
 // CreateProjectInfo creates a new project.
-func (c *Client) CreateProjectInfo(ctx context.Context, name string) (*database.ProjectInfo, error) {
-	info := database.NewProjectInfo(name)
+func (c *Client) CreateProjectInfo(
+	ctx context.Context,
+	name string,
+	owner types.ID,
+) (*database.ProjectInfo, error) {
+	encodedOwner, err := encodeID(owner)
+	if err != nil {
+		return nil, err
+	}
+
+	info := database.NewProjectInfo(name, owner)
 	result, err := c.collection(colProjects).InsertOne(ctx, bson.M{
 		"name":       info.Name,
+		"owner":      encodedOwner,
 		"public_key": info.PublicKey,
 		"secret_key": info.SecretKey,
 		"created_at": info.CreatedAt,
@@ -217,8 +235,18 @@ func (c *Client) CreateProjectInfo(ctx context.Context, name string) (*database.
 }
 
 // ListProjectInfos returns all project infos.
-func (c *Client) ListProjectInfos(ctx context.Context) ([]*database.ProjectInfo, error) {
-	cursor, err := c.collection(colProjects).Find(ctx, bson.M{})
+func (c *Client) ListProjectInfos(
+	ctx context.Context,
+	owner types.ID,
+) ([]*database.ProjectInfo, error) {
+	encodedOwnerID, err := encodeID(owner)
+	if err != nil {
+		return nil, err
+	}
+
+	cursor, err := c.collection(colProjects).Find(ctx, bson.M{
+		"owner": encodedOwnerID,
+	})
 	if err != nil {
 		logging.From(ctx).Error(err)
 		return nil, err
@@ -250,9 +278,19 @@ func (c *Client) FindProjectInfoByPublicKey(ctx context.Context, publicKey strin
 }
 
 // FindProjectInfoByName returns a project by name.
-func (c *Client) FindProjectInfoByName(ctx context.Context, name string) (*database.ProjectInfo, error) {
+func (c *Client) FindProjectInfoByName(
+	ctx context.Context,
+	owner types.ID,
+	name string,
+) (*database.ProjectInfo, error) {
+	encodedOwner, err := encodeID(owner)
+	if err != nil {
+		return nil, err
+	}
+
 	result := c.collection(colProjects).FindOne(ctx, bson.M{
-		"name": name,
+		"name":  name,
+		"owner": encodedOwner,
 	})
 
 	projectInfo := database.ProjectInfo{}
@@ -291,9 +329,14 @@ func (c *Client) FindProjectInfoByID(ctx context.Context, id types.ID) (*databas
 // UpdateProjectInfo updates the project info.
 func (c *Client) UpdateProjectInfo(
 	ctx context.Context,
+	owner types.ID,
 	id types.ID,
 	fields *types.UpdatableProjectFields,
 ) (*database.ProjectInfo, error) {
+	encodedOwner, err := encodeID(owner)
+	if err != nil {
+		return nil, err
+	}
 	encodedID, err := encodeID(id)
 	if err != nil {
 		return nil, err
@@ -312,7 +355,8 @@ func (c *Client) UpdateProjectInfo(
 	updatableFields["updated_at"] = gotime.Now()
 
 	res := c.collection(colProjects).FindOneAndUpdate(ctx, bson.M{
-		"_id": encodedID,
+		"_id":   encodedID,
+		"owner": encodedOwner,
 	}, bson.M{
 		"$set": updatableFields,
 	}, options.FindOneAndUpdate().SetReturnDocument(options.After))
diff --git a/server/backend/database/project_info.go b/server/backend/database/project_info.go
index 3375eb31..be022586 100644
--- a/server/backend/database/project_info.go
+++ b/server/backend/database/project_info.go
@@ -38,6 +38,9 @@ type ProjectInfo struct {
 	// Name is the name of this project.
 	Name string `bson:"name"`
 
+	// Owner is the owner of this project.
+	Owner types.ID `bson:"owner"`
+
 	// PublicKey is the API key of this project.
 	PublicKey string `bson:"public_key"`
 
@@ -58,9 +61,10 @@ type ProjectInfo struct {
 }
 
 // NewProjectInfo creates a new ProjectInfo of the given name.
-func NewProjectInfo(name string) *ProjectInfo {
+func NewProjectInfo(name string, owner types.ID) *ProjectInfo {
 	return &ProjectInfo{
-		Name: name,
+		Name:  name,
+		Owner: owner,
 		// TODO(hackerwins): Use random generated Key.
 		PublicKey: xid.New().String(),
 		SecretKey: xid.New().String(),
@@ -73,6 +77,7 @@ func ToProjectInfo(project *types.Project) *ProjectInfo {
 	return &ProjectInfo{
 		ID:                 project.ID,
 		Name:               project.Name,
+		Owner:              project.Owner,
 		PublicKey:          project.PublicKey,
 		SecretKey:          project.SecretKey,
 		AuthWebhookURL:     project.AuthWebhookURL,
@@ -91,6 +96,7 @@ func (i *ProjectInfo) DeepCopy() *ProjectInfo {
 	return &ProjectInfo{
 		ID:                 i.ID,
 		Name:               i.Name,
+		Owner:              i.Owner,
 		PublicKey:          i.PublicKey,
 		SecretKey:          i.SecretKey,
 		AuthWebhookURL:     i.AuthWebhookURL,
@@ -118,6 +124,7 @@ func (i *ProjectInfo) ToProject() *types.Project {
 	return &types.Project{
 		ID:                 i.ID,
 		Name:               i.Name,
+		Owner:              i.Owner,
 		AuthWebhookURL:     i.AuthWebhookURL,
 		AuthWebhookMethods: i.AuthWebhookMethods,
 		PublicKey:          i.PublicKey,
diff --git a/server/projects/projects.go b/server/projects/projects.go
index 82eb3e2b..f593849d 100644
--- a/server/projects/projects.go
+++ b/server/projects/projects.go
@@ -28,9 +28,10 @@ import (
 func CreateProject(
 	ctx context.Context,
 	be *backend.Backend,
+	owner types.ID,
 	name string,
 ) (*types.Project, error) {
-	info, err := be.DB.CreateProjectInfo(ctx, name)
+	info, err := be.DB.CreateProjectInfo(ctx, name, owner)
 	if err != nil {
 		return nil, err
 	}
@@ -42,8 +43,9 @@ func CreateProject(
 func ListProjects(
 	ctx context.Context,
 	be *backend.Backend,
+	owner types.ID,
 ) ([]*types.Project, error) {
-	infos, err := be.DB.ListProjectInfos(ctx)
+	infos, err := be.DB.ListProjectInfos(ctx, owner)
 	if err != nil {
 		return nil, err
 	}
@@ -60,9 +62,26 @@ func ListProjects(
 func GetProject(
 	ctx context.Context,
 	be *backend.Backend,
+	owner types.ID,
 	name string,
 ) (*types.Project, error) {
-	info, err := be.DB.FindProjectInfoByName(ctx, name)
+	info, err := be.DB.FindProjectInfoByName(ctx, owner, name)
+	if err != nil {
+		return nil, err
+	}
+
+	return info.ToProject(), nil
+}
+
+// UpdateProject updates a project.
+func UpdateProject(
+	ctx context.Context,
+	be *backend.Backend,
+	owner types.ID,
+	id types.ID,
+	fields *types.UpdatableProjectFields,
+) (*types.Project, error) {
+	info, err := be.DB.UpdateProjectInfo(ctx, owner, id, fields)
 	if err != nil {
 		return nil, err
 	}
@@ -73,7 +92,7 @@ func GetProject(
 // GetProjectFromAPIKey returns a project from an API key.
 func GetProjectFromAPIKey(ctx context.Context, be *backend.Backend, apiKey string) (*types.Project, error) {
 	if apiKey == "" {
-		info, err := be.DB.FindProjectInfoByName(ctx, database.DefaultProjectName)
+		info, err := be.DB.FindProjectInfoByID(ctx, database.DefaultProjectID)
 		if err != nil {
 			return nil, err
 		}
@@ -87,18 +106,3 @@ func GetProjectFromAPIKey(ctx context.Context, be *backend.Backend, apiKey strin
 
 	return info.ToProject(), nil
 }
-
-// UpdateProject updates a project.
-func UpdateProject(
-	ctx context.Context,
-	be *backend.Backend,
-	id types.ID,
-	fields *types.UpdatableProjectFields,
-) (*types.Project, error) {
-	info, err := be.DB.UpdateProjectInfo(ctx, id, fields)
-	if err != nil {
-		return nil, err
-	}
-
-	return info.ToProject(), nil
-}
diff --git a/server/users/context.go b/server/users/context.go
new file mode 100644
index 00000000..e0d5ad97
--- /dev/null
+++ b/server/users/context.go
@@ -0,0 +1,20 @@
+package users
+
+import (
+	"context"
+
+	"github.com/yorkie-team/yorkie/api/types"
+)
+
+// userKey is the key for the context.Context.
+type userKey struct{}
+
+// From returns the user from the context.
+func From(ctx context.Context) *types.User {
+	return ctx.Value(userKey{}).(*types.User)
+}
+
+// With returns a new context with the given user.
+func With(ctx context.Context, user *types.User) context.Context {
+	return context.WithValue(ctx, userKey{}, user)
+}
diff --git a/server/users/users.go b/server/users/users.go
index 671320b6..901fdd7c 100644
--- a/server/users/users.go
+++ b/server/users/users.go
@@ -66,3 +66,17 @@ func IsCorrectPassword(
 
 	return info.ToUser(), nil
 }
+
+// GetUser returns a user by the given username.
+func GetUser(
+	ctx context.Context,
+	be *backend.Backend,
+	username string,
+) (*types.User, error) {
+	info, err := be.DB.FindUserInfo(ctx, username)
+	if err != nil {
+		return nil, err
+	}
+
+	return info.ToUser(), nil
+}


Test patch:
diff --git a/server/backend/database/memory/database_test.go b/server/backend/database/memory/database_test.go
index b9114d3f..e8994004 100644
--- a/server/backend/database/memory/database_test.go
+++ b/server/backend/database/memory/database_test.go
@@ -39,6 +39,8 @@ func TestDB(t *testing.T) {
 	assert.NoError(t, err)
 
 	projectID := database.DefaultProjectID
+	dummyOwnerID := types.ID("000000000000000000000000")
+	otherOwnerID := types.ID("000000000000000000000001")
 	notExistsID := types.ID("000000000000000000000000")
 
 	t.Run("activate/deactivate client test", func(t *testing.T) {
@@ -73,12 +75,18 @@ func TestDB(t *testing.T) {
 	})
 
 	t.Run("project test", func(t *testing.T) {
-		info, err := db.CreateProjectInfo(ctx, t.Name())
+		_, err = db.CreateProjectInfo(ctx, t.Name(), otherOwnerID)
 		assert.NoError(t, err)
-		assert.Equal(t, t.Name(), info.Name)
 
-		_, err = db.CreateProjectInfo(ctx, t.Name())
-		assert.ErrorIs(t, err, database.ErrProjectAlreadyExists)
+		suffixes := []int{0, 1, 2}
+		for _, suffix := range suffixes {
+			_, err = db.CreateProjectInfo(ctx, fmt.Sprintf("%s-%d", t.Name(), suffix), dummyOwnerID)
+			assert.NoError(t, err)
+		}
+
+		projects, err := db.ListProjectInfos(ctx, dummyOwnerID)
+		assert.NoError(t, err)
+		assert.Len(t, projects, len(suffixes))
 	})
 
 	t.Run("user test", func(t *testing.T) {
@@ -320,83 +328,87 @@ func TestDB(t *testing.T) {
 		assert.ErrorIs(t, err, database.ErrDocumentNotFound)
 	})
 
-	t.Run("UpdateProjectInfo test", func(t *testing.T) {
-		info, err := db.CreateProjectInfo(ctx, t.Name())
+	t.Run("FindProjectInfoByID test", func(t *testing.T) {
+		info, err := db.CreateProjectInfo(ctx, t.Name(), dummyOwnerID)
 		assert.NoError(t, err)
-		existName := "already"
-		_, err = db.CreateProjectInfo(ctx, existName)
+
+		_, err = db.FindProjectInfoByName(ctx, dummyOwnerID, info.Name)
 		assert.NoError(t, err)
 
-		id := info.ID
+		_, err = db.FindProjectInfoByName(ctx, otherOwnerID, info.Name)
+		assert.ErrorIs(t, err, database.ErrProjectNotFound)
+	})
+
+	t.Run("UpdateProjectInfo test", func(t *testing.T) {
+		existName := "already"
 		newName := "changed-name"
+		newName2 := newName + "2"
 		newAuthWebhookURL := "newWebhookURL"
 		newAuthWebhookMethods := []string{
 			string(types.AttachDocument),
 			string(types.WatchDocuments),
 		}
 
-		// update all updatable fields
+		info, err := db.CreateProjectInfo(ctx, t.Name(), dummyOwnerID)
+		assert.NoError(t, err)
+		_, err = db.CreateProjectInfo(ctx, existName, dummyOwnerID)
+		assert.NoError(t, err)
+
+		id := info.ID
+
+		// 01. Update all fields test
 		fields := &types.UpdatableProjectFields{
 			Name:               &newName,
 			AuthWebhookURL:     &newAuthWebhookURL,
 			AuthWebhookMethods: &newAuthWebhookMethods,
 		}
-
-		err = fields.Validate()
-		assert.NoError(t, err)
-		res, err := db.UpdateProjectInfo(ctx, id, fields)
+		assert.NoError(t, fields.Validate())
+		res, err := db.UpdateProjectInfo(ctx, dummyOwnerID, id, fields)
 		assert.NoError(t, err)
-
 		updateInfo, err := db.FindProjectInfoByID(ctx, id)
 		assert.NoError(t, err)
-
 		assert.Equal(t, res, updateInfo)
 		assert.Equal(t, newName, updateInfo.Name)
 		assert.Equal(t, newAuthWebhookURL, updateInfo.AuthWebhookURL)
 		assert.Equal(t, newAuthWebhookMethods, updateInfo.AuthWebhookMethods)
 
-		// update name field
-		newName2 := newName + "2"
+		// 02. Update name field test
 		fields = &types.UpdatableProjectFields{
 			Name: &newName2,
 		}
-		err = fields.Validate()
+		assert.NoError(t, fields.Validate())
+		res, err = db.UpdateProjectInfo(ctx, dummyOwnerID, id, fields)
 		assert.NoError(t, err)
-		res, err = db.UpdateProjectInfo(ctx, id, fields)
-		assert.NoError(t, err)
-
 		updateInfo, err = db.FindProjectInfoByID(ctx, id)
 		assert.NoError(t, err)
-
-		// check only name is updated
 		assert.Equal(t, res, updateInfo)
 		assert.NotEqual(t, newName, updateInfo.Name)
 		assert.Equal(t, newAuthWebhookURL, updateInfo.AuthWebhookURL)
 		assert.Equal(t, newAuthWebhookMethods, updateInfo.AuthWebhookMethods)
 
-		// check duplicate name error
-		fields = &types.UpdatableProjectFields{Name: &existName}
-		_, err = db.UpdateProjectInfo(ctx, id, fields)
-		assert.ErrorIs(t, err, database.ErrProjectNameAlreadyExists)
-
-		// update auth-webhook-url field
+		// 03. Update authWebhookURL test
 		newAuthWebhookURL2 := newAuthWebhookURL + "2"
 		fields = &types.UpdatableProjectFields{
 			AuthWebhookURL: &newAuthWebhookURL2,
 		}
-		err = fields.Validate()
-		assert.NoError(t, err)
-		res, err = db.UpdateProjectInfo(ctx, id, fields)
+		assert.NoError(t, fields.Validate())
+		res, err = db.UpdateProjectInfo(ctx, dummyOwnerID, id, fields)
 		assert.NoError(t, err)
-
 		updateInfo, err = db.FindProjectInfoByID(ctx, id)
 		assert.NoError(t, err)
-
-		// check only auth-webhook-url is update
 		assert.Equal(t, res, updateInfo)
 		assert.Equal(t, newName2, updateInfo.Name)
 		assert.NotEqual(t, newAuthWebhookURL, updateInfo.AuthWebhookURL)
 		assert.Equal(t, newAuthWebhookMethods, updateInfo.AuthWebhookMethods)
 
+		// 04. Duplicated name test
+		fields = &types.UpdatableProjectFields{Name: &existName}
+		_, err = db.UpdateProjectInfo(ctx, dummyOwnerID, id, fields)
+		assert.ErrorIs(t, err, database.ErrProjectNameAlreadyExists)
+
+		// 05. OwnerID not match test
+		fields = &types.UpdatableProjectFields{Name: &existName}
+		_, err = db.UpdateProjectInfo(ctx, otherOwnerID, id, fields)
+		assert.ErrorIs(t, err, database.ErrProjectNotFound)
 	})
 }
diff --git a/server/backend/database/mongo/client_test.go b/server/backend/database/mongo/client_test.go
index 00a385a0..a226a1df 100644
--- a/server/backend/database/mongo/client_test.go
+++ b/server/backend/database/mongo/client_test.go
@@ -41,11 +41,13 @@ func TestClient(t *testing.T) {
 	cli, err := mongo.Dial(config)
 	assert.NoError(t, err)
 
+	dummyOwnerID := types.ID("000000000000000000000000")
+
 	t.Run("UpdateProjectInfo test", func(t *testing.T) {
-		info, err := cli.CreateProjectInfo(ctx, t.Name())
+		info, err := cli.CreateProjectInfo(ctx, t.Name(), dummyOwnerID)
 		assert.NoError(t, err)
 		existName := "already"
-		_, err = cli.CreateProjectInfo(ctx, existName)
+		_, err = cli.CreateProjectInfo(ctx, existName, dummyOwnerID)
 		assert.NoError(t, err)
 
 		id := info.ID
@@ -65,7 +67,7 @@ func TestClient(t *testing.T) {
 
 		err = fields.Validate()
 		assert.NoError(t, err)
-		res, err := cli.UpdateProjectInfo(ctx, id, fields)
+		res, err := cli.UpdateProjectInfo(ctx, dummyOwnerID, id, fields)
 		assert.NoError(t, err)
 
 		updateInfo, err := cli.FindProjectInfoByID(ctx, id)
@@ -83,7 +85,7 @@ func TestClient(t *testing.T) {
 		}
 		err = fields.Validate()
 		assert.NoError(t, err)
-		res, err = cli.UpdateProjectInfo(ctx, id, fields)
+		res, err = cli.UpdateProjectInfo(ctx, dummyOwnerID, id, fields)
 		assert.NoError(t, err)
 
 		updateInfo, err = cli.FindProjectInfoByID(ctx, id)
@@ -97,7 +99,7 @@ func TestClient(t *testing.T) {
 
 		// check duplicate name error
 		fields = &types.UpdatableProjectFields{Name: &existName}
-		_, err = cli.UpdateProjectInfo(ctx, id, fields)
+		_, err = cli.UpdateProjectInfo(ctx, dummyOwnerID, id, fields)
 		assert.ErrorIs(t, err, database.ErrProjectNameAlreadyExists)
 	})
 }
diff --git a/server/backend/database/project_info_test.go b/server/backend/database/project_info_test.go
index eb15b745..adc6ab67 100644
--- a/server/backend/database/project_info_test.go
+++ b/server/backend/database/project_info_test.go
@@ -27,7 +27,8 @@ import (
 
 func TestProjectInfo(t *testing.T) {
 	t.Run("update fields test", func(t *testing.T) {
-		project := database.NewProjectInfo(t.Name())
+		dummyOwnerID := types.ID("000000000000000000000000")
+		project := database.NewProjectInfo(t.Name(), dummyOwnerID)
 
 		testName := "testName"
 		testURL := "testUrl"
@@ -41,5 +42,6 @@ func TestProjectInfo(t *testing.T) {
 
 		project.UpdateFields(&types.UpdatableProjectFields{AuthWebhookMethods: &testMethods})
 		assert.Equal(t, testMethods, project.AuthWebhookMethods)
+		assert.Equal(t, dummyOwnerID, project.Owner)
 	})
 }
diff --git a/server/rpc/server_test.go b/server/rpc/server_test.go
index 1ac6aee7..1ee3f34b 100644
--- a/server/rpc/server_test.go
+++ b/server/rpc/server_test.go
@@ -85,7 +85,10 @@ func TestMain(m *testing.M) {
 		log.Fatal(err)
 	}
 
-	project, err := be.DB.FindProjectInfoByName(context.Background(), database.DefaultProjectName)
+	project, err := be.DB.FindProjectInfoByID(
+		context.Background(),
+		database.DefaultProjectID,
+	)
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/test/integration/history_test.go b/test/integration/history_test.go
index 01c8421b..b0f4e9f6 100644
--- a/test/integration/history_test.go
+++ b/test/integration/history_test.go
@@ -1,4 +1,4 @@
-//go:build integration && amd64
+//go:build integration
 
 /*
  * Copyright 2022 The Yorkie Authors. All rights reserved.
@@ -20,39 +20,17 @@ package integration
 
 import (
 	"context"
-	"fmt"
-	"log"
-	"reflect"
 	"testing"
 
-	"bou.ke/monkey"
 	"github.com/stretchr/testify/assert"
 
-	"github.com/yorkie-team/yorkie/api/types"
-	"github.com/yorkie-team/yorkie/client"
 	"github.com/yorkie-team/yorkie/pkg/document"
 	"github.com/yorkie-team/yorkie/pkg/document/json"
 	"github.com/yorkie-team/yorkie/pkg/document/key"
-	"github.com/yorkie-team/yorkie/server"
-	"github.com/yorkie-team/yorkie/server/backend/background"
-	"github.com/yorkie-team/yorkie/server/logging"
 	"github.com/yorkie-team/yorkie/test/helper"
 )
 
 func TestHistory(t *testing.T) {
-	var b *background.Background
-	monkey.PatchInstanceMethod(
-		reflect.TypeOf(b),
-		"AttachGoroutine",
-		func(_ *background.Background, f func(c context.Context)) {
-			f(context.Background())
-		},
-	)
-	defer monkey.UnpatchInstanceMethod(
-		reflect.TypeOf(b),
-		"AttachGoroutine",
-	)
-
 	clients := activeClients(t, 1)
 	cli := clients[0]
 	defer cleanupClients(t, clients)
@@ -98,69 +76,4 @@ func TestHistory(t *testing.T) {
 		assert.Equal(t, `{"todos":["buy coffee"]}`, changes[1].Snapshot)
 		assert.Equal(t, `{"todos":["buy coffee","buy bread"]}`, changes[0].Snapshot)
 	})
-
-	t.Run("history test with purging changes", func(t *testing.T) {
-		conf := helper.TestConfig()
-		conf.Backend.SnapshotWithPurgingChanges = true
-		testServer, err := server.New(conf)
-		if err != nil {
-			log.Fatal(err)
-		}
-
-		if err := testServer.Start(); err != nil {
-			logging.DefaultLogger().Fatal(err)
-		}
-
-		cli, err := client.Dial(
-			testServer.RPCAddr(),
-			client.WithPresence(types.Presence{"name": fmt.Sprintf("name-%d", 0)}),
-		)
-		assert.NoError(t, err)
-
-		err = cli.Activate(context.Background())
-		assert.NoError(t, err)
-		defer func() {
-			assert.NoError(t, cli.Deactivate(context.Background()))
-			assert.NoError(t, cli.Close())
-		}()
-
-		adminCli := helper.CreateAdminCli(t, testServer.AdminAddr())
-		assert.NoError(t, err)
-		defer func() { assert.NoError(t, adminCli.Close()) }()
-
-		ctx := context.Background()
-
-		doc := document.New(key.Key(t.Name()))
-		assert.NoError(t, cli.Attach(ctx, doc))
-		defer func() { assert.NoError(t, cli.Detach(ctx, doc)) }()
-
-		assert.NoError(t, doc.Update(func(root *json.Object) error {
-			root.SetNewArray("todos")
-			return nil
-		}, "create todos"))
-		assert.Equal(t, `{"todos":[]}`, doc.Marshal())
-
-		assert.NoError(t, doc.Update(func(root *json.Object) error {
-			root.GetArray("todos").AddString("buy coffee")
-			return nil
-		}, "buy coffee"))
-		assert.Equal(t, `{"todos":["buy coffee"]}`, doc.Marshal())
-
-		assert.NoError(t, doc.Update(func(root *json.Object) error {
-			root.GetArray("todos").AddString("buy bread")
-			return nil
-		}, "buy bread"))
-		assert.Equal(t, `{"todos":["buy coffee","buy bread"]}`, doc.Marshal())
-		assert.NoError(t, cli.Sync(ctx))
-
-		changes, err := adminCli.ListChangeSummaries(ctx, "default", doc.Key(), 0, 0, true)
-		assert.NoError(t, err)
-		assert.Len(t, changes, 3)
-
-		assert.NoError(t, cli.Sync(ctx))
-
-		changes2, err := adminCli.ListChangeSummaries(ctx, "default", doc.Key(), 0, 0, true)
-		assert.NoError(t, err)
-		assert.Len(t, changes2, 0)
-	})
 }
diff --git a/test/integration/retention_test.go b/test/integration/retention_test.go
new file mode 100644
index 00000000..a42a8fb3
--- /dev/null
+++ b/test/integration/retention_test.go
@@ -0,0 +1,118 @@
+//go:build integration && amd64
+
+/*
+ * Copyright 2022 The Yorkie Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package integration
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"reflect"
+	"testing"
+
+	"bou.ke/monkey"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/yorkie-team/yorkie/api/types"
+	"github.com/yorkie-team/yorkie/client"
+	"github.com/yorkie-team/yorkie/pkg/document"
+	"github.com/yorkie-team/yorkie/pkg/document/json"
+	"github.com/yorkie-team/yorkie/pkg/document/key"
+	"github.com/yorkie-team/yorkie/server"
+	"github.com/yorkie-team/yorkie/server/backend/background"
+	"github.com/yorkie-team/yorkie/server/logging"
+	"github.com/yorkie-team/yorkie/test/helper"
+)
+
+func TestRetention(t *testing.T) {
+	var b *background.Background
+	monkey.PatchInstanceMethod(
+		reflect.TypeOf(b),
+		"AttachGoroutine",
+		func(_ *background.Background, f func(c context.Context)) {
+			f(context.Background())
+		},
+	)
+	defer monkey.UnpatchInstanceMethod(
+		reflect.TypeOf(b),
+		"AttachGoroutine",
+	)
+
+	t.Run("history test with purging changes", func(t *testing.T) {
+		conf := helper.TestConfig()
+		conf.Backend.SnapshotWithPurgingChanges = true
+		testServer, err := server.New(conf)
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		if err := testServer.Start(); err != nil {
+			logging.DefaultLogger().Fatal(err)
+		}
+
+		cli, err := client.Dial(
+			testServer.RPCAddr(),
+			client.WithPresence(types.Presence{"name": fmt.Sprintf("name-%d", 0)}),
+		)
+		assert.NoError(t, err)
+		assert.NoError(t, cli.Activate(context.Background()))
+		defer func() {
+			assert.NoError(t, cli.Deactivate(context.Background()))
+			assert.NoError(t, cli.Close())
+		}()
+
+		adminCli := helper.CreateAdminCli(t, testServer.AdminAddr())
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, adminCli.Close()) }()
+
+		ctx := context.Background()
+
+		doc := document.New(key.Key(t.Name()))
+		assert.NoError(t, cli.Attach(ctx, doc))
+		defer func() { assert.NoError(t, cli.Detach(ctx, doc)) }()
+
+		assert.NoError(t, doc.Update(func(root *json.Object) error {
+			root.SetNewArray("todos")
+			return nil
+		}, "create todos"))
+		assert.Equal(t, `{"todos":[]}`, doc.Marshal())
+
+		assert.NoError(t, doc.Update(func(root *json.Object) error {
+			root.GetArray("todos").AddString("buy coffee")
+			return nil
+		}, "buy coffee"))
+		assert.Equal(t, `{"todos":["buy coffee"]}`, doc.Marshal())
+
+		assert.NoError(t, doc.Update(func(root *json.Object) error {
+			root.GetArray("todos").AddString("buy bread")
+			return nil
+		}, "buy bread"))
+		assert.Equal(t, `{"todos":["buy coffee","buy bread"]}`, doc.Marshal())
+		assert.NoError(t, cli.Sync(ctx))
+
+		changes, err := adminCli.ListChangeSummaries(ctx, "default", doc.Key(), 0, 0, true)
+		assert.NoError(t, err)
+		assert.Len(t, changes, 3)
+
+		assert.NoError(t, cli.Sync(ctx))
+
+		changes2, err := adminCli.ListChangeSummaries(ctx, "default", doc.Key(), 0, 0, true)
+		assert.NoError(t, err)
+		assert.Len(t, changes2, 0)
+	})
+}