구조 패턴 - 프록시 패턴 (Proxy Pattern)

French Marigold·2024년 4월 24일
1

디자인패턴

목록 보기
7/10

정의

  • 프록시의 사전적 정의는 “대리자” 이다. 어떤 일을 대신해서 처리해주는 사람을 뜻한다.
  • 즉, 프록시 패턴 (Proxy Pattern)이란 Client가 대상 객체를 직접 사용하는 것이 아닌 프록시 (대리자)를 거쳐서 사용하는 패턴이다. 예를 들어, 메소드를 사용한다고 하면 Client가 대상 객체에 직접 접근하여 메소드를 사용하는 것이 아닌 프록시 객체의 메소드에 접근한 후, 추가적인 로직을 처리한 뒤 대상 객체에 접근하는 방식으로 처리한다.
  • 바로 객체에 접근하지 않고 굳이 프록시 객체를 거쳐서 접근하는 이유는 다음과 같다.
    • 접근 제어
      • 대상 객체가 민감한 정보를 가지고 있을 경우, 어떤 조건을 만족해야만 대상 객체에 접근하도록 Proxy에서 조절한다.
    • 초기화 지연
      • 대상 객체가 인스턴스화 하기에 너무 무거울 경우, 반드시 필요할 때에만 대상 객체에 접근하도록 Proxy에서 조절한다.
    • 로깅
      • 대상 객체에 뭔가 메세지를 남기고 싶을 때, Proxy에서 대상 객체 중간에 끼어들어 로그 메세지를 남길 수 있게끔 조절한다.
    • 캐싱 ⇒
      • Proxy에서 내부 캐시를 만들어서 특정 데이터가 캐시에 존재하지 않을 경우에만 대상 객체에서 작업이 실행되도록 조절한다.
    • 네트워크 통신 ⇒
      • 네트워크 통신과 관련된 불필요한 작업들을 Proxy에서 모두 쳐 내어 결과값만 반환할 수 있다.

프록시 패턴 (Proxy Pattern) 의 구조

  • Subject ⇒
    • 프록시(Proxy)와 대상 객체(RealSubject)를 하나로 연결하는 프로토콜.
  • RealSubject ⇒
    • 원본 대상 객체
  • Proxy ⇒
    • 프록시는 “원본 대상 객체와 같은 메소드를 실행”한다. 같은 메소드를 실행하므로 그 내부에 추가적인 로직을 할당할 수 있다.
    • 단, 프록시는 원본 객체의 결과값을 조작하거나 변경시켜서는 안 된다. ⭐️⭐️
  • Client
    • Subject 프로토콜을 이용하여 프록시 객체를 생성하여 이용한다.
    • Client는 Proxy를 통해서 RealSubject와 값을 주고 받을 수 있다.

프록시 패턴 적용 예시

  • 프록시 패턴에는 여러 가지 종류가 있으며, 그 종류는 다음과 같다.

    • 가상 프록시 (지연 초기화)
    • 보호 프록시 (엑세스 제어)
    • 로깅 프록시 (로그 메세지 작성)
    • 원격 프록시 (서비스 객체가 원격에 존재하는 경우)
    • 캐싱 프록시 (데이터가 큰 경우 캐싱하여 재사용을 유도)
    • 스마트 참조 (Swift에는 ARC라는 메모리 해제 방법이 존재하므로 이 방법은 잘 사용하지 않음)
  • 가상 프록시 적용 예시

    • 지연 초기화 방식이 필요할 때 사용된다.
    • 이 구현은 “실제 객체가 무겁지만 사용 빈도는 낮을 때 쓰는 방식”이다.
    • 객체의 인스턴스를 직접 생성하는 대신에 객체 초기화가 실제로 필요한 시점에 초기화될수 있도록 지연할 수 있다.
protocol SubjectProtocol {
    func action()
}

class RealSubject: SubjectProtocol {
    func action() {
        print("무거운 로직들이 가득 담겨 있음")
    }
}

// 프록시 객체에 해당하는 클래스
class Proxy: SubjectProtocol {
		// 대상 객체를 옵셔널 타입으로 선언한다. nil 값을 할당할 수 있게끔 하기 위해서이다. 
    private var realSubject: RealSubject? 

    init() {
    }

    func action() {
        // 프록시 객체는 Client측에서 action 메소드를 호출할 때에만 
        // 실제 객체를 생성하는 방식으로 구현.
        if realSubject == nil {
            realSubject = RealSubject()
        }
        realSubject?.action() // 위임. 옵셔널 체이닝 사용
    }
}

// 클라이언트 코드
class Client {
    static func main() {
        let sub: SubjectProtocol = Proxy()
	      // Client 측에서 action 메소드를 실행할 경우에만 RealSubject 객체를 찍어낸다. 
        sub.action()
    }
}

// 메인 함수 실행
Client.main()
  • 보호 프록시 적용 예시
    • 프록시 객체를 통해 “특정 조건이 충족될 경우에만 서비스 객체에 요청을 전달”할 수 있게 한다.
protocol SubjectProtocol {
    func action()
}

// 실제 대상 객체
class RealSubject: SubjectProtocol {
    func action() {
        print("실제 객체 액션")
    }
}

// 프록시 객체
class Proxy: SubjectProtocol {
    private var realSubject: RealSubject 
    private var access: Bool // 접근 권한을 제어할 Bool 타입
    
    init(realSubject: RealSubject, access: Bool) {
        self.realSubject = realSubject
        self.access = access
    }
    
    func action() {
			  // true일 경우 RealSubject의 action 메소드에 접근할 수 있다.
        if access {
            realSubject.action() // 위임
            
            print("추가적인 작업 실행")
        } else {
		        // false일 경우 RealSubject에 접근할 수 없음.
            print("접근 권한 없음")
        }
    }
}

// 클라이언트 사용 예
class Client {
    static func main() {
		    // Client에서 false를 적용하였으므로 실제 객체에 접근할 수 없음
				// 실제 코드에서는 이렇게 정적으로 처리하기 보다는 동적으로 처리하는 게 좋음
        let sub: SubjectProtocol = Proxy(realSubject: RealSubject(), access: false)
        sub.action()
    }
}

// 메인 실행
Client.main()
  • 로깅 프록시 예시
    • 실제 객체의 요청 이력을 프록시 객체를 통해 로그를 남길 수 있다.
protocol SubjectProtocol {
    func action()
}

// 실제 대상 객체
class RealSubject: SubjectProtocol {
    func action() {
        print("실제 객체 액션")
    }
}

// 프록시 객체
class Proxy: SubjectProtocol {
    private var realSubject: RealSubject 

    init(realSubject: RealSubject) {
        self.realSubject = realSubject
    }

    func action() {
        print("로그 메세지 작성")
        
        realSubject.action() // 위임
        // 추가 작업 수행
        print("프록시 객체 액션 추가")

        print("로그 메세지 작성")
    }
}

// 클라이언트 사용 예
class Client {
    static func main() {
        let sub: SubjectProtocol = Proxy(realSubject: RealSubject())
        sub.action()
    }
}

// 메인 실행
Client4.main()
  • 원격 프록시 예시
    • 프록시 클래스는 로컬에 있고, 대상 객체는 원격 서버에 존재하는 경우에 사용한다.
    • 프록시 객체는 네트워크를 통해 클라이언트의 요청을 전달하여 네트워크와 관련된 불필요한 작업들을 처리하고 결과값만 반환할 수 있다.
    • 원격 프록시 객체는 네트워크 결과값을 캐싱함으로써, 불필요한 네트워크 호출을 줄일 수 있다. (캐싱 프록시)
    • 원격 프록시는 클라이언트의 요청을 검증하여 조건에 맞지 않으면 실행을 취소할 수 있다.

protocol WeatherService {
    func fetchWeather(city: String, api: String, completion: @escaping (Weather?, Error?) -> Void)
}

struct Weather: Codable {
    var currentWeather: String
    var city: String
}

class RealWeatherService: WeatherService {
    func fetchWeather(city: String, api: String, completion: @escaping (Weather?, Error?) -> Void) {
            let url = URL(string: "https://weather.com/api/users\(api)")!
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                completion(nil, error)
                return
            }
            guard let data = data, let user = try? JSONDecoder().decode(Weather.self, from: data) else {
                completion(nil, NSError(domain: "", code: 0, userInfo: nil))
                return
            }
            completion(user, nil)
        }
        task.resume()
    }
}

class WeatherServiceProxy: WeatherService {
    private let realWeatherService: RealWeatherService
    
    init(realWeatherService: RealWeatherService) {
        self.realWeatherService = realWeatherService
    }
    
    func fetchWeather(city: String, api: String, completion: @escaping (Weather?, Error?) -> Void)  {
        // 요청 검증 로직 (예: 도시 이름이 유효한지 확인)
        guard city.count > 0 else {
            print("오류: 유효하지 않은 도시 이름입니다.")
            return
        }
        
        // 보안 조치 (예: API 키 검증, 요청 빈도 제한 확인 등)
        guard api == "올바른_API_키" else {
            print("오류: 잘못된 API 입니다")
            return
        }
        
        // 실제 서비스에 요청 전달
        return realWeatherService.fetchWeather(city: city, api: api) { weather, error in
            completion(weather, error)
        }
    }
}

class Client {
    static let apiKey = "올바른_API_키"
    
    static func main() {
        let proxy = WeatherServiceProxy(realWeatherService: RealWeatherService())
        proxy.fetchWeather(city: "서울", api: apiKey) { weather, error in
            print(weather)
        }
    }
}

Client.main()
  • 캐싱 프록시 예시
    • 캐싱이란 자주 사용하는 데이터나 값을 미리 복사해놓는 임시 장소를 의미한다. Swift 같은 경우 캐시를 일반적으로 딕셔너리로 관리함
    • 데이터가 큰 경우 캐싱하여 재사용을 유도한다.
    • 클라이언트 요청의 결과를 캐시하고 이 캐시를 Client가 관리한다.
// 네트워크 서비스를 프로토콜로 처리 
// 실제 객체와 프록시 객체가 모두 해당 프로토콜을 채택할 것이다. 
protocol UserService {
    func fetchUser(id: Int, completion: @escaping (User?, Error?) -> Void)
}

struct User: Decodable {
    var id: Int
    var name: String
}

// 실제 원격 서비스
class RemoteUserService: UserService {
    func fetchUser(id: Int, completion: @escaping (User?, Error?) -> Void) {
        let url = URL(string: "https://example.com/api/users/\(id)")!
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                completion(nil, error)
                return
            }
            guard let data = data, let user = try? JSONDecoder().decode(User.self, from: data) else {
                completion(nil, NSError(domain: "", code: 0, userInfo: nil))
                return
            }
            completion(user, nil)
        }
        task.resume()
    }
}

// 원격 프록시
class UserProxy: UserService {
    private let remoteUserService: RemoteUserService
    private var cache: [Int: User] = [:]
    
    init(remoteUserService: RemoteUserService) {
        self.remoteUserService = remoteUserService
    }
    
    func fetchUser(id: Int, completion: @escaping (User?, Error?) -> Void) {
        // 캐시에 값이 있을 경우, 네트워크 처리를 하지 않고 값을 캐시로부터 가져옴. ⭐️
        if let cachedUser = cache[id] {
            print("저장되어 있는 캐싱 데이터 반환")
            completion(cachedUser, nil)
            return
        }
        
        // 저장되어 있는 캐시가 없을 경우 외부에서 네트워킹 처리를 직접 하여 값을 가져온다. ⭐️
        remoteUserService.fetchUser(id: id) { [weak self] user, error in
            guard let self = self, let user = user else {
                completion(nil, error)
                return
            }
            
            // 네트워크 처리를 한 후에 캐시에 저장.
            self.cache[id] = user
            print("외부 데이터 반환")
            completion(user, nil)
        }
    }
}

class Client {
    static func main() {
        // 실제 원격 객체 서비스를 프록시에 할당
        let userProxy = UserProxy(remoteUserService: RemoteUserService())
        
        // 캐시값을 이용해 네트워크 처리 실행
        // 단, 호출할 때마다 네트워크의 값이 변경될 경우 캐시를 사용하는 것은 비추천한다. ⭐️
        // 네트워크 값이 고정되어 나타날 경우에만 캐시를 사용하는 것을 권장.
        userProxy.fetchUser(id: 1) { user, error in
            if let user = user {
                print("사용자 이름: \(user.name)")
            } else if let error = error {
                print("오류 발생: \(error.localizedDescription)")
            }
        }

    }
}

Client.main()

패턴 사용 시기

  • 기존 객체에서 원래 하려던 동작을 수행하면서 초기화 지연, 접근 제어, 로깅, 캐싱 등의 부가적인 작업을 추가하고자 할 때

패턴의 장점

  • OCP 준수 ⇒
    • 원본 대상 객체를 수정하지 않고도 프로토콜을 통해 기능을 추가할 수 있다.
  • SRP 준수 ⇒
    • 원본 대상 객체는 자신의 기능에만 충실하고 그 외 부가적인 기능들을 프록시가 처리하므로 책임이 단일하다.
  • 원본 대상 객체를 신경쓰지 않고도 원본 대상 객체를 제어하고 관리할 수 있다.

패턴의 단점

  • 많은 프록시 코드를 도입하면서 복잡도가 크게 증가한다.
  • 프록시 객체 안에 코드 로직이 많아질 경우, 응답이 늦어질 가능성이 있다.

참고 문헌

profile
꽃말 == 반드시 오고야 말 행복

1개의 댓글

comment-user-thumbnail
2024년 4월 25일

객체지향 프로그래밍, 프로토콜지향 프로그래밍은 원리는 단순한데 적용이 어려운 것 같아요
잘 적용하려면 그만큼의 경험과 지식이 필요한 것 같습니다

만약 RealSubject 객체가 기존 코드로 있고 SubjectProtocol을 채택하지 않은 상황이라면
Proxy 클래스를 추가할 때는 SubjectProtocol을 생성해서
RealSubject가 SubjectProtocol를 채택하도록 한 뒤에 Proxy 클래스를 만들어야 할까요?

답글 달기