F-lab Java 1주차 / Phase 3 / Unit 3.5 본격 학습 자료 — Phase 3 마지막!
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Unit 3.4 (ISP), Unit 2.4 (다형성)
다음 Phase: Phase 4 — JVM 메모리 모델이 Unit의 의미: SOLID의 정점. Spring DI의 진짜 의미.
Hexagonal Architecture, Clean Architecture, MSA — 모든 현대 아키텍처가 DIP 위에 있다.
DIP를 정확히 이해하면 5주차 Spring, 11-12주차 JPA, 17주차 MSA 가 자연스럽게 흡수된다.
집에 콘센트가 있습니다. 이 콘센트에 다양한 가전을 꽂을 수 있어요:
누가 누구에게 의존할까요?
상식적 사고 (의존의 자연스러운 방향):
그러나 진짜 본질:
만약 콘센트가 가전마다 다르다면?:
만약 표준 없이 직접 연결된다면?:
→ 이게 DIP의 정신. 상위와 하위 모두 추상화 (표준) 에 의존 하면 자유롭게 교체 가능.
USB-C 등장 전:
USB-C 등장 후:
DIP 관점:
→ "양쪽이 표준에 의존" = DIP 의 본질.
"구체에 의존하지 말고 추상화에 의존하라."
DIP의 두 측면:
비유 정리:
| 비유 요소 | DIP 적용 |
|---|---|
| 가전제품 | 상위 모듈 |
| 콘센트 | 하위 모듈 |
| 220V 표준 | 추상화 (인터페이스) |
| 가전이 콘센트에 직접 의존 | DIP 위반 |
| 둘 다 표준에 의존 | DIP 만족 |
DIP (Dependency Inversion Principle) — Uncle Bob 이 1996년 제안.
원래 정의 (두 가지 진술):
"A. High-level modules should not depend on low-level modules. Both should depend on abstractions."
("상위 모듈은 하위 모듈에 의존하지 않아야 한다. 둘 다 추상화에 의존해야 한다.")"B. Abstractions should not depend on details. Details should depend on abstractions."
("추상화는 세부사항에 의존하지 않아야 한다. 세부사항이 추상화에 의존해야 한다.")
핵심 키워드: "역전(Inversion)" — 의존 방향을 뒤집는다.
전통적인 프로그래밍에서 자연스러운 의존 방향:
[고수준 정책] (비즈니스 로직)
↓ 의존
[저수준 세부] (DB, 외부 API, 파일 시스템)
예시:
public class OrderService { // 고수준
private MySQLOrderRepository repository; // ← 저수준에 직접 의존
private SmtpEmailSender emailSender; // ← 저수준에 직접 의존
}
문제:
OrderService 수정OrderService 수정"비즈니스 로직 (고수준) 이 인프라 세부사항 (저수준) 에 의존하는 것은 비정상이다.
반대여야 한다."
비즈니스의 본질:
그런데 현실:
Before (전통):
[OrderService] → [MySQLOrderRepository]
고수준 저수준
(고수준이 저수준에 의존 ← 위험)
After (DIP):
[OrderService] → [OrderRepository] ← [MySQLOrderRepository]
고수준 추상화 저수준
(양쪽이 추상화에 의존 ← 안전)
(저수준이 추상화에 의존 ← 역전!)
핵심 변화:
OrderRepository 도입OrderService 는 인터페이스에 의존MySQLOrderRepository 가 인터페이스를 구현→ 의존 방향이 거꾸로 (역전) 된 셈.
2000년대 초 Spring Framework 가 등장하면서 DIP가 자바 표준으로 자리잡음.
Spring DI = DIP의 직접적 구현:
@Service
public class OrderService {
private final OrderRepository repository; // 인터페이스
public OrderService(OrderRepository repository) {
this.repository = repository; // Spring이 알아서 구현체 주입
}
}
@Repository
public class JpaOrderRepository implements OrderRepository { ... }
Spring 이 한 일:
→ Spring 자체가 DIP의 거대한 응용.
DIP가 더 발전된 형태:
Hexagonal Architecture (Alistair Cockburn, 2005):
Clean Architecture (Uncle Bob, 2017):
→ 현대 백엔드 아키텍처의 토대. 17주차에서 본격.
"DIP는 '의존 방향을 통제' 하는 원칙이다."
자연스러운 의존 방향 (고수준 → 저수준) 은 비즈니스 코드를 인프라에 종속시킨다. 인터페이스(추상화)를 사이에 두어 양쪽이 추상화에 의존하게 만들면, 고수준은 저수준의 변경으로부터 독립 한다.
이게 Spring DI, JPA Repository, Hexagonal/Clean Architecture, MSA 의 모든 토대. 현대 자바 아키텍처의 출발점이자 정점.
DIP를 위반했을 때의 구체적 문제를 ILIC 시나리오로 보겠습니다.
운임 등록 시:
1. DB에 저장
2. 이메일로 고객 통지
3. 통계 집계
4. 외부 ERP 연동
// ❌ DIP 위반
public class FareService {
// 구체 클래스에 직접 의존
private final MySQLFareRepository repository = new MySQLFareRepository();
private final SmtpEmailSender emailSender = new SmtpEmailSender();
private final RedisStatsAggregator statsAggregator = new RedisStatsAggregator();
private final SapErpClient erpClient = new SapErpClient();
public void registerFare(FareRequest request) {
// 1. 저장
Fare fare = new Fare(request);
repository.save(fare);
// 2. 이메일
emailSender.send(request.getCustomerEmail(),
"운임 등록",
"운임이 등록되었습니다");
// 3. 통계
statsAggregator.increment("fare.registered");
// 4. ERP
erpClient.syncFare(fare);
}
}
DB를 MySQL → PostgreSQL 변경 시:
// 모든 의존 코드 수정
private final PostgreSQLFareRepository repository = ...; // ❌
→ FareService 직접 수정. 비즈니스 코드를 인프라 변경 때문에 건드림.
@Test
void 운임_등록_테스트() {
FareService service = new FareService();
service.registerFare(request);
// 그런데 이 테스트는:
// - 진짜 MySQL에 데이터 INSERT ❌
// - 진짜 SMTP로 이메일 발송 ❌
// - 진짜 Redis에 데이터 ❌
// - 진짜 SAP ERP에 호출 ❌
// → 단위 테스트 불가능 ❌
}
→ 단위 테스트 자체가 불가능. 통합 테스트만 가능 → 느림 + 깨지기 쉬움.
A 개발자: 운임 비즈니스 로직 작성
B 개발자: DB 스키마 결정 안 됨
A는 B를 기다려야 함. 비즈니스 작업이 인프라 결정을 기다림 ❌
import com.smtp.SmtpEmailSender; // 특정 라이브러리에 종속
→ 라이브러리 변경 시 전체 시스템 영향.
@Test
void 테스트() {
// 의존성이 new로 생성됨
// → Mockito로 mock 불가
// → 테스트 통제 불가
}
public void registerFare(FareRequest request) {
// 검증 (비즈니스)
if (request.getAmount() < 0) throw ...;
// SQL 만들기 (인프라)
String sql = "INSERT INTO fares ...";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setInt(1, request.getAmount());
// 비즈니스 → 인프라 → 비즈니스 → 인프라 ❌
// 코드 가독성 ↓
}
→ DIP 위반 시 환경별 구현 교체 어려움.
// 1. 추상화 정의 (인터페이스)
public interface FareRepository {
void save(Fare fare);
Optional<Fare> findById(Long id);
}
public interface NotificationService {
void send(String recipient, String subject, String message);
}
public interface StatsAggregator {
void increment(String metric);
}
public interface ErpSyncClient {
void syncFare(Fare fare);
}
// 2. 비즈니스 로직 — 추상화에만 의존
@Service
@RequiredArgsConstructor
public class FareService {
private final FareRepository repository; // 인터페이스
private final NotificationService notification; // 인터페이스
private final StatsAggregator stats; // 인터페이스
private final ErpSyncClient erpClient; // 인터페이스
public void registerFare(FareRequest request) {
// 검증 (비즈니스)
if (request.getAmount() < 0) throw new IllegalArgumentException();
Fare fare = new Fare(request);
// 추상화를 통한 작업 — 구체 모름
repository.save(fare);
notification.send(request.getCustomerEmail(), "운임 등록", "...");
stats.increment("fare.registered");
erpClient.syncFare(fare);
}
}
// 3. 구현체 — 추상화를 따름 (역전)
@Repository
public class JpaFareRepository implements FareRepository { ... }
@Component
public class SmtpNotificationService implements NotificationService { ... }
@Component
public class RedisStatsAggregator implements StatsAggregator { ... }
@Component
public class SapErpSyncClient implements ErpSyncClient { ... }
| 문제 | 해결 |
|---|---|
| 변경의 폭발 | 구현체 교체만 |
| 테스트 불가 | Mock 가능 |
| 동시 개발 불가 | 인터페이스만 정해지면 OK |
| 라이브러리 종속 | 인터페이스 뒤로 격리 |
| Mock 불가 | 의존성 주입 → Mock OK |
| 코드 혼재 | 비즈니스만 남음 |
| 환경별 교체 | Profile/조건별 빈 |
→ 이게 DIP의 진짜 가치.
DIP 적용 단계 ⭐ :
"이 클래스가 무엇에 의존하나?"
- DB
- 외부 API
- 파일 시스템
- 메시징
- ...
→ 변경 가능성이 있는 것 들이 모두 후보.
// 비즈니스가 필요한 능력을 인터페이스로
public interface FareRepository {
void save(Fare fare);
Optional<Fare> findById(Long id);
List<Fare> findByCustomer(Long customerId);
}
중요한 원칙: 인터페이스를 누가 정의하나?
[비즈니스 영역] [인프라 영역]
- FareRepository (인터페이스) ◄── 인프라가 따름
- FareService (사용)
- JpaFareRepository (구현)
→ 비즈니스가 인터페이스를 정의, 인프라가 따름. 이게 진짜 의존 역전.
@Service
public class FareService {
private final FareRepository repository; // 인터페이스
public FareService(FareRepository repository) { // 외부 주입
this.repository = repository;
}
}
→ new 사용 X. 의존성을 외부에서 받음.
@Repository
public class JpaFareRepository implements FareRepository {
@Override
public void save(Fare fare) { ... }
}
@Configuration
public class AppConfig {
@Bean
public FareService fareService(FareRepository repository) {
return new FareService(repository);
}
}
// 또는 @Service + @Repository 자동 인식
→ Spring이 알아서 구현체를 주입.
IoC = "제어 역전"
DI = "의존성 주입" (IoC의 한 형태)
전통적 제어 흐름:
public class FareService {
public FareService() {
this.repository = new MySQLFareRepository();
// → 내가 직접 만들고 제어
}
}
IoC 적용:
public class FareService {
public FareService(FareRepository repository) { // 외부에서 받음
this.repository = repository;
}
}
// 외부 (Spring) 가 만들고 주입
제어가 역전됨:
FareService 가 MySQLFareRepository 생성 제어FareService 는 사용만→ IoC = 객체 생성/관리 제어를 외부에 위임.
IoC 컨테이너 = Spring (5주차에서 본격).
@Service
@RequiredArgsConstructor // Lombok
public class FareService {
private final FareRepository repository;
private final NotificationService notification;
}
장점:
final 가능 → 불변성@Service
public class FareService {
private FareRepository repository;
@Autowired
public void setRepository(FareRepository repository) {
this.repository = repository;
}
}
언제: 선택적 의존성. 거의 안 씀.
@Service
public class FareService {
@Autowired
private FareRepository repository;
}
문제:
final 불가// 도메인 영역 — 인터페이스 정의
public interface FareRepository {
void save(Fare fare);
Optional<Fare> findById(Long id);
}
// 인프라 영역 — 구현
@Repository
public class JpaFareRepository implements FareRepository {
private final FareJpaRepo jpaRepo; // Spring Data JPA
@Override
public void save(Fare fare) {
jpaRepo.save(fare);
}
}
장점:
→ JPA 사용해도 DIP는 별개로 적용.
DIP의 아키텍처 레벨 적용:
[도메인 (비즈니스)]
↑ ↑
┌───────┘ └───────┐
[Inbound Port] [Outbound Port]
(인터페이스) (인터페이스)
↑ ↑
[Adapters] [Adapters]
- Web Controller - JPA Repository
- Kafka Listener - REST Client
- CLI - File System
핵심:
ILIC 적용:
// 도메인
public interface FareNotificationPort { // 인바운드 포트
void notifyRegistered(Fare fare);
}
// 어댑터
@Component
public class EmailFareNotificationAdapter implements FareNotificationPort { ... }
@Component
public class SmsFareNotificationAdapter implements FareNotificationPort { ... }
→ 17주차에서 본격 학습.
DIP는 설계 원칙이지만, Spring 의 IoC 컨테이너 동작 을 이해하면 더 깊어집니다.
@Service
public class FareService {
private final FareRepository repository;
public FareService(FareRepository repository) { // ① 의존성 선언
this.repository = repository;
}
}
@Repository
public class JpaFareRepository implements FareRepository { ... } // ② 구현체
Spring 시작 시 흐름:
1. ApplicationContext 생성
↓
2. 모든 @Component, @Service, @Repository 스캔
- FareService 발견
- JpaFareRepository 발견
↓
3. 각 빈의 의존성 분석
- FareService: FareRepository 필요
- JpaFareRepository: 의존성 없음
↓
4. 의존성 그래프 생성 (DAG)
FareService → FareRepository
↓
5. 의존 순서대로 빈 생성
- 먼저 JpaFareRepository 인스턴스화
- 그 다음 FareService 인스턴스화 (생성자에 JpaFareRepository 주입)
↓
6. ApplicationContext에 빈 등록
↓
7. 사용 준비 완료
→ 개발자는 인터페이스만 선언, Spring이 구현체 자동 주입.
FareService → FareRepository ✅
NotificationService → EmailSender ✅
EmailSender → NotificationService ❌ 순환!
Spring이 시작 시 순환을 감지하면 에러:
The dependencies of some of the beans in the application context form a cycle
→ DIP가 잘 적용되면 순환 자체가 안 일어남. 단방향 의존.
컴파일 시점:
public class FareService {
private final FareRepository repository; // FareRepository 인터페이스만 의존
}
컴파일 의존성:
FareService → FareRepository (인터페이스)→ JpaFareRepository를 모름. 컴파일 시 무관.
런타임 시점:
FareService service = applicationContext.getBean(FareService.class);
service.registerFare(request);
// → 내부의 repository는 실제로 JpaFareRepository 인스턴스
런타임 의존성:
FareService → JpaFareRepository (인스턴스)→ 컴파일과 런타임 의존성 분리. 이게 DIP의 본질.
기존 라이브러리를 DIP에 맞게 감싸기:
// 외부 라이브러리 (변경 불가)
public class ExternalEmailSDK {
public void sendMail(String to, String body) { ... }
}
// 우리의 추상화
public interface NotificationService {
void send(String recipient, String subject, String message);
}
// 어댑터 — 외부 라이브러리를 우리 인터페이스에 맞춤
@Component
public class ExternalEmailAdapter implements NotificationService {
private final ExternalEmailSDK sdk = new ExternalEmailSDK();
@Override
public void send(String recipient, String subject, String message) {
sdk.sendMail(recipient, subject + "\n" + message);
}
}
→ 외부 라이브러리에 직접 의존 X. 어댑터를 통해 격리.
public interface FareRepository { ... }
@Profile("dev")
@Repository
public class InMemoryFareRepository implements FareRepository { ... }
@Profile("test")
@Repository
public class H2FareRepository implements FareRepository { ... }
@Profile("prod")
@Repository
public class PostgreSQLFareRepository implements FareRepository { ... }
# application.yml
spring:
profiles:
active: prod # 또는 dev, test
→ DIP 덕분에 환경별 구현 교체 가능. 비즈니스 코드 안 건드림.
// ===== 도메인 영역 =====
// 1. 도메인 객체
public class Fare {
private Long id;
private int amount;
private FareStatus status;
private Long customerId;
// 도메인 로직...
}
// 2. Repository 추상화 (도메인이 정의)
public interface FareRepository {
void save(Fare fare);
Optional<Fare> findById(Long id);
List<Fare> findByCustomer(Long customerId);
}
// 3. Notification 추상화
public interface FareNotificationPort {
void notifyRegistered(Fare fare, Customer customer);
}
// 4. ERP 연동 추상화
public interface ErpSyncPort {
void syncFare(Fare fare);
}
// 5. 비즈니스 로직 — 추상화에만 의존
@Service
@RequiredArgsConstructor
public class FareService {
private final FareRepository repository;
private final FareNotificationPort notification;
private final ErpSyncPort erpSync;
@Transactional
public Fare registerFare(FareRequest request, Customer customer) {
// 비즈니스 로직만
Fare fare = Fare.create(request, customer);
repository.save(fare);
notification.notifyRegistered(fare, customer);
erpSync.syncFare(fare);
return fare;
}
}
// ===== 인프라 영역 =====
// 6. JPA 구현
@Repository
@RequiredArgsConstructor
public class JpaFareRepositoryAdapter implements FareRepository {
private final FareJpaRepository jpaRepo;
@Override
public void save(Fare fare) {
jpaRepo.save(fare);
}
@Override
public Optional<Fare> findById(Long id) {
return jpaRepo.findById(id);
}
@Override
public List<Fare> findByCustomer(Long customerId) {
return jpaRepo.findByCustomerId(customerId);
}
}
// 7. 이메일 알림 구현
@Component
@RequiredArgsConstructor
public class EmailFareNotificationAdapter implements FareNotificationPort {
private final EmailSender emailSender;
@Override
public void notifyRegistered(Fare fare, Customer customer) {
String message = String.format("운임 #%d 등록 완료 (%d원)",
fare.getId(), fare.getAmount());
emailSender.send(customer.getEmail(), "운임 등록", message);
}
}
// 8. ERP 연동 구현
@Component
@RequiredArgsConstructor
public class SapErpSyncAdapter implements ErpSyncPort {
private final SapErpClient sapClient;
@Override
public void syncFare(Fare fare) {
sapClient.callSapApi(fare.toSapFormat());
}
}
효과:
// 테스트 코드
@Test
void 운임_등록_시_알림이_발송된다() {
// Given - Mock 의존성
FareRepository mockRepo = mock(FareRepository.class);
FareNotificationPort mockNotification = mock(FareNotificationPort.class);
ErpSyncPort mockErp = mock(ErpSyncPort.class);
FareService service = new FareService(mockRepo, mockNotification, mockErp);
FareRequest request = new FareRequest(50000, 100L);
Customer customer = new Customer("alice@example.com");
// When
Fare result = service.registerFare(request, customer);
// Then
verify(mockRepo).save(any(Fare.class));
verify(mockNotification).notifyRegistered(eq(result), eq(customer));
verify(mockErp).syncFare(eq(result));
}
→ DB, 이메일, ERP 없이 비즈니스 로직 검증.
// ❌ 시간에 직접 의존
public class FareService {
public void register() {
Fare fare = new Fare();
fare.setCreatedAt(LocalDateTime.now()); // 테스트 시 통제 불가
}
}
해결:
// 추상화
public interface Clock {
LocalDateTime now();
}
@Component
public class SystemClock implements Clock {
public LocalDateTime now() { return LocalDateTime.now(); }
}
@TestConfiguration
public class TestClockConfig {
@Bean
public Clock testClock() {
return () -> LocalDateTime.of(2024, 1, 1, 12, 0); // 고정
}
}
// 사용
@Service
@RequiredArgsConstructor
public class FareService {
private final Clock clock;
public void register() {
Fare fare = new Fare();
fare.setCreatedAt(clock.now()); // 테스트 시 고정 시간
}
}
→ 시간도 의존성. DIP로 통제 가능.
// 추상화
public interface PaymentGateway {
PaymentResult process(PaymentRequest request);
}
// 토스 페이먼츠 구현
@Component("tossPayment")
public class TossPaymentAdapter implements PaymentGateway {
private final TossClient tossClient;
@Override
public PaymentResult process(PaymentRequest request) {
TossResponse response = tossClient.requestPayment(...);
return PaymentResult.from(response);
}
}
// 카카오 페이 구현
@Component("kakaoPayment")
public class KakaoPaymentAdapter implements PaymentGateway {
private final KakaoClient kakaoClient;
@Override
public PaymentResult process(PaymentRequest request) {
KakaoResponse response = kakaoClient.requestPayment(...);
return PaymentResult.from(response);
}
}
// 사용
@Service
@RequiredArgsConstructor
public class CheckoutService {
private final Map<String, PaymentGateway> gateways; // Spring이 자동 주입
public void checkout(String gatewayName, PaymentRequest request) {
PaymentGateway gateway = gateways.get(gatewayName);
gateway.process(request);
}
}
→ 새 결제 게이트웨이 추가 시 새 어댑터만. 기존 코드 수정 X.
5가지 원칙이 모두 적용된 ILIC 시스템:
// SRP — 한 책임
@Service
public class FareRegistrationService { // 운임 등록만
private final FareRepository repository; // DIP — 추상화 의존
private final List<FareValidator> validators; // OCP — 검증 규칙 확장 가능
private final FareNotificationPort notification; // ISP — 알림만
@Transactional
public Fare register(FareRequest request) {
// 검증 (LSP — 모든 validator가 같은 약속 지킴)
validators.forEach(v -> v.validate(request));
// 저장 (DIP — 인터페이스에 위임)
Fare fare = Fare.create(request);
repository.save(fare);
// 알림 (DIP)
notification.notifyRegistered(fare);
return fare;
}
}
→ SOLID가 모두 적용된 모범.
@Service
public class FareService {
private final JpaFareRepository repository; // ❌ 구체 클래스
public FareService(JpaFareRepository repository) { // ❌
this.repository = repository;
}
}
해결 — 인터페이스에 의존:
@Service
public class FareService {
private final FareRepository repository; // ✅ 인터페이스
}
[infrastructure/jpa/]
- FareRepository (인터페이스)
- JpaFareRepository (구현)
[domain/]
- FareService → infrastructure 패키지에 의존 ❌
해결 — 인터페이스를 도메인에:
[domain/]
- FareRepository (인터페이스) ← 도메인이 정의
- FareService (사용)
[infrastructure/jpa/]
- JpaFareRepository implements FareRepository ← 구현
→ 도메인이 인프라를 모르도록. 의존 방향 역전.
// ❌ 인터페이스가 JPA 의존
public interface FareRepository {
void save(Fare fare);
Page<Fare> findAll(Pageable pageable); // ← Spring Data 의존
}
→ 인터페이스가 Spring Data 라이브러리에 종속.
해결:
public interface FareRepository {
void save(Fare fare);
List<Fare> findAll(int page, int size); // 도메인 타입만
}
@Service
public class FareService {
private final FareRepository repository = new JpaFareRepository(); // ❌
}
→ DIP 위반 + 테스트 불가.
해결 — 생성자 주입:
@Service
@RequiredArgsConstructor
public class FareService {
private final FareRepository repository; // 외부 주입
}
@Service
public class FareService {
@Autowired
private FareRepository repository; // ❌ 비권장
}
해결 — 생성자 주입:
@Service
@RequiredArgsConstructor
public class FareService {
private final FareRepository repository; // ✅
}
// ❌ 단순한 것까지 추상화
public interface NameProvider {
String getName();
}
public class CustomerNameProvider implements NameProvider {
public String getName() { return "Alice"; }
}
→ 변화 가능성 없는 곳까지 추상화. 과도한 설계.
원칙: 변화 가능성이 있는 곳만 추상화.
public interface FareRepository { ... }
public class FareRepositoryImpl implements FareRepository { ... }
// 1:1 매핑 — 의미 없을 수 있음 ⚠️
판단 기준:
→ "인터페이스가 진짜 가치가 있는가" 자문.
@Service
public class FareService {
private final FareRepository repository;
public void process(Fare fare) {
if (repository instanceof JpaFareRepository) { // ❌
((JpaFareRepository) repository).flushDb();
}
}
}
→ 추상화에 의존하지 않고 구체에 의존. DIP 위반.
해결 — 추상화에 메서드 추가:
public interface FareRepository {
void save(Fare fare);
void flush(); // 인터페이스에 추가
}
[SRP] — 한 책임 ✓
↓
[OCP] — 확장 가능 ✓
↓
[LSP] — 안전한 다형성 ✓
↓
[ISP] — 인터페이스 분리 ✓
↓
[DIP] ★ ← 지금 여기 — 의존 역전 (SOLID 마지막)
→ SOLID 5원칙 완성. Phase 3 완료!
[SRP] 책임 분리
↓
[OCP] 추상화 추가 (다형성)
↓
[LSP] 자식이 약속 지킴
↓
[ISP] 인터페이스 분리
↓
[DIP] 의존 방향 통제
↓
=> 좋은 객체지향 설계 ✅
→ SOLID는 한 사고의 5가지 표현.
| Phase 2 학습 | DIP 적용 |
|---|---|
| Unit 2.4 (다형성) | DIP의 토대 |
| Unit 2.5 (instanceof) | 남발하면 DIP 위반 |
| Unit 2.6 (Anonymous) | 즉석 추상화 (콜백) |
→ 다형성 없는 DIP는 불가능.
DIP가 모든 미래 학습의 토대:
3주차 (제네릭/람다):
Function, Consumer, Supplier = DIP의 함수형 표현5주차 (Spring DI/IoC) ★:
@Autowired, @Component, @Service5주차 (디자인 패턴):
8-9주차 (AOP):
11-12주차 (JPA):
13-14주차 (DB):
15주차 (Spring MVC):
17주차 (MSA):
18주차 (Security):
UserDetailsService, AuthenticationProvider 추상화19주차 (테스트):
→ DIP는 미래 모든 학습의 통로.
Robert C. Martin (Uncle Bob):
"Source code dependencies must point only inward, toward higher-level policies."
("소스 코드 의존성은 안쪽, 즉 더 높은 수준의 정책을 향해야 한다.")
Alistair Cockburn (Hexagonal):
"Allow an application to equally be driven by users, programs, automated test or batch scripts."
("애플리케이션이 사용자, 프로그램, 자동화 테스트, 배치 스크립트에 의해 동등하게 구동되도록.")
Eric Evans (DDD):
"The domain is the heart of the software."
("도메인이 소프트웨어의 심장이다.")
→ 모두 DIP의 본질을 다른 표현으로.
| 질문 | 이 Unit에서의 답 |
|---|---|
| "DIP가 뭔가요?" | 구체에 의존하지 말고 추상화에 의존하라 |
| "IoC와 DI?" | IoC는 제어 역전, DI는 그 한 형태 |
| "왜 생성자 주입?" | final 가능, 누락 시 컴파일 에러, 테스트 명시적 |
| "DIP 적용 시 주의?" | 인터페이스를 도메인에 둠, 추상화가 구체에 의존 X |
| "Spring DI와 DIP?" | Spring DI가 DIP의 직접적 구현 |
| "Hexagonal Architecture?" | DIP의 아키텍처 레벨 적용 |
1️⃣ DIP는 "구체 대신 추상에 의존하라" 의 원칙이다.
Uncle Bob 정의: "상위 모듈은 하위 모듈에 의존하지 않아야 한다. 둘 다 추상화에 의존해야 한다." 핵심은 의존 방향의 역전 — 자연스러운 방향 (고수준 → 저수준) 을 인터페이스를 사이에 두어 양쪽이 추상화에 의존하게 만든다. 콘센트 표준, USB-C 표준 처럼 양쪽이 표준에 의존하는 구조.
2️⃣ DIP는 Spring DI, Hexagonal/Clean Architecture 의 토대다.
Spring 의 IoC 컨테이너가 DIP의 직접적 구현 — 비즈니스는 인터페이스만 선언하고, Spring 이 구현체를 주입. 인터페이스를 도메인이 정의, 인프라가 따름 이 진짜 의존 역전. Hexagonal Architecture (Ports and Adapters) 와 Clean Architecture 모두 DIP의 시스템 레벨 적용. 현대 백엔드 아키텍처의 출발점이자 정점.
3️⃣ DIP는 모든 SOLID 원칙의 통합점이자 자바 학습의 통로다.
SOLID 5원칙이 DIP에서 만남: SRP의 책임 분리, OCP의 다형성, LSP의 안전한 대체, ISP의 인터페이스 분리가 DIP의 추상화로 통합. Phase 4부터의 모든 학습 (Spring, JPA, AOP, MVC, MSA, Security, Test) 이 DIP 위에 서 있음. 단, 변화 가능성 있는 곳에만 추상화 — 모든 곳에 인터페이스는 과도한 설계.
박승제님의 ILIC 코드 점검:
DIP 위반 신호 ⚠️:
JpaXxxRepository)new 로 인프라 객체 생성@Autowired private) 사용3개 이상 해당 = DIP 적용 가치 큼.
| Unit | 원칙 | 핵심 |
|---|---|---|
| 3.1 | SRP | 한 클래스, 한 변경 이유 |
| 3.2 | OCP ★★★ | 확장 열림, 수정 닫힘 |
| 3.3 | LSP | 자식이 부모 대체 가능 |
| 3.4 | ISP | 인터페이스 분리 |
| 3.5 | DIP ★★★ | 추상화에 의존 |
"SOLID 는 5가지 따로 원칙이 아닌 1가지 사고의 5가지 표현이다."
책임을 분리하고 (SRP) → 확장 가능하게 만들고 (OCP) → 안전한 다형성으로 (LSP) → 인터페이스를 분리하고 (ISP) → 의존을 역전시키면 (DIP) → 좋은 객체지향 설계.
이제 추상화의 세계에서 물리적 실행의 세계 로:
→ Phase 1~3이 '어떻게 설계할까', Phase 4~5가 '어떻게 동작할까'.