Proxy
Proxy Pattern이란?
프록시 패턴Proxy Pattern은 객체에 대한 접근을 제어하기 위해 대리 객체Proxy를 사용하는 구조적 디자인 패턴입니다. 프록시 객체는 실제 객체와 동일한 인터페이스를 구현하여 클라이언트가 실제 객체를 직접 접근하는 대신, 프록시 객체를 통해 간접적으로 접근하도록 만듭니다. 이를 통해 객체에 접근하기 전후로 추가적인 처리를 하거나, 실제 객체의 생성이나 접근을 지연시키는 등의 작업을 수행할 수 있습니다.
Proxy Pattern의 구성 요소
주체Subject
실제 객체와 프록시 객체가 구현해야 하는 인터페이스 또는 추상 클래스입니다.
실제 객체Real Subject
프록시 객체가 대리하는 실제 객체로, 클라이언트가 수행하려는 실제 작업을 처리합니다.
프록시Proxy
실제 객체와 동일한 인터페이스를 구현하며, 실제 객체에 대한 접근을 제어하는 역할을 합니다. 프록시는 클라이언트의 요청을 실제 객체에 전달하거나, 그 전에 추가적인 작업을 수행할 수 있습니다.
Proxy Pattern 구조
- Client → Proxy
- 클라이언트는
Proxy
객체에Request()
를 호출합니다. Proxy
는Subject
인터페이스를 구현하므로, 클라이언트는Subject
타입으로Proxy
를 사용할 수 있습니다.
- 클라이언트는
- Proxy → RealSubject
Proxy
는 전처리Pre-Processing 작업을 수행한 후,RealSubject
의Request()
를 호출하여 실제 작업을 수행합니다.
- Proxy
Subject
인터페이스를 구현하며, 요청의 전후에 추가적인 작업(Pre/Post Processing)을 수행합니다.
- RealSubject
Subject
인터페이스를 구현하며, 실제 비즈니스 로직을 처리합니다.
Proxy Pattern 적용
잘못된 직접 접근 방식
프록시 패턴을 사용하지 않고, 클라이언트가 직접적으로 실제 객체에 접근하는 경우, 예를 들어 도서관 관리 시스템에서 데이터베이스에 접근하여 책 정보를 로드하는 코드가 있다고 가정해보겠습니다.
// 실제 객체: 데이터베이스에서 책 정보를 로드
public class BookRepository
{
public Book LoadBook(string bookId)
{
Console.WriteLine("책 정보를 데이터베이스에서 로드 중...");
// 데이터베이스 접근 코드 (생략)
return new Book { Id = bookId, Title = "C# Programming", Author = "John Doe" };
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
BookRepository repository = new BookRepository();
Book book = repository.LoadBook("123");
Console.WriteLine($"Book Title: {book.Title}");
}
}
높은 비용의 객체 생성
데이터베이스에서 책 정보를 로드하는 작업은 시간이 오래 걸리거나 자원을 많이 소모할 수 있습니다. 모든 접근에서 이 작업을 수행하면 성능에 문제가 생길 수 있습니다.
접근 제어의 어려움
이 코드에서는 클라이언트가 직접 데이터베이스에 접근하므로, 접근 제어가 어려워집니다. 데이터베이스 연결을 관리하거나 접근 권한을 제어하는 기능을 추가하기 어렵습니다.
Proxy Pattern 적용 예시
프록시 패턴을 사용해 데이터베이스에 대한 접근을 제어하고, 지연 로딩 기능을 추가할 수 있습니다.
// 주체 인터페이스
public interface IBookRepository
{
Book LoadBook(string bookId);
}
// 실제 객체: 데이터베이스에서 책 정보를 로드
public class BookRepository : IBookRepository
{
public Book LoadBook(string bookId)
{
Console.WriteLine("책 정보를 데이터베이스에서 로드 중...");
// 데이터베이스 접근 코드 (생략)
return new Book { Id = bookId, Title = "C# Programming", Author = "John Doe" };
}
}
// 프록시 객체: 데이터베이스 접근을 제어하고 캐싱을 추가
public class BookRepositoryProxy : IBookRepository
{
private readonly BookRepository _bookRepository;
private Dictionary<string, Book> _cache = new Dictionary<string, Book>();
public BookRepositoryProxy()
{
_bookRepository = new BookRepository();
}
public Book LoadBook(string bookId)
{
if (_cache.ContainsKey(bookId))
{
Console.WriteLine("캐시에서 책 정보를 반환합니다.");
return _cache[bookId];
}
var book = _bookRepository.LoadBook(bookId);
_cache[bookId] = book;
return book;
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
IBookRepository repository = new BookRepositoryProxy();
Book book = repository.LoadBook("123"); // 첫 번째 접근: 데이터베이스 로드
Console.WriteLine($"Book Title: {book.Title}");
book = repository.LoadBook("123"); // 두 번째 접근: 캐시에서 로드
Console.WriteLine($"Book Title: {book.Title}");
}
}
IBookRepository 인터페이스
실제 객체와 프록시 객체가 구현해야 하는 공통 인터페이스입니다.
BookRepository 클래스
실제 데이터베이스에서 책 정보를 로드하는 클래스입니다.
BookRepositoryProxy 클래스
BookRepository
객체에 대한 프록시 역할을 하며, 데이터베이스 접근을 제어하고 결과를 캐싱합니다. 이를 통해 성능을 최적화하고 불필요한 데이터베이스 접근을 줄일 수 있습니다.
클라이언트 코드
클라이언트는 IBookRepository
인터페이스를 통해 책 정보를 요청하며, 실제로는 프록시 객체가 이 요청을 처리합니다.
Proxy Pattern 장단점
장점
지연 초기화
프록시 패턴을 통해 실제 객체의 생성 시점을 필요할 때까지 지연Lazy Initialization시킬 수 있습니다. 이는 성능 최적화에 도움이 됩니다.
접근 제어
프록시 패턴은 실제 객체에 대한 접근을 제어할 수 있습니다. 예를 들어, 접근 권한을 확인하거나, 특정 조건에서만 객체에 접근하도록 제한할 수 있습니다.
성능 최적화
프록시 패턴을 사용하여 결과를 캐싱함으로써, 동일한 요청에 대해 반복적으로 객체를 생성하는 대신, 캐시된 결과를 반환하여 성능을 최적화할 수 있습니다.
로깅 및 모니터링
프록시 패턴을 통해 실제 객체에 대한 접근을 로깅하거나, 실행 전후에 모니터링 기능을 추가할 수 있습니다.
단점
복잡성 증가
프록시 객체를 추가함으로써 클래스 수가 증가하고, 시스템의 복잡성이 증가할 수 있습니다. 이는 특히 프록시가 많아질 경우 코드의 가독성과 유지보수성을 떨어뜨릴 수 있습니다.
성능 저하 가능성
프록시 패턴 자체가 객체 접근에 추가적인 레이어를 도입하기 때문에, 성능이 중요한 시스템에서는 오히려 성능 저하를 초래할 수 있습니다. 특히, 프록시에서 캐싱이나 로깅 등의 작업을 과도하게 수행하면 성능에 부정적인 영향을 미칠 수 있습니다.
단점 해결 방안
프록시 사용 최소화
프록시 패턴을 필요한 경우에만 사용하고, 모든 객체에 대해 프록시를 적용하는 것을 피해야 합니다. 성능이 중요한 객체나 특정 작업에 대해서만 프록시 패턴을 적용하여 복잡성과 성능 저하를 최소화할 수 있습니다.
캐싱 전략의 최적화
프록시에서 캐싱을 사용할 경우, 캐시의 수명과 저장소를 효율적으로 관리하여 불필요한 메모리 사용을 줄일 수 있습니다. 예를 들어, 캐시 만료 시간을 설정하거나, 사용하지 않는 캐시 항목을 정리하는 정책을 도입할 수 있습니다.
성능 모니터링 도구 사용
프록시 패턴을 사용할 때 성능을 주기적으로 모니터링하여, 프록시로 인해 발생할 수 있는 성능 저하를 미리 감지하고 대응할 수 있도록 합니다. 로깅이나 캐싱 작업이 과도하게 이루어지고 있는지 확인하고, 필요하다면 최적화할 수 있습니다.
// 간단한 캐시 만료 예시
public class CachedBookRepositoryProxy : IBookRepository
{
private readonly BookRepository _bookRepository;
private Dictionary<string, (Book book, DateTime timestamp)> _cache = new Dictionary<string, (Book, DateTime)>();
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(10);
public CachedBookRepositoryProxy()
{
_bookRepository = new BookRepository();
}
public Book LoadBook(string bookId)
{
if (_cache.ContainsKey(bookId))
{
var (cachedBook, timestamp) = _cache[bookId];
if (DateTime.Now - timestamp < _cacheDuration)
{
Console.WriteLine("캐시에서 책 정보를 반환합니다.");
return cachedBook;
}
else
{
// 캐시 만료
_cache.Remove(bookId);
}
}
var book = _bookRepository.LoadBook(bookId);
_cache[bookId] = (book, DateTime.Now);
return book;
}
}
불필요한 프록시 레이어 제거
시스템이 성숙해지면서 특정 프록시가 더 이상 필요하지 않거나, 성능에 부정적인 영향을 미친다면, 해당 프록시를 제거하고 클라이언트가 실제 객체에 직접 접근하도록 코드를 리팩토링하는 것이 좋습니다.
객체지향 원칙과의 관계
캡슐화
프록시 패턴은 실제 객체에 대한 접근을 캡슐화하여, 클라이언트가 객체의 내부 구현이나 접근 방식을 알 필요 없이, 프록시를 통해 객체에 접근할 수 있도록 합니다. 이는 캡슐화를 강화하여, 클라이언트와 실제 객체 간의 결합도를 낮춥니다.
상속
프록시 패턴에서는 상속보다는 구성을 중시합니다. 프록시 클래스는 실제 객체를 구성(Composition)하여 사용하며, 이를 통해 상속보다 더 유연한 구조를 제공합니다.
다형성
프록시 패턴은 다형성을 활용하여, 클라이언트가 실제 객체와 프록시 객체를 동일한 인터페이스로 취급할 수 있게 합니다. 이를 통해 프록시 객체가 실제 객체를 대체할 수 있으며, 클라이언트 코드는 변경되지 않습니다.
추상화
프록시 패턴은 추상화를 활용하여, 실제 객체의 구체적인 구현을 숨기고, 클라이언트가 추상화된 인터페이스를 통해 객체와 상호작용할 수 있도록 합니다. 이를 통해 시스템의 유연성과 확장성이 향상됩니다.
단일 책임 원칙
프록시 패턴은 실제 객체와 프록시 객체의 책임을 분리하여, 프록시는 접근 제어, 로깅, 캐싱 등과 같은 부가적인 기능을 처리하고, 실제 객체는 본연의 작업에 집중할 수 있도록 합니다. 이를 통해 각 클래스가 하나의 책임만 가지게 되어 SRP를 잘 준수합니다.
개방_폐쇄 원칙
프록시 패턴은 기존 코드를 수정하지 않고도 새로운 프록시를 추가하여 기능을 확장할 수 있습니다. 예를 들어, 로깅 기능이 필요한 경우, 기존 코드에 프록시를 추가하는 것만으로 로깅 기능을 도입할 수 있습니다. 이는 OCP를 잘 준수하는 설계입니다.
리스코프 치환 원칙
프록시 패턴은 실제 객체와 프록시 객체가 동일한 인터페이스를 구현하므로, 프록시 객체를 실제 객체 대신 사용할 수 있습니다. 이는 LSP를 잘 준수하며, 클라이언트 코드의 일관성을 유지할 수 있습니다.
인터페이스 분리 원칙
프록시 패턴은 클라이언트가 필요한 기능만을 인터페이스로 제공받을 수 있게 합니다. 예를 들어, 프록시 클래스는 클라이언트가 사용하지 않는 인터페이스를 구현할 필요가 없으며, 클라이언트는 자신이 필요로 하는 기능만 접근할 수 있습니다.
의존 역전 원칙
프록시 패턴은 클라이언트가 구체적인 클래스에 의존하지 않고, 인터페이스에 의존하도록 설계됩니다. 프록시는 실제 객체 대신 인터페이스를 통해 접근을 제어하며, 이는 DIP를 잘 준수합니다.
Proxy Pattern의 유형
프록시 패턴은 다양한 상황에서 활용될 수 있으며, 주요 유형은 다음과 같습니다:
Virtual Proxy
가상 프록시Virtual Proxy는 실제 객체의 생성과 초기화를 지연시키는 역할을 합니다. 이 프록시는 실제 객체가 필요할 때만 객체를 생성하여 메모리 사용을 최적화하고, 초기화 비용을 줄입니다. 예를 들어, 대규모 이미지 파일을 로드할 때, 이미지가 실제로 필요한 순간까지 로딩을 지연시킬 수 있습니다.
public class ImageProxy : IImage
{
private RealImage _realImage;
private readonly string _fileName;
public ImageProxy(string fileName)
{
_fileName = fileName;
}
public void Display()
{
if (_realImage == null)
{
_realImage = new RealImage(_fileName);
}
_realImage.Display();
}
}
Remote Proxy
원격 프록시Remote Proxy는 실제 객체가 원격지(예: 다른 서버, 클라우드)에 있을 때, 그 객체에 대한 접근을 프록시가 대리합니다. 클라이언트는 로컬에서 프록시를 통해 원격 객체와 통신하게 됩니다. 원격 프록시는 네트워크 통신을 관리하고, 데이터를 직렬화하여 원격 서버와의 상호작용을 처리합니다.
public class RemoteServiceProxy : IRemoteService
{
private readonly string _remoteServiceUrl;
public RemoteServiceProxy(string remoteServiceUrl)
{
_remoteServiceUrl = remoteServiceUrl;
}
public void Execute()
{
// 네트워크를 통해 원격 서비스 호출
Console.WriteLine($"Connecting to remote service at {_remoteServiceUrl}...");
// 실제 원격 서비스 호출 코드 (생략)
}
}
Protection Proxy
보호 프록시Protection Proxy는 실제 객체에 대한 접근을 제한하여, 권한이 없는 클라이언트가 객체에 접근하는 것을 방지합니다. 보안이 중요한 시스템에서 유용하게 사용됩니다. 예를 들어, 사용자가 특정 파일에 접근할 때, 보호 프록시가 사용자 권한을 확인하고, 권한이 없으면 접근을 차단할 수 있습니다.
public class ProtectedResourceProxy : IResource
{
private readonly Resource _resource;
private readonly User _user;
public ProtectedResourceProxy(Resource resource, User user)
{
_resource = resource;
_user = user;
}
public void Access()
{
if (_user.HasAccessRights)
{
_resource.Access();
}
else
{
Console.WriteLine("접근 권한이 없습니다.");
}
}
}
Smart Proxy
스마트 프록시Smart Proxy는 객체에 접근하는 과정에서 추가적인 행동을 수행합니다. 예를 들어, 참조 횟수를 세거나, 로깅 및 성능 모니터링을 수행할 수 있습니다. 이 프록시를 이용하여 프록시를 통해 객체의 수명 주기를 관리할 수 있습니다. 스마트 프록시를 활용하여 객체의 참조 횟수를 추적하고, 더 이상 필요하지 않은 객체를 적절히 해제하여 메모리 사용을 최적화할 수 있습니다.
public class SmartProxy : IResource
{
private readonly Resource _resource;
private int _accessCount;
public SmartProxy(Resource resource)
{
_resource = resource;
_accessCount = 0;
}
public void Access()
{
_accessCount++;
Console.WriteLine($"Resource accessed {_accessCount} times.");
_resource.Access();
}
}
맺음말
프록시 패턴은 실제 객체에 대한 접근을 제어하고, 부가적인 기능을 제공할 수 있는 강력한 디자인 패턴입니다. 이를 통해 지연 초기화, 접근 제어, 캐싱, 로깅 등의 기능을 손쉽게 구현할 수 있으며, 시스템의 유연성과 확장성을 크게 향상시킬 수 있습니다. 다만, 불필요한 프록시의 사용으로 인해 복잡성이 증가할 수 있으므로, 필요에 따라 신중하게 적용하는 것이 중요합니다.
심화 학습
Proxy와 Decorator
프록시 패턴과 데코레이터 패턴을 조합하여 사용하는 예시는 기능의 동적 확장과 접근 제어를 동시에 처리해야 하는 상황에서 유용하게 사용할 수 있습니다. 도서관 관리 시스템에서 책 정보를 데이터베이스에서 로드할 때, 데이터베이스 접근을 제어하고, 동시에 로깅 기능을 추가하고 싶다고 가정해보겠습니다. 프록시 패턴은 데이터베이스 접근을 제어하는 역할을 하고, 데코레이터 패턴은 로깅을 추가하는 역할을 맡습니다.
// 주체 인터페이스
public interface IBookRepository
{
Book LoadBook(string bookId);
}
// 실제 객체: 데이터베이스에서 책 정보를 로드
public class BookRepository : IBookRepository
{
public Book LoadBook(string bookId)
{
Console.WriteLine("책 정보를 데이터베이스에서 로드 중...");
// 데이터베이스 접근 코드 (생략)
return new Book { Id = bookId, Title = "C# Programming", Author = "John Doe" };
}
}
// 프록시 객체: 데이터베이스 접근을 제어하고 캐싱을 추가
public class BookRepositoryProxy : IBookRepository
{
private readonly IBookRepository _realRepository;
private readonly Dictionary<string, Book> _cache = new Dictionary<string, Book>();
public BookRepositoryProxy(IBookRepository realRepository)
{
_realRepository = realRepository;
}
public Book LoadBook(string bookId)
{
if (_cache.ContainsKey(bookId))
{
Console.WriteLine("캐시에서 책 정보를 반환합니다.");
return _cache[bookId];
}
var book = _realRepository.LoadBook(bookId);
_cache[bookId] = book;
return book;
}
}
// 데코레이터 클래스: 로깅 기능 추가
public class LoggingBookRepositoryDecorator : IBookRepository
{
private readonly IBookRepository _decoratedRepository;
public LoggingBookRepositoryDecorator(IBookRepository decoratedRepository)
{
_decoratedRepository = decoratedRepository;
}
public Book LoadBook(string bookId)
{
Console.WriteLine($"Logging: 책 정보 로드를 시작합니다 (ID: {bookId}).");
var book = _decoratedRepository.LoadBook(bookId);
Console.WriteLine($"Logging: 책 정보 로드 완료 (ID: {bookId}, Title: {book.Title}).");
return book;
}
}
public class Program
{
public static void Main(string[] args)
{
// 실제 객체 생성
IBookRepository realRepository = new BookRepository();
// 프록시 객체 생성 (캐싱 기능 포함)
IBookRepository proxyRepository = new BookRepositoryProxy(realRepository);
// 로깅 데코레이터 적용
IBookRepository loggingRepository = new LoggingBookRepositoryDecorator(proxyRepository);
// 클라이언트 코드
Book book = loggingRepository.LoadBook("123"); // 첫 번째 접근: 데이터베이스 로드 + 로깅
Console.WriteLine($"Book Title: {book.Title}");
book = loggingRepository.LoadBook("123"); // 두 번째 접근: 캐시에서 로드 + 로깅
Console.WriteLine($"Book Title: {book.Title}");
}
}