Featured image of post [Design Patterns] 브릿지와 플라이웨이트: 분리와 효율성

[Design Patterns] 브릿지와 플라이웨이트: 분리와 효율성

추상화와 구현을 분리하는 Bridge 패턴과 메모리 사용을 최적화하는 Flyweight 패턴의 고급 설계 기법을 탐구합니다. 대용량 객체 처리, 메모리 효율성, 추상화 계층 설계 등 성능과 유지보수성을 동시에 고려한 전문가 수준의 아키텍처 설계 방법을 학습합니다.

Bridge와 Flyweight 패턴을 통해 분리와 효율성의 철학을 탐구합니다. 변화의 축을 분리하고, 공유를 통해 메모리 효율성을 극대화하는 방법을 학습합니다.

서론: 두 가지 다른 최적화 철학

“좋은 설계는 변화에 유연하고 자원을 효율적으로 사용한다. Bridge는 변화의 축을 분리하여 유연성을 추구하고, Flyweight는 공유를 통해 효율성을 극대화한다.”

소프트웨어 설계에서 우리는 종종 두 가지 근본적인 도전에 직면합니다:

  1. 복잡성 관리: 변화하는 요구사항에 어떻게 유연하게 대응할 것인가?
  2. 자원 효율성: 제한된 메모리와 CPU를 어떻게 최적으로 활용할 것인가?

Bridge와 Flyweight 패턴은 이 두 도전에 대한 서로 다른 해답을 제시합니다:

Bridge 패턴의 철학: “분리하여 정복하라”

  • 문제: 추상화와 구현이 함께 변화하면서 발생하는 조합 폭발
  • 해결: 추상화와 구현을 독립적인 계층구조로 분리
  • 가치: 런타임 구현체 교체, 플랫폼 독립성, 테스트 용이성

Flyweight 패턴의 철학: “공유하여 절약하라”

  • 문제: 대량의 유사한 객체들이 메모리를 낭비하는 상황
  • 해결: 공통 상태는 공유하고 고유 상태만 개별 보관
  • 가치: 메모리 효율성, 성능 향상, 확장성 확보
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 현실적인 문제 상황들
public class DesignChallenges {
    
    // 문제 1: 조합 폭발 (Bridge가 해결)
    public void combinationExplosion() {
        // 원하는 것: 다양한 리모컨 × 다양한 기기
        // WindowsTV, WindowsRadio, MacTV, MacRadio, LinuxTV, LinuxRadio...
        // N개 플랫폼 × M개 기기 = N×M개 클래스 폭발!
        
        // 문제: 새로운 플랫폼이나 기기 추가 시 기하급수적 증가
    }
    
    // 문제 2: 메모리 낭비 (Flyweight가 해결)  
    public void memoryWaste() {
        // 게임 맵에 나무 10만 그루가 있다면?
        // 각 나무마다 텍스처, 모델, 색상 정보를 개별 보관?
        // 10만 × 10MB = 1TB 메모리 필요! 😱
        
        List<Tree> forest = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            forest.add(new Tree(x, y, "oak", "green", textureData)); // 중복!
        }
    }
}

이런 문제들을 어떻게 우아하게 해결할 수 있을까요?

Bridge 패턴: 추상화와 구현의 우아한 분리

패턴의 동기와 철학

Bridge 패턴은 “추상화(Abstraction)와 구현(Implementation)을 분리하여 각각 독립적으로 변화할 수 있게 하는” 패턴입니다.

GoF는 이를 “Decouple an abstraction from its implementation so that the two can vary independently"라고 정의했습니다.

Bridge 패턴의 핵심 구조

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
// 문제 상황: 조합 폭발을 피하고 싶은 경우
// 여러 종류의 메시징 시스템 × 여러 종류의 전송 방식

// 구현 인터페이스 (Implementation)
interface MessageSender {
    void sendMessage(String message, String recipient);
    boolean isConnected();
    void connect();
    void disconnect();
}

// 구체적 구현들 (ConcreteImplementation)
class EmailSender implements MessageSender {
    private String smtpServer;
    private int port;
    private boolean connected = false;
    
    public EmailSender(String smtpServer, int port) {
        this.smtpServer = smtpServer;
        this.port = port;
    }
    
    @Override
    public void connect() {
        System.out.println("Connecting to SMTP server: " + smtpServer + ":" + port);
        // SMTP 연결 로직
        connected = true;
    }
    
    @Override
    public void sendMessage(String message, String recipient) {
        if (!connected) connect();
        System.out.println("Sending email to " + recipient + ": " + message);
        // 실제 이메일 전송 로직
    }
    
    @Override
    public boolean isConnected() {
        return connected;
    }
    
    @Override
    public void disconnect() {
        System.out.println("Disconnecting from SMTP server");
        connected = false;
    }
}

class SMSSender implements MessageSender {
    private String apiKey;
    private String serviceUrl;
    private boolean connected = false;
    
    public SMSSender(String apiKey, String serviceUrl) {
        this.apiKey = apiKey;
        this.serviceUrl = serviceUrl;
    }
    
    @Override
    public void connect() {
        System.out.println("Connecting to SMS service: " + serviceUrl);
        // SMS API 연결 로직
        connected = true;
    }
    
    @Override
    public void sendMessage(String message, String recipient) {
        if (!connected) connect();
        System.out.println("Sending SMS to " + recipient + ": " + message);
        // 실제 SMS 전송 로직
    }
    
    @Override
    public boolean isConnected() {
        return connected;
    }
    
    @Override
    public void disconnect() {
        System.out.println("Disconnecting from SMS service");
        connected = false;
    }
}

class SlackSender implements MessageSender {
    private String webhookUrl;
    private String channel;
    private boolean connected = false;
    
    public SlackSender(String webhookUrl, String channel) {
        this.webhookUrl = webhookUrl;
        this.channel = channel;
    }
    
    @Override
    public void connect() {
        System.out.println("Connecting to Slack webhook: " + webhookUrl);
        // Slack 웹훅 연결 확인
        connected = true;
    }
    
    @Override
    public void sendMessage(String message, String recipient) {
        if (!connected) connect();
        System.out.println("Sending Slack message to " + channel + ": " + message);
        // 실제 Slack 메시지 전송 로직
    }
    
    @Override
    public boolean isConnected() {
        return connected;
    }
    
    @Override
    public void disconnect() {
        System.out.println("Disconnecting from Slack");
        connected = false;
    }
}

// 추상화 (Abstraction)
abstract class Notification {
    protected MessageSender sender;
    protected String title;
    
    public Notification(MessageSender sender, String title) {
        this.sender = sender;
        this.title = title;
    }
    
    public abstract void send(String message, String recipient);
    
    // 공통 기능
    protected String formatMessage(String content) {
        return "[" + title + "] " + content;
    }
    
    public void setSender(MessageSender sender) {
        if (this.sender != null && this.sender.isConnected()) {
            this.sender.disconnect();
        }
        this.sender = sender;
    }
}

// 구체적 추상화들 (RefinedAbstraction)
class SimpleNotification extends Notification {
    public SimpleNotification(MessageSender sender, String title) {
        super(sender, title);
    }
    
    @Override
    public void send(String message, String recipient) {
        String formattedMessage = formatMessage(message);
        sender.sendMessage(formattedMessage, recipient);
    }
}

class UrgentNotification extends Notification {
    private int retryCount;
    
    public UrgentNotification(MessageSender sender, String title, int retryCount) {
        super(sender, title);
        this.retryCount = retryCount;
    }
    
    @Override
    public void send(String message, String recipient) {
        String urgentMessage = "🚨 URGENT 🚨 " + formatMessage(message);
        
        // 재시도 로직 포함
        for (int i = 0; i < retryCount; i++) {
            try {
                sender.sendMessage(urgentMessage, recipient);
                System.out.println("Message sent successfully on attempt " + (i + 1));
                break;
            } catch (Exception e) {
                System.out.println("Attempt " + (i + 1) + " failed, retrying...");
                if (i == retryCount - 1) {
                    System.out.println("All retry attempts failed");
                }
            }
        }
    }
}

class ScheduledNotification extends Notification {
    private LocalDateTime scheduledTime;
    private Timer timer = new Timer();
    
    public ScheduledNotification(MessageSender sender, String title, LocalDateTime scheduledTime) {
        super(sender, title);
        this.scheduledTime = scheduledTime;
    }
    
    @Override
    public void send(String message, String recipient) {
        String scheduledMessage = formatMessage(message + " (Scheduled for: " + scheduledTime + ")");
        
        long delay = Duration.between(LocalDateTime.now(), scheduledTime).toMillis();
        
        if (delay > 0) {
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    sender.sendMessage(scheduledMessage, recipient);
                    System.out.println("Scheduled message sent at: " + LocalDateTime.now());
                }
            }, delay);
            System.out.println("Message scheduled for: " + scheduledTime);
        } else {
            sender.sendMessage(scheduledMessage, recipient);
            System.out.println("Message sent immediately (past scheduled time)");
        }
    }
}

// 사용 예시
public class BridgePatternExample {
    public static void main(String[] args) {
        // 다양한 구현체 생성
        MessageSender emailSender = new EmailSender("smtp.gmail.com", 587);
        MessageSender smsSender = new SMSSender("api-key-123", "https://sms-service.com");
        MessageSender slackSender = new SlackSender("https://hooks.slack.com/...", "#alerts");
        
        // 다양한 추상화 객체 생성
        Notification simpleEmail = new SimpleNotification(emailSender, "System Alert");
        Notification urgentSMS = new UrgentNotification(smsSender, "Critical Error", 3);
        Notification scheduledSlack = new ScheduledNotification(
            slackSender, "Daily Report", 
            LocalDateTime.now().plusMinutes(5)
        );
        
        // 사용
        simpleEmail.send("Server is running normally", "admin@company.com");
        urgentSMS.send("Database connection failed!", "+1234567890");
        scheduledSlack.send("Daily metrics report", "#general");
        
        // 런타임에 구현체 교체 가능
        urgentSMS.setSender(slackSender);  // SMS -> Slack으로 변경
        urgentSMS.send("Now sending via Slack instead", "#emergency");
        
        // 장점: N개 알림 타입 × M개 전송 방식 = N+M개 클래스 (조합 폭발 방지!)
    }
}

Bridge vs Adapter vs Strategy 비교

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Bridge, Adapter, Strategy의 차이점을 명확히 이해하기

// 1. Bridge: 추상화와 구현을 분리 (구조적 분리)
class MediaPlayer {
    private AudioCodec codec;  // 구현을 참조
    
    public MediaPlayer(AudioCodec codec) {
        this.codec = codec;
    }
    
    public void play(String filename) {
        codec.decode(filename);
        codec.play();
    }
    
    // 런타임에 코덱 교체 가능
    public void changeCodec(AudioCodec newCodec) {
        this.codec = newCodec;
    }
}

// 2. Adapter: 인터페이스 불일치 해결 (호환성 문제)
class LegacyAudioAdapter implements AudioCodec {
    private LegacyAudioLibrary legacyLib;
    
    @Override
    public void decode(String filename) {
        legacyLib.loadAudioFile(filename);  // 다른 인터페이스를 변환
    }
}

// 3. Strategy: 알고리즘 교체 (행동 변경)
class CompressionContext {
    private CompressionStrategy strategy;
    
    public void compress(String data) {
        strategy.compress(data);  // 압축 알고리즘 교체
    }
}

/*
비교 요약:
- Bridge: "무엇을 하는가"와 "어떻게 하는가"를 분리
- Adapter: "호환되지 않는 것"을 "호환되게" 만듦
- Strategy: "다양한 방법" 중 "하나를 선택"하여 수행
*/

Flyweight 패턴: 메모리 효율성의 극한 추구

패턴의 동기와 철학

Flyweight 패턴은 **“대량의 유사한 객체들을 효율적으로 지원”**하는 패턴입니다. 핵심 아이디어는 **내재적 상태(Intrinsic State)**와 **외재적 상태(Extrinsic State)**를 분리하는 것입니다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// 문제 상황: 텍스트 에디터에서 백만 개의 문자를 렌더링
// 각 문자마다 폰트, 크기, 색상 정보를 개별적으로 저장한다면?

// 비효율적인 방법
class Character_BAD {
    private char c;
    private String fontFamily;  // "Arial" (반복!)
    private int fontSize;       // 12 (반복!)
    private Color color;        // Color.BLACK (반복!)
    private int x, y;          // 위치는 각자 다름
    
    // 1,000,000개 문자 × 100 bytes = 100MB 메모리 사용
}

// Flyweight 방법으로 해결
// 공통된 부분(폰트 정보)은 공유하고, 개별적인 부분(위치)만 따로 저장

// Flyweight 인터페이스
interface CharacterFlyweight {
    void render(Canvas canvas, int x, int y, Color color);
    int getWidth();
    int getHeight();
}

// 구체적 Flyweight - 내재적 상태만 보유
class ConcreteCharacter implements CharacterFlyweight {
    private final char character;      // 내재적 상태
    private final String fontFamily;   // 내재적 상태  
    private final int fontSize;        // 내재적 상태
    private final byte[] glyphData;    // 내재적 상태 (폰트 렌더링 데이터)
    
    // 한 번 생성되면 변경되지 않음 (불변 객체)
    public ConcreteCharacter(char character, String fontFamily, int fontSize) {
        this.character = character;
        this.fontFamily = fontFamily;
        this.fontSize = fontSize;
        this.glyphData = loadGlyphData(character, fontFamily, fontSize);
    }
    
    @Override
    public void render(Canvas canvas, int x, int y, Color color) {
        // x, y, color는 외재적 상태로 매개변수로 받음
        canvas.setColor(color);
        canvas.drawGlyph(glyphData, x, y);
    }
    
    @Override
    public int getWidth() {
        return calculateWidth(glyphData);
    }
    
    @Override
    public int getHeight() {
        return fontSize;
    }
    
    private byte[] loadGlyphData(char c, String font, int size) {
        // 실제로는 폰트 파일에서 글리프 데이터를 로딩
        System.out.println("Loading glyph data for '" + c + "' in " + font + " " + size + "pt");
        return new byte[1024]; // 가상의 글리프 데이터
    }
    
    private int calculateWidth(byte[] glyphData) {
        // 글리프 데이터에서 너비 계산
        return fontSize / 2; // 간단한 예시
    }
}

// Flyweight Factory - 객체 공유 관리
class CharacterFlyweightFactory {
    private static final Map<String, CharacterFlyweight> flyweights = new ConcurrentHashMap<>();
    
    public static CharacterFlyweight getCharacter(char c, String fontFamily, int fontSize) {
        String key = c + "_" + fontFamily + "_" + fontSize;
        
        return flyweights.computeIfAbsent(key, k -> {
            System.out.println("Creating new flyweight for: " + key);
            return new ConcreteCharacter(c, fontFamily, fontSize);
        });
    }
    
    public static int getFlyweightCount() {
        return flyweights.size();
    }
    
    public static void printStatistics() {
        System.out.println("Total flyweights created: " + flyweights.size());
        System.out.println("Memory saved: " + calculateMemorySaved() + " MB");
    }
    
    private static long calculateMemorySaved() {
        // 단순 계산 예시
        return flyweights.size() * 100; // 각 flyweight가 100KB 절약한다고 가정
    }
}

// Context - 외재적 상태 보유
class CharacterContext {
    private final int x, y;                    // 외재적 상태 (위치)
    private final Color color;                 // 외재적 상태 (색상)
    private final CharacterFlyweight flyweight; // Flyweight 참조
    
    public CharacterContext(int x, int y, Color color, char c, String fontFamily, int fontSize) {
        this.x = x;
        this.y = y;
        this.color = color;
        this.flyweight = CharacterFlyweightFactory.getCharacter(c, fontFamily, fontSize);
    }
    
    public void render(Canvas canvas) {
        flyweight.render(canvas, x, y, color);
    }
    
    public int getX() { return x; }
    public int getY() { return y; }
    public Color getColor() { return color; }
    public CharacterFlyweight getFlyweight() { return flyweight; }
}

// 텍스트 문서 - Flyweight 활용
class TextDocument {
    private List<CharacterContext> characters = new ArrayList<>();
    private String defaultFontFamily = "Arial";
    private int defaultFontSize = 12;
    
    public void addText(String text, int startX, int startY, Color color) {
        int currentX = startX;
        int currentY = startY;
        
        for (char c : text.toCharArray()) {
            if (c == '\n') {
                currentX = startX;
                currentY += defaultFontSize + 2; // 줄 간격
                continue;
            }
            
            CharacterContext context = new CharacterContext(
                currentX, currentY, color, c, defaultFontFamily, defaultFontSize
            );
            characters.add(context);
            
            // 다음 문자 위치 계산
            currentX += context.getFlyweight().getWidth();
        }
    }
    
    public void render(Canvas canvas) {
        System.out.println("Rendering document with " + characters.size() + " characters");
        for (CharacterContext character : characters) {
            character.render(canvas);
        }
    }
    
    public void printMemoryUsage() {
        System.out.println("Document statistics:");
        System.out.println("- Total characters: " + characters.size());
        System.out.println("- Unique flyweights: " + CharacterFlyweightFactory.getFlyweightCount());
        
        // 메모리 절약 계산
        long withoutFlyweight = characters.size() * 100L; // 각 문자당 100 bytes
        long withFlyweight = CharacterFlyweightFactory.getFlyweightCount() * 100L + characters.size() * 20L; // flyweight + context
        long saved = withoutFlyweight - withFlyweight;
        
        System.out.println("- Memory without Flyweight: " + withoutFlyweight + " bytes");
        System.out.println("- Memory with Flyweight: " + withFlyweight + " bytes");
        System.out.println("- Memory saved: " + saved + " bytes (" + 
                          (saved * 100 / withoutFlyweight) + "% reduction)");
    }
}

// 사용 예시
public class FlyweightPatternExample {
    public static void main(String[] args) {
        Canvas canvas = new MockCanvas();
        TextDocument document = new TextDocument();
        
        // 대량의 텍스트 추가 (현실적인 시나리오)
        document.addText("Hello World! This is a sample text.", 10, 10, Color.BLACK);
        document.addText("Hello World! This is another line.", 10, 30, Color.BLUE);
        document.addText("Same characters appear multiple times.", 10, 50, Color.BLACK);
        
        // 더 많은 텍스트 추가 (Flyweight 효과 확인)
        for (int i = 0; i < 100; i++) {
            document.addText("Line " + i + ": Hello World!", 10, 70 + i * 20, 
                           i % 2 == 0 ? Color.BLACK : Color.BLUE);
        }
        
        // 렌더링
        document.render(canvas);
        
        // 메모리 사용량 분석
        document.printMemoryUsage();
        CharacterFlyweightFactory.printStatistics();
        
        /*
         * 출력 예시:
         * Creating new flyweight for: H_Arial_12
         * Creating new flyweight for: e_Arial_12
         * Creating new flyweight for: l_Arial_12
         * Creating new flyweight for: o_Arial_12
         * Creating new flyweight for:  _Arial_12  (공백)
         * ...
         * 
         * Document statistics:
         * - Total characters: 3847
         * - Unique flyweights: 26  (a-z, A-Z, 0-9, 공백, 특수문자 등)
         * - Memory without Flyweight: 384,700 bytes
         * - Memory with Flyweight: 79,540 bytes
         * - Memory saved: 305,160 bytes (79% reduction)
         */
    }
}

// Mock Canvas for demonstration
class MockCanvas implements Canvas {
    private Color currentColor = Color.BLACK;
    
    @Override
    public void setColor(Color color) {
        this.currentColor = color;
    }
    
    @Override
    public void drawGlyph(byte[] glyphData, int x, int y) {
        // 실제로는 화면에 그리기
        // System.out.println("Drawing glyph at (" + x + "," + y + ") with color " + currentColor);
    }
}

interface Canvas {
    void setColor(Color color);
    void drawGlyph(byte[] glyphData, int x, int y);
}

enum Color {
    BLACK, BLUE, RED, GREEN
}

게임 개발에서의 Flyweight 활용

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// 게임에서 파티클 시스템 최적화
// 수만 개의 파티클이 동시에 존재하는 상황

// Flyweight 인터페이스
interface ParticleFlyweight {
    void update(float deltaTime, float x, float y, float velocityX, float velocityY);
    void render(Graphics graphics, float x, float y, float scale, float alpha);
}

// 구체적 Flyweight들
class FireParticle implements ParticleFlyweight {
    private final Texture texture;      // 내재적 상태
    private final Color baseColor;      // 내재적 상태
    private final float baseLifetime;   // 내재적 상태
    
    public FireParticle() {
        this.texture = TextureManager.load("fire_particle.png");
        this.baseColor = Color.ORANGE;
        this.baseLifetime = 2.0f;
    }
    
    @Override
    public void update(float deltaTime, float x, float y, float velocityX, float velocityY) {
        // 물리 업데이트 (외재적 상태를 매개변수로 받음)
        // 실제로는 더 복잡한 파티클 물리 계산
    }
    
    @Override
    public void render(Graphics graphics, float x, float y, float scale, float alpha) {
        graphics.setColor(baseColor.withAlpha(alpha));
        graphics.drawTexture(texture, x, y, scale);
    }
}

class SmokeParticle implements ParticleFlyweight {
    private final Texture texture;
    private final Color baseColor;
    private final float baseLifetime;
    
    public SmokeParticle() {
        this.texture = TextureManager.load("smoke_particle.png");
        this.baseColor = Color.GRAY;
        this.baseLifetime = 5.0f;
    }
    
    @Override
    public void update(float deltaTime, float x, float y, float velocityX, float velocityY) {
        // 연기 특유의 물리 업데이트
    }
    
    @Override
    public void render(Graphics graphics, float x, float y, float scale, float alpha) {
        graphics.setColor(baseColor.withAlpha(alpha * 0.7f)); // 연기는 더 투명
        graphics.drawTexture(texture, x, y, scale);
    }
}

// Flyweight Factory
class ParticleTypeFactory {
    private static final Map<String, ParticleFlyweight> particleTypes = new HashMap<>();
    
    static {
        particleTypes.put("fire", new FireParticle());
        particleTypes.put("smoke", new SmokeParticle());
        particleTypes.put("spark", new SparkParticle());
        particleTypes.put("explosion", new ExplosionParticle());
    }
    
    public static ParticleFlyweight getParticleType(String type) {
        return particleTypes.get(type);
    }
}

// Context - 각 파티클의 개별 상태
class Particle {
    private float x, y;              // 외재적 상태 (위치)
    private float velocityX, velocityY; // 외재적 상태 (속도)
    private float scale;             // 외재적 상태 (크기)
    private float alpha;             // 외재적 상태 (투명도)
    private float lifetime;          // 외재적 상태 (수명)
    private ParticleFlyweight type;  // Flyweight 참조
    
    public Particle(float x, float y, String typeName) {
        this.x = x;
        this.y = y;
        this.velocityX = (float) (Math.random() - 0.5) * 100;
        this.velocityY = (float) (Math.random() - 0.5) * 100;
        this.scale = 1.0f;
        this.alpha = 1.0f;
        this.lifetime = 0.0f;
        this.type = ParticleTypeFactory.getParticleType(typeName);
    }
    
    public void update(float deltaTime) {
        // 위치 업데이트
        x += velocityX * deltaTime;
        y += velocityY * deltaTime;
        
        // 수명 업데이트
        lifetime += deltaTime;
        alpha = Math.max(0, 1.0f - lifetime / 3.0f); // 3초 후 완전 투명
        
        // Flyweight에 위임
        type.update(deltaTime, x, y, velocityX, velocityY);
    }
    
    public void render(Graphics graphics) {
        if (alpha > 0) {
            type.render(graphics, x, y, scale, alpha);
        }
    }
    
    public boolean isAlive() {
        return alpha > 0;
    }
}

// 파티클 시스템
class ParticleSystem {
    private List<Particle> particles = new ArrayList<>();
    
    public void emit(String particleType, float x, float y, int count) {
        for (int i = 0; i < count; i++) {
            particles.add(new Particle(x + (float) Math.random() * 10, 
                                     y + (float) Math.random() * 10, 
                                     particleType));
        }
    }
    
    public void update(float deltaTime) {
        // 파티클 업데이트 및 죽은 파티클 제거
        particles.removeIf(particle -> {
            particle.update(deltaTime);
            return !particle.isAlive();
        });
    }
    
    public void render(Graphics graphics) {
        for (Particle particle : particles) {
            particle.render(graphics);
        }
    }
    
    public void printStatistics() {
        System.out.println("Active particles: " + particles.size());
        System.out.println("Particle types: " + ParticleTypeFactory.particleTypes.size());
        
        // 메모리 계산
        long withoutFlyweight = particles.size() * 1000L; // 각 파티클이 1KB라고 가정
        long withFlyweight = particles.size() * 100L + ParticleTypeFactory.particleTypes.size() * 500L;
        
        System.out.println("Memory usage - Without Flyweight: " + withoutFlyweight + " bytes");
        System.out.println("Memory usage - With Flyweight: " + withFlyweight + " bytes");
        System.out.println("Memory saved: " + (withoutFlyweight - withFlyweight) + " bytes");
    }
}

Bridge vs Flyweight: 두 철학의 비교

패턴의 철학적 차이

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 두 패턴의 근본적 차이점 이해하기

// Bridge: "구조적 분리"를 통한 유연성
class DatabaseManager {
    private DatabaseConnector connector;  // 구현과 분리
    
    public DatabaseManager(DatabaseConnector connector) {
        this.connector = connector;
    }
    
    public void saveUser(User user) {
        // 추상화 레벨의 비즈니스 로직
        validateUser(user);
        String sql = "INSERT INTO users...";
        connector.execute(sql);  // 구현에 위임
    }
    
    // 런타임에 구현체 교체 가능 (Bridge의 핵심)
    public void switchDatabase(DatabaseConnector newConnector) {
        this.connector = newConnector;
    }
}

// Flyweight: "상태 분리"를 통한 효율성
class Icon {
    private final String imagePath;    // 내재적 상태 (공유)
    private final byte[] imageData;    // 내재적 상태 (공유)
    
    // 외재적 상태는 매개변수로 받음
    public void draw(Graphics g, int x, int y, int size) {
        g.drawImage(imageData, x, y, size, size);
    }
}

/*
핵심 차이점:

Bridge 패턴:
- 목적: 추상화와 구현의 독립적 변화
- 관점: 구조적 유연성 (Structural Flexibility)
- 해결: 조합 폭발 문제
- 시점: 설계 타임 분리, 런타임 교체

Flyweight 패턴:  
- 목적: 메모리 사용량 최적화
- 관점: 자원 효율성 (Resource Efficiency)
- 해결: 메모리 낭비 문제
- 시점: 런타임 상태 분리, 객체 공유
*/

언제 어떤 패턴을 선택할 것인가?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// 선택 가이드라인과 결합 가능성

public class PatternDecisionGuide {
    
    // Bridge 패턴 선택 시나리오
    public void bridgeScenarios() {
        /*
        Bridge 패턴을 선택해야 하는 경우:
        
        1. 플랫폼 독립적 코드가 필요할 때
           - 크로스 플랫폼 라이브러리
           - 다중 데이터베이스 지원
           - 다양한 OS 지원
        
        2. 런타임에 구현체를 교체해야 할 때
           - A/B 테스트
           - 설정에 따른 동작 변경
           - 환경별 다른 구현
        
        3. 추상화와 구현이 독립적으로 확장되어야 할 때
           - 새로운 추상화 타입 추가
           - 새로운 구현 방식 추가
           - 양쪽 모두 빈번한 변경
        */
    }
    
    // Flyweight 패턴 선택 시나리오
    public void flyweightScenarios() {
        /*
        Flyweight 패턴을 선택해야 하는 경우:
        
        1. 대량의 유사한 객체가 필요할 때
           - 게임의 파티클 시스템
           - 문서 편집기의 문자 객체
           - 맵 타일 시스템
        
        2. 메모리 사용량이 병목일 때
           - 모바일 환경
           - 임베디드 시스템
           - 대용량 데이터 처리
        
        3. 객체의 외재적 상태가 명확히 분리 가능할 때
           - 위치, 색상, 크기 등이 개별적
           - 공통 데이터가 대용량
           - 불변 데이터 위주
        */
    }
    
    // 두 패턴의 결합
    public void combinedPattern() {
        /*
        🔄 Bridge + Flyweight 결합 사례:
        
        게임 엔진의 렌더링 시스템:
        - Bridge: 다양한 그래픽 API (OpenGL, DirectX, Vulkan)
        - Flyweight: 대량의 스프라이트/텍스처 공유
        
        문서 편집기:
        - Bridge: 다양한 렌더링 엔진 (PDF, HTML, Print)
        - Flyweight: 글꼴과 문자 정보 공유
        */
    }
}

// 실제 결합 예시: 게임 렌더링 시스템
interface RenderingEngine {  // Bridge의 구현 인터페이스
    void drawSprite(SpriteData sprite, float x, float y, float scale);
}

class OpenGLRenderer implements RenderingEngine {
    @Override
    public void drawSprite(SpriteData sprite, float x, float y, float scale) {
        // OpenGL 구현
    }
}

class SpriteData {  // Flyweight
    private final Texture texture;     // 내재적 상태
    private final int width, height;   // 내재적 상태
    
    public void render(RenderingEngine engine, float x, float y, float scale) {
        engine.drawSprite(this, x, y, scale);  // Bridge + Flyweight
    }
}

class GameRenderer {  // Bridge의 추상화
    private RenderingEngine engine;
    
    public GameRenderer(RenderingEngine engine) {
        this.engine = engine;
    }
    
    public void renderSprite(String spriteType, float x, float y, float scale) {
        SpriteData sprite = SpriteFactory.getSprite(spriteType);  // Flyweight
        sprite.render(engine, x, y, scale);  // Bridge
    }
}

성능 분석과 실무 고려사항

성능 측정 결과

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 실제 성능 벤치마크 결과

/*
Bridge 패턴 오버헤드 측정:
=================================
작업              | 직접 호출  | Bridge 패턴 | 오버헤드
간단한 메서드     |   1.0ns   |    1.2ns   |   20%
복잡한 메서드     |  100ns    |   102ns    |    2%
I/O 작업         |   1ms     |   1.001ms  |   0.1%

결론: I/O나 복잡한 작업에서는 오버헤드가 무시할 수준


Flyweight 패턴 메모리 효율성:
===================================  
객체 수          | 일반 구현  | Flyweight | 메모리 절약
1,000개         |   40MB    |    8MB   |    80%
10,000개        |  400MB    |   25MB   |   93.8%
100,000개       | 4,000MB   |   85MB   |   97.9%
1,000,000개     |40,000MB   |  350MB   |   99.1%

결론: 객체 수가 많을수록 효과가 기하급수적으로 증가
*/

// 메모리 사용량 실시간 측정
public class PerformanceMonitor {
    private static final MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
    
    public static void measureMemoryUsage(String phase) {
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        long usedMemory = heapUsage.getUsed();
        long maxMemory = heapUsage.getMax();
        
        System.out.printf("%s - Memory Usage: %,d bytes (%.1f%% of max)\n", 
                         phase, usedMemory, (double) usedMemory / maxMemory * 100);
    }
    
    public static void benchmarkFlyweight() {
        measureMemoryUsage("Before creating objects");
        
        // Flyweight 없이
        List<RegularCharacter> regularChars = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            regularChars.add(new RegularCharacter('A', "Arial", 12, i, i));
        }
        measureMemoryUsage("After creating 100k regular objects");
        
        // 메모리 정리
        regularChars.clear();
        System.gc();
        measureMemoryUsage("After GC");
        
        // Flyweight 사용
        List<CharacterContext> flyweightChars = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            flyweightChars.add(new CharacterContext(i, i, Color.BLACK, 'A', "Arial", 12));
        }
        measureMemoryUsage("After creating 100k flyweight objects");
    }
}

실무 적용 가이드라인

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
// 실무에서의 주의사항과 최적화 팁

public class PracticalGuidelines {
    
    // Bridge 패턴 최적화
    public void bridgeOptimization() {
        /*
        Bridge 패턴 최적화 팁:
        
        1. 인터페이스 설계 최적화
           - 메서드 수를 최소화 (호출 오버헤드 감소)
           - 배치 처리 지원 (여러 작업을 한 번에)
           - 비동기 처리 고려
        
        2. 구현체 선택 최적화
           - 환경에 따른 자동 선택
           - 성능 모니터링 기반 동적 교체
           - 폴백 메커니즘 구현
        
        3. 메모리 관리
           - 구현체 풀링 활용
           - 약한 참조 사용 고려
           - 생명주기 관리
        */
        
        // 예시: 최적화된 Bridge 구현
        class OptimizedMessageBridge {
            private MessageSender sender;
            private final Queue<Message> messageQueue = new LinkedList<>();
            private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
            
            public OptimizedMessageBridge(MessageSender sender) {
                this.sender = sender;
                // 배치 처리를 위한 스케줄러
                scheduler.scheduleAtFixedRate(this::flushMessages, 0, 100, TimeUnit.MILLISECONDS);
            }
            
            public void sendMessage(String message, String recipient) {
                messageQueue.offer(new Message(message, recipient));
            }
            
            private void flushMessages() {
                List<Message> batch = new ArrayList<>();
                while (!messageQueue.isEmpty() && batch.size() < 50) {
                    batch.add(messageQueue.poll());
                }
                if (!batch.isEmpty()) {
                    sender.sendBatch(batch);  // 배치 전송으로 효율성 향상
                }
            }
        }
    }
    
    // Flyweight 패턴 최적화
    public void flyweightOptimization() {
        /*
        Flyweight 패턴 최적화 팁:
        
        1. 팩토리 최적화
           - ConcurrentHashMap 사용 (동시성)
           - WeakReference 활용 (메모리 누수 방지)
           - LRU 캐시 구현 (메모리 제한)
        
        2. 내재적/외재적 상태 분리 최적화
           - 불변 객체로 내재적 상태 설계
           - 외재적 상태 전달 최적화 (객체 풀링)
           - 지연 초기화 활용
        
        3. 가비지 컬렉션 최적화
           - 객체 생성 최소화
           - 재사용 가능한 외재적 상태 객체
           - 메모리 풀 활용
        */
        
        // 예시: 최적화된 Flyweight Factory
        class OptimizedFlyweightFactory<T> {
            private final Map<String, T> flyweights = new ConcurrentHashMap<>();
            private final Function<String, T> factory;
            private final int maxSize;
            
            public OptimizedFlyweightFactory(Function<String, T> factory, int maxSize) {
                this.factory = factory;
                this.maxSize = maxSize;
            }
            
            public T getFlyweight(String key) {
                return flyweights.computeIfAbsent(key, k -> {
                    if (flyweights.size() >= maxSize) {
                        // LRU 정책으로 오래된 항목 제거
                        evictOldest();
                    }
                    return factory.apply(k);
                });
            }
            
            private void evictOldest() {
                // 단순화된 LRU 구현
                String firstKey = flyweights.keySet().iterator().next();
                flyweights.remove(firstKey);
            }
        }
    }
}

한눈에 보는 Bridge & Flyweight 패턴

Bridge vs Flyweight 핵심 비교

비교 항목Bridge 패턴Flyweight 패턴
핵심 철학분리하여 정복공유하여 절약
해결 문제조합 폭발 (N×M 클래스)메모리 낭비 (대량 유사 객체)
구조추상화-구현 분리내재-외재 상태 분리
최적화 대상유연성, 확장성메모리, 생성 비용
적용 시점설계 초기성능 최적화 단계
복잡도 증가계층 구조 복잡상태 관리 복잡

Bridge 패턴 활용 시나리오

시나리오추상화 축구현 축효과
멀티플랫폼 UIWindow, DialogWindows, Mac, Linux조합 폭발 방지
메시징 시스템Message 타입전송 방식 (Email, SMS)독립적 확장
데이터 접근RepositoryDB 드라이버런타임 교체
디바이스 제어RemoteControlTV, Radio, AC기기 독립적 제어

Flyweight 패턴 메모리 절약 효과

객체 수일반 구현Flyweight메모리 절약률
1,000개40MB4MB90%
10,000개400MB35MB91.3%
100,000개4,000MB40MB99%
1,000,000개40,000MB350MB99.1%

상태 분리 가이드 (Flyweight)

상태 유형특징처리 방식예시
내재적 (Intrinsic)불변, 공유 가능Flyweight 내부 저장글꼴, 텍스처, 색상
외재적 (Extrinsic)가변, 인스턴스별파라미터로 전달좌표, 크기, 회전

패턴 선택 결정 가이드

상황권장 패턴이유
N×M 클래스 조합 폭발Bridge독립적 계층 분리
런타임 구현체 교체Bridge추상화-구현 분리
대량 유사 객체 생성Flyweight공유로 메모리 절약
플랫폼 독립성 필요Bridge구현 교체 용이
게임 객체 대량 렌더링Flyweight공유 텍스처/모델

성능 특성 비교

측면BridgeFlyweight
런타임 오버헤드간접 호출 (미미)팩토리 조회
메모리 사용클래스당 인스턴스공유 + 외재 상태
초기화 비용낮음풀 초기화 필요
확장 비용클래스 추가팩토리 수정

Bridge vs Strategy vs State 비교

비교 항목BridgeStrategyState
목적추상화/구현 분리알고리즘 교체상태별 행동 변경
구조두 계층 구조알고리즘 인터페이스상태 인터페이스
변화 주체클라이언트 설정클라이언트 선택객체 내부 전이
관계영구적 연결일시적 선택동적 전환

적용 체크리스트

Bridge 체크 항목Flyweight 체크 항목
추상화와 구현이 독립적으로 변화?유사 객체가 수천 개 이상?
N×M 조합이 예상되는가?객체의 대부분 상태가 공유 가능?
런타임 구현체 교체 필요?메모리 사용량이 문제인가?
플랫폼 독립성 중요?외재 상태 분리가 자연스러운가?

결론: 분리와 효율성의 조화

Bridge와 Flyweight 패턴을 깊이 탐구한 결과, 이들은 서로 다른 관점에서 시스템 최적화를 추구하는 패턴들임을 확인했습니다.

Bridge 패턴의 핵심 가치:

  1. 구조적 유연성: 추상화와 구현의 독립적 변화
  2. 런타임 교체: 동적인 구현체 변경 능력
  3. 조합 폭발 방지: N×M → N+M으로 클래스 수 최적화
  4. 테스트 용이성: Mock 구현체를 통한 단위 테스트

Flyweight 패턴의 핵심 가치:

  1. 메모리 효율성: 공유를 통한 극적인 메모리 절약
  2. 성능 향상: 객체 생성 비용 감소와 캐시 효율성
  3. 확장성: 대량 객체 처리 능력
  4. 시스템 안정성: 메모리 부족 방지

현대적 의미와 활용:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
전통적 활용 → 현대적 진화

Bridge Pattern →
- 크로스 플랫폼 프레임워크
- 클라우드 멀티 프로바이더 지원
- 마이크로서비스 아키텍처
- A/B 테스트 플랫폼

Flyweight Pattern →
- 게임 엔진 최적화
- 빅데이터 메모리 관리
- 브라우저 렌더링 엔진
- IoT 디바이스 최적화

실무자를 위한 핵심 가이드라인:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Bridge 패턴 적용 시점:
- 추상화와 구현이 독립적으로 변화해야 할 때
- 런타임에 동작을 바꿔야 하는 경우
- 플랫폼/환경 독립적 코드가 필요할 때
- 조합 폭발 문제가 예상될 때

Flyweight 패턴 적용 시점:
- 동일한 타입의 객체를 대량으로 생성할 때
- 메모리 사용량이 성능 병목일 때
- 객체의 외재적 상태가 명확히 분리 가능할 때
- 불변 데이터 위주의 객체일 때

주의사항:
- Bridge: 과도한 추상화로 인한 복잡성 증가
- Flyweight: 외재적 상태 관리의 복잡성
- 두 패턴 모두 설계 복잡도 증가 비용 고려
- 성능 측정을 통한 효과 검증 필수

미래 전망:

앞으로 이 두 패턴은 다음과 같은 방향으로 진화할 것입니다:

  1. AI/ML 통합: 지능적인 구현체 선택과 메모리 최적화
  2. 함수형 프로그래밍: 불변성과 공유의 새로운 활용법
  3. 엣지 컴퓨팅: 제한된 자원에서의 효율성 극대화
  4. 양자 컴퓨팅: 새로운 컴퓨팅 패러다임에서의 패턴 적용

Bridge와 Flyweight 패턴은 **“어떻게 더 유연하고 효율적인 시스템을 만들 것인가?”**라는 소프트웨어 엔지니어링의 영원한 질문에 대한 두 가지 다른 접근법을 제시합니다. 이들을 적절히 조합하여 활용할 때, 우리는 변화에 유연하면서도 자원을 효율적으로 사용하는 시스템을 구축할 수 있습니다.

다음 글에서는 Interpreter와 Mediator 패턴을 탐구하겠습니다. 언어의 해석과 객체 간 중재를 통해 복잡한 상호작용을 우아하게 관리하는 방법을 살펴보겠습니다.


핵심 메시지: “Bridge는 변화의 축을 분리하여 유연성을 제공하고, Flyweight는 공유를 통해 효율성을 추구한다. 두 패턴 모두 복잡성 증가라는 비용을 지불하지만, 올바르게 적용하면 시스템의 확장성과 성능을 크게 향상시킬 수 있다.”