language/blazor

[Blazor Web App] 10. UserInfo 생성 책임을 LoginService로 옮기기

렁치 2026. 8. 7. 21:32

지난 작업에서는 UserInfo 모델을 만들고, 로그인한 사용자 정보를 LoginStateService에 저장하도록 바꿨다.

처음에는 이 구조도 괜찮아 보였다.

LoginStateService
 ├─ IsLoggedIn
 ├─ CurrentUser
 └─ StatusMessage

그리고 로그인 성공 시 LoginStateService.Login(userId) 안에서 임시 사용자 정보를 만들었다.

public void Login(string userId)
{
    UserInfo userInfo;

    userInfo = new UserInfo();

    userInfo.UserId = userId;
    userInfo.UserName = "테스트 사용자";
    userInfo.UserGrade = "테스트 회원";

    IsLoggedIn = true;
    CurrentUser = userInfo;
    StatusMessage = "";
}

테스트도 정상적으로 됐다.

회원 페이지에서는 CurrentUser를 통해 사용자 정보를 출력했다.

<p>아이디: @LoginStateService.CurrentUser.UserId</p>
<p>이름: @LoginStateService.CurrentUser.UserName</p>
<p>회원 등급: @LoginStateService.CurrentUser.UserGrade</p>

그런데 코드를 다시 보니 역할이 조금 애매해 보였다.

LoginStateService가 로그인 상태를 저장하는 건 자연스럽다.
그런데 UserInfo를 직접 만드는 것도 LoginStateService의 역할일까?

이 질문에서 이번 리팩토링이 시작됐다.

 


LoginStateService의 역할을 다시 생각했다

LoginStateService는 이름 그대로 로그인 상태를 관리하는 서비스다.

지금 이 서비스가 들고 있는 값은 이런 것들이다.

public bool IsLoggedIn { get; private set; }
public UserInfo CurrentUser { get; private set; } = new();
public string StatusMessage { get; private set; } = "";

이 값들은 상태에 가깝다.

현재 로그인 상태인가?
현재 로그인한 사용자는 누구인가?
로그아웃 후 보여줄 메시지가 있는가?

LoginStateService는 다음 역할에 집중하는 게 자연스럽다.

로그인 상태 저장
현재 사용자 저장
상태 메시지 저장

반면 사용자 정보를 만드는 일은 조금 다르다.

아이디와 비밀번호가 맞는지 확인
로그인 성공 여부 판단
성공한 사용자의 정보 생성

이 일은 LoginService 쪽에 더 가깝다.

그래서 역할을 이렇게 바꾸기로 했다.

기존:
LoginStateService가 UserInfo 생성

변경:
LoginService가 UserInfo 생성
LoginResult에 담아 반환
LoginStateService는 전달받은 UserInfo를 저장

LoginResult에 UserInfo 추가하기

먼저 로그인 결과 모델인 LoginResult를 수정했다.

기존 LoginResult는 성공 여부와 메시지만 가지고 있었다.

namespace Study.Web.Models
{
    public class LoginResult
    {
        public bool IsSuccess { get; set; }
        public string Message { get; set; } = "";
    }
}

이제 로그인 성공 시 사용자 정보도 같이 반환해야 하므로 UserInfo를 추가했다.

namespace Study.Web.Models
{
    public class LoginResult
    {
        public bool IsSuccess { get; set; }
        public string Message { get; set; } = "";
        public UserInfo UserInfo { get; set; } = new();
    }
}

이제 LoginResult는 세 가지를 담는다.

IsSuccess
→ 로그인 성공 여부

Message
→ 로그인 결과 메시지

UserInfo
→ 로그인 성공 시 사용자 정보

실패했을 때는 UserInfo가 빈 객체 상태로 남아 있어도 괜찮다.

실패 경로에서는 UserInfo를 사용하지 않기 때문이다.


LoginService에서 UserInfo 만들기

이제 LoginService에서 로그인 성공 시 UserInfo를 만든다.

수정 후 코드는 이런 형태다.

using Study.Web.Models;

namespace Study.Web.Services
{
    public class LoginService
    {
        private readonly string testUserId = "admin";
        private readonly string testPassword = "1234";

        public LoginResult Login(LoginRequest loginRequest)
        {
            LoginResult loginResult;
            UserInfo userInfo;

            bool isMatchedUserId;
            bool isMatchedPassword;

            isMatchedUserId = false;
            isMatchedPassword = false;

            loginResult = new LoginResult();
            userInfo = new UserInfo();

            if (loginRequest == null)
            {
                loginResult.IsSuccess = false;
                loginResult.Message = "로그인 요청 정보가 없습니다.";
                return loginResult;
            }

            if (string.IsNullOrWhiteSpace(loginRequest.UserId))
            {
                loginResult.IsSuccess = false;
                loginResult.Message = "아이디를 입력하세요.";
                return loginResult;
            }

            if (string.IsNullOrWhiteSpace(loginRequest.Password))
            {
                loginResult.IsSuccess = false;
                loginResult.Message = "비밀번호를 입력하세요.";
                return loginResult;
            }

            isMatchedUserId = loginRequest.UserId == testUserId;
            isMatchedPassword = loginRequest.Password == testPassword;

            if (isMatchedUserId == false)
            {
                loginResult.IsSuccess = false;
                loginResult.Message = "아이디가 올바르지 않습니다.";
                return loginResult;
            }

            if (isMatchedPassword == false)
            {
                loginResult.IsSuccess = false;
                loginResult.Message = "비밀번호가 올바르지 않습니다.";
                return loginResult;
            }

            userInfo.UserId = loginRequest.UserId;
            userInfo.UserName = "테스트 사용자";
            userInfo.UserGrade = "테스트 회원";

            loginResult.IsSuccess = true;
            loginResult.Message = $"{loginRequest.UserId}님 환영합니다. 로그인 성공.";
            loginResult.UserInfo = userInfo;

            return loginResult;
        }
    }
}

핵심은 이 부분이다.

userInfo.UserId = loginRequest.UserId;
userInfo.UserName = "테스트 사용자";
userInfo.UserGrade = "테스트 회원";

loginResult.UserInfo = userInfo;

지금은 테스트용 값을 직접 넣고 있지만, 나중에 DB를 붙이면 이 부분이 바뀔 것이다.

현재:
코드에서 테스트 사용자 정보 생성

나중:
DB에서 사용자 정보 조회

즉, 지금 구조는 나중에 DB로 자연스럽게 이어질 수 있다.


LoginStateService는 저장만 하게 바꾸기

이제 LoginStateServiceUserInfo를 직접 만들지 않는다.

기존에는 string userId를 받았다.

public void Login(string userId)
{
    UserInfo userInfo;

    userInfo = new UserInfo();

    userInfo.UserId = userId;
    userInfo.UserName = "테스트 사용자";
    userInfo.UserGrade = "테스트 회원";

    IsLoggedIn = true;
    CurrentUser = userInfo;
    StatusMessage = "";
}

변경 후에는 이미 만들어진 UserInfo를 받는다.

using Study.Web.Models;

namespace Study.Web.Services
{
    // 로그인 성공 시 상태 저장
    public class LoginStateService
    {
        public bool IsLoggedIn { get; private set; }
        public UserInfo CurrentUser { get; private set; } = new();
        public string StatusMessage { get; private set; } = "";

        public void Login(UserInfo userInfo)
        {
            IsLoggedIn = true;
            CurrentUser = userInfo;
            StatusMessage = "";
        }

        public void Logout()
        {
            IsLoggedIn = false;
            CurrentUser = new UserInfo();
            StatusMessage = "로그아웃되었습니다.";
        }

        public void ClearStatusMessage()
        {
            StatusMessage = "";
        }
    }
}

이제 역할이 훨씬 단순해졌다.

LoginStateService
→ 전달받은 UserInfo를 CurrentUser에 저장
→ IsLoggedIn 상태 변경
→ StatusMessage 정리

사용자 정보를 만드는 책임은 사라졌다.


Home.razor에서 LoginResult.UserInfo 전달하기

이제 Home.razor도 수정해야 한다.

기존에는 로그인 성공 시 loginRequest.UserId를 넘겼다.

LoginStateService.Login(loginRequest.UserId);

이제는 LoginService가 만들어준 사용자 정보를 넘긴다.

LoginStateService.Login(loginResult.UserInfo);

수정 후 Login() 함수는 이런 형태가 됐다.

private void Login()
{
    LoginResult loginResult;

    loginResult = LoginService.Login(loginRequest);

    if (loginResult.IsSuccess)
    {
        LoginStateService.Login(loginResult.UserInfo);

        NavigationManager.NavigateTo("/member");

        return;
    }

    loginMessage = loginResult.Message;
    messageClass = "app-message error-message";
}

이 흐름이 더 자연스럽다.

Home.razor
→ LoginService.Login(loginRequest)
→ LoginResult 받음
→ 성공하면 LoginResult.UserInfo를 LoginStateService에 저장
→ /member 이동

Home.razor는 사용자 정보를 직접 만들지 않는다.

그저 로그인 결과에 담긴 정보를 상태 서비스에 넘긴다.


Member.razor는 그대로 둬도 됐다

Member.razor는 수정할 필요가 없었다.

회원 정보 출력은 이미 LoginStateService.CurrentUser를 보고 있었기 때문이다.

<p>아이디: @LoginStateService.CurrentUser.UserId</p>
<p>이름: @LoginStateService.CurrentUser.UserName</p>
<p>회원 등급: @LoginStateService.CurrentUser.UserGrade</p>
<p>접속상태: 로그인 완료</p>

바뀐 것은 화면 출력 방식이 아니라, CurrentUser가 만들어지는 위치다.

기존:
LoginStateService가 UserInfo 생성 후 CurrentUser에 저장

변경:
LoginService가 UserInfo 생성
LoginResult에 담아 반환
LoginStateService가 CurrentUser로 저장

이런 변화는 화면에서 바로 티가 나지는 않는다.

하지만 내부 책임은 더 명확해졌다.


CurrentUser와 userInfo가 헷갈렸다

이번 작업을 하면서 변수 이름도 헷갈렸다.

public UserInfo CurrentUser { get; private set; } = new();

public void Login(UserInfo userInfo)
{
    CurrentUser = userInfo;
}

처음에는 CurrentUseruserInfo가 둘 다 사용자 정보인데 왜 이름이 다른지 헷갈렸다.

정리하면 이렇다.

CurrentUser
→ LoginStateService가 계속 들고 있는 현재 로그인 사용자 정보
→ 프로퍼티

userInfo
→ Login() 메서드로 잠깐 들어온 사용자 정보
→ 매개변수

즉 둘 다 UserInfo 타입이지만 역할이 다르다.

userInfo
→ 외부에서 전달받은 값

CurrentUser
→ 서비스 내부에 저장해두는 현재 상태

그래서 아래 코드는 “전달받은 사용자 정보를 현재 사용자 상태로 저장한다”는 뜻이다.

CurrentUser = userInfo;

필드, 프로퍼티, 지역 변수도 다시 정리했다

이번에 대문자와 소문자 기준도 다시 헷갈렸다.

C#에서는 대체로 이런 규칙을 따른다.

클래스
→ PascalCase
→ LoginService, UserInfo

메서드
→ PascalCase
→ Login, Logout

public 프로퍼티
→ PascalCase
→ IsLoggedIn, CurrentUser, UserInfo

지역 변수
→ camelCase
→ loginResult, userInfo

매개변수
→ camelCase
→ loginRequest, userInfo

private 필드
→ camelCase 또는 _camelCase
→ testUserId 또는 _testUserId

C++, Java에서는 클래스 안의 멤버 변수를 보통 소문자로 시작하는 경우가 많았다.

C#도 private 필드는 소문자로 시작할 수 있다.

private readonly string testUserId = "admin";

하지만 public 프로퍼티는 보통 대문자로 시작한다.

public string UserId { get; set; } = "";

그래서 처음에 헷갈렸던 것 같다.

C#에서는 특히 필드와 프로퍼티를 구분해서 보는 게 중요하다.

필드
→ 클래스 내부에서 직접 쓰는 저장 변수
→ 보통 private

프로퍼티
→ 외부에서 읽거나 쓰게 하는 값 접근 통로
→ 보통 public

예를 들어 LoginService의 테스트 계정은 내부 비교용 값이므로 필드다.

private readonly string testUserId = "admin";
private readonly string testPassword = "1234";

반면 UserInfo의 값들은 다른 클래스와 화면에서 읽어야 하므로 프로퍼티다.

public string UserId { get; set; } = "";
public string UserName { get; set; } = "";
public string UserGrade { get; set; } = "";

주석 위치도 조금 정리했다

중간에 이런 코드가 있었다.

namespace Study.Web.Services
// 로그인 성공 시 상태 저장
{
    public class LoginStateService
    {
    }
}

동작에는 문제가 없지만, 주석 위치가 어색했다.

그래서 아래처럼 namespace 안쪽, class 바로 위로 옮기는 편이 자연스럽다고 느꼈다.

namespace Study.Web.Services
{
    // 로그인 성공 시 상태 저장
    public class LoginStateService
    {
    }
}

그리고 이전 방식으로 비교하던 주석 처리 코드도 남아 있었다.

/*
if (loginRequest.UserId == testUserId)
{
    isMatchedUserId = true;
}
if (loginRequest.Password == testPassword)
{
    isMatchedPassword = true;
}
*/

이미 아래 코드로 대체됐기 때문에 삭제하는 게 맞다고 판단했다.

isMatchedUserId = loginRequest.UserId == testUserId;
isMatchedPassword = loginRequest.Password == testPassword;

Git으로 기록이 남기 때문에, 더 이상 필요 없는 주석 처리 코드는 남겨두지 않는 게 좋다.


테스트 결과

수정 후 기능 테스트를 다시 했다.

1. / 접속
→ 로그인 화면 표시

2. 아이디 비움
→ 아이디를 입력하세요.

3. 비밀번호 비움
→ 비밀번호를 입력하세요.

4. 아이디 또는 비밀번호 틀림
→ 오류 메시지 표시

5. admin / 1234 로그인
→ /member 이동

6. 회원 정보 보기 클릭
→ 아이디, 이름, 회원 등급 정상 표시

7. 로그아웃 클릭
→ / 이동

8. 로그아웃되었습니다. 메시지 표시

9. /member 직접 입력
→ / 로 이동

겉보기 기능은 기존과 같다.

하지만 내부 구조는 바뀌었다.

기존:
LoginStateService가 UserInfo 생성

변경:
LoginService가 UserInfo 생성
LoginResult가 UserInfo 전달
LoginStateService는 저장만 담당

커밋 메시지

이번 작업은 기능 추가라기보다는 구조 정리다.

그래서 커밋 메시지는 feat보다 refactor가 더 어울린다.

git add Skyz.Web/Models/LoginResult.cs `
        Skyz.Web/Services/LoginService.cs `
        Skyz.Web/Services/LoginStateService.cs `
        Skyz.Web/Components/Pages/Home.razor

git commit -m "refactor: return user info from login result"

git push

의미는 이렇다.

로그인 결과가 사용자 정보를 반환하도록 구조를 정리했다.

이번 작업 결과

이번 리팩토링으로 역할이 더 분명해졌다.

LoginService
→ 로그인 검증
→ 성공 시 UserInfo 생성
→ LoginResult 반환

LoginResult
→ 성공 여부
→ 메시지
→ 사용자 정보

Home.razor
→ LoginResult.UserInfo를 LoginStateService에 전달

LoginStateService
→ 로그인 상태 저장
→ CurrentUser 저장
→ StatusMessage 관리

Member.razor
→ CurrentUser를 화면에 출력

이 구조는 나중에 DB로 넘어가기에도 더 좋다.

지금은 LoginService 안에서 테스트 사용자 정보를 직접 만들고 있지만, 나중에는 이 부분을 DB 조회로 바꾸면 된다.

현재:
userInfo.UserName = "테스트 사용자";
userInfo.UserGrade = "테스트 회원";

나중:
DB에서 사용자 조회
→ UserInfo에 담기
→ LoginResult.UserInfo로 반환

오늘 배운 점

  • 상태를 저장하는 서비스와 로그인 결과를 만드는 서비스는 역할이 다르다.
  • LoginStateService는 상태 저장에 집중하는 편이 자연스럽다.
  • LoginService는 로그인 검증과 성공 결과 생성을 담당하는 편이 자연스럽다.
  • LoginResultUserInfo를 담으면 로그인 성공 결과를 더 풍부하게 표현할 수 있다.
  • 화면이 바뀌지 않아도 내부 책임 구조가 좋아지는 리팩토링이 있다.
  • CurrentUser는 서비스가 보관하는 현재 사용자 상태이고, userInfo는 메서드로 전달된 매개변수다.
  • C#에서는 public 프로퍼티는 대문자로 시작하고, 지역 변수와 매개변수는 소문자로 시작하는 경우가 일반적이다.
  • 필드는 클래스 내부 저장 변수이고, 프로퍼티는 외부에 공개되는 값 접근 통로에 가깝다.
  • 필요 없어진 주석 처리 코드는 Git 기록을 믿고 삭제하는 편이 좋다.
  • 기능 추가가 아니라 구조 정리라면 커밋 메시지에 refactor를 쓰는 것이 자연스럽다.

다음 작업

다음에는 사용자 정보를 더 현실적인 테스트 데이터 구조로 분리해볼 수 있을 것 같다.

지금은 LoginService 안에 테스트 사용자 정보가 직접 들어가 있다.

userInfo.UserName = "테스트 사용자";
userInfo.UserGrade = "테스트 회원";

다음 단계에서는 테스트 사용자 목록을 별도 구조로 분리하거나, DB 연결 전에 임시 사용자 저장소를 만들어볼 수 있을 것 같다.

예상 흐름은 이렇다.

LoginService 내부 하드코딩
↓
테스트 사용자 목록 분리
↓
아이디로 사용자 조회
↓
비밀번호 확인
↓
UserInfo 반환

이렇게 하면 실제 DB로 넘어가기 전, Repository나 임시 저장소 구조를 가볍게 연습할 수 있을 것 같다.