재우니의 블로그

💡 서론: 더 나은 개발 습관 만들기

소프트웨어 개발은 끊임없는 개선과 배움의 과정입니다. 이 글에서는 C# 코드 예제와 함께 실무에서 바로 적용할 수 있는 10가지 개발 습관을 소개합니다.

본론: 실천법 10가지

1. 🔍 작은 커밋을 유지하라

커밋은 하나의 논리적 변경에 집중해야 합니다. 예를 들어, 사용자 인증 기능을 추가할 때:

// 첫 번째 커밋: 기본 사용자 모델 추가
public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Email { get; set; }
    public string PasswordHash { get; set; }
}

// 두 번째 커밋: 패스워드 해싱 서비스 추가
public interface IPasswordHasher
{
    string HashPassword(string password);
    bool VerifyPassword(string password, string hash);
}

public class BCryptPasswordHasher : IPasswordHasher
{
    public string HashPassword(string password)
    {
        return BCrypt.Net.BCrypt.HashPassword(password);
    }

    public bool VerifyPassword(string password, string hash)
    {
        return BCrypt.Net.BCrypt.Verify(password, hash);
    }
}

// 세 번째 커밋: 사용자 서비스 구현
public class UserService
{
    private readonly IPasswordHasher _passwordHasher;

    public UserService(IPasswordHasher passwordHasher)
    {
        _passwordHasher = passwordHasher;
    }

    public async Task<User> RegisterUser(string username, string email, string password)
    {
        var user = new User
        {
            Username = username,
            Email = email,
            PasswordHash = _passwordHasher.HashPassword(password)
        };
        // DB 저장 로직...
        return user;
    }
}

2. 🔄 리팩토링을 생활화하라

기존 코드를 더 나은 구조로 개선하는 예제:

// 리팩토링 전: 모든 로직이 한 메서드에 있음
public class OrderProcessor
{
    public void ProcessOrder(Order order)
    {
        // 재고 확인
        if (order.Items.Any(item => !IsInStock(item)))
            throw new OutOfStockException();

        // 결제 처리
        var payment = ProcessPayment(order.Total);
        if (!payment.IsSuccessful)
            throw new PaymentFailedException();

        // 배송 처리
        CreateShipment(order);

        // 이메일 발송
        SendConfirmationEmail(order);
    }
}

// 리팩토링 후: 책임이 분리된 작은 클래스들
public class OrderProcessor
{
    private readonly IStockValidator _stockValidator;
    private readonly IPaymentProcessor _paymentProcessor;
    private readonly IShipmentService _shipmentService;
    private readonly IEmailService _emailService;

    public OrderProcessor(
        IStockValidator stockValidator,
        IPaymentProcessor paymentProcessor,
        IShipmentService shipmentService,
        IEmailService emailService)
    {
        _stockValidator = stockValidator;
        _paymentProcessor = paymentProcessor;
        _shipmentService = shipmentService;
        _emailService = emailService;
    }

    public async Task ProcessOrder(Order order)
    {
        await _stockValidator.ValidateStock(order);
        await _paymentProcessor.ProcessPayment(order);
        await _shipmentService.CreateShipment(order);
        await _emailService.SendConfirmation(order);
    }
}

3. 🚀 배포 가능한 상태를 유지하라

피처 플래그를 사용한 안전한 배포 예제:

public class NewFeatureToggle
{
    private readonly IFeatureManager _featureManager;

    public NewFeatureToggle(IFeatureManager featureManager)
    {
        _featureManager = featureManager;
    }

    public async Task ProcessRequest(Request request)
    {
        if (await _featureManager.IsEnabledAsync("NewAlgorithm"))
        {
            // 새로운 처리 로직
            await ProcessWithNewAlgorithm(request);
        }
        else
        {
            // 기존 로직
            await ProcessWithOldAlgorithm(request);
        }
    }
}

4. 🤝 프레임워크를 신뢰하라

ASP.NET Core의 기능을 활용하는 예제:

// 프레임워크의 DI 컨테이너 활용
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // 내장 lifecycle 관리 활용
        services.AddScoped<IUserService, UserService>();
        services.AddTransient<IPasswordHasher, BCryptPasswordHasher>();
        services.AddSingleton<ICacheService, RedisCacheService>();

        // 내장 미들웨어 활용
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer();

        // 자동 모델 검증 활용
        services.AddControllers()
            .AddFluentValidation();
    }
}

// 컨트롤러에서는 최소한의 코드만 작성
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserService _userService;

    public UsersController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpPost]
    public async Task<ActionResult<UserDto>> Create(CreateUserRequest request)
    {
        // 모델 검증은 프레임워크가 자동으로 처리
        var user = await _userService.CreateUser(request);
        return Ok(user);
    }
}

5. 🗂️ 독립적인 모듈로 관리하라

모듈화된 구조의 예제:

// Notifications 모듈
public class NotificationModule
{
    public interface INotificationService
    {
        Task SendAsync(Notification notification);
    }

    public class EmailNotificationService : INotificationService
    {
        public async Task SendAsync(Notification notification)
        {
            // 이메일 발송 로직
        }
    }

    public class PushNotificationService : INotificationService
    {
        public async Task SendAsync(Notification notification)
        {
            // 푸시 알림 로직
        }
    }
}

// Payments 모듈
public class PaymentModule
{
    public interface IPaymentGateway
    {
        Task<PaymentResult> ProcessPayment(Payment payment);
    }

    public class StripeGateway : IPaymentGateway
    {
        public async Task<PaymentResult> ProcessPayment(Payment payment)
        {
            // Stripe 결제 로직
        }
    }
}

6. 🧪 API 설계 시 테스트 우선 접근법

테스트 주도 API 설계 예제:

// 먼저 테스트 작성
public class OrderServiceTests
{
    [Fact]
    public async Task CreateOrder_WithValidItems_ShouldSucceed()
    {
        // Arrange
        var service = new OrderService(mockRepository.Object);
        var orderRequest = new CreateOrderRequest
        {
            Items = new[] { new OrderItem { ProductId = 1, Quantity = 2 } }
        };

        // Act
        var result = await service.CreateOrder(orderRequest);

        // Assert
        Assert.NotNull(result);
        Assert.Equal(OrderStatus.Created, result.Status);
    }
}

// 테스트를 만족하는 구현
public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }

    public async Task<Order> CreateOrder(CreateOrderRequest request)
    {
        var order = new Order
        {
            Items = request.Items.Select(i => new OrderItem
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity
            }).ToList(),
            Status = OrderStatus.Created
        };

        return await _repository.Create(order);
    }
}

7. 📋 복붙의 유혹에 넘어가지 마라

중복 코드를 제거하는 예제:

// 중복된 코드
public class UserService
{
    public async Task<User> GetAdminUser(int id)
    {
        var user = await _repository.GetById(id);
        if (user == null) throw new NotFoundException();
        if (!user.IsAdmin) throw new UnauthorizedException();
        return user;
    }

    public async Task<User> GetManagerUser(int id)
    {
        var user = await _repository.GetById(id);
        if (user == null) throw new NotFoundException();
        if (!user.IsManager) throw new UnauthorizedException();
        return user;
    }
}

// 리팩토링 후
public class UserService
{
    private async Task<User> GetUserByRole(int id, Func<User, bool> roleValidator)
    {
        var user = await _repository.GetById(id);
        if (user == null) throw new NotFoundException();
        if (!roleValidator(user)) throw new UnauthorizedException();
        return user;
    }

    public Task<User> GetAdminUser(int id) => 
        GetUserByRole(id, user => user.IsAdmin);

    public Task<User> GetManagerUser(int id) => 
        GetUserByRole(id, user => user.IsManager);
}

8. 🧹 디자인의 변화 수용하기

확장 가능한 디자인 예제:

// 초기 설계
public interface IOrderProcessor
{
    Task ProcessOrder(Order order);
}

// 변화 수용: 새로운 요구사항 (주문 처리 전/후 훅 추가)
public interface IOrderProcessor
{
    Task ProcessOrder(Order order);
    Task<bool> CanProcess(Order order);
    Task OnProcessed(Order order);
}

// 데코레이터 패턴으로 기능 확장
public class LoggingOrderProcessor : IOrderProcessor
{
    private readonly IOrderProcessor _inner;
    private readonly ILogger _logger;

    public LoggingOrderProcessor(IOrderProcessor inner, ILogger logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public async Task ProcessOrder(Order order)
    {
        _logger.LogInformation($"Processing order {order.Id}");
        await _inner.ProcessOrder(order);
        _logger.LogInformation($"Processed order {order.Id}");
    }
}

9. 💼 기술 부채의 유형을 이해하라

기술 부채 관리 예제:

// 1. 지금 당장 문제가 되는 부채
public class LegacyDatabase
{
    // 심각한 성능 문제를 일으키는 코드
    public async Task<IEnumerable<Order>> GetAllOrders()
    {
        var orders = new List<Order>();
        // N+1 쿼리 문제
        foreach (var id in await GetAllOrderIds())
        {
            orders.Add(await GetOrderById(id));
        }
        return orders;
    }
}

// 즉시 수정이 필요한 리팩토링
public class OptimizedDatabase
{
    public async Task<IEnumerable<Order>> GetAllOrders()
    {
        return await _context.Orders
            .Include(o => o.Items)
            .ToListAsync();
    }
}

// 2. 잠재적인 문제
public class UserManager
{
    // 확장성 문제가 있는 코드
    public async Task<User> GetUser(int id)
    {
        // 캐싱이 없어 DB 부하가 높음
        return await _context.Users.FindAsync(id);
    }
}

// 개선된 버전
public class CachedUserManager
{
    private readonly ICache _cache;

    public async Task<User> GetUser(int id)
    {
        var cacheKey = $"user_{id}";
        return await _cache.GetOrSetAsync(cacheKey, 
            () => _context.Users.FindAsync(id));
    }
}

10. ✅ 테스트 가능한 설계 만들기

테스트 용이한 설계 예제:

// 테스트하기 어려운 설계
public class OrderService
{
    public async Task ProcessOrder(Order order)
    {
        // 직접적인 정적 메서드 호출
        var paymentResult = await PaymentGateway.Process(order.Total);
        if (paymentResult.Success)
        {
            await EmailSender.Send(order.CustomerEmail, "주문 완료");
        }
    }
}

// 테스트 가능한 설계
public class OrderService
{
    private readonly IPaymentGateway _paymentGateway;
    private readonly IEmailSender _emailSender;
    
    public OrderService(
        IPaymentGateway paymentGateway,
        IEmailSender emailSender)
    {
        _paymentGateway = paymentGateway;
        _emailSender = emailSender;
    }
    
    public async Task ProcessOrder(Order order)
    {
        var paymentResult = await _paymentGateway.Process(order.Total);
        if (paymentResult.Success)
        {
            await _emailSender.Send(order.CustomerEmail, "주문 완료");
        }
    }
}

// 테스트

 

참고 사이트

 

https://zarar.dev/good-software-development-habits/

 

Good software development habits

Note: This got and got some attention. This post is not advice, it's what's working for me. It's easy to pick up bad habits and hard to create good o...

zarar.dev