EF Core 디자인 도구로 Supabase의 테이블 관리하기

우선 Supabase 에 관해서는 아래의 글을 참고하시기 바랍니다.

영상에서는 매우 짧게 언급한 부분인데, Supabase는API 접근 뿐만 아니라, DB (Postgresql) 서버로 직접 접근도 허용하기 때문에, EF Core의 관리도구 DB를 관리를 할 수 있습니다.

이 글은 EF 도구를 이용해서 Supabase 테이블을 관리하는 방법을 보여줍니다.

  1. Supabase 프로젝트 생성

위 글의 영상을 참고해서 프로젝트를 생성합니다.
예제를 위해 아래와 같이 생성했습니다.

프로젝트를 생성할 때, 데이터 베이스 관리자 계정(postgres)의 비밀 번호를 잘 저장해 둡니다.

  1. 1 연결 문자열 저장.

대시보드 => 설정 => 데이터베이스 => 연결문자열 => .Net 을 선택해서, 연결 문자열을 복사해서 잘 저장해둡니다.

  1. DB 관리 용 프로젝트 생성.

클래스 라이브러리 프로젝트를 생성합니다.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="EntityConfiguraions\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="EFCore.NamingConventions" Version="7.0.2" />
    <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

</Project>

사용한 패키지는 아래와 같습니다.

  • EFCore.NamingConventions : DB 식별자를 스네이크 케이스로 만들기 위한 컨벤션 생성도구입니다.
  • Microsoft.EntityFrameworkCore.Design : 디자인을 위한 패키지입니다.
  • Npgsql.EntityFrameworkCore.PostgreSQL: DbContext 의 PostgreSQL 제공자입니다.

Supabase API를 사용하는 클라이언트 앱은 CRUD를 위해, DbContext(와 IQueryable) 대신, Supabase가 제공하는 API를 사용합니다. (이에 관해서는 API 문서를 확인하시기 바랍니다.)
이때, Supabase가 발급한 API key를 이용하게 되는데, 이 키는 보안 문제에 대해 좀 더 자유롭습니다.

이 프로젝트는 Supabase의 DB를 관리하기 위한 용도로, 클라이언트 앱과 무관해서 함께 배포되지 않습니다. 그렇기 때문에 이 프로젝트에는 시크릿(DB 암호 등)을 마구 써도 상관이 없습니다.

  1. 모델 클래스 생성.

Supabase는 사용자 관리를 auth 스키마의 user 테이블을 통해 자체적으로 관리하는데, 사용자는 이 테이블을 관리할 수 없습니다. 이는, EF 관리도구로 관리할 수 없음을 의미합니다.

따라서, 사용자 프로필을 위한 테이블을 public 스키마에 별도로 만들고, public 스키마의 다른 테이블들이 이 테이블을 마치 User 테이블로 참조하도록 만듭니다.

    public class UserProfile
    {
        public Guid Id { get; set; }
        public DateOnly? Birth { get; set; }
        public Gender? Gender { get; set; }
        public string? Address { get; set; }
        public string? Zipcode { get; set; }
        public string? City { get; set; }
        public string? Country { get; set; }
        public Guid UserId { get; set; }
    }

이 테이블과 auth.user 테이블의 관계는 EF 도구가 아닌, Supabase 대시 보드에서 수동으로 설정합니다. (잠시 후에 보여집니다)

  1. DbContext
    public class AppDbContext : DbContext
    {
        readonly string _dbConnectionString;

        public AppDbContext(string connectionString)
        {
            _dbConnectionString = connectionString;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseNpgsql(_dbConnectionString) // DB 제공자
                .UseSnakeCaseNamingConvention(); // 스네이크 케이스 컨벤션
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // PK에 UUID 생성 함수 사용.
            modelBuilder.HasPostgresExtension("uuid-ossp"); 

            // IEntityTypeConfiguration 구현한 모든 객체들의 테이블 생성.
            modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); 
        }
    }

이 객체의 생성자는 DbContext의 일반적인 형태와 달리, string 을 매개변수로 받도록 했습니다.
이는 아무 생각 없이 CRUD 용으로 사용했을 때, 에러를 유발하여 상기시키도록 만들기 위한 것입니다.

  1. 엔티티 타입 설정자
    internal class UserProfileConfiguration : IEntityTypeConfiguration<UserProfile>
    {
        public void Configure(EntityTypeBuilder<UserProfile> builder)
        {
            builder.Property(x => x.UserId)
                .IsRequired();
        }
    }

보통 데이터 모델에 특성을 부여하여 컬럼 속성을 지정하는 어노테이션 방식을 많이 사용하시는데, 저는 configuration 방식을 선호합니다.

보시다시피, UserId 속성은 관계 설정이 없는 그냥 데이터 컬럼에 지나지 않습니다. 따라서, EF 도구는 이 컬럼에 대해 별다른 관리를 하지 않습니다.

  1. 디자인 타임 팩토리

EF 디자인 도구는 디자인 타임에 DbContext를 생성할 때, IDesignTimeDbContextFactory<T> 가 있다면, 그 객체의 CreateDbContext를 호출합니다.

    public class DTFactoryAppDbContext : IDesignTimeDbContextFactory<AppDbContext>
    {
        public AppDbContext CreateDbContext(string[] args)
        {
            var cs = "User Id=postgres;Password=LE705uGXd8NnbZS7;Server=db.alpnbkjusdyyaobvvskf.supabase.co;Port=5432;Database=postgres";

            return new(cs);
        }
    }

앞서 저장한 데이터 베이스 비밀번호를 연결 문자열에 삽입하여 생성자에 주입했습니다.
앞서 언급했 듯이 이 프로젝트는 배포되는 프로젝트가 아니기에, 코드에 연결문자열을 직접 노출해도 상관이 없습니다.

  1. DB 관리

모든 준비는 끝났습니다. 이제 부터는 code-first 접근법으로 DB를 관리하면 됩니다.
예제를 위해, cli 툴로 마이그레이션을 생성하고, 데이터 베이스를 업데이트합니다.

dotnet ef migrations add Initial

dotnet ef database update

  1. 관계 수동 설정.

앞서 설명한 대로, auth.users 테이블과 public.user_profile의 관계는 ef 도구로 설정할 수 없기 때문에, Supabase 대시보드에서 이 둘의 관계를 맺어 줍니다.

모든 게 문제가 없다면, Supabase 에 public.user_profile 테이블이 생성되어 있을 것입니다.

user_id 컬럼의 제일 오른 쪽에 있는 편집 버튼을 누르고

“외래키 관계 추가” 버튼을 누르고,

아래와 같이 설정을 합니다.

보시다시피, 사용자가 탈퇴를 하면, 관련 프로필도 삭제되도록 Cascade delete을 설정했습니다.

수동으로 설정되는 부분은 이 곳이 전부입니다.

이제부터는 클라이언트 앱들은 public.user_profile 을 마치 user 처럼 사용하면 됩니다.

예제를 위해, Entity를 하나 더 추가합니다.

Entity 추가

프로젝트에 UserProfile 과 관계를 맺는 Destination 엔티티를 추가합니다.

    public class Destination
    {
        public Guid Id { get; set; }
        public string Alias { get; set; } = string.Empty;
        public string ConsgineeName { get; set; } = string.Empty;
        public string PhoneNumber { get; set; } = string.Empty;
        public string Address { get; set; } = string.Empty;
        public string Zipcode { get; set; } = string.Empty;
        public string City { get; set; } = string.Empty;
        public string Country { get; set; } = string.Empty;

        // Navigation
        public UserProfile UserProfile { get; set; } = new();
    }

관계를 Navigation 속성으로 표현했는데, 이로 인해 user_profile_id 컬럼이 테이블에 추가됩니다.
auth.users 의 Row 가 Delete 되었을 때, 프로필 테이블의 행위를 정의하기 위해, 설정 객체를 추가합니다.

    internal class DestinationConfiguration : IEntityTypeConfiguration<Destination>
    {
        public void Configure(EntityTypeBuilder<Destination> builder)
        {
            builder.HasOne(x => x.UserProfile)
                .WithMany()
                .IsRequired()
                .OnDelete(DeleteBehavior.Cascade);
        }
    }

다시 DB를 업데이트 합니다.

dotnet ef migrations add DestinationCreated

dotnet ef database update

대시보드를 통해 destination 테이블의 user_profile_id 의 외래키 관계를 확인해보면, 코드에서 설정한 대로 설정되었음을 알 수 있습니다.

결론

@honeyhead 님께서 말씀하셨듯, 완벽하지는 않지만 Supabase는 C# API 를 제공합니다.

데스크탑 앱, 모바일 앱을 위한 다양한 프레임워크가 존재하는 닷넷의 입장에서는, API 서비스를 쉽고 빠르게 구축할 수 있어, 큰 장점이 될 것 같습니다. 또한 무료로 사용할 수 있는 것도 큰 장점이고, 유료도 비싸지 않아 초기 운영비 절감에 많은 도움이 될 것 같습니다.

여기에 EF 도구를 이용하면, 데이터 베이스 관리가 더 안전하고 효율적이기 때문에, 다른 개발 플랫폼보다 사용 용이성이 더 크다고 할 수 있을 것 같습니다.

물론 부잣집 개발자 분들은 공감하시지 못하겠지만요. ^^

9개의 좋아요

무료티어의 스펙도 훌륭하네요. EF 까지 지원한다니 이거 너무 괜찮네요.

3개의 좋아요

그렇죠? ^^

아시다시피, 외부 DB를 배포되는 앱에서 직접 접근하는 것보다는 API를 통해 접근하는 것이 안전하기에, DB 관리만 EF를 쓰고, CRUD 는 Supabase API를 쓰는 것을 가정했습니다.

Supabase API 사용법을 알아야 하는 귀차니즘이 있지만, API 서버 꾸미고, 호스팅 관리하고 어쩌고 하는 것에 비할 바가 아니죠.

저도 이 참에 블레이저 서버에서 정적 호스팅이 가능한 웹어셈블리로 갈아 타는 중입니다.

1개의 좋아요