Dependency Injection: 객체지향 설계를 파괴하고 승리한 안티패턴

발행: (2025년 12월 8일 오후 05:47 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Enterprise applications didn’t get better with DI – they became slower, harder to change, harder to test, harder to upgrade, and more expensive to maintain. DI didn’t fix complexity; it became the complexity.

Imagine you go to the doctor because you have a sore thumb.
The doctor pulls out a sledgehammer and smashes your foot.
You limp out screaming.
He smiles: “Good. You’re not thinking about your thumb anymore, are you?”
That is Dependency Injection.
EJB2 was the sore thumb. DI was the sledgehammer.
EJB3 cured the thumb in 2006.
We have spent twenty years smashing the rest of the body and calling it “enterprise best practice”.

1. The Historical Accident (2003–2006)

  • EJB2 forced JNDI lookups and Home interfaces, creating unnecessary layers of indirection and boilerplate for container‑managed beans.
  • Rod Johnson built a lighter container in Spring to hide that pain, making it easier to work around the lifecycle mismatches.
  • Then EJB3/JPA arrived, simplifying everything—no more home interfaces, no more ugly lookups—and the original pain disappeared forever.

Yet, instead of celebrating the freedom to instantiate objects directly, we kept the sledgehammer and made it mandatory for every class in the system.

2. A Concrete Example Everyone Recognises

In DI frameworks today, something as simple as permanent storage gets bloated with abstractions:

interface Storage { /* … */ }

@Component
@Qualifier("perm")
class S3Storage implements Storage { /* … */ }

@Service
class OrderService(
    @Qualifier("perm") Storage storage
) { /* … */ }

The meaning of “permanent” is now an invisible configuration detail scattered across qualifiers, annotations, and YAML files. Change the backend? You’re hunting through the entire codebase for wiring mismatches.

Contrast that with a responsibility‑centered approach:

final class PermanentStorage {
    private static final S3Client s3 = resilientInstrumentedS3Client();

    public static Identifier save(Object o) { /* … */ }
    public static Object load(Identifier id) { /* … */ }
}

Just two public methods. All the gritty details—retries, metrics, tracing, multipart uploads, auth rotation—live inside that one class forever. No callers ever touch AmazonS3 directly. Switching to a different vendor? Change one file, and the rest of the system doesn’t even notice.

This isn’t just cleaner; it’s how encapsulation was meant to work.

3. DI Is Incompatible with Object‑Oriented Design

In true OO, a constructor creates a fully valid object, owning its invariants and hiding implementation details behind a clean API.

With DI containers, constructors become mere dependency laundry lists, declaring what the container should inject rather than guaranteeing the object’s validity. Invariants are pushed out to configuration files or annotations, making it impossible to reason about an object’s state at creation time. You can’t even instantiate most classes manually without spinning up the entire context—turning new into a code smell and replacing local reasoning with global magic.

Most arguments in favour of DI today are not based on functional necessity or architectural clarity, but on cultural inertia. Developers defend it because it’s what they were taught, not because it solves real problems in modern codebases.

4. The SOLID Justification Is a Myth (DIP and ISP Have Nothing to Do With DI)

The industry loves to justify DI by pointing at SOLID — especially DIP (Dependency Inversion Principle) and ISP (Interface Segregation Principle). This is a category error.

4.1 What DIP Actually Says

The real DIP is a static design rule about source‑code dependency direction, not runtime injection.

DIP means

  • High‑level modules shouldn’t depend on low‑level modules.
  • Both should depend on abstractions.
  • Those abstractions should represent meaningful roles in the domain.

That’s it.

DIP does not require:

  • a DI container
  • annotation‑based wiring
  • interface‑per‑class
  • configuration‑driven object graphs

DIP simply encourages stable, focused abstractions — small interfaces with a single, coherent responsibility (not necessarily one method, but one purpose).

DI, however, promotes the exact opposite:

  • one‑implementation interfaces
  • constructor laundry lists
  • leaking implementation details into the API
  • global container coupling instead of local reasoning

Where DIP tries to reduce coupling, DI introduces a new global kind of coupling — to the container itself.

4.2 What ISP Actually Says

ISP is even simpler: Clients should not be forced to depend on methods they do not use.

In practice:

  • Interfaces should be small
  • Interfaces should be cohesive
  • Interfaces should match a role, not a convenience artifact

ISP is about shrinking interfaces, not creating more of them.

DI culture twists ISP into:

  • “Every class must have an interface.”
  • “Every dependency should be injected via abstraction.”

This is backwards. Creating a pointless wrapper interface for a single implementation is a violation of ISP, not compliance with it.

4.3 The Core Problem

  • DIP and ISP are design principles.
  • DI is a runtime framework mechanism.

They solve different problems. Confusing them led the industry to believe:

  • “If we use DI, we’re applying SOLID!”
  • “If we annotate everything, we’re doing OO!”

In reality:

  • DIP wants clean, minimal, meaningful abstractions.
  • ISP wants focused role‑based interfaces.
  • DI produces an explosion of meaningless interfaces and container‑driven wiring — the opposite of both principles.

The result: a massive ecosystem treating DI as moral correctness while quietly eroding encapsulation, readability, and evolutionary design.

5. DI Actively Prevents Evolutionary Architecture

Enterprise software doesn’t die after launch—it lives for 10–25 years, with shifting business rules and tech stacks. You want evolutionary change: small updates, continuous cleanup, minimal surprises.

Rich domain objects enable this by encapsulating logic in plain Java, with minimal framework interference. Framework upgrades touch only a few boundary lines, and the core stays intact.

DI does the opposite, breeding revolutionary software that needs full rewrites every 5–7 years. Behavior is scattered into anemic service classes, invariants are lost, and logic is de‑contextualized across configuration, qualifiers, proxies, and AOP. Every class is wired into a global graph, so small changes ripple everywhere.

The framework becomes the skeleton, dominating the domain, and upgrades like Spring Boot 2 → 3 become multi‑man‑year nightmares—complete with seven‑figure consulting fees—because the real program lives in proxies and YAML, not readable code.

Without containers, the difference is night and day:

  • builds run faster without classloader noise or spin‑ups,
  • code is easier to read with explicit logic instead of magic,
  • the application becomes transparent—like removing a blanket from your hi‑fi speakers.

Logic centralizes in plain Java objects, not scattered across services and config files, making evolution incremental instead of catastrophic.

6. My Personal Controlled Experiment

I used Spring for years. I also used EJB2. And here is the dirty secret I discovered:

EJB2 was already simpler than modern Spring Boot.
The overhead and clutter of EJB2 with RMI for each call got expanded to container maintenance in Spring. In fact, the legacy of lifecycle management that was present in EJB2 has been amplified by DI by container managing everything and thus making the application unnecessarily …

Back to Blog

관련 글

더 보기 »

Adapter Pattern을 실제 예제로 설명

소개: 타사 결제 게이트웨이를 애플리케이션에 통합하고 있습니다. 모든 것이 순조로워 보이지만, 그들의 SDK가 comple…를 사용한다는 것을 깨닫게 됩니다.