어느덧 Structural Patterns의 마지막 키워드 입니다!
1. 목적
Proxy는 다른 객체에 대한 대체(Substitute) 또는 Placeholder를 제공하는 패턴입니다.
또한 원래 객체에 대한 접근을 제어하므로, 요청이 원래 객체에 전달되기 전 또는 후에 무언가를 수행할 수 있도록 합니다.

2. 문제 상황
왜 객체에 대한 접근을 제한할까요?
고민을 위해 엄청난 양의 시스템 리소스를 소비하는 큰 객체가 있다고 가정해봅시다.
이 객체는 필요할 때가 있기는 하지만, 항상 필요한 것은 아닙니다.

개발자는 실제로 필요한 순간에만 이 객체를 만들어서 지연 초기화(Lazy Initialization)을 구현할 수 있습니다.
그러면 객체를 사용하는 모든 Client 코드에서 지연 초기화 코드를 실행해야 합니다.
하지만 이는 많은 중복 코드를 만들게 될 수 있습니다.
아래 예시를 봅시다.
class RealImage {
private var filename: String
init(filename: String) {
self.filename = filename
loadFromDisk()
}
func display() {
print("Displaying \(filename)")
}
private func loadFromDisk() {
print("Loading \(filename) from disk")
}
}
디스크로부터 이미지를 불러오는 클래스가 있습니다.
이 작업은 파일IO가 발생하기 때문에 시스템 리소스를 다소 사용합니다.
Client에서 사용된다면 아래와 같은 형태로 사용될 것입니다.
let image1 = RealImage(filename: "image1.jpg")
image1.display()
let image2 = RealImage(filename: "image2.jpg")
image2.display()
이처럼 필요할 때마다 객체를 초기화하면 반복적인 초기화 코드가 발생하고, 시스템 자원을 낭비하게 될 수 있습니다.
이상적인 상황에서는 코드를 클래스에 직접 넣을 수 있겠지만, 항상 가능하지는 않습니다.
클래스가 폐쇄된(Closed) 타사의 라이브러리의 일부일 수 있기 때문입니다.
3. 해결책
Proxy 패턴은 이러한 상황에서 기존의 객체와 동일한 인터페이스로 새로운 Proxy 클래스를 생성합니다.
모든 클라이언트들은 Proxy 객체를 전달받아 앱을 업데이트 합니다.
클라이언트가 요청을 하면 Proxy 실제 서비스 객체를 생성하고 모든 작업을 서비스 객체에 위임합니다.

Proxy는 지연 초기화 및 데이터 캐싱을 클라이언트와 실제 서비스 객체가 알지 못하는 상태에서 처리할 수 있습니다.
이 과정은 개발자가 서비스 객체 로직의 실행 전(전처리) 혹은 후(후처리)에 무언가를 실행해야하는 경우에
서비스 객체를 변경하지 않고도 무언가를 수행하게 합니다.
또한 기존 서비스 객체와 같은 인터페이스로 구현되기 때문에 클라이언트가 기대하는 서비스의 모든 기능을 제공합니다.
비유를 통해 쉽게 이해해봅시다.

신용 카드는 현금과 마찬가지로 결제에 사용할 수 있습니다.
이때 신용 카드는 은행 계좌의 Proxy이며, 은행 계좌는 현금의 Proxy입니다.
둘 다 같은 인터페이스로 구현되기 때문에 둘 다 결제에 사용할 수 있습니다.
신용 카드를 사용하면 많은 현금을 가지고 다닐 필요가 없기 때문에 더욱 안전합니다.
즉 신용 카드를 통해 은행 계좌 -> 현금의 의도치 않은 변동을 최소화 할 수 있습니다.
4. 구조

새로운 상황을 가정하여 Proxy 구조로 코드를 작성해봅시다.
먼저 비디오 다운로드를 위한 클래스가 있다고 가정합니다.
// Service Interface
protocol ThirdPartyYouTubeLib {
// + operation()
func listVideos() -> [String: Data]
// + operation()
func getVideoInfo(id: String) -> Data
// + operation()
func downloadVideo(id: String)
}
// Service
class ThirdPartyYouTubeClass: ThirdPartyYouTubeLib {
// + operation()
func listVideos() -> [String: Data] {
print("Send an API request to YouTube.")
return [:]
}
// + operation()
func getVideoInfo(id: String) -> Data {
print("Get metadata about some video.")
return .init()
}
// + operation()
func downloadVideo(id: String) {
print("Download a video file from YouTube.")
}
}
하지만 사용자의 인터넷 속도는 한정되어있기 때문에 동시에 많은 요청이 있다면 속도가 느려질 겁니다.
이제 Proxy 클래스를 만들어 동일한 인터페이스를 구현하고 모든 작업을 위임합니다.
Proxy 클래스는 동일한 비디오를 여러 번 요청한다면 첫 번째 다운로드 비디오 데이터를 캐싱하고 재사용합니다.
// Proxy
class CachedYouTubeClass: ThirdPartyYouTubeLib {
// - realService: Service
private var service: ThirdPartyYouTubeLib
private var listCache: [String: Data] = [:]
private var videoCache: Data?
var needReset: Bool = false
// + Proxy(s: Service)
init(service: ThirdPartyYouTubeLib) {
self.service = service
}
func checkAccess() -> Bool {
print("is Valid Access")
return true
}
// + operation()
func listVideos() -> [String: Data] {
if (listCache.isEmpty || needReset) {
// if (checkAccess())
if checkAccess() {
// realService.operation()
listCache = service.listVideos()
}
}
return listCache
}
// + operation()
func getVideoInfo(id: String) -> Data {
if (videoCache == nil || needReset) {
// if (checkAccess())
if checkAccess() {
// realService.operation()
videoCache = service.getVideoInfo(id: id)
}
}
return videoCache ?? .init()
}
// + operation()
func downloadVideo(id: String) {
if (listCache[id] == nil || needReset) {
// if (checkAccess())
if checkAccess() {
// realService.operation()
service.downloadVideo(id: id)
}
}
}
}
그리고 실제 GUI에서 기능을 사용하는 클래스와 실제 클라이언트에서는 어떻게 사용되는지 살펴봅시다.
// Client
class YouTubeManager {
private var service:ThirdPartyYouTubeLib
init(service: ThirdPartyYouTubeLib) {
self.service = service
}
func renderVideoPage(id: String) {
let info = service.getVideoInfo(id: id)
print("Render the video page by using \(info).")
}
func renderListPanel() {
let list = service.listVideos()
print("Render the list of video thumbnails by using \(list).")
}
func reactOnUserInput(id: String) {
self.renderVideoPage(id: id)
self.renderListPanel()
}
}
class Application {
init() {
let aYouTubeService = ThirdPartyYouTubeClass()
let aYouTubeProxy = CachedYouTubeClass(service: aYouTubeService)
let manager = YouTubeManager(service:aYouTubeProxy)
manager.reactOnUserInput(id: "VIDEO_ID")
}
}
Proxy는 서비스와 동일한 인터페이스로 구현되었기 때문에 서비스 객체 대신 Proxy 객체를 넘겨줄 수 있게 됩니다.
5. 장단점
SOLID 원칙 중 OCP(Open/Closed Principle, 개방/폐쇄 원칙)이 준수됩니다.
클라이언트들이 알지 못하는 상태에서 서비스 객체를 제어할 수 있기에,
신경 쓰지 않는 상태에서 서비스 객체의 생명 주기를 관리할 수 있습니다.
서비스 객체가 준비되지 않았거나 사용할 수 없는 경우에도 작동합니다.
하지만 새로운 클래스의 도입으로 인해 코드가 복잡해지거나,
Proxy의 전/후 처리 과정으로 인해 서비스의 응답이 늦어질 수 있습니다.
6. 실제 사용 사례
두 가지 사례를 가져와봤습니다.
6-1. Network Caching
구조에서 확인한 코드는 네트워크 로직이 외부 라이브러리에 속해있다고 볼 수 있는 코드입니다.
하지만 아래와 같이 Proxy구조를 활용할 수도 있습니다.
// Service Interface
protocol APIService {
func fetchData(endpoint: String, completion: @escaping (String) -> Void)
}
// Service
class RealAPIService: APIService {
// operation()
func fetchData(endpoint: String, completion: @escaping (String) -> Void) {
print("Fetching data from network: \(endpoint)")
// 실제 네트워크 요청 (간단히 시뮬레이션)
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
completion("Data from \(endpoint)")
}
}
}
// Proxy
class ProxyAPIService: APIService {
// service
private let realService = RealAPIService()
private var cache: [String: String] = [:]
// operation()
func fetchData(endpoint: String, completion: @escaping (String) -> Void) {
if let cachedData = cache[endpoint] {
print("Returning cached data for: \(endpoint)")
completion(cachedData)
} else {
realService.fetchData(endpoint: endpoint) { data in
self.cache[endpoint] = data
completion(data)
}
}
}
}
이와 같이 캐싱 혹은 요청 관리를 확장할 수 있습니다.
6-2. 보안성 강화
민감한 데이터를 다루거나, 특정 작업에 대해서 접근 권한을 확인 및 제어해야하는 경우에도 Proxy 패턴을 사용할 수 있습니다.
// Service Interface
protocol SecureService {
func fetchSensitiveData() -> String
}
// Service
class RealSecureService: SecureService {
func fetchSensitiveData() -> String {
return "Sensitive Data"
}
}
// Proxy
class SecureServiceProxy: SecureService {
private let realService = RealSecureService()
private let allowedUsers: [String]
private let currentUser: String
init(currentUser: String, allowedUsers: [String]) {
self.currentUser = currentUser
self.allowedUsers = allowedUsers
}
func fetchSensitiveData() -> String {
if allowedUsers.contains(currentUser) {
return realService.fetchSensitiveData()
} else {
return "Access Denied"
}
}
}
// Client
let secureProxy = SecureServiceProxy(currentUser: "admin", allowedUsers: ["admin", "manager"])
secureProxy.fetchSensitiveData() // "Sensitive Data" (Access Allowance)
let unauthorizedProxy = SecureServiceProxy(currentUser: "guest", allowedUsers: ["admin", "manager"])
unauthorizedProxy.fetchSensitiveData() // "Access Denied"
로이써 클라이언트는 Proxy를 통해서만 민감한 객체에 접근할 수 있으며,
인증 및 권한 검사, 난독화 코드 추가 등 다양한 보안 로직을 수행할 수 있게 됩니다.
6-3. Core Data
Core Data도 내부적으로 Proxy 패턴이 활용되어 있습니다.
NSManangedObject는 실제 데이터베이스 값을 읽기 전에 Proxy 객체로 동작하여 데이터를 지연 로드(Lazy Load)하게됩니다.
따라서 데이터에서 연관된 데이터를 필요할 때만 가져오는 faulting 매커니즘이 적용되어 있습니다.
// Core Data가 내부적으로 Proxy와 유사한 동작을 구현
let person = managedObjectContext.object(with: personID) as! Person
print(person.name) // 이 시점에서만 데이터베이스 액세스 발생
따라서 불필요한 데이터를 미리 로드하지 않게 됩니다.
https://developer.apple.com/documentation/coredata/nsmanagedobject#1653939
NSManagedObject | Apple Developer Documentation
The base class that all Core Data model objects inherit from.
developer.apple.com
이제는 옛날 것이라고 해야할지 모르지만 추후에 CoreData에 대해서도 알아볼 시간이 있었으면 좋겠습니다.
Conclusion
Proxy 패턴은 객체에 대한 접근을 제어하고, 리소스 관리를 효율적으로 하게 합니다.
또한 보안과 접근 제어를 구현하는데 있어서도 유용한 패턴이라고 할 수 있습니다.
Reference
https://refactoring.guru/design-patterns/proxy
Proxy
There are dozens of ways to utilize the Proxy pattern. Let’s go over the most popular uses. Access control (protection proxy). This is when you want only specific clients to be able to use the service object; for instance, when your objects are crucial p
refactoring.guru
'Design Pattern > GoF - Structural Patterns' 카테고리의 다른 글
[GoF Design Patterns] Flyweight (2) | 2024.11.28 |
---|---|
[GoF Design Patterns] Facade (0) | 2024.11.27 |
[GoF Design Patterns] Decorator (3) | 2024.11.13 |
[GoF Design Patterns] Composite (0) | 2024.11.10 |
[GoF Design Patterns] Bridge (4) | 2024.10.28 |