C# 및 .NET 면접 질문: 2026년 완벽 가이드

가장 자주 출제되는 C# 및 .NET 면접 질문 17선입니다. LINQ, async/await, 의존성 주입, Entity Framework, ASP.NET Core 등 상세한 답변과 코드 예제를 다룹니다.

C# 및 .NET 면접 질문 - 완벽 가이드

C# 및 .NET 면접에서는 언어 숙련도, Microsoft 생태계에 대한 이해, 견고하고 고성능인 애플리케이션을 설계하는 능력을 평가합니다. 이 가이드에서는 언어 기초부터 고급 아키텍처 패턴까지 핵심 질문을 다룹니다.

면접 팁

면접관은 단순한 문법 지식이 아니라 .NET 내부 메커니즘에 대한 이해를 보여주는 답변을 높이 평가합니다. 각 개념의 "왜"를 설명하는 것이 합격과 불합격을 가르는 핵심입니다.

C# 기초

질문 1: 값 타입과 참조 타입의 차이점은 무엇입니까?

이 근본적인 차이는 메모리 할당, 성능, 매개변수 전달 시 동작에 영향을 줍니다.

ValueVsReference.cscsharp
// Demonstrating behavior differences

// VALUE TYPES: stored on the Stack, copied by value
struct Point
{
    public int X;
    public int Y;
}

// REFERENCE TYPES: stored on the Heap, copied by reference
class Person
{
    public string Name;
}

public class Demo
{
    public static void Main()
    {
        // Value type: independent copy
        Point p1 = new Point { X = 10, Y = 20 };
        Point p2 = p1;  // Complete value copy
        p2.X = 100;     // Does NOT modify p1
        Console.WriteLine($"p1.X = {p1.X}"); // 10

        // Reference type: same object in memory
        Person person1 = new Person { Name = "Alice" };
        Person person2 = person1;  // Reference copy
        person2.Name = "Bob";      // MODIFIES person1 too
        Console.WriteLine($"person1.Name = {person1.Name}"); // Bob

        // Special case: string is immutable
        string s1 = "Hello";
        string s2 = s1;
        s2 = "World";  // Creates a new string
        Console.WriteLine($"s1 = {s1}"); // Hello
    }
}

값 타입(int, struct, enum)은 스택에 할당되며 자동으로 해제됩니다. 참조 타입(class, interface, delegate)은 힙에 할당되며 가비지 컬렉터가 관리합니다.

질문 2: ref, out, in 키워드를 설명하십시오

이 수식자들은 메서드에 매개변수를 전달하는 방식을 제어하며, 성능과 가변성에 영향을 줍니다.

ParameterModifiers.cscsharp
// The three pass-by-reference modifiers

public class ParameterDemo
{
    // REF: variable MUST be initialized before the call
    // Can be read AND modified in the method
    public static void ModifyWithRef(ref int value)
    {
        Console.WriteLine($"Received value: {value}");
        value = value * 2;  // Modification visible to caller
    }

    // OUT: variable does NOT need to be initialized
    // MUST be assigned before method exits
    public static bool TryParse(string input, out int result)
    {
        // result MUST be assigned in all execution paths
        if (int.TryParse(input, out result))
        {
            return true;
        }
        result = 0;  // Required assignment
        return false;
    }

    // IN: read-only pass-by-reference (C# 7.2+)
    // Avoids copying for large structs without allowing modification
    public static double CalculateDistance(in Point3D p1, in Point3D p2)
    {
        // p1.X = 10; // ERROR: cannot modify 'in' parameter
        return Math.Sqrt(
            Math.Pow(p2.X - p1.X, 2) +
            Math.Pow(p2.Y - p1.Y, 2) +
            Math.Pow(p2.Z - p1.Z, 2)
        );
    }

    public static void Main()
    {
        // Using ref
        int number = 5;
        ModifyWithRef(ref number);
        Console.WriteLine($"After ref: {number}"); // 10

        // Using out
        if (TryParse("123", out int parsed))
        {
            Console.WriteLine($"Parsed: {parsed}"); // 123
        }

        // Using in (optimal for large structs)
        var point1 = new Point3D(0, 0, 0);
        var point2 = new Point3D(3, 4, 0);
        var distance = CalculateDistance(in point1, in point2);
    }
}

public readonly struct Point3D
{
    public readonly double X, Y, Z;
    public Point3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);
}

in은 복사를 방지하면서 불변성을 보장하므로 큰 구조체에 특히 유용합니다. 고성능 코드에서 자주 사용되는 패턴입니다.

in의 성능

16바이트보다 큰 구조체에 in을 사용하면 복사를 방지하여 성능이 향상됩니다. 작은 구조체의 경우 값에 의한 전달이 여전히 더 효율적입니다.

질문 3: .NET의 가비지 컬렉터는 어떻게 동작합니까?

.NET GC는 세대별 알고리즘을 사용하여 자동 메모리 관리를 최적화합니다.

GarbageCollectorDemo.cscsharp
// Understanding GC behavior

public class GCDemo
{
    public static void DemonstrateGenerations()
    {
        // Generation 0: newly allocated objects
        var shortLived = new byte[1000];
        Console.WriteLine($"Generation: {GC.GetGeneration(shortLived)}"); // 0

        // Force collection to promote the object
        GC.Collect();
        Console.WriteLine($"After GC: {GC.GetGeneration(shortLived)}"); // 1

        GC.Collect();
        Console.WriteLine($"After 2nd GC: {GC.GetGeneration(shortLived)}"); // 2

        // Memory statistics
        var info = GC.GetGCMemoryInfo();
        Console.WriteLine($"Total heap: {info.HeapSizeBytes / 1024 / 1024}MB");
    }

    // IDisposable pattern for unmanaged resources
    public class DatabaseConnection : IDisposable
    {
        private IntPtr _nativeHandle;
        private bool _disposed = false;

        public DatabaseConnection()
        {
            _nativeHandle = AllocateNativeResource();
        }

        // Public Dispose method
        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);  // Prevents finalizer call
        }

        // Protected Dispose pattern
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    // Free managed resources
                }

                // Free unmanaged resources
                if (_nativeHandle != IntPtr.Zero)
                {
                    FreeNativeResource(_nativeHandle);
                    _nativeHandle = IntPtr.Zero;
                }

                _disposed = true;
            }
        }

        // Finalizer (destructor) - called by GC if Dispose wasn't called
        ~DatabaseConnection()
        {
            Dispose(disposing: false);
        }

        private IntPtr AllocateNativeResource() => IntPtr.Zero;
        private void FreeNativeResource(IntPtr handle) { }
    }
}

// Recommended usage with using
public class Usage
{
    public void Example()
    {
        // C# 8+: using declaration
        using var connection = new GCDemo.DatabaseConnection();
        // ... usage
        // Dispose() called automatically at end of scope
    }
}

GC는 0세대를 자주(밀리초 단위), 1세대를 가끔, 2세대를 드물게 수집합니다. LOH(Large Object Heap > 85KB) 객체는 별도로 처리됩니다.

LINQ와 컬렉션

질문 4: IEnumerable과 IQueryable의 차이점은 무엇입니까?

이 질문은 지연 실행과 쿼리 성능을 이해하는 데 핵심적입니다.

EnumerableVsQueryable.cscsharp
// Fundamental execution differences

public class LinqDemo
{
    public static void CompareExecution(AppDbContext context)
    {
        // IEnumerable: executes IN MEMORY (client-side)
        IEnumerable<Product> enumerable = context.Products.AsEnumerable();
        var filteredEnum = enumerable
            .Where(p => p.Price > 100)  // Filtering in C#
            .ToList();
        // Generated SQL: SELECT * FROM Products (ALL loaded)

        // IQueryable: executes on DATABASE (server-side)
        IQueryable<Product> queryable = context.Products;
        var filteredQuery = queryable
            .Where(p => p.Price > 100)  // Translated to SQL WHERE
            .ToList();
        // Generated SQL: SELECT * FROM Products WHERE Price > 100

        // Query composition with IQueryable
        var query = context.Products.AsQueryable();

        // Each operation adds to the Expression Tree
        query = query.Where(p => p.IsActive);
        query = query.Where(p => p.CategoryId == 5);
        query = query.OrderBy(p => p.Name);

        // Execution happens HERE, with a single optimized SQL query
        var results = query.ToList();
    }

    // Generic method that works with both
    public static IEnumerable<T> FilterByCondition<T>(
        IEnumerable<T> source,
        Func<T, bool> predicate)
    {
        return source.Where(predicate);
    }

    // Optimized version for IQueryable
    public static IQueryable<T> FilterByCondition<T>(
        IQueryable<T> source,
        Expression<Func<T, bool>> predicate)
    {
        // Expression<Func<>> enables SQL translation
        return source.Where(predicate);
    }
}

Entity Framework에서는 IQueryable을 사용하여 데이터베이스 측에서 필터링을 수행해야 합니다. IEnumerable은 인메모리 컬렉션이나 모든 데이터가 이미 로드된 경우에 적합합니다.

질문 5: LINQ의 지연 실행을 설명하십시오

지연 실행은 성능과 쿼리 동작에 영향을 미치는 근본적인 개념입니다.

DeferredExecution.cscsharp
// Understanding when queries actually execute

public class DeferredExecutionDemo
{
    public static void Demonstrate()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5 };

        // Query is DEFINED but NOT EXECUTED
        var query = numbers.Where(n => {
            Console.WriteLine($"Evaluating {n}");
            return n > 2;
        });

        Console.WriteLine("Query defined, but nothing happened yet");

        // Modifying source BEFORE execution
        numbers.Add(6);
        numbers.Add(7);

        Console.WriteLine("Starting iteration:");
        // EXECUTION happens HERE during enumeration
        foreach (var n in query)
        {
            Console.WriteLine($"Result: {n}");
        }
        // Output includes 6 and 7 because they were added before execution
    }

    // Methods that FORCE immediate execution
    public static void ImmediateExecution()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5 };

        // ToList(), ToArray(), ToDictionary() = immediate execution
        var list = numbers.Where(n => n > 2).ToList();

        // Count(), First(), Single(), Any() = immediate execution
        var count = numbers.Where(n => n > 2).Count();
        var first = numbers.First(n => n > 2);

        // Aggregate(), Sum(), Max(), Min() = immediate execution
        var sum = numbers.Where(n => n > 2).Sum();
    }

    // Danger: multiple enumeration
    public static void MultipleEnumerationProblem()
    {
        var numbers = GetNumbers();  // IEnumerable returned by yield

        // WARNING: EACH use re-executes the query
        var count = numbers.Count();   // 1st enumeration
        var first = numbers.First();   // 2nd enumeration

        // SOLUTION: materialize once
        var materializedList = numbers.ToList();
        var countOk = materializedList.Count;    // No re-execution
        var firstOk = materializedList.First();  // No re-execution
    }

    private static IEnumerable<int> GetNumbers()
    {
        Console.WriteLine("GetNumbers called");
        yield return 1;
        yield return 2;
        yield return 3;
    }
}
다중 열거

ReSharper나 Rider 같은 분석기를 사용하여 미묘한 버그와 성능 문제를 일으킬 수 있는 다중 열거 문제를 감지하십시오.

Async/Await와 멀티스레딩

질문 6: async/await와 Task의 동작 원리를 설명하십시오

비동기 프로그래밍은 현대 애플리케이션에 필수적입니다. 내부 동작 원리를 이해하는 것은 고급 역량을 입증합니다.

AsyncAwaitDemo.cscsharp
// Internal mechanisms of asynchronous programming

public class AsyncDemo
{
    // async transforms the method into a state machine
    public async Task<string> FetchDataAsync(string url)
    {
        using var client = new HttpClient();

        // await releases the thread during I/O wait
        // Thread returns to pool and can process other requests
        var response = await client.GetStringAsync(url);

        // After await, execution resumes (possibly on different thread)
        return ProcessData(response);
    }

    // Pattern for parallel execution
    public async Task<(User, List<Order>)> GetUserWithOrdersAsync(int userId)
    {
        // Both calls start SIMULTANEOUSLY
        var userTask = GetUserAsync(userId);
        var ordersTask = GetOrdersAsync(userId);

        // await waits for both results
        await Task.WhenAll(userTask, ordersTask);

        return (userTask.Result, ordersTask.Result);
    }

    // ConfigureAwait for libraries
    public async Task<string> LibraryMethodAsync()
    {
        // ConfigureAwait(false) avoids capturing SynchronizationContext
        // Recommended in libraries to avoid deadlocks
        var data = await FetchDataAsync("https://api.example.com")
            .ConfigureAwait(false);

        return data.ToUpper();
    }

    // Anti-pattern: async void (except for event handlers)
    public async void BadAsyncMethod()
    {
        // Exceptions cannot be caught
        // Impossible to await completion
        await Task.Delay(100);
    }

    // Correct: async Task
    public async Task GoodAsyncMethod()
    {
        await Task.Delay(100);
    }

    private Task<User> GetUserAsync(int id) => Task.FromResult(new User());
    private Task<List<Order>> GetOrdersAsync(int id) => Task.FromResult(new List<Order>());
    private string ProcessData(string data) => data;
}

public class User { }
public class Order { }

컴파일러는 async 메서드를 상태 머신으로 변환합니다. 각 await는 스레드가 해제되는 일시 중단 지점을 나타냅니다.

질문 7: async/await에서 데드락을 방지하는 방법은 무엇입니까?

비동기 데드락은 SynchronizationContext가 있는 애플리케이션(UI, 클래식 ASP.NET)에서 전형적인 함정입니다.

DeadlockPrevention.cscsharp
// Patterns to avoid deadlocks

public class DeadlockDemo
{
    private readonly IDataService _service;

    // DEADLOCK in classic ASP.NET or WinForms/WPF
    public string GetDataDeadlock()
    {
        // .Result or .Wait() blocks the UI/Request thread
        // async tries to resume on that same thread = deadlock
        return _service.FetchAsync().Result;
    }

    // Solution 1: async all the way
    public async Task<string> GetDataAsync()
    {
        return await _service.FetchAsync();
    }

    // Solution 2: ConfigureAwait(false) in the library
    public async Task<string> FetchAsync()
    {
        var data = await HttpClient.GetStringAsync("url")
            .ConfigureAwait(false);  // Don't capture context
        return data;
    }

    // Solution 3: Task.Run to isolate (if really necessary)
    public string GetDataWithTaskRun()
    {
        // Runs on thread pool without SynchronizationContext
        return Task.Run(async () => await _service.FetchAsync()).Result;
    }

    // Pattern for proper cancellation
    public async Task<string> FetchWithCancellation(CancellationToken cancellationToken)
    {
        using var client = new HttpClient();

        try
        {
            var response = await client.GetStringAsync("url", cancellationToken);
            return response;
        }
        catch (OperationCanceledException)
        {
            // Handle cancellation gracefully
            return string.Empty;
        }
    }

    // Timeout pattern
    public async Task<string> FetchWithTimeout(TimeSpan timeout)
    {
        using var cts = new CancellationTokenSource(timeout);

        try
        {
            return await FetchWithCancellation(cts.Token);
        }
        catch (OperationCanceledException)
        {
            throw new TimeoutException("Request timed out");
        }
    }

    private static readonly HttpClient HttpClient = new();
}

public interface IDataService
{
    Task<string> FetchAsync();
}

핵심 원칙은 "async all the way"입니다. 동기 코드와 비동기 코드를 혼합하지 마십시오. ASP.NET Core에서는 SynchronizationContext가 존재하지 않으므로 데드락 위험이 줄어듭니다.

.NET 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

의존성 주입과 아키텍처

질문 8: DI 수명(Scoped, Transient, Singleton)의 차이를 설명하십시오

수명을 이해하는 것은 동시성 버그와 메모리 누수를 방지하는 데 필수적입니다.

DependencyInjectionLifetimes.cscsharp
// The three lifetimes and their implications

// SINGLETON: single instance for the entire application
public class SingletonService
{
    private readonly Guid _id = Guid.NewGuid();
    public Guid Id => _id;

    // DANGER: no mutable state without synchronization
    // private int _counter; // Possible race conditions
}

// SCOPED: one instance per HTTP request (or scope)
public class ScopedService
{
    private readonly Guid _id = Guid.NewGuid();
    public Guid Id => _id;

    // Safe: each request has its own instance
    // Ideal for DbContext, UnitOfWork
}

// TRANSIENT: new instance on every injection
public class TransientService
{
    private readonly Guid _id = Guid.NewGuid();
    public Guid Id => _id;

    // Ideal for lightweight, stateless services
}

// Configuration in Program.cs
public static class ServiceConfiguration
{
    public static void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<SingletonService>();
        services.AddScoped<ScopedService>();
        services.AddTransient<TransientService>();

        // Entity Framework: ALWAYS Scoped
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(connectionString));

        // HttpClient: use IHttpClientFactory
        services.AddHttpClient<IApiClient, ApiClient>();
    }
}

// CAPTIVE DEPENDENCY: Singleton depending on Scoped
public class BadSingletonService
{
    // WARNING: ScopedService will be captured and reused indefinitely
    // Causes concurrency bugs and stale data
    private readonly ScopedService _scoped;

    public BadSingletonService(ScopedService scoped)
    {
        _scoped = scoped;
    }
}

// SOLUTION: use IServiceScopeFactory
public class GoodSingletonService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public GoodSingletonService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task DoWork()
    {
        // Create explicit scope to get fresh ScopedService
        using var scope = _scopeFactory.CreateScope();
        var scoped = scope.ServiceProvider.GetRequiredService<ScopedService>();
        // Use scoped...
    }
}

원칙: 서비스는 자신보다 수명이 짧은 서비스에 의존해서는 안 됩니다. Singleton -> Scoped -> Transient 순서를 준수해야 합니다.

질문 9: .NET의 주요 디자인 패턴은 무엇입니까?

면접관은 패턴의 정의뿐만 아니라 실무적 지식을 기대합니다.

DesignPatterns.cscsharp
// Common patterns in C#/.NET

// REPOSITORY: data access abstraction
public interface IUserRepository
{
    Task<User?> GetByIdAsync(int id);
    Task<IEnumerable<User>> GetAllAsync();
    Task AddAsync(User user);
    Task UpdateAsync(User user);
    Task DeleteAsync(int id);
}

public class UserRepository : IUserRepository
{
    private readonly AppDbContext _context;

    public UserRepository(AppDbContext context) => _context = context;

    public async Task<User?> GetByIdAsync(int id)
        => await _context.Users.FindAsync(id);

    public async Task<IEnumerable<User>> GetAllAsync()
        => await _context.Users.ToListAsync();

    public async Task AddAsync(User user)
        => await _context.Users.AddAsync(user);

    public async Task UpdateAsync(User user)
        => _context.Users.Update(user);

    public async Task DeleteAsync(int id)
    {
        var user = await GetByIdAsync(id);
        if (user != null) _context.Users.Remove(user);
    }
}

// UNIT OF WORK: transaction coordination
public interface IUnitOfWork : IDisposable
{
    IUserRepository Users { get; }
    IOrderRepository Orders { get; }
    Task<int> SaveChangesAsync();
}

public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _context;

    public UnitOfWork(AppDbContext context)
    {
        _context = context;
        Users = new UserRepository(context);
        Orders = new OrderRepository(context);
    }

    public IUserRepository Users { get; }
    public IOrderRepository Orders { get; }

    public async Task<int> SaveChangesAsync()
        => await _context.SaveChangesAsync();

    public void Dispose() => _context.Dispose();
}

// FACTORY: complex object creation
public interface INotificationFactory
{
    INotification Create(NotificationType type);
}

public class NotificationFactory : INotificationFactory
{
    public INotification Create(NotificationType type) => type switch
    {
        NotificationType.Email => new EmailNotification(),
        NotificationType.Sms => new SmsNotification(),
        NotificationType.Push => new PushNotification(),
        _ => throw new ArgumentException($"Unknown type: {type}")
    };
}

// DECORATOR: adding behaviors dynamically
public interface IUserService
{
    Task<User> GetUserAsync(int id);
}

public class UserService : IUserService
{
    private readonly IUserRepository _repository;
    public UserService(IUserRepository repository) => _repository = repository;

    public async Task<User> GetUserAsync(int id)
        => await _repository.GetByIdAsync(id)
           ?? throw new NotFoundException($"User {id} not found");
}

// Decorator that adds caching
public class CachedUserService : IUserService
{
    private readonly IUserService _inner;
    private readonly IMemoryCache _cache;

    public CachedUserService(IUserService inner, IMemoryCache cache)
    {
        _inner = inner;
        _cache = cache;
    }

    public async Task<User> GetUserAsync(int id)
    {
        var cacheKey = $"user:{id}";

        if (_cache.TryGetValue(cacheKey, out User? cached))
            return cached!;

        var user = await _inner.GetUserAsync(id);
        _cache.Set(cacheKey, user, TimeSpan.FromMinutes(5));

        return user;
    }
}

이 패턴들은 전문적인 .NET 애플리케이션에서 매일 사용됩니다. Repository와 Unit of Work 패턴의 조합은 Entity Framework와 함께 특히 일반적입니다.

Entity Framework Core

질문 10: EF Core의 성능을 최적화하는 방법은 무엇입니까?

EF Core는 사용 방법에 따라 매우 빠를 수도 매우 느릴 수도 있습니다. 이 질문은 모범 사례에 대한 지식을 평가합니다.

EFCoreOptimization.cscsharp
// Query optimization techniques

public class EFCorePerformance
{
    private readonly AppDbContext _context;

    // N+1 problem: one query per order
    public async Task<List<User>> GetUsersWithOrdersBad()
    {
        var users = await _context.Users.ToListAsync();

        foreach (var user in users)
        {
            // N additional queries!
            var orders = await _context.Orders
                .Where(o => o.UserId == user.Id)
                .ToListAsync();
        }

        return users;
    }

    // Eager Loading with Include
    public async Task<List<User>> GetUsersWithOrdersGood()
    {
        return await _context.Users
            .Include(u => u.Orders)        // SQL JOIN
            .ThenInclude(o => o.Products)  // Nested include
            .ToListAsync();
    }

    // Projection to load only necessary data
    public async Task<List<UserDto>> GetUserSummaries()
    {
        return await _context.Users
            .Select(u => new UserDto
            {
                Id = u.Id,
                Name = u.Name,
                OrderCount = u.Orders.Count,  // Calculated SQL-side
                TotalSpent = u.Orders.Sum(o => o.Total)
            })
            .ToListAsync();
    }

    // Split Query for large collections
    public async Task<List<User>> GetUsersWithSplitQuery()
    {
        return await _context.Users
            .Include(u => u.Orders)
            .AsSplitQuery()  // Generates separate queries instead of large JOIN
            .ToListAsync();
    }

    // No Tracking for read-only operations
    public async Task<List<User>> GetUsersReadOnly()
    {
        return await _context.Users
            .AsNoTracking()  // No change tracking = faster
            .ToListAsync();
    }

    // Batch operations (EF Core 7+)
    public async Task DeleteInactiveUsers()
    {
        // Single DELETE query instead of load then delete
        await _context.Users
            .Where(u => !u.IsActive && u.LastLoginAt < DateTime.UtcNow.AddYears(-1))
            .ExecuteDeleteAsync();
    }

    // Bulk update
    public async Task DeactivateOldUsers()
    {
        await _context.Users
            .Where(u => u.LastLoginAt < DateTime.UtcNow.AddMonths(-6))
            .ExecuteUpdateAsync(u => u.SetProperty(x => x.IsActive, false));
    }

    // Compiled Queries for frequent queries
    private static readonly Func<AppDbContext, int, Task<User?>> GetUserById =
        EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
            ctx.Users.FirstOrDefault(u => u.Id == id));

    public async Task<User?> GetUserOptimized(int id)
    {
        return await GetUserById(_context, id);
    }
}
쿼리 모니터링

개발 환경에서 optionsBuilder.LogTo(Console.WriteLine)로 SQL 로깅을 활성화하여 문제가 되는 쿼리를 식별하십시오. 프로덕션에서는 MiniProfiler나 Application Insights 같은 도구를 사용하십시오.

질문 11: 마이그레이션과 스키마 관리를 설명하십시오

마이그레이션 관리는 프로덕션 배포에 핵심적입니다.

MigrationStrategies.cscsharp
// Professional EF Core migration management

// DbContext configuration with conventions
public class AppDbContext : DbContext
{
    public DbSet<User> Users => Set<User>();
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Apply all IEntityTypeConfiguration configurations
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);

        // Global convention for dates
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            foreach (var property in entityType.GetProperties())
            {
                if (property.ClrType == typeof(DateTime))
                {
                    property.SetColumnType("datetime2");
                }
            }
        }
    }
}

// Separate fluent configuration
public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.ToTable("Users");

        builder.HasKey(u => u.Id);

        builder.Property(u => u.Email)
            .IsRequired()
            .HasMaxLength(256);

        builder.HasIndex(u => u.Email)
            .IsUnique();

        builder.HasMany(u => u.Orders)
            .WithOne(o => o.User)
            .HasForeignKey(o => o.UserId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}

// Data seeding
public class DataSeeder
{
    public static void Seed(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Role>().HasData(
            new Role { Id = 1, Name = "Admin" },
            new Role { Id = 2, Name = "User" }
        );
    }
}

필수 마이그레이션 명령어는 다음과 같습니다.

  • dotnet ef migrations add MigrationName - 마이그레이션 생성
  • dotnet ef database update - 마이그레이션 적용
  • dotnet ef migrations script - SQL 스크립트 생성
  • dotnet ef migrations remove - 마지막 마이그레이션 제거

ASP.NET Core

질문 12: ASP.NET Core의 미들웨어 파이프라인을 설명하십시오

미들웨어 파이프라인은 ASP.NET Core의 핵심입니다. 동작 원리를 이해하는 것이 필수적입니다.

MiddlewarePipeline.cscsharp
// Request pipeline architecture

// Custom Middleware - full class
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // BEFORE: executed on the way in (request)
        var stopwatch = Stopwatch.StartNew();
        _logger.LogInformation("Request: {Method} {Path}",
            context.Request.Method,
            context.Request.Path);

        try
        {
            // Pass to next middleware
            await _next(context);
        }
        finally
        {
            // AFTER: executed on the way out (response)
            stopwatch.Stop();
            _logger.LogInformation("Response: {StatusCode} in {ElapsedMs}ms",
                context.Response.StatusCode,
                stopwatch.ElapsedMilliseconds);
        }
    }
}

// Extension for registration
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app)
    {
        return app.UseMiddleware<RequestLoggingMiddleware>();
    }
}

// Pipeline configuration in Program.cs
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        // ORDER is CRITICAL!

        // 1. Exception handling (must be first)
        app.UseExceptionHandler("/error");

        // 2. HTTPS Redirection
        app.UseHttpsRedirection();

        // 3. Static files (short-circuits if found)
        app.UseStaticFiles();

        // 4. Routing (determines endpoint)
        app.UseRouting();

        // 5. CORS (must be between Routing and Auth)
        app.UseCors();

        // 6. Authentication (who are you?)
        app.UseAuthentication();

        // 7. Authorization (are you allowed?)
        app.UseAuthorization();

        // 8. Custom middleware
        app.UseRequestLogging();

        // 9. Endpoints (executes controller/action)
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapRazorPages();
        });
    }
}

// Conditional middleware
public static class ConditionalMiddleware
{
    public static IApplicationBuilder UseWhen(
        this IApplicationBuilder app,
        Func<HttpContext, bool> predicate,
        Action<IApplicationBuilder> configuration)
    {
        // Conditional branch of the pipeline
        return app.UseWhen(predicate, configuration);
    }

    public static void Example(IApplicationBuilder app)
    {
        // Apply middleware only for /api/*
        app.UseWhen(
            context => context.Request.Path.StartsWithSegments("/api"),
            apiApp => apiApp.UseMiddleware<ApiRateLimitingMiddleware>()
        );
    }
}

미들웨어는 요청 시 등록 순서대로, 응답 시 역순으로 실행됩니다.

질문 13: JWT 인증을 구현하는 방법은 무엇입니까?

JWT 인증은 현대 REST API의 표준입니다.

JwtAuthentication.cscsharp
// Complete JWT authentication configuration

public static class JwtConfiguration
{
    public static void AddJwtAuthentication(this IServiceCollection services, IConfiguration config)
    {
        var jwtSettings = config.GetSection("Jwt").Get<JwtSettings>()!;

        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = jwtSettings.Issuer,
                ValidAudience = jwtSettings.Audience,
                IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(jwtSettings.SecretKey)),
                ClockSkew = TimeSpan.Zero  // No tolerance on expiration
            };

            // Events for logging/debugging
            options.Events = new JwtBearerEvents
            {
                OnAuthenticationFailed = context =>
                {
                    if (context.Exception is SecurityTokenExpiredException)
                    {
                        context.Response.Headers.Add("Token-Expired", "true");
                    }
                    return Task.CompletedTask;
                }
            };
        });
    }
}

public class JwtSettings
{
    public string SecretKey { get; set; } = string.Empty;
    public string Issuer { get; set; } = string.Empty;
    public string Audience { get; set; } = string.Empty;
    public int ExpirationMinutes { get; set; } = 60;
}

// Token generation service
public class TokenService
{
    private readonly JwtSettings _settings;

    public TokenService(IOptions<JwtSettings> settings)
    {
        _settings = settings.Value;
    }

    public string GenerateToken(User user, IEnumerable<string> roles)
    {
        var securityKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_settings.SecretKey));
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new(JwtRegisteredClaimNames.Email, user.Email),
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new("name", user.Name)
        };

        // Add roles as claims
        claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));

        var token = new JwtSecurityToken(
            issuer: _settings.Issuer,
            audience: _settings.Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(_settings.ExpirationMinutes),
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public ClaimsPrincipal? ValidateToken(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.UTF8.GetBytes(_settings.SecretKey);

        try
        {
            var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = true,
                ValidIssuer = _settings.Issuer,
                ValidateAudience = true,
                ValidAudience = _settings.Audience,
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero
            }, out _);

            return principal;
        }
        catch
        {
            return null;
        }
    }
}

// Usage in a controller
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly TokenService _tokenService;
    private readonly IUserService _userService;

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginDto dto)
    {
        var user = await _userService.ValidateCredentialsAsync(dto.Email, dto.Password);
        if (user == null)
            return Unauthorized(new { message = "Invalid credentials" });

        var roles = await _userService.GetRolesAsync(user.Id);
        var token = _tokenService.GenerateToken(user, roles);

        return Ok(new { token, expiresIn = 3600 });
    }

    [Authorize]  // Requires valid token
    [HttpGet("profile")]
    public IActionResult GetProfile()
    {
        var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        return Ok(new { userId });
    }

    [Authorize(Roles = "Admin")]  // Requires Admin role
    [HttpGet("admin")]
    public IActionResult AdminOnly()
    {
        return Ok(new { message = "Welcome, Admin!" });
    }
}

.NET 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

고급 질문

질문 14: Span<T>과 Memory<T>란 무엇입니까?

이 타입들은 할당 없이 메모리를 조작할 수 있게 하며, 고성능 코드에 필수적입니다.

SpanAndMemory.cscsharp
// Types for performant memory manipulation

public class HighPerformanceDemo
{
    // Span`<T>`: view over contiguous memory region (stack only)
    public static void SpanBasics()
    {
        // Span over an array
        int[] numbers = { 1, 2, 3, 4, 5 };
        Span<int> span = numbers.AsSpan();

        // Slice without allocation
        Span<int> slice = span.Slice(1, 3);  // [2, 3, 4]

        // Modification affects original array
        slice[0] = 100;
        Console.WriteLine(numbers[1]); // 100

        // Span on the stack (stackalloc)
        Span<int> stackSpan = stackalloc int[100];
        stackSpan.Fill(42);
    }

    // Parsing without allocation using Span
    public static bool TryParseDate(ReadOnlySpan<char> input, out DateTime date)
    {
        // Format: "2024-01-15"
        date = default;

        if (input.Length != 10) return false;

        // Slicing without creating new strings
        var yearSpan = input.Slice(0, 4);
        var monthSpan = input.Slice(5, 2);
        var daySpan = input.Slice(8, 2);

        if (!int.TryParse(yearSpan, out int year)) return false;
        if (!int.TryParse(monthSpan, out int month)) return false;
        if (!int.TryParse(daySpan, out int day)) return false;

        date = new DateTime(year, month, day);
        return true;
    }

    // Memory`<T>`: like Span but can be stored on the heap
    public async Task<int> ProcessDataAsync(Memory<byte> buffer)
    {
        // Memory can cross async boundaries
        await Task.Delay(100);

        // Convert to Span for processing
        Span<byte> span = buffer.Span;
        int sum = 0;
        foreach (var b in span)
        {
            sum += b;
        }

        return sum;
    }

    // ArrayPool: array reuse to avoid allocations
    public static void UseArrayPool()
    {
        // Rent an array from the pool
        byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

        try
        {
            // Use the buffer...
            // Note: may be larger than requested
            Console.WriteLine($"Buffer size: {buffer.Length}");
        }
        finally
        {
            // ALWAYS return to pool
            ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
        }
    }

    // Comparative benchmark
    public static string SubstringTraditional(string input, int start, int length)
    {
        // Creates new string = allocation
        return input.Substring(start, length);
    }

    public static ReadOnlySpan<char> SubstringWithSpan(ReadOnlySpan<char> input, int start, int length)
    {
        // Returns a view = NO allocation
        return input.Slice(start, length);
    }
}

Span<T>은 할당 없이 문자열 처리, 파싱, 배열 연산에 적합합니다.

질문 15: Record와 그 활용 사례를 설명하십시오

Record(C# 9+)는 값 기반 동등성을 가진 불변 참조 타입으로, DTO와 값 객체에 적합합니다.

RecordsDemo.cscsharp
// Features and use cases for records

// Record class (reference, immutable by default)
public record Person(string FirstName, string LastName, DateOnly BirthDate)
{
    // Computed property
    public int Age => DateTime.Today.Year - BirthDate.Year;

    // Additional method
    public string FullName => $"{FirstName} {LastName}";
}

// Record with validation
public record Email
{
    public string Value { get; }

    public Email(string value)
    {
        if (!IsValidEmail(value))
            throw new ArgumentException("Invalid email format");
        Value = value;
    }

    private static bool IsValidEmail(string email)
        => !string.IsNullOrEmpty(email) && email.Contains('@');
}

// Record struct (value, C# 10+)
public readonly record struct Point(double X, double Y)
{
    public double Distance => Math.Sqrt(X * X + Y * Y);
}

public class RecordUsageDemo
{
    public void DemonstrateFeatures()
    {
        // Creation
        var person1 = new Person("John", "Doe", new DateOnly(1990, 5, 15));

        // Value-based equality (not reference)
        var person2 = new Person("John", "Doe", new DateOnly(1990, 5, 15));
        Console.WriteLine(person1 == person2); // True

        // Mutation with 'with' (creates a copy)
        var person3 = person1 with { LastName = "Smith" };
        Console.WriteLine(person1.LastName); // "Doe" (unchanged)
        Console.WriteLine(person3.LastName); // "Smith"

        // Deconstruction
        var (firstName, lastName, _) = person1;
        Console.WriteLine($"{firstName} {lastName}");

        // Auto-generated ToString()
        Console.WriteLine(person1);
        // Output: Person { FirstName = John, LastName = Doe, BirthDate = 15/05/1990 }
    }

    // Records as DTOs (data transfer)
    public record CreateUserRequest(string Email, string Password, string Name);
    public record UserResponse(int Id, string Email, string Name, DateTime CreatedAt);

    // Records as Value Objects (DDD)
    public record Money(decimal Amount, string Currency)
    {
        public static Money operator +(Money a, Money b)
        {
            if (a.Currency != b.Currency)
                throw new InvalidOperationException("Currency mismatch");
            return new Money(a.Amount + b.Amount, a.Currency);
        }
    }

    // Record with inheritance
    public abstract record Shape(string Color);
    public record Circle(string Color, double Radius) : Shape(Color);
    public record Rectangle(string Color, double Width, double Height) : Shape(Color);
}

Record는 DTO, Value Object, 불변 구성, 참조가 아닌 값에 기반하여 동일성을 판단해야 하는 모든 객체에 적합합니다.

질문 16: 분산 캐시 시스템을 구현하는 방법은 무엇입니까?

캐싱은 대규모 애플리케이션 성능에 필수적입니다.

DistributedCaching.cscsharp
// Cache implementation with Redis

public interface ICacheService
{
    Task<T?> GetAsync<T>(string key);
    Task SetAsync<T>(string key, T value, TimeSpan? expiration = null);
    Task RemoveAsync(string key);
    Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null);
}

public class RedisCacheService : ICacheService
{
    private readonly IDistributedCache _cache;
    private readonly JsonSerializerOptions _jsonOptions;

    public RedisCacheService(IDistributedCache cache)
    {
        _cache = cache;
        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
    }

    public async Task<T?> GetAsync<T>(string key)
    {
        var data = await _cache.GetStringAsync(key);

        if (string.IsNullOrEmpty(data))
            return default;

        return JsonSerializer.Deserialize<T>(data, _jsonOptions);
    }

    public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null)
    {
        var options = new DistributedCacheEntryOptions();

        if (expiration.HasValue)
        {
            options.AbsoluteExpirationRelativeToNow = expiration;
        }
        else
        {
            options.SlidingExpiration = TimeSpan.FromMinutes(10);
        }

        var json = JsonSerializer.Serialize(value, _jsonOptions);
        await _cache.SetStringAsync(key, json, options);
    }

    public async Task RemoveAsync(string key)
    {
        await _cache.RemoveAsync(key);
    }

    // Cache-Aside pattern with factory
    public async Task<T> GetOrSetAsync<T>(
        string key,
        Func<Task<T>> factory,
        TimeSpan? expiration = null)
    {
        var cached = await GetAsync<T>(key);

        if (cached != null)
            return cached;

        var value = await factory();
        await SetAsync(key, value, expiration);

        return value;
    }
}

// Usage in a service
public class ProductService
{
    private readonly ICacheService _cache;
    private readonly IProductRepository _repository;

    public ProductService(ICacheService cache, IProductRepository repository)
    {
        _cache = cache;
        _repository = repository;
    }

    public async Task<Product?> GetProductAsync(int id)
    {
        var cacheKey = $"product:{id}";

        return await _cache.GetOrSetAsync(
            cacheKey,
            async () => await _repository.GetByIdAsync(id),
            TimeSpan.FromMinutes(30)
        );
    }

    // Cache invalidation
    public async Task UpdateProductAsync(int id, UpdateProductDto dto)
    {
        await _repository.UpdateAsync(id, dto);

        // Invalidate cache
        await _cache.RemoveAsync($"product:{id}");
    }
}

// Configuration in Program.cs
public static class CacheConfiguration
{
    public static void AddCaching(this IServiceCollection services, IConfiguration config)
    {
        services.AddStackExchangeRedisCache(options =>
        {
            options.Configuration = config.GetConnectionString("Redis");
            options.InstanceName = "MyApp:";
        });

        services.AddSingleton<ICacheService, RedisCacheService>();
    }
}
캐시 무효화

"컴퓨터 과학에서 어려운 문제는 캐시 무효화와 이름 짓기 두 가지뿐이다." 오래된 데이터를 방지하려면 명확한 캐시 무효화 전략을 정의하는 것이 필수적입니다.

질문 17: 분산 트랜잭션을 처리하는 방법은 무엇입니까?

마이크로서비스 아키텍처에서 분산 트랜잭션은 특정 패턴이 필요합니다.

DistributedTransactions.cscsharp
// Patterns for consistency in distributed systems

// SAGA Pattern with Orchestration
public class OrderSaga
{
    private readonly IOrderRepository _orderRepository;
    private readonly IPaymentService _paymentService;
    private readonly IInventoryService _inventoryService;
    private readonly INotificationService _notificationService;

    public async Task<OrderResult> ProcessOrderAsync(CreateOrderCommand command)
    {
        Order? order = null;
        PaymentResult? payment = null;
        InventoryReservation? reservation = null;

        try
        {
            // Step 1: Create order
            order = await _orderRepository.CreateAsync(command);

            // Step 2: Reserve inventory
            reservation = await _inventoryService.ReserveAsync(order.Items);

            // Step 3: Process payment
            payment = await _paymentService.ProcessAsync(order.Total, command.PaymentMethod);

            // Step 4: Confirm order
            await _orderRepository.ConfirmAsync(order.Id);

            // Step 5: Notification (non-critical)
            await _notificationService.SendOrderConfirmationAsync(order);

            return OrderResult.Success(order.Id);
        }
        catch (Exception ex)
        {
            // COMPENSATION: undo previous steps in reverse order

            if (payment?.IsSuccessful == true)
            {
                await _paymentService.RefundAsync(payment.TransactionId);
            }

            if (reservation != null)
            {
                await _inventoryService.ReleaseReservationAsync(reservation.Id);
            }

            if (order != null)
            {
                await _orderRepository.CancelAsync(order.Id, ex.Message);
            }

            return OrderResult.Failure(ex.Message);
        }
    }
}

// Outbox Pattern for reliable event publishing
public class OutboxProcessor
{
    private readonly AppDbContext _context;
    private readonly IMessageBus _messageBus;

    public async Task ProcessOutboxAsync()
    {
        var pendingMessages = await _context.OutboxMessages
            .Where(m => m.ProcessedAt == null)
            .OrderBy(m => m.CreatedAt)
            .Take(100)
            .ToListAsync();

        foreach (var message in pendingMessages)
        {
            try
            {
                // Publish message
                await _messageBus.PublishAsync(message.Type, message.Payload);

                // Mark as processed
                message.ProcessedAt = DateTime.UtcNow;
                await _context.SaveChangesAsync();
            }
            catch (Exception ex)
            {
                message.RetryCount++;
                message.Error = ex.Message;
                await _context.SaveChangesAsync();
            }
        }
    }
}

// Outbox model
public class OutboxMessage
{
    public Guid Id { get; set; }
    public string Type { get; set; } = string.Empty;
    public string Payload { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public DateTime? ProcessedAt { get; set; }
    public int RetryCount { get; set; }
    public string? Error { get; set; }
}

// Extension to add outbox message within a transaction
public static class DbContextExtensions
{
    public static void AddOutboxMessage<T>(this AppDbContext context, T @event)
    {
        var message = new OutboxMessage
        {
            Id = Guid.NewGuid(),
            Type = typeof(T).Name,
            Payload = JsonSerializer.Serialize(@event),
            CreatedAt = DateTime.UtcNow
        };

        context.OutboxMessages.Add(message);
    }
}

SAGA 패턴은 분산 시스템에서 최종 일관성을 보장합니다. Outbox 패턴은 장애 발생 시에도 안정적인 이벤트 발행을 보장합니다.

결론

C# 및 .NET 면접에서는 런타임과 언어에 대한 이론적 지식, 그리고 아키텍처와 애플리케이션 개발에 대한 실무 역량을 종합적으로 평가합니다. 기본 개념을 숙달하면서 고급 패턴을 이해하는 것이 시니어 개발자를 구별하는 핵심입니다.

준비 체크리스트

  • 값 타입과 참조 타입의 차이를 이해하십시오
  • async/await를 숙달하고 데드락을 방지하십시오
  • IEnumerable과 IQueryable의 차이를 파악하십시오
  • Entity Framework Core 쿼리를 최적화하십시오
  • IDisposable 패턴을 올바르게 구현하십시오
  • 적절한 수명으로 의존성 주입을 구성하십시오
  • JWT로 API를 보호하십시오
  • 고성능 코드를 위해 Span<T>과 Memory<T>를 활용하십시오

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

면접 준비는 이론과 실습을 병행해야 합니다. 개인 프로젝트를 구축하고, .NET 오픈 소스 생태계에 기여하며, HackerRank나 LeetCode 같은 플랫폼에서 문제를 풀어보는 것이 가장 까다로운 면접을 위한 실력을 견고히 합니다.

태그

#csharp
#dotnet
#interview
#aspnet core
#technical interview

공유

관련 기사