Login Service
GRPC 기반 사용자 인증 및 로그인 시스템
2025.01 | 개발 기간 2주
기술 스택
프로젝트 개요
이 프로젝트는 멀티플레이 게임을 위한 안전하고 빠른 인증 시스템을 구축하는 것이 목표였습니다. 특히 한 계정으로 여러 기기에서 동시 접속하는 중복 로그인을 실시간으로 차단해야 했습니다.
Unity 클라이언트와 ASP.NET Core 백엔드 간의 안전한 통신을 위해 JWT 기반 인증을 구현했고, 메모리 기반 세션 관리로 실시간 중복 로그인 차단과 높은 성능을 동시에 달성했습니다.
해결해야 할 문제
멀티플레이 게임의 요구사항
멀티플레이 게임에서는 게임 밸런스와 공정성을 위해 한 계정당 하나의 기기에서만 로그인할 수 있어야 합니다. 계정 공유를 차단하고 실시간으로 세션을 관리해야 했습니다.
- 중복 로그인 방지 필수
- 실시간 세션 관리
- 계정 공유 차단
- 빠른 응답 속도 (50ms 이하)
보안 요구사항
- 안전한 인증 토큰 필요 (JWT)
- 비밀번호 평문 저장 금지
- HTTPS 통신 필수
- 토큰 만료 및 검증
핵심 과제
데이터베이스 부하를 최소화하면서 실시간으로 중복 로그인을 차단하고, 동시 접속자 1000명 이상을 안정적으로 처리할 수 있는 시스템 구축
개발 과정 - 2번의 시도
최적의 솔루션을 찾기까지 2번의 시도와 개선 과정을 거쳤습니다.
DB Flag Approach (IsLoggedIn)
실패접근 방법:
- User 테이블에 IsLoggedIn bool 컬럼 추가
- 로그인 시 true, 로그아웃 시 false로 업데이트
- 로그인 시도마다 DB에서 상태 확인
문제점:
- 비정상 종료 처리 불가: 앱 강제 종료, 네트워크 끊김 시 DB에 true가 영구적으로 남아 재로그인 불가
- DB 부하 문제: 로그인마다 UPDATE/SELECT 쿼리 실행으로 동시 접속 1000명 시 병목 현상
- 동기화 문제: 여러 서버 환경에서 DB 업데이트 지연으로 Race Condition 발생 가능
배운 점:
DB 상태는 '과거'의 기록일 뿐, 실시간 세션 관리에는 '현재' 상태를 반영하는 메모리 기반 솔루션이 필요합니다.
Memory Session Approach
성공접근 방법:
- ConcurrentDictionary로 메모리 기반 세션 관리
- sessionId를 Key로 User 객체 저장
- 로그인 시 Dictionary 확인 후 중복 체크
- 로그아웃 시 Dictionary에서 제거
개선점:
- 실시간 중복 로그인 방지: 메모리 접근으로 1ms 이내 체크
- DB 쿼리 90% 감소: 세션 체크는 메모리에서만 처리
- 응답 속도 3배 향상: 50ms 이하로 로그인 처리
- Thread-safe: ConcurrentDictionary로 동시성 보장
핵심 개념:
- static 키워드로 컨트롤러 인스턴스 간 공유
- ConcurrentDictionary로 동시 접근 안전성 확보
- Guid 기반 sessionId로 유니크 식별
- 서버 재시작 시 자동 초기화 (장점으로 작용)
최종 솔루션
메모리 기반 세션 관리와 JWT 인증을 결합한 Layered Architecture로 안전하고 빠른 인증 시스템을 완성했습니다.
핵심 기술 4가지
1. Memory Session Management
ConcurrentDictionary를 사용한 메모리 기반 세션 관리로 실시간 중복 로그인을 차단합니다. static 키워드로 모든 컨트롤러 인스턴스가 같은 세션 저장소를 공유하며, Thread-safe한 작업이 보장됩니다. 메모리 접근만으로 1ms 이내에 중복 체크가 가능합니다.
2. JWT Authentication
JSON Web Token을 사용한 Stateless 인증으로 서버 확장성을 확보했습니다. 256bit HMAC SHA256 서명으로 토큰 위조를 방지하고, Claims에 userId와 sessionId를 포함하여 사용자 식별과 세션 검증을 동시에 수행합니다. 토큰 만료 시간은 1시간으로 설정했습니다.
3. Repository Pattern
IUserRepository 인터페이스로 데이터 접근 로직을 추상화하여 테스트 용이성과 유지보수성을 향상시켰습니다. Dependency Injection으로 느슨한 결합을 유지하며, 비동기 메서드(async/await)로 높은 처리량을 달성했습니다.
4. Entity Framework Core + PostgreSQL
Code First 방식으로 데이터베이스 스키마를 관리하고, EF Core Migrations로 자동 동기화합니다. PostgreSQL의 안정성과 성능을 활용하며, Fluent API로 제약 조건과 인덱스를 정의합니다. Username과 Nickname에 Unique Index를 적용하여 중복 방지를 보장합니다.
핵심 알고리즘 코드
Memory Session 관리
// Memory Session Storage (static)
private static readonly ConcurrentDictionary<Guid, User> _loginSessions
= new ConcurrentDictionary<Guid, User>();
// Login 처리
public async Task<IActionResult> Login([FromBody] LoginDto dto)
{
// 1. 사용자 인증
var user = await _userRepository.GetByUserNameAsync(dto.id);
if (user == null || user.Password != dto.password)
return Unauthorized("Invalid credentials");
// 2. 중복 로그인 체크 (메모리에서 즉시 확인)
var alreadyLoggedIn = _loginSessions.Values
.Any(u => u.Username == dto.id);
if (alreadyLoggedIn)
return Conflict("Already logged in from another device");
// 3. 세션 생성
var sessionId = Guid.NewGuid();
_loginSessions.TryAdd(sessionId, user);
// 4. JWT 토큰 생성
var jwt = JwtUtils.Generate(
user.Id.ToString(),
sessionId.ToString(),
TimeSpan.FromHours(1)
);
return Ok(new { jwt, userId = user.Id, nickname = user.Nickname });
}
// Logout 처리
public IActionResult Logout([FromHeader] string sessionId)
{
if (Guid.TryParse(sessionId, out var guid))
{
_loginSessions.TryRemove(guid, out _);
return Ok();
}
return BadRequest();
}JWT 토큰 생성
public static string Generate(string userId, string sessionId, TimeSpan lifetime)
{
var securityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_secretKey)
);
var credentials = new SigningCredentials(
securityKey,
SecurityAlgorithms.HmacSha256
);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userId),
new Claim("session_id", sessionId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken(
issuer: "LoginService",
audience: "UnityClient",
claims: claims,
expires: DateTime.UtcNow.Add(lifetime),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}시스템 아키텍처
Layered Architecture Pattern을 적용하여 관심사를 분리하고 유지보수성을 높였습니다.
- Presentation Layer: AuthController, UserController로 HTTP 요청/응답 처리, DTO를 통한 데이터 전송
- Security Layer: JwtUtils로 토큰 생성/검증, Memory Session으로 중복 로그인 차단
- Business Layer: IUserRepository 인터페이스로 비즈니스 로직과 데이터 접근 분리
- Data Layer: UserRepository 구현체, GameDbContext로 EF Core 연동, PostgreSQL 데이터베이스
핵심 성공 요인
메모리 기반 세션 관리와 JWT 인증의 결합이 핵심이었습니다. 실시간 성능(50ms 이하)과 보안성을 모두 달성할 수 있었습니다.
특히 ConcurrentDictionary의 Thread-safe 특성과 static 키워드를 활용한 인스턴스 간 공유 방식이 시스템 안정성을 크게 향상시켰습니다.
기술 상세 설명
JWT (JSON Web Token)
JWT는 세 부분으로 구성됩니다: Header, Payload, Signature. 각 부분은 Base64로 인코딩되며 점(.)으로 구분됩니다.
장점:
- Stateless 인증: 서버가 세션 정보를 저장할 필요 없음
- 확장성: 여러 서버 환경에서 동일한 토큰 사용 가능
- CSRF 공격 방어: 쿠키가 아닌 Authorization 헤더 사용
- 서명 검증: Secret Key로 토큰 위조 방지
토큰 검증 과정
public static ClaimsPrincipal? Validate(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_secretKey);
try
{
var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = "LoginService",
ValidateAudience = true,
ValidAudience = "UnityClient",
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5) // 시간 오차 허용
}, out var validatedToken);
return principal;
}
catch
{
return null;
}
}ConcurrentDictionary의 Thread-Safety
C#의 ConcurrentDictionary는 멀티스레드 환경에서 안전하게 동작하는 Dictionary입니다. 내부적으로 Lock-Free 알고리즘을 사용하여 높은 성능을 제공합니다.
- TryAdd: Key가 없을 때만 추가, 원자적 연산
- TryRemove: Key를 제거하고 값을 반환, 원자적 연산
- Values: 현재 저장된 모든 값에 안전하게 접근
- Lock-Free: 전통적인 lock 없이 동시성 제어
Entity Framework Core Migrations
Code First 방식으로 C# 클래스를 먼저 정의하고, Migrations를 통해 데이터베이스 스키마를 자동 생성합니다.
// User Entity
public class User
{
public Guid Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string? Nickname { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastConnected { get; set; }
public int SkinIndex { get; set; }
}
// DbContext Configuration
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Username).IsRequired();
entity.Property(e => e.Password).IsRequired();
entity.Property(e => e.Nickname).HasMaxLength(12);
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("CURRENT_TIMESTAMP");
// Unique Indexes
entity.HasIndex(e => e.Username).IsUnique();
entity.HasIndex(e => e.Nickname).IsUnique();
});
}Repository Pattern의 장점
Repository Pattern은 데이터 접근 로직을 추상화하여 여러 장점을 제공합니다.
- 테스트 용이성: Mock 객체로 쉽게 단위 테스트 작성
- DB 교체 용이: PostgreSQL에서 다른 DB로 변경 시 Repository 구현체만 교체
- 비즈니스 로직 분리: Controller는 비즈니스 로직에만 집중
- 재사용성: 여러 Controller에서 같은 Repository 사용
프로젝트 성과
50ms
로그인 응답 속도
DB Flag 방식 대비 3배 향상
100%
중복 로그인 차단
실시간 메모리 기반 세션 관리
1000+
동시 접속 지원
안정적인 성능 유지
성능 비교
| 시도 | 방법 | 응답 속도 | DB 쿼리 | 안정성 |
|---|---|---|---|---|
| Attempt #1 | DB Flag | 150ms | 많음 | 60% |
| Final | Memory Session | 50ms | 90% 감소 | 100% |
핵심 배움
- 실시간 상태는 메모리가 최선: DB는 과거의 기록이지 현재 상태를 반영하기에는 지연이 발생합니다.
- Architecture Pattern의 중요성: Layered Architecture와 Repository Pattern으로 유지보수성과 테스트 용이성을 크게 향상시켰습니다.
- JWT의 Stateless 특성: 서버 확장성을 고려한 인증 방식의 선택이 중요합니다.
- 동시성 제어: ConcurrentDictionary 같은 Thread-safe 자료구조의 올바른 사용이 시스템 안정성의 핵심입니다.
향후 계획
- Redis 도입: 다중 서버 환경에서 세션을 공유하기 위해 Redis 같은 분산 캐시 시스템 도입
- bcrypt 비밀번호 해싱: 현재 평문 저장된 비밀번호를 bcrypt로 안전하게 해싱 처리
- Refresh Token 구현: Access Token 만료 시 재로그인 없이 갱신할 수 있는 Refresh Token 메커니즘 추가
- Rate Limiting: DDoS 공격 방어를 위한 요청 속도 제한 구현
- 로그 시스템: Serilog를 활용한 구조화된 로깅 시스템 구축