애플리케이션 객체를 제작하고 의존성을 연결하는 과정은 별도로 분리되어야 한다.
public Service getService() { if (service == null) service = new MyServiceImpl(...); return service; }
구현에 직접 의존: getService()가 MyServiceImpl의 생성자 인수에 의존한다.
테스트 어려움: service가 null인 경우와 아닌 경우를 모두 테스트해야 한다.
책임 혼재: 설정 로직과 실행 로직이 한 메서드에 섞여 있다.
생성 코드는 main이나 main이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생성되었다고 가정한다. 시스템의 나머지는 객체 생성 과정을 전혀 모른다.
객체가 생성되는 시점을 어플리케이션이 결정하는 기법. 어플리케이션은 언제 만들지는 알지만 구체적인 방법은 모른다.
// ❌ 직접 생성 public class OrderService { private readonly IPaymentGateway _gateway = new StripeGateway(); } // ✅ DI — 생성은 컨테이너가 public class OrderService { private readonly IPaymentGateway _gateway; public OrderService(IPaymentGateway gateway) { _gateway = gateway; } }
컨테이너에 종속된 코드는 테스트도 확장도 어렵다.
public class Bank : IBank { public void AddAccount(AccountDTO accountDTO) { // ⚠️ JNDI lookup 같은 역할 (직접 서비스 탐색) var accountService = new AccountServiceFactory().Create(); var account = accountService.Create(accountDTO); _accounts.Add(account); } // ⚠️ 이상한 생명주기 메서드들 public void SetEntityContext(object ctx) { } public void Activate() { } public void Passivate() { } public void Load() { } public void Store() { } public void Remove() { } }
컨테이너 종속: 비즈니스 로직이 컨테이너에 타이트하게 연결되어 있다. 컨테이너가 제공해야 하는 인터페이스를 반드시 구현해야 한다.
테스트/상속 어려움: 개발자가 비즈니스 로직과 관계없는 코드까지 억지로 작성해야 한다.
persistence, logging, security 등은 코드 곳곳에 흩어진다. AOP로 모듈화하라.
public class OrderService { public void PlaceOrder(Order order) { _logger.Log("PlaceOrder called"); // 중복 관심사 _transaction.Begin(); // 중복 관심사 // 실제 비즈니스 로직 _orderRepo.Save(order); _transaction.Commit(); _logger.Log("PlaceOrder completed"); } }
[Log] [Transactional] public void PlaceOrder(Order order) { // 순수 비즈니스 로직만 _orderRepo.Save(order); }
Persistency를 예시로 들면, persistency를 적용할 객체와 속성을 선언한 후 영속성 책임을 프레임워크로 위임한다.
AOP 프레임워크는 해당 코드에 영향을 미치지 않는 상태로 동작 방식을 변경한다.
관심사의 분리 → 각 모듈은 자신의 책임만 집중하게 된다.
시스템을 깨끗하게 유지하는 네 가지 핵심 전략
시스템 시작과 실행은 다른 관심사. main에서 구성, 앱에서 사용.
객체 생성 책임을 DI 컨테이너에 위임하라. 코드는 추상에만 의존.
비즈니스 로직을 컨테이너와 분리하라. 테스트와 확장이 쉬워진다.
logging, transaction은 비즈니스 코드가 아니다. AOP로 분리하라.