본문으로 바로가기

[DB] 동시성의 중요성

category 데이터베이스 2026. 3. 20. 15:39
반응형

MSSQL에서 ConcurrencyStamp란? ASP.NET Core Identity에서 꼭 알아야 할 동시성 개념

ASP.NET Core Identity를 MSSQL과 함께 사용하다 보면 AspNetUsers 테이블이나 AspNetRoles 테이블에서 ConcurrencyStamp 컬럼을 보게 됩니다. 처음 보면 “이게 암호 관련 값인가?”, “삭제해도 되는 컬럼인가?” 같은 궁금증이 생기기 쉽습니다.

결론부터 말하면, ConcurrencyStamp사용자나 역할 정보가 동시에 수정될 때 충돌을 감지하기 위한 값입니다. 즉, 인증 그 자체를 위한 값이라기보다 동시성 제어(optimistic concurrency) 를 위한 토큰이라고 이해하면 됩니다.


먼저 예시부터 보기

아래처럼 AspNetUsers 테이블을 조회해 보면 ConcurrencyStampSecurityStamp가 함께 보이는 경우가 많습니다.

SELECT TOP 10
    Id,
    UserName,
    Email,
    ConcurrencyStamp,
    SecurityStamp
FROM AspNetUsers
ORDER BY UserName;

예를 들어 결과가 이렇게 나올 수 있습니다.

Id                                   UserName   Email              ConcurrencyStamp                         SecurityStamp
-----------------------------------  ---------  -----------------  ---------------------------------------  ---------------------------------------
9b2b4f7a-2d2b-4a52-9ef0-1d4f7c4e1001  admin      admin@test.com     5f46ef2d-1c6c-4b8f-8c95-2f6d9fd8f4aa     9aaea4f8-61d5-4c7b-a3c5-5a8480b4bc91

여기서 핵심은 다음입니다.

  • ConcurrencyStamp: 데이터 충돌 감지용 값
  • SecurityStamp: 보안 자격 증명 변경 추적용 값

이 둘은 이름이 비슷하지만 역할이 다릅니다.


ConcurrencyStamp란?

ConcurrencyStamp는 ASP.NET Core Identity의 IdentityUserIdentityRole에 들어 있는 속성입니다. 공식 문서에서는 이 값을 사용자 또는 역할이 저장소에 반영될 때 변경되어야 하는 임의 값으로 설명합니다.

쉽게 말해, 어떤 사용자를 읽어 온 뒤 수정하려고 할 때, 내가 처음 읽었던 시점과 지금 DB 상태가 같은지 확인하기 위한 “버전표” 같은 역할을 합니다.

즉, 두 요청이 거의 동시에 같은 사용자 정보를 수정하면 마지막 저장 시점에 충돌을 감지해서 덮어쓰기 사고를 줄이는 장치라고 볼 수 있습니다.


왜 MSSQL 테이블에 존재할까?

중요한 포인트는 ConcurrencyStampSQL Server(MSSQL)의 고유 시스템 컬럼이 아니라, ASP.NET Core Identity가 EF Core 모델을 통해 기본적으로 매핑하는 컬럼이라는 점입니다.

기본 Identity 모델에서는:

  • 사용자 엔터티가 AspNetUsers 테이블에 매핑되고
  • 역할 엔터티가 AspNetRoles 테이블에 매핑되며
  • ConcurrencyStamp는 EF Core의 Concurrency Token 으로 설정됩니다.

즉, DB에 컬럼은 존재하지만 “MSSQL 자체 기능”이라기보다는 Identity + EF Core가 동시성 체크를 위해 저장하는 값이라고 이해하는 것이 정확합니다.


내부적으로 어떻게 동작할까?

EF Core의 낙관적 동시성(optimistic concurrency)은 특정 컬럼을 concurrency token으로 설정해 두고, 업데이트 또는 삭제 시점에 처음 읽었을 때의 값과 현재 DB의 값을 비교하는 방식으로 동작합니다.

예를 들어 어떤 사용자를 조회했을 때 ConcurrencyStamp = A였다고 가정해 보겠습니다.

  1. 요청 A가 사용자 정보를 읽어 옵니다.
  2. 요청 B도 같은 사용자 정보를 읽어 옵니다.
  3. 요청 A가 먼저 수정 저장을 수행합니다.
  4. 이때 Identity는 새로운 ConcurrencyStamp 값으로 갱신합니다.
  5. 요청 B가 이전 값 A를 기준으로 저장하려고 하면, DB에는 이미 다른 값이 들어 있으므로 충돌이 발생합니다.

이 흐름 때문에 나중 요청은 “이미 다른 곳에서 수정됐다”는 사실을 알 수 있습니다.


C# 예시: UserManager.UpdateAsync()로 사용자 수정하기

실무에서는 보통 DbContextAspNetUsers를 직접 만지기보다 UserManager를 통해 수정하는 편이 안전합니다.

public async Task UpdateUserEmailAsync(UserManager<IdentityUser> userManager, string userId, string newEmail)
{
    var user = await userManager.FindByIdAsync(userId);

    if (user == null)
    {
        throw new InvalidOperationException("사용자를 찾을 수 없습니다.");
    }

    user.Email = newEmail;
    user.UserName = newEmail;

    var result = await userManager.UpdateAsync(user);

    if (!result.Succeeded)
    {
        var concurrencyError = result.Errors
            .FirstOrDefault(e => e.Code == nameof(IdentityErrorDescriber.ConcurrencyFailure));

        if (concurrencyError != null)
        {
            throw new InvalidOperationException("다른 요청이 먼저 사용자를 수정했습니다. 최신 데이터를 다시 조회한 뒤 재시도하세요.");
        }

        var message = string.Join(", ", result.Errors.Select(x => x.Description));
        throw new InvalidOperationException(message);
    }
}

이 방식이 중요한 이유는, Identity의 UserStore.UpdateAsync()가 내부적으로 사용자를 attach한 뒤 ConcurrencyStamp를 새 GUID 문자열로 바꾸고 저장하기 때문입니다. 그리고 저장 중 동시성 예외가 발생하면 ConcurrencyFailure() 결과를 반환하도록 구현돼 있습니다.


SecurityStamp와는 무엇이 다를까?

많이 헷갈리는 부분이라 꼭 구분해야 합니다.

1) ConcurrencyStamp

  • 목적: 동시 수정 충돌 감지
  • 관심사: 데이터 저장 시점의 버전 비교
  • 대표 상황: 관리자 화면에서 같은 사용자를 동시에 수정하는 경우

2) SecurityStamp

  • 목적: 보안 관련 변경 추적
  • 관심사: 비밀번호 변경, 로그인 제거 같은 자격 증명 변경
  • 대표 상황: 비밀번호 변경 후 기존 로그인 세션 재검증

즉,

  • ConcurrencyStamp데이터 일관성
  • SecurityStamp보안 상태

이라고 생각하면 이해가 훨씬 쉽습니다.


MSSQL의 rowversion과도 같은 걸까?

비슷해 보이지만 완전히 같지는 않습니다.

EF Core 문서에서는 SQL Server의 rowversionDB가 자동으로 갱신하는 네이티브 동시성 토큰으로 설명합니다. 반면 ConcurrencyStamp는 애플리케이션이 관리하는 문자열 기반 토큰에 가깝습니다.

즉,

  • rowversion: MSSQL이 자동 관리
  • ConcurrencyStamp: Identity 애플리케이션 코드가 관리

라는 차이가 있습니다.

그래서 ConcurrencyStamp는 “MSSQL 기능”이 아니라, Identity가 MSSQL 위에 저장해서 사용하는 동시성 값으로 보는 것이 맞습니다.


실무에서 꼭 알아둘 점

1) AspNetUsers를 직접 SQL로 업데이트할 때는 주의

단순히 SQL로 AspNetUsers를 직접 수정하면서 ConcurrencyStamp 갱신 흐름을 무시하면, Identity가 기대하는 동시성 체크와 어긋날 수 있습니다.

예를 들어 아래처럼 직접 SQL을 실행하는 패턴은 조심해야 합니다.

UPDATE AspNetUsers
SET Email = 'newadmin@test.com'
WHERE Id = '사용자ID';

이런 방식은 빠르게 보일 수 있지만, 애플리케이션이 기대하는 동시성 처리 흐름을 우회할 수 있습니다.

2) 사용자 수정은 가능하면 UserManager를 사용

사용자/역할 수정 로직은 UserManager, RoleManager 같은 Identity API를 우선 사용하는 편이 안전합니다.

3) 충돌은 “오류”가 아니라 “정상적인 경쟁 상황”으로 봐야 함

동시성 충돌은 시스템이 고장 났다는 뜻이 아니라, 서로 다른 요청이 같은 데이터를 동시에 바꾸려 했다는 신호입니다. 따라서 서비스에서는 아래 중 하나로 대응하면 됩니다.

  • 최신 데이터 다시 조회 후 재시도
  • 사용자에게 “다른 곳에서 먼저 수정되었습니다” 안내
  • 필요한 필드만 다시 병합해서 저장

정리

ConcurrencyStamp는 MSSQL의 특수 기능이 아니라, ASP.NET Core Identity가 사용자/역할 데이터의 동시성 충돌을 감지하기 위해 사용하는 값입니다.

한 줄로 정리하면 이렇습니다.

ConcurrencyStamp는 “이 사용자를 내가 읽어 온 뒤, 다른 누군가가 먼저 수정했는지”를 확인하기 위한 버전 체크 값이다.

Identity를 사용하는 프로젝트에서 AspNetUsersConcurrencyStamp를 발견했다면, 삭제 대상이나 의미 없는 컬럼으로 보기보다 동시성 제어를 담당하는 중요한 메타데이터로 이해하는 것이 맞습니다.

특히 관리자 페이지, 사용자 정보 수정 API, 권한 관리 화면을 만들고 있다면 이 개념을 알고 있는 것만으로도 예기치 않은 덮어쓰기 문제를 훨씬 줄일 수 있습니다.


마무리 한 문장

ConcurrencyStamp사용자 데이터의 버전표, SecurityStamp보안 상태의 변경표라고 기억하면 대부분의 상황에서 헷갈리지 않습니다.

반응형