기존 아키텍처를 유지하며 새로운 타입 추가하기
앱의 광고 시스템은 AdManager에서 관리하고 있었습니다. AdManager는 서버로부터 데이터를 받아와서 AdInfo 객체를 생성하고, 이를 AdBanner라는 Widget에 주입하는 방식이었습니다.
- AdManager : 광고 로딩 및 데이터 관리 (내부 데이터베이스 & 서버 비즈니스 로직 담당)
- AdInfo : 광고 id, 이미지 URL, 링크, 타이틀 등을 담은 데이터 객체
- AdBanner : AdInfo를 받아 렌더링하는 위젯
기존에는 이미지 배너 형태만 존재했기에 이 구조는 매우 심플하고 효율적이었습니다.
새로운 요구사항
"서버에 등록된 광고가 없을 땐 광고 플랫폼 광고를 보여주면 좋을 것 같아요."
비즈니스 요구사항은 간단했지만, 기술적인 문제는 간단하지 않았습니다. 가장 큰 난관은 데이터 포맷의 불일치였습니다.
- 기존 광고: ImageUrl 기반의 이미지 렌더링
- 외부 플랫폼 광고: HTML 스크립트 기반의 WebView 렌더링
고민
"어떻게 하면 기존 아키텍처와 클래스를 그대로 사용하면서 새로운 요구사항을 수용할 수 있을까?"
가장 쉬운 방법은 외부 광고 플랫폼을 위한 AdManager, AdInfo, AdBanner를 새로 만드는 것입니다. 그러나 이렇게 처리하면 광고를 호출하는 모든 화면에서 if-else 분기 처리를 해야하며, 또 다른 플랫폼이 들어오면 코드는 더욱 복잡해집니다.
통합 모델로 확장
Widget은 데이터의 출처를 몰라야 한다는 원칙으로 접근했습니다. AdBanner는 AdInfo를 받아서 그리기만 하면 됩니다. AdInfo가 어디서 왔는지 AdBanner에게는 중요하지 않습니다.
이를 위해 어댑터 패턴(Adapter Pattern)과 유사한 방식으로 두 데이터를 아우르는 UnifiedAdInfo을 설계하고, UI 계층에 중재자 역할의 위젯(AppAdWidget)을 도입했습니다.
Data : AdInfo → UnifiedAdInfo
기존에 사용하던 AdInfo 클래스는 광고가 표시될 위치에 따라서 Key, Value 형태로 광고 이미지를 가져오고 있었습니다.
// 기존에 사용하던 AdInfo 클래스
class AdInfo {
final String id;
final AdLocation location;
final String? name;
final String? url;
final String? redirectUrl;
...
}
변경된 AdInfo에서는 AdType enum을 추가하고 Factory 생성자를 활용해 서로 다른 소스(내부 서버 vs 외부 SDK)의 데이터를 하나의 통일된 객체로 변환합니다.
enum AdType {
app, // 기존 광고
platform, // 플랫폼 광고
}
class UnifiedAdInfo {
final String id;
final AdLocation location;
final AdType type;
// 기존 AdInfo 속성
final String? url;
final String? redirectUrl;
// 플랫폼 광고 속성
final String? htmlContent;
final int? width;
final int? height;
...
// 기존 AdInfo를 UnifiedAdInfo로 변환하는 팩토리 생성자
factory UnifiedAdInfo.fromAdInfo(AdInfo info) {
return UnifiedAdInfo(
id: info.id.toString(),
type: AdType.app,
location: info.location,
redirectUrl: info.redirectUrl,
url: info.url,
);
}
// PlatformAdResponse를 UnifiedAdInfo로 변환하는 팩토리 생성자
factory UnifiedAdInfo.fromPlatform(
PlatformAdResponse response,
AdLocation location,
) {
return UnifiedAdInfo(
id: response.adunitId.toString(),
type: AdType.platform,
location: location,
htmlContent: response.ads?.adm ?? response.houseAds?.adm,
width: response.width,
height: response.height,
);
}
UI Layer : AdBanner 중재자 역할
기존에 사용하던 AdBanner는 size, imageUrl, onClickListener 등을 받아서 처리하는 간단한 위젯입니다.
class AdBanner extends StatelessWidget {
const AdeBanner({
super.key,
this.height,
this.width,
this.adInfo,
this.onPressed,
});
...
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
onPressed?.call();
},
child: Container(
...
child: CachedNetworkImage(
imageUrl: adInfo.url ?? '기본 이미지',
width: width ?? MediaQuery.of(context).size.width,
fit: BoxFit.fitWidth,
...
}
}
기존 AdBanner는 이미지 렌더링에 특화되어 있으므로 수정하지 않고 그대로 둡니다. 대신 AppAdWidget이라는 Wrapper Widget을 만들어, 여기서 타입에 따라 적절한 하위 위젯(AdBanner vs PlatformAdWidget)을 선택하도록 했습니다. 이 안에서 AdType에 따라 분기처리를 하여 광고가 노출되도록 코드를 수정하였습니다.
class AppAdWidget extends StatelessWidget {
...
final UnifiedAdInfo? adInfo;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
switch (adInfo.type) {
case AdType.app:
return AdBanner(
adInfo: adInfo,
onPressed: onPressed,
);
case AdType.platform:
return PlatformAdWidget(
adInfo: adInfo,
);
default:
return SizedBox.shrink();
}
}
}
이로써 OCP(Open-Closed Principle, 개방-폐쇄 원칙)를 준수할 수 있게 되었습니다. 새로운 광고 타입이 추가되어도 기존 AdBanner 코드는 수정할 필요가 없습니다.
Logic : AdManager
기존 AdManager는 매우 간단한 형식이었습니다. 앱에서 일정 간격으로 광고를 요청하고 만약 요청한 광고가 많으면 적절한 알고리즘을 통해 Return 하는 형식입니다.
Future<AdInfo?> getAd(AdLocation adLocation) async {
final ad = await _getAd(adType);
return ad;
}
이제 AdManager는 단순히 광고를 가져오는 것을 넘어, 우선순위 로직을 수행합니다. 반환 타입은 이제 AdInfo가 아닌 UnifiedAdInfo가 됩니다.
Future<AdInfo?> getAd(AdLocation adLocation, AdUnitId adUnitId) async {
final ad = await _getAd(adType);
if (ad != null) {
return ad;
}
// API 호출 로직
final platformAd = await repository.call(adUnitId);
...
return null;
}

결론
이번 리팩터링의 핵심은 기존 코드를 건드리지 않고(Closed), 새로운 기능을 추가(Open)하는 것이었습니다.
- Unified Model: 서로 다른 형태의 데이터(Image vs HTML)를 하나의 추상화된 객체로 묶었습니다.
- Wrapper Widget: 렌더링 방식의 차이를 래퍼 위젯 내부로 격리시켰습니다.
덕분에 기존 코드를 그대로 사용할 수 있으며, 추후 또 다른 광고 네트워크(예: Google AdMob)가 추가되더라도 UnifiedAdInfo와 AppAdWidget의 switch 문만 확장하면 되는 유연한 구조를 갖추게 되었습니다.