<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발블로그</title>
    <link>https://devman-hoon.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 05:18:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>atawlee</managingEditor>
    <item>
      <title>50 EF Core Interview Questions (1~25)</title>
      <link>https://devman-hoon.tistory.com/32</link>
      <description>&lt;p&gt;Linked In에서 본 50가지 Entity Framework Core에 질문 입니다.&lt;br&gt;&lt;a href=&quot;https://www.linkedin.com/posts/anton-martyniuk_dotnet-aspnetcore-efcore-activity-7294981068124741633-zBWi/&quot;&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;**전체 질문에 대한 한글 번역, 그리고 제 답안 입니다.&lt;/h2&gt;
&lt;p&gt;AI 제네레이트 하지 말고 답변해 보라고 했지만, 저도 모르던 부분이 있었고,&lt;br&gt;제가 알지 못했던 부분은 AI의 1차 답안을 가지고, 실제 테스트 해보거나 검색하여 정보를 추가적으로 확인했습니다. &lt;/p&gt;
&lt;h3&gt;&lt;em&gt;1. EF는 LINQ를 SQL 명령으로 어떻게 변환하나요?&lt;/em&gt;&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
표현식 트리(Expression Tree) 생성: LINQ 쿼리는 표현식 트리로 변환됩니다. 표현식 트리는 쿼리의 구조와 내용을 표현하는 데이터 구조로, 쿼리의 각 부분(예: Where, Select, Join 등)이 노드로 표현됩니다.&lt;/br&gt;
쿼리 변환 및 최적화: EF Core는 이 표현식 트리를 분석하여 해당 데이터베이스에 맞는 SQL 쿼리로 변환합니다. 이 과정에서 쿼리의 최적화도 수행되어 효율적인 SQL 문이 생성됩니다. 생성된 SQL문은 쿼리실행 프로시저의 매개변수로 전달되어 실행됩니다.
&lt;/details&gt;

&lt;h3&gt;2. DI에서 DbContext의 기본 수명 주기는 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
닷넷에서 요청단위인 Scoped 생명주기를 가지고 있습니다. 
&lt;/details&gt;

&lt;h3&gt;3. 싱글톤 서비스에서 Scoped DbContext를 어떻게 사용할 수 있나요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
위에서 설명했듯이 DbContext 자체는 Scoped 생명주기를 갖고있기 때문에 생성이후 프로세스가 유지되는 동안 한개의 인스턴스로 사용되는 싱글턴에서는 DbContext를 직접 사용 할 수 없고, IDbContextFactory&lt;T&gt;.CreateDbContext() 를 사용해서 싱글턴 서비스에서 메서드를 사용할때마다  생성하고 닫아야합니다. 
  &lt;/details&gt;


&lt;h3&gt;4. EF에서 풀링(Pooling)은 어떻게 작동하나요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
DbContextPool을 사용하면 컨텍스트 사용을 완료하더라도 연결을 유지했다가 다시 사용하는 방식으로 동작합니다.
DbContextPool 사용시 주의해야하는 점은 내부에서 캐시처리가 되므로 변경된 데이터가 나오지 않도록 신경써야 합니다. 
&lt;/details&gt;


&lt;h3&gt;5. Pooled DbContext를 생성하는 두 가지 방법은 무엇인가요?&lt;/h3&gt;
  &lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
services.AddDbContextPool&lt;TContext&gt;() &lt;/ br&gt;
services.AddPooledDbContextFactory&lt;TContext&gt;() &lt;/br&gt;
DbContextFactory도 내부적으로 DbContextPool을 사용하여 풀링 합니다.
  &lt;/details&gt;


&lt;h3&gt;6. 언제 DbContextFactory를 사용해야 하나요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    백그라운드 서비스를 사용할때 IHostedService 는 기본적으로 싱글턴으로 유지되는데 DbContext는 Scoped 이므로 싱글턴 인스턴스에서 DbContext를 부르는 경우 문제가 될수 있으므로 DbContextFactory를 사용하여 생성하는게 좋습니다.
&lt;/br&gt;
그외에도 멀티스레드, 병렬처리와 같은 작업에서 DbContext객체는 스레드세이프하지 않기 때문에 DbContextFactory를 사용해서 각각의 인스턴스를 생성해서 사용해야합니다. 
&lt;/details&gt;


&lt;h3&gt;7. 생성된 EF 마이그레이션을 어떻게 수정할 수 있나요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    dotnet ef migrations add '마이그레이션태그' 하게되면 마이그레이션시 실행될 코드가 마이그레이션 디렉토리에 생성됩니다. 해당 코드에는 마이그레이션 sql쿼리문도 포함되어있는데, 해당 쿼리문을 수정하면 됩니다. &lt;/br&gt;
    아에 마이그레이션 내용을 날리고싶다면 dotnet ef migrations remove 명령어를 쓰면됩니다.  &lt;/br&gt;
    이미 db까지 적용되었다면 dotnet ef database update '다운그레이드하고자하는마이그레이션버전명' 으로 마이그레이션 된 버전을 이동할 수 있습니다. 
    &lt;/details&gt;


&lt;h3&gt;8. 운영 데이터베이스에서 마이그레이션을 어떻게 실행하나요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    migrations add 까지 완료된 내용을 직접 실행보다 스크립트로 생성하는 dotnet ef migrations script -o migration.sql
 명령어가 있습니다. 이걸로 스크립트를 생성해서 해당 스크립트를 직접 확인하고 업데이트 하는 것이 좋습니다. ef tool update로 적용되는 것으로만 믿기 어려울 수 있고, 실제 Live Database 에서 문제되는 상황이 발생되면 치명적일 수 있기 때문에 검토후 적용하는 과정이 필요합니다. 
    &lt;/details&gt;


&lt;h3&gt;9. EF에서 초기 데이터를 데이터베이스에 시드(Seed)하는 방법은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    DbContext OnModelCreating에서 HasData()를 사용하여 엔터티 초기 데이터  생성 할 수 있습니다.
    &lt;/details&gt;


&lt;h3&gt;10. EF 9에서 데이터 시딩(Seeding)은 어떻게 변경되었나요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    OnModelCreating이 아닌 AddDbContext에서 처리가 가능해졌습니다. .UseSeeding을 사용해서 필요한 데이터가 없는경우 추가할수 있습니다. 
&lt;/details&gt;


&lt;h3&gt;11. EF를 사용하여 데이터베이스를 생성하는 방법은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    context.Database.EnsureCreated() 또는 dotnet ef database update 사용
    &lt;/details&gt;

&lt;h3&gt;12. EF Core에서 여러 데이터베이스에 대한 마이그레이션을 생성할 수 있나요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    DbContext는 단일 DB에 대해서만 처리하도록 되어있습니다.  별도의 DbContext를 사용하고, 마이그레이션시에는 DbContext를 명확히 지정하여 마이그레이션을 진행하도록 하면됩니다.
    &lt;/details&gt;

&lt;h3&gt;13. EF Core 인터셉터(Interceptor)는 무엇을 위해 사용되나요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    성능 모니터링 혹은 보안정책 적용을 위해서 사용 할 수 있습니다. 소프트 삭제와 같은 전역적 우회를 하는 방법에도 적용할 수 있습니다. 
    &lt;/details&gt;

&lt;h3&gt;14. EF에서 소프트 삭제(Soft Delete)를 구현하는 방법은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    SaveChanges, saveChangesAsync를 오버라이드하는 방법, 인터셉트를 사용해서 실제 delete가 아닌 isdeleted 컬럼에 true 값을 주는 방법. 
    &lt;/details&gt;


&lt;h3&gt;15. EF에서 여러 개의 새 엔터티를 생성하는 방법은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
        context.Users.AddRange([...,...,...,]);
        context.Set&lt;TEntity&gt;().AddRange([...,...,...,])
    &lt;/details&gt;


&lt;h3&gt;16. EF에서 기존 엔터티를 업데이트하는 방법은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
        기존 엔터티를 불러와서 수정후 savechanges 하는 방법 (엔터티 수정 시마다 셀렉문이 추가 발생 문제 있음)  ,
&lt;/details&gt;


&lt;h3&gt;17. EF에서 기존 엔터티를 삭제하는 방법은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
        기존 엔터티를 불러와서 db.entity.Remove(entityinstance);  savechanges(); 하는 방법 (엔터티 삭제 시마다 셀렉문이 추가 발생 문제 있음)  ,
&lt;/details&gt;



&lt;h3&gt;18. 기존 엔터티를 데이터베이스에서 조회하지 않고 업데이트하는 방법은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
        await context.Users
    .Where(u =&gt; u.Id == 1)
    .ExecuteUpdateAsync(s =&gt; s.SetProperty(u =&gt; u.Name, &quot;Updated Name&quot;));
&lt;/details&gt;

&lt;h3&gt;19. 기존 엔터티를 데이터베이스에서 조회하지 않고 삭제하는 방법은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
        await context.Users
    .Where(u =&gt; u.Id == 1)
    .ExecuteDeleteAsync();
&lt;/details&gt;


&lt;h3&gt;20. BatchUpdate 및 BatchDelete를 사용할 때 발생할 수 있는 문제점은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    배치 실행될때 트랜잭션 처리를 하지 않으면 일부만 업데이트 일부만 델리트 될수 있습니다. 
&lt;/details&gt;


&lt;h3&gt;21. EF에서 수동으로 데이터베이스 트랜잭션을 생성하는 방법은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
         using var transaction = context.Database.BeginTransaction();
         try {
        // 작업 수행
        context.SaveChanges();
        transaction.Commit();
        } catch {
        transaction.Rollback();
        }
&lt;/details&gt;

&lt;h3&gt;22. 트랜잭션의 격리 수준(Isolation Level)에는 어떤 것들이 있나요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
- Read Uncommitted: 다른 트랜잭션의 변경을 읽을 수 있음&lt;br&gt;
- Read Committed: 커밋된 데이터만 읽음&lt;br&gt;
- Repeatable Read: 동일한 데이터를 반복 조회할 때 일관성 유지&lt;br&gt;
- Serializable: 트랜잭션 간 충돌 방지&lt;br&gt;
단 DBMS 마다 적용 가능 한 것이 다릅니다.
&lt;/details&gt;


&lt;h3&gt;23. DbUpdateConcurrencyException은 언제 발생하나요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    트랜잭션 처리가 되어있는 경우, 혹은 낙관적 동시성 처리가 되어있는 경우
    동일한 레코드를 여러 사용자가 수정하면 발생합니다.
&lt;/details&gt;


&lt;h3&gt;24. EF에서 낙관적 잠금(Optimistic Locking)을 구현하는 방법은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
        TiemStamp 애트리뷰트(버전관리컬럼), ConcurrencyCheck (특정컬럼 이전값 비교) 애트리뷰트 적용하면 동시 접근시 해당 컬럼이 다를 경우 익셉션이 발생합니다. 
&lt;/details&gt;


&lt;h3&gt;25. EF에서 커스텀 SQL 명령을 실행하는 방법은 무엇인가요?&lt;/h3&gt;
&lt;details&gt;
    &lt;summary&gt;답안보기(클릭)&lt;/summary&gt;
    var user = new SqlParameter(&quot;user&quot;, &quot;johndoe&quot;);  &lt;br&gt;
    context.Database.ExecuteSql($&quot;SELECT * FROM Products WHERE ProductName = {user}&quot;); &lt;br&gt;
    sql 인젝션 방지를 위해서 ExecuteSqlRaw 와 같은 날쿼리 실행방식보다 위와같이 SqlParameter를 사용하는 것을 권장함.
&lt;/details&gt;</description>
      <category>.NET/Database</category>
      <author>atawlee</author>
      <guid isPermaLink="true">https://devman-hoon.tistory.com/32</guid>
      <comments>https://devman-hoon.tistory.com/32#entry32comment</comments>
      <pubDate>Tue, 11 Mar 2025 11:22:42 +0900</pubDate>
    </item>
    <item>
      <title>EF Core 테이블 상속 매핑 (TPH)</title>
      <link>https://devman-hoon.tistory.com/31</link>
      <description>&lt;h4 data-end=&quot;80&quot; data-start=&quot;40&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&amp;nbsp;TPH(Table Per Hierarchy)란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;256&quot; data-start=&quot;81&quot; data-ke-size=&quot;size16&quot;&gt;TPH는 &lt;b&gt;상속 계층을 하나의 테이블에 저장하는 방식&lt;/b&gt;으로, 부모 클래스와 모든 자식 클래스의 데이터를 단일 테이블에서 관리하는 전략입니다.&lt;br /&gt;이를 통해 &lt;b&gt;조인을 최소화하여 조회 성능을 향상&lt;/b&gt;시키며, EF Core는 &lt;b&gt;Discriminator(구분자) 컬럼을 자동 추가하여 엔터티 유형을 구분&lt;/b&gt;합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;256&quot; data-start=&quot;81&quot; data-ke-size=&quot;size23&quot;&gt;Payment 관련 클래스 생성&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1741611673252&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace Database.Entity;
using System;
using System.ComponentModel.DataAnnotations;

public abstract class Payment
{
    [Key]
    public int Id { get; set; }

    public decimal Amount { get; set; } // 결제 금액

    public DateTimeOffset PaymentDate { get; set; } = DateTimeOffset.UtcNow; // UTC 0 기준 저장

    [MaxLength(20)]
    public string Status { get; set; } = &quot;Pending&quot;; // &quot;Pending&quot;, &quot;Completed&quot;, &quot;Failed&quot;
}

public class CreditCardPayment : Payment
{
    [MaxLength(20)]
    public string CardNumber { get; set; } = string.Empty; // 마스킹된 카드번호 (예: ****-****-****-1234)

    [MaxLength(50)]
    public string CardHolder { get; set; } = string.Empty; // 카드 소유자 이름

    [MaxLength(5)]
    public string ExpirationDate { get; set; } = string.Empty; // 유효기간 MM/YY
}

public class PayPalPayment : Payment
{
    [MaxLength(100)]
    public string PayPalEmail { get; set; } = string.Empty; // 페이팔 계정 이메일
}

public class BankTransferPayment : Payment
{
    [MaxLength(30)]
    public string BankAccountNumber { get; set; } = string.Empty; // 가상 계좌번호

    [MaxLength(50)]
    public string BankName { get; set; } = string.Empty; // 은행명
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741611739469&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore;
using Database.Entity;

namespace Database.Context;

public partial class ShopDbContext : DbContext
{
    #region TPH
    public DbSet&amp;lt;Payment&amp;gt; Payments { get; set; }
    #endregion

    public ShopDbContext(DbContextOptions&amp;lt;ShopDbContext&amp;gt; options) 
        :base(options)
    {
        
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&amp;lt;Payment&amp;gt;()
            .UseTphMappingStrategy()
            .HasDiscriminator&amp;lt;string&amp;gt;(&quot;PaymentType&quot;) // Discriminator 컬럼 이름 변경
            .HasValue&amp;lt;CreditCardPayment&amp;gt;(&quot;CreditCard&quot;)
            .HasValue&amp;lt;PayPalPayment&amp;gt;(&quot;PayPal&quot;)
            .HasValue&amp;lt;BankTransferPayment&amp;gt;(&quot;BankTransfer&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-end=&quot;439&quot; data-start=&quot;399&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DbContext에서 Discriminator 컬럼 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;594&quot; data-start=&quot;441&quot; data-ke-size=&quot;size16&quot;&gt;EF Core에서 UseTphMappingStrategy()를 사용하여 TPH 전략을 적용하며,&lt;br /&gt;.HasDiscriminator&amp;lt;string&amp;gt;(&quot;PaymentType&quot;)을 통해 Discriminator 컬럼의 이름을 &quot;PaymentType&quot;으로 지정합니다.&lt;/p&gt;
&lt;p data-end=&quot;712&quot; data-start=&quot;596&quot; data-ke-size=&quot;size16&quot;&gt;각 서브 클래스(CreditCardPayment, PayPalPayment, BankTransferPayment)는&lt;br /&gt;.HasValue&amp;lt;T&amp;gt;(&quot;값&quot;)을 통해 고유한 값을 갖도록 설정됩니다.&lt;/p&gt;
&lt;p data-end=&quot;801&quot; data-start=&quot;714&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 Payments 테이블에 PaymentType 컬럼이 추가되며,&lt;br /&gt;각 행이 &lt;b&gt;어떤 결제 타입인지 구분할 수 있도록 저장&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-end=&quot;801&quot; data-start=&quot;714&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uCDpk/btsMFutOQJE/hIoqRe62GIgKzz1KHfwauK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uCDpk/btsMFutOQJE/hIoqRe62GIgKzz1KHfwauK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uCDpk/btsMFutOQJE/hIoqRe62GIgKzz1KHfwauK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuCDpk%2FbtsMFutOQJE%2FhIoqRe62GIgKzz1KHfwauK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;291&quot; height=&quot;329&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-end=&quot;837&quot; data-start=&quot;803&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테이블 구조: Payments 하나만 생성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1015&quot; data-start=&quot;839&quot; data-ke-size=&quot;size16&quot;&gt;이 방식에서는 CreditCardPayment, PayPalPayment, BankTransferPayment 등의 개별 테이블이 생성되지 않습니다.&lt;br /&gt;대신, &lt;b&gt;하나의 Payments 테이블만 존재하며&lt;/b&gt;,&lt;br /&gt;모든 결제 관련 데이터가 &lt;b&gt;공통 컬럼 + 개별 타입별 컬럼&lt;/b&gt; 형태로 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;데이터 사용&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1741612078581&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//데이터 추가
context.Payments.AddAsync(new CreditCardPayment
{
    Amount = 3001,
    PaymentDate = DateTimeOffset.UtcNow,
    Status = &quot;Completed&quot;,
    CardNumber = &quot;0000-0000-0000-1234&quot;,
    CardHolder = &quot;John Doe&quot;,
    ExpirationDate = &quot;12/23&quot;
});

//데이터 가져오기
var list = await context.Payments.OfType&amp;lt;CreditCardPayment&amp;gt;().ToListAsync();
foreach (var creditCardPayment in list)
{
    Console.WriteLine($&quot;Id: {creditCardPayment.Id}, Amount: {creditCardPayment.Amount}, PaymentDate: {creditCardPayment.PaymentDate}, Status: {creditCardPayment.Status}, CardNumber: {creditCardPayment.CardNumber}, CardHolder: {creditCardPayment.CardHolder}, ExpirationDate: {creditCardPayment.ExpirationDate}&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mCybF/btsMGNlC1hV/jCA6RkBOKLmwXBf6h6asEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mCybF/btsMGNlC1hV/jCA6RkBOKLmwXBf6h6asEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mCybF/btsMGNlC1hV/jCA6RkBOKLmwXBf6h6asEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmCybF%2FbtsMGNlC1hV%2FjCA6RkBOKLmwXBf6h6asEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2392&quot; height=&quot;341&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2718&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OWxDQ/btsMHnGZidA/f9VcrC8JuriDaSPknMXqnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OWxDQ/btsMHnGZidA/f9VcrC8JuriDaSPknMXqnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OWxDQ/btsMHnGZidA/f9VcrC8JuriDaSPknMXqnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOWxDQ%2FbtsMHnGZidA%2Ff9VcrC8JuriDaSPknMXqnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2718&quot; height=&quot;313&quot; data-origin-width=&quot;2718&quot; data-origin-height=&quot;313&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블은 단한개로 유지되기 때문에 장입시의 사용하는 타입의 데이터 부분만 Insert 하는 것을 볼수있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가져올때는 OfType&amp;lt;T&amp;gt;() 를 통해서 가져올 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같이 TPH는 &lt;b&gt;조회 성능을 극대화하는 대신, NULL 값이 많아질 수 있다는 점을 고려해야 하는 전략&lt;/b&gt;입니다.&lt;br /&gt;EF Core의 HasDiscriminator()를 활용하면 손쉽게 구현할 수 있으며,&lt;br /&gt;도메인 모델의 특성과 테이블 크기를 고려하여 적절한 전략을 선택하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>.NET/Database</category>
      <author>atawlee</author>
      <guid isPermaLink="true">https://devman-hoon.tistory.com/31</guid>
      <comments>https://devman-hoon.tistory.com/31#entry31comment</comments>
      <pubDate>Mon, 10 Mar 2025 22:11:33 +0900</pubDate>
    </item>
    <item>
      <title>EF Core 테이블 상속 매핑(TPC)</title>
      <link>https://devman-hoon.tistory.com/30</link>
      <description>&lt;h3 data-end=&quot;34&quot; data-start=&quot;0&quot; data-ke-size=&quot;size23&quot;&gt;TPC (Table Per Concrete) 매핑 전략&lt;/h3&gt;
&lt;p data-end=&quot;126&quot; data-start=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;TPC(Table Per Concrete) 매핑 방법은 부모 클래스의 테이블을 생성하지 않고, 자식 테이블에서 부모 타입의 속성을 공통적으로 포함하는 전략입니다.&lt;/p&gt;
&lt;p data-end=&quot;126&quot; data-start=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;126&quot; data-start=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-start=&quot;1086&quot; data-end=&quot;1109&quot;&gt;1.&lt;span&gt; Log 클래스 작성&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1741528296215&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Database.Entity;

public abstract class Log
{
    [Key]
    public int Id { get; set; }
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;
    [MaxLength(1000)]
    public string Message { get; set; } = string.Empty;
}

public class SystemLog : Log
{
    [MaxLength(50)]
    public string LogLevel { get; set; } = &quot;Info&quot;;  // 예: &quot;Info&quot;, &quot;Warning&quot;, &quot;Critical&quot;
}

public class ErrorLog : Log
{
    [MaxLength(1000)]
    public string ExceptionMessage { get; set; } = string.Empty;
    [MaxLength(1000)]
    public string StackTrace { get; set; } = string.Empty;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-start=&quot;1086&quot; data-end=&quot;1109&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;b&gt;DbContext 작업&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1741528908438&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//이전 작업내용은 생략합니다. 

public partial class ShopDbContext : DbContext
{
    public DbSet&amp;lt;SystemLog&amp;gt; SystemLogs { get; set; }
    public DbSet&amp;lt;ErrorLog&amp;gt; ErrorLogs { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&amp;lt;Log&amp;gt;()
            .UseTpcMappingStrategy();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1109&quot; data-start=&quot;1086&quot; data-ke-size=&quot;size23&quot;&gt;3. &lt;b&gt;TPC 매핑 전략 특징&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1271&quot; data-start=&quot;1110&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1131&quot; data-start=&quot;1110&quot;&gt;Log 테이블은 생성되지 않음.&lt;/li&gt;
&lt;li data-end=&quot;1225&quot; data-start=&quot;1132&quot;&gt;SystemLogs와 ErrorLogs 테이블이 각각 생성되며, Log 클래스의 공통 속성(Id, Timestamp, Message)을 포함.&lt;/li&gt;
&lt;li data-end=&quot;1271&quot; data-start=&quot;1226&quot;&gt;DbSet&amp;lt;Log&amp;gt;이 없으므로 Log 타입만 단순히 공통 구조를 제공.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1303&quot; data-start=&quot;1278&quot; data-ke-size=&quot;size23&quot;&gt;4. &lt;b&gt;TPC 전략의 활용 포인트&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1327&quot; data-start=&quot;1304&quot; data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;언제 TPC를 사용해야 할까?&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;69&quot; data-start=&quot;28&quot; data-ke-size=&quot;size20&quot;&gt;1. &lt;b&gt;추상 클래스의 인스턴스를 직접 저장할 필요가 없을 때&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;214&quot; data-start=&quot;73&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;153&quot; data-start=&quot;73&quot;&gt;Log 테이블을 따로 만들지 않고, 공통된 컬럼을 개별 테이블(SystemLogs, ErrorLogs)에서 유지할 때 유용합니다.&lt;/li&gt;
&lt;li data-end=&quot;214&quot; data-start=&quot;157&quot;&gt;DbSet&amp;lt;Log&amp;gt;을 사용하지 않으므로, 부모 클래스의 인스턴스를 직접 저장할 필요가 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;257&quot; data-start=&quot;216&quot; data-ke-size=&quot;size20&quot;&gt;2. &lt;b&gt;공통 테이블 없이 동일한 컬럼 구조를 가지도록 할 때&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;380&quot; data-start=&quot;261&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;333&quot; data-start=&quot;261&quot;&gt;Log 테이블 없이도 Id, Timestamp, Message와 같은 공통 필드를 자동으로 포함하는 방식입니다.&lt;/li&gt;
&lt;li data-end=&quot;380&quot; data-start=&quot;337&quot;&gt;테이블 간 스키마 일관성을 유지하면서도 개별 테이블로 분리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;420&quot; data-start=&quot;382&quot; data-ke-size=&quot;size20&quot;&gt;3. &lt;b&gt;자식 클래스가 독립적인 테이블로 존재해야 할 때&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;565&quot; data-start=&quot;424&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;493&quot; data-start=&quot;424&quot;&gt;SystemLog와 ErrorLog가 개별 테이블로 저장되므로, 특정 로그 타입만 선택적으로 관리하기가 쉽습니다.&lt;/li&gt;
&lt;li data-end=&quot;565&quot; data-start=&quot;497&quot;&gt;필요에 따라 특정 로그 테이블(SystemLogs 또는 ErrorLogs)만 조회할 수 있어 운영이 간편합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;606&quot; data-start=&quot;567&quot; data-ke-size=&quot;size20&quot;&gt;4. &lt;b&gt;테이블 조인을 최소화하고 성능을 향상하고 싶을 때&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;803&quot; data-start=&quot;610&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;660&quot; data-start=&quot;610&quot;&gt;부모 테이블이 없으므로 조인 부담이 사라지며, 직접적으로 테이블을 조회할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;736&quot; data-start=&quot;664&quot;&gt;예를 들어, SystemLogs 테이블만 빠르게 검색하고 싶을 때, 추가적인 조인 없이 바로 데이터를 가져올 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;803&quot; data-start=&quot;740&quot;&gt;테이블이 독립적으로 존재하기 때문에, TPT(Table Per Type)보다 조회 성능이 개선될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2443&quot; data-start=&quot;2430&quot; data-ke-size=&quot;size23&quot;&gt;5. &lt;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;2564&quot; data-start=&quot;2444&quot; data-ke-size=&quot;size16&quot;&gt;TPC 전략은 &lt;b&gt;독립적인 엔터티를 개별적으로 관리해야 하며, 성능 최적화가 필요한 경우&lt;/b&gt; 적합합니다.&lt;br /&gt;로그 시스템처럼 서로 다른 성격의 데이터를 분리해서 저장해야 하는 경우에 효과적인 선택이 될 수 있습니다.&lt;/p&gt;</description>
      <category>.NET/Database</category>
      <author>atawlee</author>
      <guid isPermaLink="true">https://devman-hoon.tistory.com/30</guid>
      <comments>https://devman-hoon.tistory.com/30#entry30comment</comments>
      <pubDate>Mon, 10 Mar 2025 15:02:01 +0900</pubDate>
    </item>
    <item>
      <title>C# .NET 솔루션, 프로젝트 구조</title>
      <link>https://devman-hoon.tistory.com/29</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스트에서는 닷넷 프로그래밍을 할 때 솔루션(.sln)과 프로젝트(.csproj)가 무엇인지 설명합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 프로그램을 만들고자 할 때 IDE를 통해서 어떤 프로젝트를 구성할 때, 각 언어나 프레임워크에 따라서생성되는 파일의 구조, 패키지의 구성들이 다르게 생성됩니다. 닷넷으로 프로그램을 만들고자 할때도 닷넷 고유의 프로젝트 구조가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jetbrains Rider를 이용해서 새 솔루션을 선택하면 현재 다음과 같은 화면을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1793&quot; data-origin-height=&quot;1371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5XZWZ/btsMtqEHMmx/MVRpbTkTphcfWmRIpTGBg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5XZWZ/btsMtqEHMmx/MVRpbTkTphcfWmRIpTGBg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5XZWZ/btsMtqEHMmx/MVRpbTkTphcfWmRIpTGBg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5XZWZ%2FbtsMtqEHMmx%2FMVRpbTkTphcfWmRIpTGBg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1793&quot; height=&quot;1371&quot; data-origin-width=&quot;1793&quot; data-origin-height=&quot;1371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔루션 이름과 프로젝트 이름을 설정하는 부분이 각각 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;솔루션&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔루션은 여러 프로젝트를 포함 할 수 있는 관리 구조 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔루션 내의 폴더는 운영체제와는 별개의 구조로 각개의 프로젝트의 실제 위치는 os에서 각기 다른 위치하더라도 솔루션탐색기내에서는 관리하기 편하도록 보여집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 컴파일 단위로 1개의 프로젝트는 1개의 결과물을 생성할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클래스 라이브러리&lt;/b&gt; 프로젝트는 다른 프로젝트에서 참조하기 위하여 만들어지는 프로젝트 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이용해서 어플리케이션의 특정 부분을 잘 나눠서 프레임워크 구성과 별도로 동작 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 프로젝트를 생성하고 나면 다음과 같이 생성 된 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MZttS/btsMsUGj2KI/UyiHOKuuvEMlEOYEfbcMh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MZttS/btsMsUGj2KI/UyiHOKuuvEMlEOYEfbcMh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MZttS/btsMsUGj2KI/UyiHOKuuvEMlEOYEfbcMh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMZttS%2FbtsMsUGj2KI%2FUyiHOKuuvEMlEOYEfbcMh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;249&quot; height=&quot;227&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 더 필요한 클래스 라이브러리나, 혹은 같은 솔루션에서 사용할 또 다른 api 프로젝트, 블레이저와 같은 프론트엔드 프로젝트 까지 한개의 솔루션 내에서 관리 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 솔루션은 단순히 이러한 프로젝트를 묶어주는 논리구조 형태이기 때문에, 이 안에 속한 프로젝트를 다른 솔루션에서 활용하여 사용하는 방법도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마무리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 .NET 솔루션과 프로젝트 구조에 대해 간단히 알아보았습니다.&amp;nbsp; 이번 포스트가 .NET 솔루션과 프로젝트 구조를 이해하는 데 도움이 되었기를 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>.NET/Common</category>
      <author>atawlee</author>
      <guid isPermaLink="true">https://devman-hoon.tistory.com/29</guid>
      <comments>https://devman-hoon.tistory.com/29#entry29comment</comments>
      <pubDate>Sun, 23 Feb 2025 21:00:20 +0900</pubDate>
    </item>
    <item>
      <title>EF Core 테이블 상속 매핑 (TPT)</title>
      <link>https://devman-hoon.tistory.com/28</link>
      <description>&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;EF core 테이블 상속 매핑 개념&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;Entity Framework Core(이하 EF Core)는 객체 지향 프로그래밍에서 흔히 사용되는 상속 개념을 관계형 데이터베이스(RDB)와 매핑할 수 있는 다양한 전략을 제공합니다. 이 글에서는 EF Core의 테이블 상속 매핑 기법인 &lt;/span&gt;&lt;span&gt;&lt;b&gt;Table Per Hierarchy(TPH)&lt;/b&gt;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&lt;b&gt;Table Per Type(TPT)&lt;/b&gt;&lt;/span&gt;&lt;span&gt;, &lt;b&gt;Table Per Concrete Type(TPC)&lt;/b&gt;을 비교하고, 각 전략이 어떤 경우에 적합한지 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테이블 상속 방법&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 상속 방법은 클래스를 상속하는 방법과 다르지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739768793614&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using System.ComponentModel.DataAnnotations;

namespace Database.Entity;

public class DocumentBase
{
    [Key]
    public int Id { get; set; }
    [MaxLength(255)]
    public string Title { get; set; }
    [MaxLength(20)]
    public string CreatedBy { get; set; }
    public DateTime CreatedDate { get; set; }
}

public class GeneralDocumentBase : DocumentBase
{
    [MaxLength(255)]
    public string Content { get; set; }
}

public class ContractDocumentBase : DocumentBase
{
    [MaxLength(255)]
    public string ContractorName { get; set; }
    public DateTime ExpirationDate { get; set; }
}

public class TechnicalDocumentBase : DocumentBase
{
    [MaxLength(255)]
    public string TechnologyUsed { get; set; }
    public int Version { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 부모 클래스를 정의하고 이를 상속받는 자식클래스를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속하는 방식에 대한 것은 일반 클래스와 크게 다르지 않지만, 이것을 각 EFCore의 매핑방식에 테이블에 각각 다르게 적용 되는 것을 확인 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트에서는 부모 테이블과 자식 테이블을 모두 생성하고, 부모테이블에서 생성된 키를 통해서 모델을 공유할 수 있는 TPT에 대해서 진행해보도록 하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TPT(Table Per Type)&lt;/h3&gt;
&lt;pre id=&quot;code_1739798253655&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public partial class ShopDbContext : DbContext
{
    public DbSet&amp;lt;Product&amp;gt; Products { get; set; }
    
    public DbSet&amp;lt;DocumentBase&amp;gt; Documents { get; set; }
    
    public DbSet&amp;lt;TechnicalDocumentBase&amp;gt; TechnicalDocuments { get; set; }
    
    public DbSet&amp;lt;GeneralDocumentBase&amp;gt; GeneralDocuments { get; set; }
    
    public DbSet&amp;lt;ContractDocumentBase&amp;gt; ContractDocuments { get; set; }
    
    public ShopDbContext(DbContextOptions&amp;lt;ShopDbContext&amp;gt; options) 
        :base(options)
    {
        
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&amp;lt;DocumentBase&amp;gt;()
            .UseTptMappingStrategy()
            .ToTable(&quot;Documents&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 만든 DocumentBase 클래스를 상속받는 클래스와, DocumentBase가 매핑 될 테이블을 DbSet&amp;lt;T&amp;gt; 멤버로 정의하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OnModelCreating 함수에서 DocumentBase 클래스가 Tpt 전략을 취하고 있고 대상테이블이 Documents 테이블 이라는 것을 지정 합니다. 이렇게 되면 DocumentBase타입을 상속받는 테이블에 대해서는 DocumentBase의 PK를 FK로 받는 형식으로 테이블이 생성됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739798627727&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dotnet ef migrations add document
dotnet ef database update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Migrator.csproj가 위치한 폴더에서 위 명령을 실행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CenKZ/btsMmkRFDiz/vZ4G94djThka8qBDkhMcq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CenKZ/btsMmkRFDiz/vZ4G94djThka8qBDkhMcq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CenKZ/btsMmkRFDiz/vZ4G94djThka8qBDkhMcq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCenKZ%2FbtsMmkRFDiz%2FvZ4G94djThka8qBDkhMcq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;285&quot; height=&quot;368&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진과 같이 Documents와 상속받은 테이블들이 생성된 것을 확인 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 ContractDocuments 테이블에 데이터를 추가해보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739803471820&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var context= service.GetRequiredService&amp;lt;ShopDbContext&amp;gt;();
context.ContractDocuments.Add(new ContractDocumentBase
{
    Id = 0,
    Title = &quot;test&quot;,
    CreatedBy = &quot;null&quot;,
    CreatedDate = default,
    ContractorName = &quot;null&quot;,
    ExpirationDate = default
});

await context.SaveChangesAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2313&quot; data-origin-height=&quot;413&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eiZGAJ/btsMlUy5kNT/M5KSSx6SHu8UqzJ4yKz7k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eiZGAJ/btsMlUy5kNT/M5KSSx6SHu8UqzJ4yKz7k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eiZGAJ/btsMlUy5kNT/M5KSSx6SHu8UqzJ4yKz7k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeiZGAJ%2FbtsMlUy5kNT%2FM5KSSx6SHu8UqzJ4yKz7k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2313&quot; height=&quot;413&quot; data-origin-width=&quot;2313&quot; data-origin-height=&quot;413&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔에서 실행된 쿼리를 확인해보면 코드상에서는 ContractDocuments 테이블에만 삽입하였지만, 실제 데이터는 Documents 테이블과 ContractDocuments 테이블 양측에 데이터가 추가된 것을 볼수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 테이블 매핑은 위 예제와 같이 부모테이블을 만들고 공통적인 데이터들을 사용하는 자식테이블들을 추가한뒤 자식테이블들의 키값을 주소테이블에서 처리하고자 할때 유용할 것으로 보입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서는 일반클래스로 Document가 지정되어 Documents 테이블에 직접 넣는 것이 가능하지만 abstract class로 지정하게되면 부모테이블에는 직접 데이터 추가를 하지 못하도록 지정할 수도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마무리&lt;/h4&gt;
&lt;p data-end=&quot;257&quot; data-start=&quot;61&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스트에서는 &lt;b&gt;EF Core의 TPT(Table Per Type) 전략에서 베이스 테이블의 데이터 삽입 동작&lt;/b&gt;을 살펴보았습니다.&lt;br /&gt;콘솔에서 실행된 쿼리를 확인해보면 &lt;b&gt;자식 테이블(예: ContractDocuments)에 데이터가 삽입될 때, 자동으로 부모 테이블(Documents)에도 데이터가 추가됨&lt;/b&gt;을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;414&quot; data-start=&quot;259&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;414&quot; data-start=&quot;259&quot; data-ke-size=&quot;size16&quot;&gt;이러한 테이블 매핑 방식은 &lt;b&gt;부모 테이블을 활용하여 공통 속성을 관리하고, 개별적인 데이터를 자식 테이블에서 처리할 때 매우 유용&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-end=&quot;414&quot; data-start=&quot;259&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;414&quot; data-start=&quot;259&quot; data-ke-size=&quot;size16&quot;&gt;특히 &lt;b&gt;주소 관리, 계약 문서 관리, 제품 카테고리 등 공통 데이터를 공유하는 다양한 도메인 모델링에 활용할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;599&quot; data-start=&quot;416&quot; data-ke-size=&quot;size16&quot;&gt;또한, &lt;b&gt;부모 클래스(Document)를 일반 클래스로 정의하면 직접 삽입이 가능하지만, abstract class로 선언하면 직접 삽입이 불가능해져 더욱 강력한 데이터 무결성을 유지할 수 있습니다.&lt;/b&gt;&lt;br /&gt;이러한 특징을 활용하면 &lt;b&gt;TPT를 적용한 테이블 설계를 더욱 안정적이고 효과적으로 운영&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>.NET/Database</category>
      <category>EF Core</category>
      <category>entity framework core</category>
      <category>table mapping</category>
      <category>table per type</category>
      <category>TPT</category>
      <author>atawlee</author>
      <guid isPermaLink="true">https://devman-hoon.tistory.com/28</guid>
      <comments>https://devman-hoon.tistory.com/28#entry28comment</comments>
      <pubDate>Mon, 17 Feb 2025 23:56:00 +0900</pubDate>
    </item>
    <item>
      <title>Blazor Server로 풀스택 웹어플리케이션 만들기(2) - MVVM 아키텍처 적용 하기</title>
      <link>https://devman-hoon.tistory.com/27</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스트에서는 Blazor Server에서 MVVM 패턴을 적용하는 방법에 대해서 다루겠습니다. 이번 포스트는 2024년 닷넷데브에서 발표한 MVVM With Blazor 내용의 일부입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YVb03/btsL8nmTWJA/QKoTQNWK2rZUcEWCHfZ6JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YVb03/btsL8nmTWJA/QKoTQNWK2rZUcEWCHfZ6JK/img.png&quot; data-alt=&quot;출처 : baike.baidu.com/item/MVVM/96310&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YVb03/btsL8nmTWJA/QKoTQNWK2rZUcEWCHfZ6JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYVb03%2FbtsL8nmTWJA%2FQKoTQNWK2rZUcEWCHfZ6JK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;832&quot; height=&quot;191&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;191&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : baike.baidu.com/item/MVVM/96310&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MV 로 시작하는 많은 모델들이 있지만, 중요한 점은 각각의 레이어가 어떤 역할을 맡고있고 어느 방향의 의존성을 갖고 있는지가 해당 패턴이 어떤 아키텍처를 모델로 하는지가 뚜렸한 점이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVVM은 View, Model ,View Model로 나눠진 클라이언트 어플리케이션 아키텍처 입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;View&lt;/h4&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;View는 사용자가 직접 보는 화면을 의미하며, 화면에 표시되는 데이터와 이벤트 처리를 담당합니다. Blazor에서 View는 Razor 페이지 및 컴포넌트로 구성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;View에서는 &lt;/span&gt;&lt;span&gt;&lt;b&gt;데이터 바인딩&lt;/b&gt;&lt;/span&gt;&lt;span&gt;과 &lt;/span&gt;&lt;span&gt;&lt;b&gt;커맨드 패턴&lt;/b&gt;&lt;/span&gt;&lt;span&gt;을 통해 ViewModel과 상호작용합니다. 데이터 바인딩을 통해 View에서 보여줘야 할 데이터가 변경되면 Blazor의 바인딩 엔진이 자동으로 ViewModel의 프로퍼티에 변경 사항을 전달합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;사용자 이벤트 처리는 일반적으로 커맨드 패턴을 사용하지만, Blazor에서는 메서드를 직접 바인딩할 수 있습니다. WPF에서는 &lt;/span&gt;&lt;span&gt;ICommand&lt;/span&gt;&lt;span&gt; 인터페이스를 상속받아 RelayCommand로 처리하는 방법이 기본이지만, Blazor에서는 이러한 과정 없이 &lt;/span&gt;&lt;span&gt;&lt;b&gt;직접 ViewModel의 메서드에 바인딩하는 방법&lt;/b&gt;&lt;/span&gt;&lt;span&gt;이 좀 더 편리하다고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ViewModel&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel의 역할에 대해서는 오해와 실수의 여지가 많은 부분이 있습니다. &lt;span&gt;ViewModel은 UI와 관련된 데이터를 관리하며, View와 Model 간의 중재자 역할을 합니다. 하지만 ViewModel은 비즈니스 로직을 최소화하고, 사용자에게 보여지는 데이터에 집중해야 합니다. 즉, ViewModel은 &lt;/span&gt;&lt;span&gt;&lt;b&gt;View가 있기 때문에 존재하는 계층&lt;/b&gt;&lt;/span&gt;&lt;span&gt;이며, View가 없다면 ViewModel도 존재할 필요가 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;Model&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Model은 데이터 처리 및 비즈니스 로직을 담당합니다. 예를 들어, 권한 상태를 API에서 &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; 값으로 받아 비트 플래그로 처리한다고 가정해 봅시다. 하지만 View에서는 체크박스를 여러 개 배치하고, 각 체크박스를 ViewModel의 &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt; 프로퍼티와 연결해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt; 값들을 API로 전송할 때 &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; 값으로 변환하는 작업이 필요합니다. 이러한 변환 로직이 Model의 역할입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;MVVM과 같은 클라이언트 아키텍처에서는 Model 레이어의 구체적인 구현 방식이 명확하게 정의되어 있지 않습니다. 어떤 경우에는 단일 클래스로 데이터 파싱, 전송, 수신 등의 역할을 처리하기도 하고, 좀 더 구체적으로 나누어 &lt;/span&gt;&lt;span&gt;&lt;b&gt;리포지토리 패턴&lt;/b&gt;&lt;/span&gt;&lt;span&gt;을 도입하기도 합니다. 이처럼 Model 레이어는 개발자의 자율성에 따라 다양한 방식으로 설계될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Sample 코드&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샘플 코드는 Blazor Server로 이뤄진 풀스택 어플리케이션으로 준비되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 WareHouse Management System을 가정하여 준비하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 + 백엔드 + 데이터베이스 정의까지 한개의 프로젝트로 이뤄져있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/atawLee/dotnetdevSeoul2024/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/atawLee/dotnetdevSeoul2024/&amp;nbsp;&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1738767942028&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - atawLee/dotnetdevSeoul2024&quot; data-og-description=&quot;Contribute to atawLee/dotnetdevSeoul2024 development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/atawLee/dotnetdevSeoul2024/&quot; data-og-url=&quot;https://github.com/atawLee/dotnetdevSeoul2024&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/atawLee/dotnetdevSeoul2024/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/atawLee/dotnetdevSeoul2024/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - atawLee/dotnetdevSeoul2024&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to atawLee/dotnetdevSeoul2024 development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서 전체 코드를 확인 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View 샘플코드와 설명&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View 전체 코드 예시입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738767815958&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@page &quot;/mvvm&quot;
@using SimpleWMS.Data
@using SimpleWMS.Database.Entities
@using SimpleWMS.Models.Services
@using SimpleWMS.ViewModels
@inject StockHistoryViewModel _viewModel;

@code {
    
    private bool _showModal = false;

    protected override void OnInitialized()
    {
        _viewModel.Search();
    }

    private void ShowModal(StockTransactionType modalType)
    {
        _viewModel.SetModalType(modalType);
        _showModal = true; 
    }

    private void HideModal()
    {
        _showModal = false; 
    }
}

&amp;lt;h3 class=&quot;mb-4&quot;&amp;gt;입출고 이력 조회&amp;lt;/h3&amp;gt;
&amp;lt;div class=&quot;search-box&quot;&amp;gt;
    &amp;lt;div class=&quot;row mb-3&quot;&amp;gt;
        &amp;lt;div class=&quot;col&quot;&amp;gt;
            &amp;lt;label for=&quot;itemName&quot; class=&quot;form-label&quot;&amp;gt;품목명&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; class=&quot;form-control&quot; id=&quot;itemName&quot; @bind=&quot;_viewModel.SearchItemName&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;col&quot;&amp;gt;
            &amp;lt;label for=&quot;materialId&quot; class=&quot;form-label&quot;&amp;gt;바코드&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; class=&quot;form-control&quot; id=&quot;materialId&quot; @bind=&quot;_viewModel.SearchBarcode&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;col d-flex align-items-end&quot;&amp;gt;
            &amp;lt;div class=&quot;form-check&quot;&amp;gt;
                &amp;lt;input type=&quot;checkbox&quot; class=&quot;form-check-input&quot; id=&quot;stockIn&quot; @bind=&quot;_viewModel.SearchStockIn&quot;&amp;gt;
                &amp;lt;label for=&quot;stockIn&quot; class=&quot;form-check-label&quot;&amp;gt;입고 사항&amp;lt;/label&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;col d-flex align-items-end&quot;&amp;gt;
            &amp;lt;div class=&quot;form-check&quot;&amp;gt;
                &amp;lt;input type=&quot;checkbox&quot; class=&quot;form-check-input&quot; id=&quot;stockOut&quot; @bind=&quot;_viewModel.SearchStockOut&quot;&amp;gt;
                &amp;lt;label for=&quot;stockOut&quot; class=&quot;form-check-label&quot;&amp;gt;출고 사항&amp;lt;/label&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;row mb-3&quot;&amp;gt;
        &amp;lt;div class=&quot;col d-flex justify-content-between&quot;&amp;gt;
            &amp;lt;button class=&quot;btn btn-primary&quot; @onclick=&quot;_viewModel.Search&quot;&amp;gt;검색&amp;lt;/button&amp;gt;
            &amp;lt;div&amp;gt;
                &amp;lt;button class=&quot;btn btn-primary&quot; @onclick=&quot;() =&amp;gt; ShowModal(StockTransactionType.StockIn)&quot;&amp;gt;입고 추가&amp;lt;/button&amp;gt;
                &amp;lt;button class=&quot;btn btn-secondary&quot; @onclick=&quot;() =&amp;gt; ShowModal(StockTransactionType.StockOut)&quot;&amp;gt;출고 추가&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

@if (_viewModel.StockHistorySearchResults != null &amp;amp;&amp;amp; _viewModel.StockHistorySearchResults.Any())
{
    &amp;lt;table class=&quot;table&quot;&amp;gt;
        &amp;lt;thead&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;th&amp;gt;품목명&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;바코드&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;위치&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;구분&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;수량&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;일시&amp;lt;/th&amp;gt;
            &amp;lt;/tr&amp;gt;
        &amp;lt;/thead&amp;gt;
        &amp;lt;tbody&amp;gt;
            @foreach (var history in _viewModel.StockHistorySearchResults)
            {
                &amp;lt;tr&amp;gt;
                    &amp;lt;td&amp;gt;@history.ProductName&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;@history.Barcode&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;@history.Location&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;@history.StockTransactionTypeText&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;@history.Quantity&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;@history.StockDateTime&amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
            }
        &amp;lt;/tbody&amp;gt;
    &amp;lt;/table&amp;gt;
}
else
{
    &amp;lt;p&amp;gt;검색 결과가 없습니다.&amp;lt;/p&amp;gt;
}

@if (_showModal)
{
    &amp;lt;div class=&quot;modal&quot; tabindex=&quot;-1&quot; style=&quot;display:block; background-color: rgba(0,0,0,0.5);&quot; role=&quot;dialog&quot;&amp;gt;
        &amp;lt;div class=&quot;modal-dialog&quot; role=&quot;document&quot;&amp;gt;
            &amp;lt;div class=&quot;modal-content&quot;&amp;gt;
                &amp;lt;div class=&quot;modal-header&quot;&amp;gt;
                    &amp;lt;h5 class=&quot;modal-title&quot;&amp;gt;@_viewModel.ModalTitle&amp;lt;/h5&amp;gt;
                    &amp;lt;button type=&quot;button&quot; class=&quot;close&quot; @onclick=&quot;HideModal&quot;&amp;gt;
                        &amp;lt;span aria-hidden=&quot;true&quot;&amp;gt;&amp;amp;times;&amp;lt;/span&amp;gt;
                    &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;modal-body&quot;&amp;gt;
                    &amp;lt;select class=&quot;form-control mb-2&quot; @bind=&quot;_viewModel.SelectedStockInventoryId&quot;&amp;gt;
                        @foreach (var item in _viewModel.InventoryItems)
                        {
                            &amp;lt;option value=&quot;@item.Id&quot;&amp;gt;@item.Barcode&amp;lt;/option&amp;gt;
                        }
                    &amp;lt;/select&amp;gt;
                    &amp;lt;input type=&quot;number&quot; class=&quot;form-control mb-2&quot; placeholder=&quot;수량&quot; @bind=&quot;_viewModel.StockInOutQuantity&quot;&amp;gt;
                    &amp;lt;input type=&quot;date&quot; class=&quot;form-control mb-2&quot; @bind=&quot;_viewModel.StockInOutDate&quot;&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;modal-footer&quot;&amp;gt;
                    &amp;lt;button type=&quot;button&quot; class=&quot;btn btn-primary&quot; @onclick=&quot;_viewModel.AddData&quot;&amp;gt;추가&amp;lt;/button&amp;gt;
                    &amp;lt;button type=&quot;button&quot; class=&quot;btn btn-secondary&quot; @onclick=&quot;HideModal&quot;&amp;gt;닫기&amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;showModal이 있는 view 레이어에 있는 의도는 해당부분은 비즈니스로직과 중재할 부분이 없는 것으로 보았기 때문에 ui로 두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WPF에서의 사용하는 경우와 차이점이 있다면, WPF의 경우에서 저라면 showmodal또한 viewmodel 바인딩으로 처리했을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 blazor에서는 한페이지에서 코드처리가 가능하고, 뷰에서만 동작하는 내용을 뷰모델로 가져가는 것이 아쉽게 느껴져 위와같이 작성했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 또한 바로 닫아주는 이벤트에서 view의 처리가 필요할때는 내부메서드에서 viewmodel의 메서드를 호출하고, 닫아주는 방식으로 사용했습니다.&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel 설명&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel 전체 코드 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738768979087&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using SimpleWMS.Data;
using SimpleWMS.Database.Entities;
using SimpleWMS.Models.Services;

namespace SimpleWMS.ViewModels;

public class StockHistoryViewModel
{
    private readonly WareHouseManagementService _service;

    public string? SearchItemName { get; set; }
    public string? SearchBarcode { get; set; }
    public bool SearchStockIn { get; set; }
    public bool SearchStockOut { get; set; }
    public string ModalTitle { get; set; } = &quot;&quot;;
    public StockTransactionType CurrentModal { get; set; }
    public DateTime StockInOutDate { get; set; } = DateTime.Today;
    public int StockInOutQuantity { get; set; }
    public int SelectedStockInventoryId { get; set; }

    public List&amp;lt;InventoryItem&amp;gt; InventoryItems { get; private set; } = new List&amp;lt;InventoryItem&amp;gt;();
    public List&amp;lt;StockHistoryDTO&amp;gt; StockHistorySearchResults { get; private set; } = new List&amp;lt;StockHistoryDTO&amp;gt;();

    public StockHistoryViewModel(WareHouseManagementService service)
    {
        _service = service;
        InitializeViewModel();
    }

    public void InitializeViewModel()
    {
        InventoryItems = _service.GetAllInventoryItems();
        Search();
    }

    public void Search()
    {
        StockHistorySearchResults = _service.GetStockInOutsFiltered(SearchItemName, SearchBarcode, DetermineTransactionType());
    }

    private StockTransactionType? DetermineTransactionType()
    {
        if (SearchStockIn &amp;amp;&amp;amp; !SearchStockOut)
        {
            return StockTransactionType.StockIn;
        }
        else if (!SearchStockIn &amp;amp;&amp;amp; SearchStockOut)
        {
            return StockTransactionType.StockOut;
        }

        return null;
    }

    public void ShowModal(StockTransactionType modalType)
    {
        CurrentModal = modalType;
    }

    public void AddData()
    {
        try
        {
            var stockType = CurrentModal;
            var dto = new StockCommandDTO(SelectedStockInventoryId, StockInOutQuantity, stockType);
            _service.InsertStockInOut(dto, dto.Quantity, stockType);

            Search();
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($&quot;Error adding stock in/out data: {ex.Message}&quot;);
        }
    }

    public void SetModalType(StockTransactionType modalType)
    {
        ModalTitle = modalType == StockTransactionType.StockIn ? &quot;입고 추가&quot; : &quot;출고 추가&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel에서는 전반적인 중재의 역할을 하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 WPF와 달리 CommunityToolkit.Mvvm 패키지를 사용하지 않더라도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 발생시킨 이벤트에 대해서는 별다른 UI 이벤트로 매핑하지 않더라도 정상적으로 변경 반영되는 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다른 사용자가 같은 인스턴스로 사용하므로써 변경시키고 UI에 반영 시키고 싶다면 View의 StateHasChanged() 를 호출시켜야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메세지 어그리게이터 패턴으로 위와같은 방식으로 동시에 같은 인스턴스를 공유하여 실시간 통신을 구축할수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;WareHouseManagementService.cs&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739090656087&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using SimpleWMS.Data;
using SimpleWMS.Database.Entities;
using SimpleWMS.Mdels;
using SimpleWMS.Mdels.Repository;

namespace SimpleWMS.Models.Services;

public class WareHouseManagementService
{
    private readonly WarehouseRepository _warehouseRepository;
    private readonly UnitOfWork _unitOfWork;

    public WareHouseManagementService(WarehouseRepository warehouseRepository, UnitOfWork unitOfWork)
    {
        _warehouseRepository = warehouseRepository;
        _unitOfWork = unitOfWork;
    }

    public List&amp;lt;Product&amp;gt; GetAllProducts()
    {
        return _warehouseRepository.GetProducts();
    }

    public List&amp;lt;InventoryItem&amp;gt; GetAllInventoryItems()
    {
        return _warehouseRepository.GetInventoryItems();
    }

    public InventoryItem? GetInventoryItemById(int inventoryItemId)
    {
        return _warehouseRepository.GetInventoryItem(inventoryItemId);
    }

    public List&amp;lt;StockHistoryDTO&amp;gt; GetStockInOutsFiltered(string? productName = null, string? barcode = null, StockTransactionType? type = null)
    {
        return _warehouseRepository.GetStockInOuts(productName, barcode, type).Select(dbentity =&amp;gt; dbentity.ToDTO())
            .ToList();
    }

    public void InsertStockInOut(StockCommandDTO dto, int quantityChange, StockTransactionType transactionType)
    {
        try
        {
            _unitOfWork.BeginTransaction();
            var inventory = _warehouseRepository.GetInventoryItem(dto.InventoryId)
                            ?? throw new Exception(&quot;잘못된 요청.&quot;);

            quantityChange = transactionType == StockTransactionType.StockOut ? -Math.Abs(quantityChange) : Math.Abs(quantityChange);
            var stockData = dto.ToDatabaseEntity(inventory.Quantity);
            
            _warehouseRepository.InsertStockInOut(stockData);
            _warehouseRepository.UpdateInventory(inventory, stockData.AfterQuantity);
            _unitOfWork.CommitTransaction();
        }
        catch (Exception e)
        {
            _unitOfWork.RollbackTransaction();
            throw new Exception(&quot;등록 실패&quot;);
        }
        
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WarehouseRepository.cs&lt;/p&gt;
&lt;pre id=&quot;code_1739090627524&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore;
using SimpleWMS.Database.Context;
using SimpleWMS.Database.Entities;

namespace SimpleWMS.Mdels.Repository;

public class WarehouseRepository
{
    private readonly ApplicationDbContext _db;

    public WarehouseRepository(ApplicationDbContext db)
    {
        _db = db;
    }

    public List&amp;lt;Product&amp;gt; GetProducts()
    {
        return _db.Product.ToList();
    }

    public List&amp;lt;InventoryItem&amp;gt; GetInventoryItems()
    {
        return _db.InventoryItem.ToList();
    }

    public InventoryItem? GetInventoryItem(int inventoryItemId)
    {
        return _db.InventoryItem
            .FirstOrDefault(x =&amp;gt; x.Id == inventoryItemId);
    }

    public List&amp;lt;StockInOut&amp;gt; GetStockInOuts(string? productName = null, string? barcode = null,
        StockTransactionType? type = null)
    {
        var query = _db.StockInOut
            .Include(x=&amp;gt;x.InventoryItem)
            .ThenInclude(x=&amp;gt;x.Product)
            .AsQueryable();

        if (!string.IsNullOrWhiteSpace(productName))
        {
            query = query.Where(s =&amp;gt; s.InventoryItem.Product.Name == productName);
        }

        if (!string.IsNullOrWhiteSpace(barcode))
        {
            query = query.Where(s =&amp;gt; s.InventoryItem.Barcode == barcode);
        }

        if (type.HasValue)
        {
            query = query.Where(s =&amp;gt; s.TransactionType == type.Value);
        }

        return query.ToList();
    }

    public void InsertStockInOut(StockInOut stockData)
    {
        _db.StockInOut.Add(stockData);
        _db.SaveChanges();
    }

    public void UpdateInventory(InventoryItem ivt, int stockDataAfterQuantity)
    {
        var data = _db.InventoryItem.Attach(ivt);
        ivt.Quantity = stockDataAfterQuantity;
        _db.Entry(ivt).Property(x =&amp;gt; x.Quantity).IsModified = true;
        _db.SaveChanges();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MVVM에서 Model 레이어는 View와 ViewModel처럼 사용자가 보여지지 않는 영역 전체를 말합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저장소 패턴에서 흔히 사용되는 Repository 역할의 클래스도, 비즈니스 처리만을 목적으로한 Service도 MVVM 기준에서는 모두 모델레이어 입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Repository는 직접 데이터 베이스를 사용하는 형식으로 되어있지만, 단순히 블레이저를 프론트엔드로 취급하고 Db부분 대신 API콜을 한다고해도 이부분의 역할이 달라지진 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Service레이어에서는 레포지토리를 호출해서 데이터를 저장하거나, 가져오거나 하는 일들을 중재하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스트에 대해서 정리 해보자면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Model, ViewModel, View 레이어에 해당하는 클래스의 역할에 맞게 구성하는 것이 중요합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성에 대해서는 최소 범위의 의존성을 유지하는 것은 중요하지만, 트레이드 오프에 따라서 이부분을 결정하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blazor에서는 INotifyPropertyChanged와 계약하는 방식으로 ViewModel을 구현하지 않더라도, 사용자가 변경한 사항에 대해서는 데이터의 변경이 반영됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 블레이저상에서 View의 생명주기에 대한 내용을 생각하는 것이 좋은데, 해당 내용은 제가 dotnet conf 2024 seoul 에서 발표한 내용을 참고하여 주시기 바랍니다. &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://www.youtube.com/watch?v=vQkycHdnlsQ&amp;amp;list=PLFVJi7gR5oaNapfx69uag1Z6KePIbHGfA&amp;amp;index=11&amp;amp;ab_channel=%EB%8B%B7%EB%84%B7%EB%8D%B0%EB%B8%8C&quot;&gt;(2) [.NET Conf 2024 x Seoul] Blazor with MVVM - YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>.NET/WEB</category>
      <category>blazor</category>
      <category>MVVM</category>
      <category>블레이저</category>
      <category>블레이저 아키텍처</category>
      <author>atawlee</author>
      <guid isPermaLink="true">https://devman-hoon.tistory.com/27</guid>
      <comments>https://devman-hoon.tistory.com/27#entry27comment</comments>
      <pubDate>Sun, 9 Feb 2025 17:59:11 +0900</pubDate>
    </item>
    <item>
      <title>EF Core - (부록) Project Setup</title>
      <link>https://devman-hoon.tistory.com/26</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mRPcr/btsL5NG2vap/JRk9nM0mxADlZAnutYoJKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mRPcr/btsL5NG2vap/JRk9nM0mxADlZAnutYoJKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mRPcr/btsL5NG2vap/JRk9nM0mxADlZAnutYoJKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmRPcr%2FbtsL5NG2vap%2FJRk9nM0mxADlZAnutYoJKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;301&quot; height=&quot;284&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 작업을 위해서 두가지 프로젝트를 생성하였는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository 프로젝트는 DbContext를 직접사용하여 Database에 여러가지 명령을 보내는 역할을 하는 클래스들을 넣을 프로젝트 입니다. 클래스 라이브러리 프로젝트를 생성하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application 프로젝트는 여러 DI처리와 콘솔 입력에 대한 처리를 할 프로젝트인데, 콘솔 어플리케이션 프로젝트로 생성하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 Database와 연결하는 어플리케이션의 역할은 웹 백엔드가 하는 것이 일반적 이지만, 꼭 웹이 아니더라도 위와같이 구성하여 연결 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 프로젝트라 하더라도 리포지토리의 내용이 변하지 않는다는 점과, 더 간략한 실행을 보여주기 위해서 적합한 방법으로 콘솔 어플리케이션으로 구성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3HFAM/btsL6StNNUb/gGe9lsYe059YFWXgDGYOo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3HFAM/btsL6StNNUb/gGe9lsYe059YFWXgDGYOo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3HFAM/btsL6StNNUb/gGe9lsYe059YFWXgDGYOo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3HFAM%2FbtsL6StNNUb%2FgGe9lsYe059YFWXgDGYOo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;249&quot; height=&quot;254&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;533&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository 프로젝트는 Database 프로젝트를 참조하고 다음 코드를 ProductRepository에 추가 합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738590617821&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using Database.Context;
using Database.Entity;

namespace ClassLibrary1;

public class ProductRepository
{
    private readonly ShopDbContext _context;

    public ProductRepository(ShopDbContext context)
    {
        _context = context;
    }
    
    public List&amp;lt;Product&amp;gt; GetProducts()
    {
        return _context.Products.ToList();
    }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8lFM9/btsL6pFDsxt/KBP1ySTdbhF3gi9v2dA0ZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8lFM9/btsL6pFDsxt/KBP1ySTdbhF3gi9v2dA0ZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8lFM9/btsL6pFDsxt/KBP1ySTdbhF3gi9v2dA0ZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8lFM9%2FbtsL6pFDsxt%2FKBP1ySTdbhF3gi9v2dA0ZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;277&quot; height=&quot;381&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application 프로젝트는 Repository 프로젝트를 참조합니다. (Database 프로젝트는 Repository가 가지고 있기 때문에 자동 참조됩니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지는 Microsoft.Extensions.Hosting을 다운받아 참조합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Migrator에서 사용한 appsettings.json을 복사해서 Application 프로젝트에 옮겨줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Program.cs에 다음 코드를 작성합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738590578055&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using Repository;
using Database.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =&amp;gt;
    {
        var cons = context.Configuration.GetConnectionString(&quot;DefaultConnection&quot;);
        services.AddTransient&amp;lt;ProductRepository&amp;gt;(); // 의존성 등록
        services.AddDbContextPool&amp;lt;ShopDbContext&amp;gt;(x =&amp;gt; x.UseNpgsql(cons));
    })
    
    .Build();

var service = host.Services.GetRequiredService&amp;lt;ProductRepository&amp;gt;();
var products = service.GetProducts();
foreach (var product in products)
{
    Console.WriteLine(product.ProductName);
}

Console.ReadLine();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Program.cs에서 위와같이 제네릭 호스트를 사용해서 Repository와 DbContext를 주입하고 Connection String을 연결해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행해보면 다음과 같이 쿼리가 조회되어야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1619&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BB623/btsL4wzFFTm/IZrxEpboAbbkz69kbhLzI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BB623/btsL4wzFFTm/IZrxEpboAbbkz69kbhLzI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BB623/btsL4wzFFTm/IZrxEpboAbbkz69kbhLzI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBB623%2FbtsL4wzFFTm%2FIZrxEpboAbbkz69kbhLzI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1619&quot; height=&quot;218&quot; data-origin-width=&quot;1619&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 생산성을 위한 EF Core 시리즈에서도 위와같은 방식으로 테스트를 진행할 예정입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 내용은 아래 git repository에서 확인 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;github link&quot; href=&quot;https://github.com/atawLee/ProductivityImprovementEFCoreORM/tree/fa0f9ccffbb06761d414d874d9535617ce6e4706&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;atawLee/ProductivityImprovementEFCoreORM at fa0f9ccffbb06761d414d874d9535617ce6e4706&lt;/a&gt;&lt;/p&gt;</description>
      <category>.NET/Database</category>
      <category>EF Core</category>
      <category>entityframework</category>
      <category>제네릭호스트</category>
      <author>atawlee</author>
      <guid isPermaLink="true">https://devman-hoon.tistory.com/26</guid>
      <comments>https://devman-hoon.tistory.com/26#entry26comment</comments>
      <pubDate>Mon, 3 Feb 2025 22:59:41 +0900</pubDate>
    </item>
    <item>
      <title>EF Core - Code First</title>
      <link>https://devman-hoon.tistory.com/25</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Code First란 ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 스키마를 코드에서 정의하고, 이를 기반으로 데이터베이스를 생성하거나 유지보수하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;생산성을 향상하는 부분에서 Code First 방식이 필요한 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다보면 종종 SQL작업과 코드를 번갈아가면서 작업해야하는 경우가 종종 발생합니다. 하지만 여러 언어를 전환하면서 작업하는 것은 상당히 피로한 일 입니다. SQL 역시 마찬가지 입니다. 백엔드 개발자라면 SQL에 대해서 어느정도 이상 사용할 수 있는 것이 너무나도 당연하지만 전환작업에서의 피로감은 피할 수 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 부분에서 코드 퍼스트 방식을 사용하게 되면, 데이터베이스 스키마 정의와 성능상에서 지장이 없는 부분에 쿼리에 대해서 기존 개발언어만 사용해서 개발 할 수 있습니다. C#의 EF Core는 dotnet cli를 통해 굉장히 편하게 사용할 수 있도록 제공해주고 있습니다. 이번 포스트를 통해서 어떻게 C# EF Core로 코드퍼스트 방식으로 Database를 관리하는지 소개합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스트는 JetBrains Rider를 사용하여 작성하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JetBrains Rider도 Visual Studio 처럼 커뮤니티 버전은 이제 무료로 사용 할 수 있으니 설치만 하고 따라하시면 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제는 eShop을 시뮬레이션 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;준비 사항&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터베이스 생성&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 docker에 위와 같이 postgresql 도커를 생성하고 실행하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738072030760&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run --name postgres-container -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=dldpvmzhdj12!@ -e POSTGRES_DB=shop -p 5432:5432 -v postgres_data:/var/lib/postgresql/data -d postgres&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프로젝트 생성&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;1377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2wQq9/btsLKcUWz1b/ri5ijrfTpo5CNZXKeKADmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2wQq9/btsLKcUWz1b/ri5ijrfTpo5CNZXKeKADmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2wQq9/btsLKcUWz1b/ri5ijrfTpo5CNZXKeKADmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wQq9%2FbtsLKcUWz1b%2Fri5ijrfTpo5CNZXKeKADmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;631&quot; height=&quot;490&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;1377&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 솔루션 &amp;gt; 클래스 라이브러리&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타깃 프레임워크&amp;nbsp; &amp;gt; net9.0&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔루션 이름은 자유롭게 적어주시고, 프로젝트 이름은 Database라고 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;패키지 설치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;1336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOqniM/btsLJvU9m6O/TkKRK3o2X6gGwswplkoDsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOqniM/btsLJvU9m6O/TkKRK3o2X6gGwswplkoDsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOqniM/btsLJvU9m6O/TkKRK3o2X6gGwswplkoDsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOqniM%2FbtsLJvU9m6O%2FTkKRK3o2X6gGwswplkoDsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;411&quot; height=&quot;558&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;1336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1846&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eup9a0/btsLK07pcQm/UwcPdKpJISDitGpFVO6wo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eup9a0/btsLK07pcQm/UwcPdKpJISDitGpFVO6wo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eup9a0/btsLK07pcQm/UwcPdKpJISDitGpFVO6wo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feup9a0%2FbtsLK07pcQm%2FUwcPdKpJISDitGpFVO6wo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;608&quot; height=&quot;134&quot; data-origin-width=&quot;1846&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 탐색기에서 Database 프로젝트를 우클릭 후 Nuget 패키지 관리를 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 하단 패키지 관리자에서 그림에 나온 세가지 패키지를 Database 프로젝트에 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 database.csproj 파일이 있는 위치에서&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736583905479&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 9.0.0
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 9.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널로 위의 커맨드을 입력하시면 패키지가 설치됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 패키지가 어떤 역할을 하는지는 이전 포스트에서 다룬바 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 간단하게 Todo List를 기록하는 데이터베이스를 만들어 보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;디렉토리 구조 작업&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 전에 DB엔터티와 연결을 담당하는 객체인 DbContext를 넣어둘 디렉토리를 생성하여 구조를 잡습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;333&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm0XFN/btsLKdfkKC1/gjb3wtOoDDTKDhKjTscqc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm0XFN/btsLKdfkKC1/gjb3wtOoDDTKDhKjTscqc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm0XFN/btsLKdfkKC1/gjb3wtOoDDTKDhKjTscqc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm0XFN%2FbtsLKdfkKC1%2Fgjb3wtOoDDTKDhKjTscqc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;281&quot; height=&quot;152&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;333&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity 폴더에는 각 데이터베이스 테이블 스키마가 들어갈 예정이고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context에는 Db와 연결하는데 사용하는 DbContext 관련코드가 들어갈 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Product 스키마 클래스 생성&lt;/h4&gt;
&lt;pre id=&quot;code_1736584404495&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Product
{
    [Key]
    public int ProductId { get; set; }
    
    public ProductCategory Category { get; set; }

    [Required]
    [MaxLength(255)]
    public string ProductName { get; set; }

    [MaxLength(500)]
    public string? Description { get; set; }

    [Column(TypeName = &quot;decimal(10,2)&quot;)]
    public decimal Price { get; set; }
    
    public DateTimeOffset CreatedAt { get; set; } = DateTime.UtcNow;
}

public enum ProductCategory
{
    Electronic,
    Food,
    Clothing
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity 폴더에 Production.cs를 작성해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProductionCategory는 EnumType 이지만, 특별한 설정이 없다면 Database에서 int타입으로 인식합니다. EF Core에서 Enum 타입으로 넘겨주게 되면, 사용하는 개발자가 조금 더 편하게 이타입이 어떤 타입인지 인식하기가 편한 장점이 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String 타입은 code first시에는 반드시 타입 길이를 작성하는게 좋습니다. 작성하지 않을경우 최대길이로 생성됩니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DbContext 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Db와 연결되는 DbContext를 작성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736585805199&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore;
using Database.Entity;

namespace Database.Context;

public partial class ShopDbContext : DbContext
{
    public DbSet&amp;lt;Product&amp;gt; Products { get; set; }
    
    public ShopDbContext(DbContextOptions&amp;lt;ShopDbContext&amp;gt; options) 
        :base(options)
    {
        
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 이렇게 작성해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;dotnet tool ef 설치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이것을 실제 DataBase에 옮겨야합니다. 그전에 dotnet ef cli가 설치되어 있지 않다면 설치해주세요&lt;/p&gt;
&lt;pre id=&quot;code_1736585985848&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dotnet tool install --global dotnet-ef&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션 작업은 Database 프로젝트가 아닌 또 다른 프로젝트를 생성하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션 버전 관리 코드와 스키마와 dbcontext 코드가 섞여있는 것을 원하지 않기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;1357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpVDct/btsLKthSqKz/lKETc7jflmYPjW7u9Ft3dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpVDct/btsLKthSqKz/lKETc7jflmYPjW7u9Ft3dK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpVDct/btsLKthSqKz/lKETc7jflmYPjW7u9Ft3dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpVDct%2FbtsLKthSqKz%2FlKETc7jflmYPjW7u9Ft3dK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;481&quot; height=&quot;473&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;1357&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PIS 솔루션 파일을 우클릭 &amp;gt; 추가 &amp;gt; 새 프로젝트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까와 마찬가지로 net9 버전의 클래스라이브러리로 Migrator 프로젝트를 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;1072&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6Ed00/btsLKJSip52/v0koeHUfLSiMko1J5I94m0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6Ed00/btsLKJSip52/v0koeHUfLSiMko1J5I94m0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6Ed00/btsLKJSip52/v0koeHUfLSiMko1J5I94m0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6Ed00%2FbtsLKJSip52%2Fv0koeHUfLSiMko1J5I94m0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;472&quot; height=&quot;414&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;1072&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 Migration 프로젝트를 우클릭 &amp;gt; 추가 &amp;gt; 참조 &amp;gt;Database 체크 &amp;gt; 추가 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;483&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHtBNT/btsLLSAX1jS/8RsQqTHOkAnrx0QiybJktk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHtBNT/btsLLSAX1jS/8RsQqTHOkAnrx0QiybJktk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHtBNT/btsLLSAX1jS/8RsQqTHOkAnrx0QiybJktk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHtBNT%2FbtsLLSAX1jS%2F8RsQqTHOkAnrx0QiybJktk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;346&quot; height=&quot;219&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;483&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 종속 요소 &amp;gt; 프로젝트를 확장해서 보면 방금 만든 Database 프로젝트가 들어가있어야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EF Core는 콘솔, asp.net core와 같은 실행 프로젝트가 아닌 라이브러리 프로젝트에서 코드 작성 타이밍에 DB 마이그레이션을 할수 있도록 DesignTimeFactory를 제공합니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Migration 프로젝트 패키지 설치&amp;nbsp;&lt;/h4&gt;
&lt;pre id=&quot;code_1736587100554&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
dotnet add package Microsoft.Extensions.Configuration.Json --version 9.0.0
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 9.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json 값으로 connectionstring을 받기위한 패키지와 sqlite 쿼리 프로바이더 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dbms는 실습을 위해 간단한 Sqlite로 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;디자인 타임 팩토리 코드 작성&amp;nbsp;&lt;/h4&gt;
&lt;pre id=&quot;code_1736587988195&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using Database.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;

namespace Migrator;

public class TodoDbContextFactory : IDesignTimeDbContextFactory&amp;lt;TodoDbContext&amp;gt;
{
    public TodoDbContext CreateDbContext(string[] args)
    {
        IConfigurationRoot configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile($&quot;appsettings.json&quot;)
            .Build();

        var builder = new DbContextOptionsBuilder&amp;lt;TodoDbContext&amp;gt;();
        var connectionString = configuration.GetConnectionString(&quot;DefaultConnection&quot;);

        builder.UseSqlite(connectionString
            , b=&amp;gt; b.MigrationsAssembly(&quot;Migrator&quot;));

        return new TodoDbContext(builder.Options);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션 프로젝트에 위와같이 작성합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json에서 커넥션 스트링을 가져오도록 처리하고 싶으므로 appsettings.json도 작성해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736588040731&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;ConnectionStrings&quot;: {
    &quot;DefaultConnection&quot;: &quot;Server=localhost;Port=5432;Database=shop;Username=postgres;Password=dldpvmzhdj12!@;&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게하면 현재위치에 database.db 파일을 생성하고 관리하도록 되어있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터베이스에 반영하기&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Migrator.csproj 가 있는 위치에서 아래 터미널을 실행해야합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736588806907&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dotnet ef migrations add init
dotnet ef database update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 진행되었다면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;908&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnogL1/btsL30Fi2IF/Kv8nF18dcmG3spRKYr6wE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnogL1/btsL30Fi2IF/Kv8nF18dcmG3spRKYr6wE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnogL1/btsL30Fi2IF/Kv8nF18dcmG3spRKYr6wE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnogL1%2FbtsL30Fi2IF%2FKv8nF18dcmG3spRKYr6wE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;153&quot; height=&quot;908&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;908&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;661&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D1lPq/btsL2bVOQNB/aIQFVYD5Y8Zf0pjFUUDy5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D1lPq/btsL2bVOQNB/aIQFVYD5Y8Zf0pjFUUDy5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D1lPq/btsL2bVOQNB/aIQFVYD5Y8Zf0pjFUUDy5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD1lPq%2FbtsL2bVOQNB%2FaIQFVYD5Y8Zf0pjFUUDy5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;374&quot; height=&quot;370&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;661&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 데이터베이스에 정상적으로 Products 테이블이 생성되고, 마이그레이션 정보가 등록된 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후에 엔터티가 추가되는 경우에는 다시한번&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dotnet ef Migrations add &amp;lt;마이그레이션명칭&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dotnet ef database update를 통해서&amp;nbsp; 다시 마이그레이션 해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주의할 점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드퍼스트 방식을 사용 할때 주의할 점은 코드퍼스트 이후에 db에서 직접 변경시에는 마이그레이션 내용과 실제 db의 내용에 차이가 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 마이그레이션으로 처리가 안되는 상황이 온다면, 마이그레이션 파일과 테이블을 모두 지우고, 전체 스캐폴드를 받아 초기 마이그레이션으로 진행하거나, 그 상황 시점부터 DB First 방식으로 진행되어야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 방식에는 팀내에서의 합의가 되어있어야 정상적으로 활용이 가능합니다.&amp;nbsp;&lt;/b&gt;개인적으로 개발할 때에는 DB부분에 대한 관리가 적어지므로써 얻는 장점이 매우 크기 때문에, Code First는 추천합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 팀내에서 개발하고, 전문적인 DBA 분들과 함께한다면, Code First를 하지 않는게 더 좋을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 Code First 하지 않더라도 EF Core를 활용해서 얻을 수 있는 장점들은 많이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스트에서는 EF Core의 Table Mapping 방식에 대해서 설명하겠습니다.&lt;/p&gt;</description>
      <category>.NET/Database</category>
      <category>codefirst</category>
      <category>ef</category>
      <category>EF Core</category>
      <category>EFcore</category>
      <category>entityframework</category>
      <category>ORM</category>
      <category>코드퍼스트</category>
      <author>atawlee</author>
      <guid isPermaLink="true">https://devman-hoon.tistory.com/25</guid>
      <comments>https://devman-hoon.tistory.com/25#entry25comment</comments>
      <pubDate>Sat, 11 Jan 2025 18:58:07 +0900</pubDate>
    </item>
    <item>
      <title>EF Core - Domain 분리구조 만들기</title>
      <link>https://devman-hoon.tistory.com/24</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;DDD ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도메인&lt;/b&gt; 소프트웨어로 해결해야 할 비즈니스의 영역 입니다.&lt;br /&gt;&lt;b&gt;도메인 엔티티&lt;/b&gt; 도메인 주도 개발에서 실제 식별 가능한 객체&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;EF Entity&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 &lt;b&gt;테이블에 저장되는 데이터의 구조&lt;/b&gt;&lt;br /&gt;즉 테이블을 클래스화 한 것을 EF Entity라고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;EF Entity를 Domain Entity와 동일하게 사용할 경우의 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EF 엔티티를 도메인 엔티티로 그대로 사용하는 경우, 다음과 같은 문제가 발생할 수 있습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;도메인의 데이터베이스 의존성 증가&lt;/b&gt;: 도메인 엔티티가 EF 엔티티로 구현되면, 데이터베이스의 스키마 변경이 도메인 로직에 직접적인 영향을 미치게 됩니다. 이는 도메인 로직이 데이터베이스 설계에 종속되게 만들어, 도메인 모델의 변경이 불필요하게 자주 일어나게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스 스키마 노출&lt;/b&gt;: EF 엔티티가 도메인 엔티티로 사용되면, 도메인 계층이 데이터베이스의 구조를 알아야 하므로, 비즈니스 로직을 개발하는 개발자들이 데이터베이스 스키마에 지나치게 의존하게 됩니다. 이는 비즈니스 로직과 데이터 저장소 간의 분리를 어렵게 만들고, 코드의 유지보수를 복잡하게 만듭니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분리된 레이어 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해, 도메인 엔티티와 EF 엔티티를 분리하여 설계하는 것이 권장됩니다. 이를 통해 각 계층 간의 결합도를 낮추고, 시스템의 유연성과 유지보수성을 높일 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구조적인 접근:&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;도메인 엔티티 (Domain Entity)&lt;/b&gt;: 비즈니스 로직과 비즈니스 규칙을 담고 있는 순수한 객체. 도메인의 개념과 요구사항을 기반으로 설계되며, 데이터베이스나 외부 시스템에 종속되지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EF 엔티티 (EF Entity)&lt;/b&gt;: 데이터베이스의 테이블에 대응되는 클래스로, 데이터 저장 및 조회 기능을 담당합니다. EF를 사용하여 이 엔티티들은 데이터베이스와 직접적으로 상호작용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DTO(Data Transfer Object)&lt;/b&gt;: 데이터 전송 객체는 애플리케이션 레이어 간에 데이터를 전달하기 위해 사용됩니다. 도메인 엔티티와 EF 엔티티 간의 변환 과정에서 주로 사용되며, 외부 API나 서비스와의 통신을 위해 데이터를 포맷팅하는 역할도 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;매퍼 클래스 (Mapper Class)&lt;/b&gt;: 도메인 엔티티와 EF 엔티티 간의 변환을 담당하는 클래스입니다. 매퍼 클래스는 양쪽 엔티티 간의 데이터를 변환하여 전달하는 기능을 수행합니다. 이는 도메인 로직과 데이터베이스의 결합도를 낮추는 데 중요한 역할을 합니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나누어 놨을 경우에는 초기 개발시에는 코드량이 더 늘어나게 됩니다. 하지만 처음에 아무리 잘 만들어 놨다고 하더라도 계속 서비스가 유지되는 프로그램의 경우에는 지속적인 변경이 일어나게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB와 직결되어있는 ORM레이어의 엔티티 클래스와, 비즈니스로직을 다루는 도메인레이어의 엔티티 클래스가 분리되어있지 않다면, 한쪽이 바꼈을때 의존성이 걸려있는 모든 부분들을 같이 수정해야합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 도메인 엔티티와 EF 엔티티를 분리하여 설계함으로써, 데이터베이스의 변경이 비즈니스 로직에 미치는 영향을 최소화할 수 있으며, 시스템의 유지보수성을 향상시킬 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>.NET/Database</category>
      <category>DDD</category>
      <category>ef</category>
      <category>entityframework</category>
      <category>ORM</category>
      <author>atawlee</author>
      <guid isPermaLink="true">https://devman-hoon.tistory.com/24</guid>
      <comments>https://devman-hoon.tistory.com/24#entry24comment</comments>
      <pubDate>Fri, 22 Mar 2024 08:42:16 +0900</pubDate>
    </item>
    <item>
      <title>devfest 2023 - signalr_netcore</title>
      <link>https://devman-hoon.tistory.com/23</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/atawLee/devfest2023.git&quot;&gt;https://github.com/atawLee/devfest2023.git&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련소스는 여기서 확인 가능합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SignalR&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SignalR은 실시간 웹 통신기능을 사용하기 편하게 만든 라이브러리 입니다.&lt;br /&gt;기본은 WebSocket방식이지만 Long Polling등의 방식도 지원합니다.&lt;br /&gt;SignalR은 닷넷기반 라이브러리이므로 사용하려면 닷넷 서버여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버측은 닷넷에 종속되어있지만 Client로는 닷넷뿐만 아니라 js,python,dart등을 지원하는 라이브러리들이 많습니다.&lt;br /&gt;이번 devfest에서 소스에서 signalR에 대한 내용을 약간 넣었는데 해당 부분에 대한 질문을 하신분이 계셔 포스트를 작성하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이포스트에서는 SignalR 서버를 구축하는 방법을 살짝 살펴보고&lt;br /&gt;플러터 signalr_netcore에 대한 내용을 보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SignalR 서버를 구축하는 내용은 아주 간단합니다.&lt;br /&gt;서버는 닷넷에 종속되어있으므로 닷넷관련 개발도구를 설치해야합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SignalR .NET Server&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 webapi 프로젝트를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;dotnet new webapi -n server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 weather관련된 샘플소스가 program.cs에 담겨있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 signalR 패키지를 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;dotnet add package Microsoft.AspNetCore.SignalR.Common --version 8.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒤에 나온 버전은 사용하시는 닷넷버전에 맞춰서 사용하시면 됩니다.&lt;br /&gt;그리고 메세지를 처리할 클래스를 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;using Microsoft.AspNetCore.SignalR;
namespace ChatServer;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync(&quot;ReceiveMessage&quot;, user, message);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatHub에 SendMessage를 호출받으면 받은내용을 &quot;RceiveMessage&quot;를 구독하고 있는 사람들에게 보냅니다.&lt;br /&gt;원활한 확인을 위해 Url을 &lt;a href=&quot;http://localhost:5000%EC%9C%BC%EB%A1%9C&quot;&gt;http://localhost:5000으로&lt;/a&gt; 고정 하고,(vscode에서 f5로 실행시 launchsettings.json 기준 포트로 실행될 수 있습니다. 브라우저에서 직접 고정된 5000 포트로 변경해주세요. )&lt;br /&gt;그리고 Program.cs에서 SignalR을 사용할수 있도록 AddSwaggerGen 밑에 AddSignalR()을 추가해서 주입합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;using ChatServer;

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls(&quot;http://localhost:5000&quot;); //고정 포트 추가 코드
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSignalR(); //SignalR 추가 코드 

var app = builder.Build();
// 하단생략 &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 엔드포인트를 설정해줍니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;//상단 생략
app.MapHub&amp;lt;ChatHub&amp;gt;(&quot;/Chat&quot;);

app.Run();
//하단 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 채팅 서버 셋팅이 완료 되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SignalR Flutter Client&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러터 프로젝트를 만들고&lt;br /&gt;signalr_netcore를 설치해주세요&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;flutter pub add signalr_netcore&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 발표에서는 signalr_core를 사용했지만, 현재는 pubdev를 확인해보니 netcore가 더 최신버전이 올라와 해당버전을 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;signalr_core는 다음과 같은 프로토콜을 지원합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;signalr_netcore 전송 프로토콜&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebSocket&lt;/li&gt;
&lt;li&gt;Service Side Events&lt;/li&gt;
&lt;li&gt;Long Polling&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 프로토콜&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;json&lt;/li&gt;
&lt;li&gt;MessagePack&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 signalR Server와 호환되는 프로토콜입니다.&lt;br /&gt;별도의 설정을 하지않았을때 사용되는 방식은 WebSocket과 송수신하고, 데이터는 json으로 포맷팅 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 git repository에 코드가 모두 공개되어있고 어떻게 사용하는지 testapp 디렉토리에 자세하게 나와있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/sefidgaran/signalr_client&quot;&gt;sefidgaran/signalr_client: A Flutter SignalR Client for ASP.NET Core (github.com)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[GitHub - sefidgaran/signalr_client: A Flutter SignalR Client for ASP.NET Core&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A Flutter SignalR Client for ASP.NET Core. Contribute to sefidgaran/signalr_client development by creating an account on GitHub.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github.com](&lt;a href=&quot;https://github.com/sefidgaran/signalr_client&quot;&gt;https://github.com/sefidgaran/signalr_client&lt;/a&gt;)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HubConnectionBuilder&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허브커넥션 빌더는 허브 커넥션에대한 설정을 빌더패턴으로 생성할수있게 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;hubConnection = HubConnectionBuilder()
        .withUrl(&quot;$url/Chat&quot;)
        .withAutomaticReconnect()
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 설정은 withUrl을 설정하고 그냥 build하는 것 입니다.&lt;br /&gt;위의 설정에서는 withAutomaticReconnect() 설정이 있는데 연결이 끊길시 자동으로 재연결 시도를 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그외에 기능으로는 logging 설정을 지정할수있는 configureLogging(Logger logger)&lt;br /&gt;데이터 프로토콜 방식을 지정할 수 있는 withHubProtocol(IHubProtocol protocol) 옵션들이 있으니 필요한 옵션들을 설정하고 build 하면&lt;br /&gt;HubConnection 클래스의 인스턴스가 반환됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HubConnection&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HubConnection Method&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;invoke(String methodName, {List? args}) &amp;rarr; Future&amp;lt;Object?&amp;gt;&lt;br /&gt;send(String methodName, {List? args}) &amp;rarr; Future&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;invoke와 send 모두 signalr서버에 명령을 보내는 메서드 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 메서드의 다른점은 invoke는 반환타입을 가진다는 점 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;invoke에서는 메서드에 대한 응답을 대기할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;send method는 반면에 응답을 받지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;on(String methodName, MethodInvocationFunc newMethod) &amp;rarr; void&lt;br /&gt;off(String methodName, {MethodInvocationFunc? method}) &amp;rarr; void&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;on Method는 SignalR서버의 특정 메서드에 대해서 구독을 시작한다. 라는 개념으로 생각하면 편합니다.&lt;br /&gt;서버측에서 methodName으로 지정된 항목에 대해서 발행되게 되면 flutter client에서는 on으로 등록해놓은 mehtod를 실행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 서버에서는 SendMessage를 받을때마다 ReceiveMessage를 구독하고 있는 대상에게 string user, string message를 전달해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;off Method는 구독을 해제하는 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stream(String methodName, List args) &amp;rarr; Stream&amp;lt;Object?&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stream 메서드는 연속적인 수신을 받을때 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;  public async IAsyncEnumerable&amp;lt;string&amp;gt; StreamMethod(CancellationToken cancellationToken)
    {
        for (var i = 0; i &amp;lt; 10; i++)
        {
            await Task.Delay(1000, cancellationToken); // 1초 간격
            yield return $&quot;Message {i}&quot;;
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같이 서버에서 한번에 전체리스트가 오는것이 아니라 yield return으로 순차적으로 실행되어 메세지가 오는 방식이라면 stream으로 처리해서 yield return이 발생될때마다 특정메서드를 실행시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onclose(ClosedCallback callback) &amp;rarr; void&lt;br /&gt;onreconnected(ReconnectedCallback callback) &amp;rarr; dynamic&lt;br /&gt;onreconnecting(ReconnectingCallback callback) &amp;rarr; dynamic&lt;br /&gt;위 메서드들은 각이벤트들이 발생될때 콜백을 발생시키는 메서드입니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;import 'package:signalr_netcore/hub_connection.dart';
import 'package:signalr_netcore/hub_connection_builder.dart';

abstract class ChatClient {
  Future&amp;lt;void&amp;gt; connect();
  Future&amp;lt;void&amp;gt; sendMessage(String user, String content);
  void setReceiveMessage(
      Function(String user, String message) onReceiveMessage);
  void close();
}

class SignalRChatClient extends ChatClient {
  late HubConnection hubConnection;
  String url;

  SignalRChatClient({required this.url});

  @override
  Future&amp;lt;void&amp;gt; connect() async {
    hubConnection = HubConnectionBuilder()
        .withUrl(&quot;$url/Chat&quot;)
        .withAutomaticReconnect()
        .build();

    await hubConnection.start();
  }

  @override
  Future&amp;lt;void&amp;gt; sendMessage(String user, String content) async {
    await hubConnection.send('SendMessage', args: [user, content]);
  }

  @override
  void setReceiveMessage(
      Function(String user, String message) onReceiveMessage) {
    hubConnection.on('ReceiveMessage', (arguments) {
      String user = arguments![0] as String;
      String message = arguments[1] as String;
      onReceiveMessage(user, message);
    });
  }

  @override
  void close() {
    hubConnection.stop();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 hubConnection을 바탕으로 ChatClient 클래스로 추상화하고 SignalrChatClient 클래스로 구현클래스를 구성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결시에는 HubConnectionBuilder의 옵션을 추가해서 적절히 필요한 옵션을 넣고 build한뒤&lt;br /&gt;HubConnection 개체의 각 메서드들을 필요에 따라서 사용하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SignalR은 위와 같이 복잡한 단순하게 receive에 대해서는 subscribe 형식으로 사용해서 필요한 메세지를 받아볼 수 있습니다.&lt;br /&gt;위에 설명한 내용 외에도 signalr_netcore 샘플코드를 보면 Header를 추가하는 내용 또한 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 받아 추가적인 내용을 작성하였으나, 어느 부분에 대해서 더 알고싶은지 물어보았으면 더 좋았을듯 합니다.&lt;br /&gt;그래도 오늘 작성해둔 내용이 도움이 되셨기를 바랍니다.&lt;/p&gt;</description>
      <category>발표/후속 자료</category>
      <author>atawlee</author>
      <guid isPermaLink="true">https://devman-hoon.tistory.com/23</guid>
      <comments>https://devman-hoon.tistory.com/23#entry23comment</comments>
      <pubDate>Sat, 30 Dec 2023 00:04:27 +0900</pubDate>
    </item>
  </channel>
</rss>