카테고리 없음

asp.net core를 활용하여 모바일 게임 만들기 (5일차)

susot 2026. 7. 27. 20:30

이전 시간에 Validation까지 배웠었습니다.

 

Service

 

지금까지 코드를 짜면서 모든 기능을 Controller안에 넣다보니 기능이 과하게 몰리는 문제가 있었습니다.

그래서 Service 계층을 만들어서 역할 분리를 하겠습니다.

 

Services 폴더를 새로 만들어줍니다.

using GameServer.Contracts;

namespace GameServer.Services;

public interface IPlayerService
{
    PlayerSummaryResponse? GetById(int playerId);
    PlayerSummaryResponse? TryCreate(string nickname);
}

IPlayerService 인터페이스를 만듭니다.

 

using GameServer.Contracts;

namespace GameServer.Services;

public class PlayerService : IPlayerService
{
    private readonly object _syncRoot = new();

    private readonly List<PlayerSummaryResponse> _players =
        new List<PlayerSummaryResponse>
        {
            new PlayerSummaryResponse
            {
                PlayerId = 1,
                Nickname = "PlayerOne",
                Level = 5
            },
            new PlayerSummaryResponse
            {
                PlayerId = 2,
                Nickname = "PlayerTwo",
                Level = 12
            },
            new PlayerSummaryResponse
            {
                PlayerId = 3,
                Nickname = "PlayerThree",
                Level = 8
            }
        };

    private int _nextPlayerId = 4;

    public PlayerSummaryResponse? GetById(int playerId)
    {
        lock (_syncRoot)
        {
            return _players.Find(
                candidate => candidate.PlayerId == playerId);
        }
    }

    public PlayerSummaryResponse? TryCreate(string nickname)
    {
        lock (_syncRoot)
        {
            bool nicknameExists = _players.Any(player =>
                string.Equals(
                    player.Nickname,
                    nickname,
                    StringComparison.OrdinalIgnoreCase));

            if (nicknameExists)
            {
                return null;
            }

            var player = new PlayerSummaryResponse
            {
                PlayerId = _nextPlayerId,
                Nickname = nickname,
                Level = 1
            };

            _nextPlayerId++;

            _players.Add(player);

            return player;
        }
    }
}

IPlayerService를 상속받는 PlayerService도 만든 후, Controller에 있던 내용을 Service로 옮겨줍니다.

 

코드를 보면 

private readonly object _syncRoot = new();
lock (_syncRoot)
{
    ...
}

이런 코드가 있는걸 볼 수 있습니다. 이는 동일한 요청이 동시에 들어왔을때 제어하기 위한 용도입니다.

예를 들어 SUSOT이라는 이름으로 새로운 플레이어 생성 요청이 동시에 들어오면 SUSOT이라는 이름을 가진 플레이어가 둘이 될 수 있습니다. 이를 방지하기 위한 코드입니다.

_syncRoot는 여러 요청이 동시에 들어오는걸 제어하기 위한 용도입니다. lock은 현재 요청이 진행중이라면 끝날때까지 다른 요청을 실행하지 않고 기다리는 역할입니다.

 

DI

 

이제 컨트롤러가 서비스를 의존하는 구조가 생겼습니다. 하지만 컨트롤러가 서비스를 직접 생성해서 요청을 보내면 컨트롤러는 서비스를 생성도 하고 사용도 하기 때문에 결국 분리한 의미가 없어지게 됩니다. 이걸 해결하기 위해 DI (의존성 주입)이 필요합니다.

 

using GameServer.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

builder.Services.AddSingleton<IPlayerService, PlayerService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

 

다음과 같이 코드를 추가해줍니다.

builder.Services.AddSingleton<IPlayerService, PlayerService>();

이 코드에 역할은 누군가가 IPlayerService를 요청한다면 PlayerService 객체를 넘겨준다 라는 뜻입니다.

 

DI 컨테이너

asp.net core에는 내장된 DI 컨테이너가 있습니다.

 

DI 컨테이너에 Service를 등록할 때 Service 객체를 언제 만들고 얼마나 오래 유지할지 정할 수 있습니다.

이를 서비스 수명이라고 합니다.

수명 메소드 생성 기준
Transient AddTransient 요청될 때마다 새로운 객체 생성
Scoped AddScoped HTTP 요청 하나당 객체 하나 생성
Singleton AddSingleton 애플리케이션 전체에서 한 객체

 

이제 컨트롤러가 서비스를 주입받을 수 있도록 생성자 추가 및 서비스로 옮긴 기능 삭제를 하겠습니다.

 

using GameServer.Contracts;
using GameServer.Services;
using Microsoft.AspNetCore.Mvc;

namespace GameServer.Controllers;

[ApiController]
[Route("api/players")]
public sealed class PlayersController : ControllerBase
{
    private readonly IPlayerService _playerService;

    public PlayersController(IPlayerService playerService)
    {
        _playerService = playerService;
    }

    [HttpGet("{playerId}")]
    public ActionResult<PlayerSummaryResponse> GetById([FromRoute] int playerId)
    {
        if (playerId <= 0)
        {
            var error = new ApiErrorResponse
            {
                Code = "INVALID_PLAYER_ID",
                Message = "플레이어 ID는 1 이상이어야 합니다."
            };

            return BadRequest(error);
        }

        PlayerSummaryResponse? player = _playerService.GetById(playerId);

        if (player is null)
        {
            var error = new ApiErrorResponse
            {
                Code = "PLAYER_NOT_FOUND",
                Message = "플레이어를 찾을 수 없습니다."
            };

            return NotFound(error);
        }

        return Ok(player);
    }

    [HttpPost]
    public ActionResult<PlayerSummaryResponse> Create([FromBody] CreatePlayerRequest request)
    {
        PlayerSummaryResponse? player = _playerService.TryCreate(request.Nickname);

        if (player is null)
        {
            var error = new ApiErrorResponse
            {
                Code = "NICKNAME_ALREADY_EXISTS",
                Message = "이미 사용 중인 닉네임입니다."
            };

            return Conflict(error);
        }

        return CreatedAtAction(
            nameof(GetById),
            new { playerId = player.PlayerId },
            player);
    }
}

 

생성자에서 IPlayerService를 주입받고 플레이어 조회와 생성 역할을 서비스가 하도록 역할 분리가 되었습니다.

 

이제 테스트를 해보겠습니다.

 

요청들이 잘 처리되는 모습을 볼 수 있습니다. 예외 처리도 잘 작동이 되네요.