플러그인 시스템이 조용히 실패하는 이유: 약한 경계 설명

작성자

카테고리:

← 피드로
DEV Community · Chris Roy · 2026-08-09 개발(SW)

Most plugin systems continue to work just well enough.

That is what makes them dangerous.

A typical plugin system starts from a reasonable goal of letting people extend functionality without touching the core. The core stays stable. Optional functionality lives at the edges. Responsibility is distributed cleanly, at least on the whiteboard. In the early stages this holds up fine in practice, not just on paper. Plugins are few. Contracts are narrow enough that a plugin author can read the documented interface once and know the whole shape of what they’re allowed to touch. A new contributor can look at the system and reason about it locally, without holding the whole thing in their head. Nobody has had to break a promise to anyone yet.

That equilibrium doesn’t last, and it rarely announces its own ending.

The failure is almost never malice, and it’s rarely incompetence either. It comes from accommodation. A plugin needs access to a little more state than the contract allows. Another needs to intercept a request earlier in the lifecycle than anyone originally envisioned. A third needs to touch rendering, configuration, and persistence at once, because the feature genuinely spans all three and splitting it feels artificial. Each request, taken on its own, is defensible. Nobody who approved any single one of them was wrong to do it.

Read any one of those changes in a code review and it looks fine. A reviewer sees a small diff, a plausible justification, a deadline attached to it, and approves it. What a reviewer almost never sees is the fifteenth version of that same request, made by a different plugin, for a different reason, landing on a boundary that’s already been quietly widened fourteen times before. Each decision is local. The damage is cumulative, and cumulative damage is exactly the kind that doesn’t show up in any single code review.

But the plugin system stops being an extension mechanism somewhere in that process. It becomes a second system running alongside the first, wired to it at dozens of points nobody wrote down.

The first quiet failure: the erosion of boundaries

A plugin system only works if there’s a hard line between what the core owns and what plugins are allowed to touch. Almost every real system compromises that line early, because compromising it is the path of least resistance. Internal data structures get exposed. Lifecycle hooks get added for one plugin’s sake. Mutable globals stay reachable because locking them down would mean telling three plugin authors their code is about to break. The justification is always some version of “trust the plugin author to use it responsibly.”

Trust doesn’t scale the way access does.

Once a plugin depends on an internal detail, that detail stops being internal, whatever the documentation says. It becomes a contract by accident, and accidental contracts are the ones nobody can safely break, because nobody agreed to them on purpose in the first place.

Firefox lived this for over a decade. Its original extension model, built on XUL and XPCOM, gave add-ons direct access to the browser’s internal component model, i.e., the same libraries that handled networking, rendering, and security, wide open to any extension that wanted them. It made Firefox’s extension ecosystem one of the richest on the web for years. It also meant that by the time Mozilla needed to rebuild Firefox around a multiprocess architecture, in pursuit of the same stability and performance gains Chrome had already shipped, legacy XUL extensions were incompatible with it by construction. They reached into places a multiprocess browser couldn’t safely expose anymore. Mozilla’s own reasoning was direct: reliance on XUL and XPCOM had become an obstacle to work the browser needed to do, specifically the Electrolysis multiprocess project and the Servo rendering engine, both of which required a much cleaner separation between the browser’s internals and whatever code an extension was running. You can’t safely split a browser across multiple processes if any extension might be reaching directly into a data structure that now lives in a different process entirely. The fix wasn’t a patch. It was a new extension API, WebExtensions, deliberately narrower and modeled closely on Chrome’s, and a hard cutoff: as of Firefox 57 in November 2017, the old add-ons simply stopped loading. Years of accumulated extensions broke at once, not because Mozilla stopped caring about them, but because the contract those extensions depended on had never really been a contract. It was just access nobody had gotten around to closing off, and closing it off years later took a browser-wide breaking release to do what a narrower API would have made unnecessary from the start.

That’s the shape of this failure everywhere it shows up. The break, when it finally comes, doesn’t look proportional to what caused it. It looks catastrophic, because years of small, reasonable accommodations were never priced as debt until the bill came due all at once.

The second quiet failure: shared responsibility without ownership

In a plugin system that’s still healthy, you can always answer one question cleanly: who owns this invariant. Either the core enforces it, or the invariant doesn’t really exist. Once a system starts failing, that question stops having a clean answer. Invariants get enforced collectively, informally, by convention. One plugin assumes another has already run. The core assumes plugins will clean up their own state. Execution order becomes, in modern LLMs’ favorite phrase — load-bearing, and nobody wrote it down because nobody designed it on purpose.

At that point behavior isn’t really being designed anymore. It’s being negotiated, extension by extension, mostly by accident.

Atom’s package system got there. Packages ran with full access to the DOM and to Node’s runtime, which was also the thing that made Atom feel so hackable. You could install a package, and it could reshape almost anything about how the editor behaved, down to intercepting keystrokes before the editor’s own core ever saw them. That same openness meant packages could and did collide in ways nobody upstream could fully predict, each one reasonable in isolation, none of them designed against each other. Two packages might both assume they were the first to touch a given buffer. Neither was wrong on its own. Together, loaded in whatever order the user happened to install them, they’d produce a bug that only existed because of the combination, and no one owned the combination.

GitHub announced Atom’s sunset in June 2022, archiving the project that December. The stated reason was that community involvement and feature development had stalled, which is true, but it’s also true that the editor Microsoft chose to keep investing in instead, Visual Studio Code, had been built from day one on a fundamentally more restrained model for what an extension was allowed to do. Atom’s own performance problems, widely attributed at the time to its Electron foundation, were at least partly a symptom of the same openness. An editor that lets every package touch everything has a much harder time optimizing anything, because it can never be sure what depends on the thing it wants to change.

This is why debugging a plugin-heavy system is so often miserable. Failures don’t map to a single component. They emerge from interaction, from two pieces of code that were each individually correct and jointly wrong. Removing one plugin “fixes” the bug, and nobody on the team can explain why beyond a shrug. The system still runs. Nobody quite trusts it anymore.

The third quiet failure: capability leakage

A lot of plugin systems expose power where they should be exposing interfaces. Instead of a narrow, enforceable contract, they hand out a hook into some broad execution context and call it flexibility. It is flexible. It’s also the reason the core can no longer promise anything about what happens after a plugin runs.

When a plugin can do anything, the core can guarantee nothing. This flexibility gets mistaken for resilience.

ActiveX is the clean historical case. Introduced by Microsoft in 1996, it let a web page embed a control that ran with essentially the same privileges as any other program on the machine, no sandbox, no meaningfully scoped permission model. That an ActiveX control could do anything a desktop application could do, right inside the browser, was the appeal. It was also, as one retrospective later put it bluntly, the web’s biggest mistake. A long, ugly stream of vulnerabilities followed, most of them boiling down to the same root cause where a hostile web page could get an ActiveX control to run arbitrary code as the user, no exploit chain required beyond convincing someone to visit the wrong site. Microsoft spent years bolting on warnings and kill bits before finally disabling ActiveX by default across Office and Microsoft 365 in 2024, nearly three decades after it shipped, rather than ever retrofitting real boundaries into a system built without any.

ActiveX wasn’t alone in this. The broader family of browser plugin technologies built around NPAPI, the interface that let Flash, Java applets, and Silverlight run inside a browser tab, made a similar bet: give the plugin a wide-open door into the host process in exchange for whatever capability the plugin wanted to offer. Chrome and Firefox spent the 2010s walking that bet back, first with sandboxing, then with click-to-play defaults, then by dropping NPAPI support entirely. Chrome removed it in 2015, Firefox followed in 2017. The technology wasn’t uniquely badly built. It was built on the same premise as ActiveX: broad access is a reasonable price for broad capability. Browsers eventually decided it wasn’t, and every plugin depending on that access had to be rebuilt or abandoned to match.

The pattern repeats in less dramatic form constantly, inside systems that never make headlines for it. Defensive code creeps into the core because nobody trusts what a plugin might have already touched. Validation gets duplicated at every boundary because there’s no boundary anyone’s confident is real. Execution paths fill up with checks for plugin interference that shouldn’t need to exist. The system gets harder to reason about, not because the underlying problem demands it, but because the extension mechanism made that complexity permissible instead of impossible.

None of this arrives as a single moment. There’s no line the system visibly crosses. It just gets slowly harder to reason about, harder to test, harder to change, until the honest answer to “where does this system end” is that nobody’s sure anymore.

The systems that age better, and why

Some plugin systems avoid all three failures for a long time, and they don’t do it by being smarter about accommodation. They do it by refusing more of it.

Eclipse is one of the oldest working examples. Its plugin model, formalized on top of OSGi, is built around what Eclipse’s own architecture documentation calls the plug-in contract, a small, explicit set of extension points, each one declared up front, each one owned by whichever plugin defines it. A plugin can’t reach into another plugin’s internals just because both happen to be loaded in the same process. It has to go through a declared extension point or it doesn’t get in at all. OSGi’s bundle model reinforces this at the runtime level, with each bundle getting its own classloader and explicit version boundaries between dependencies. That discipline is a large part of why OSGi has stayed a working foundation for enterprise software for close to two decades, hot-swapping components in running systems without the whole platform destabilizing every time somebody ships an update.

Visual Studio Code took a related but distinct approach to the same discipline. Instead of trusting extensions with the same process as the editor, it doesn’t give them the choice. Every extension runs in a separate process, the extension host, talking to the main application only through a defined API surface and an RPC layer. An extension can be slow, buggy, even hostile, and the editor’s own responsiveness is largely insulated from it, because the extension was never given the kind of direct access that would let it take the rest of the application down with it.

> Aside: I don’t like VSCode personally. I like what Zed is doing and it is a major personal goal to one day create the best and greatest IDE. For now though, that seems a few years away.

That’s the same problem Atom’s package model ran into, solved by refusing the access in the first place instead of hoping plugin authors would behave. Where Eclipse enforces its boundary through a declared contract that extensions have to go through, VS Code enforces a version of the same idea through a process boundary that extensions can’t get around even if they wanted to. Different mechanism, same underlying refusal.

Eclipse and VS Code both made their refusal at the architecture level, before any extension author ever showed up wanting more. Chrome’s Manifest V3 shows what the same refusal looks like made in public, years into an established ecosystem, against developers who had built real businesses on the access being taken away. Chrome’s older extension API let extensions intercept and modify every network request a browser made, through the webRequest API, which is exactly the kind of broad, ambient power that makes an extension platform expressive and also exactly the kind of power that makes it impossible to reason about what any given extension might be doing to your traffic. Google’s replacement, declarativeNetRequest, asks extensions to declare their filtering rules up front instead of intercepting requests live, which is a real reduction in what an extension can express. Ad blocker developers noticed immediately. Raymond Hill, who maintains uBlock Origin, warned as early as January 2019 that the change threatened to break his tool’s core functionality, and the backlash from extension developers has continued through every stage of the rollout since. Google shipped the restriction anyway. The ecosystem got smaller and louder about it in roughly equal measure, and Chrome’s extension platform got more predictable in exchange, which was the actual trade being made.

None of these systems are more flexible than the ones that failed. They’re quite opposite in the sense that these allow fewer extension points, narrower contracts, a core that stays authoritative over its own invariants no matter how reasonable the request to bend them sounds. When something can’t be expressed cleanly through the declared contract, it gets rejected, or it gets folded into the core through real design work instead of a workaround. These systems do get criticized constantly for being inflexible, for having smaller ecosystems, for saying no to things a more permissive competitor would say yes to. But it’s also why, years later, you can still point to who’s responsible when something breaks, and why a change to the system is something someone decided rather than something that just happened.

This is not a different philosophy of extensibility; it’s a different tolerance for ambiguity, albeit held consistently even when holding it costs adoption.

A plugin system that’s built to last is conservative on purpose. It doesn’t try to be extensible for every use case someone can imagine. It picks a small number of extension points and defends them, and it would rather refuse a plugin author’s request than accommodate one more exception it can’t fully account for, even when the refusal shrinks the ecosystem. Every one of the systems above paid a real, visible cost for that discipline: Eclipse’s plugin authors write against a stricter model than a looser one would demand, VS Code extensions can’t reach into the editor’s internals the way an Atom package once could, and Chrome’s ad blockers lost capability they’d relied on for years. None of that cost showed up as a headline the way a launch does. It showed up as a smaller, slower-growing ecosystem that people complained about constantly and kept building on anyway, because it kept working the way it was supposed to.

The alternative isn’t a dramatic failure. It’s a system that keeps technically functioning while quietly becoming harder to change, harder to understand, and harder to trust, until the day someone tries to fix that and discovers the ecosystem depending on the mess is now too large to break without taking the whole thing down with it. By then it’s usually too late to close the boundary without breaking everything built on top.

The system keeps running. It just stops moving, one deferred refusal at a time, until moving it again costs more than anyone’s willing to pay.

None of that is an accident. It’s the ordinary, predictable outcome of extension without boundaries, arrived at one reasonable accommodation at a time, by people who were right about every single decision except the sum of all of them.

Sources

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다