프록시(Proxy) 패턴은 어떤 객체(RealSubject)에 대한 접근을 제어하거나, 실제 객체의 대리(Proxy) 역할을 수행하는 구조(Structural) 디자인 패턴이다.
“대리인(Proxy)”이라는 뜻처럼, “원본(RealSubject)”을 대신하여 동작, 접근 제어, 로깅, 캐싱 등의 역할을 수행할 수 있다.
클라이언트는 프록시를 원본 객체처럼 사용하지만, 실제 호출은 내부적으로 원본 객체에게 위임하거나, 혹은 프록시 자체가 추가 로직을 실행한다.
간단히 말하면 어댑터 패턴은 다른 인터페이스를, 프록시 패턴은 같은 인터페이스를 랩핑하여 객체를 사용하는 방식이라고 할 수 있다.
특정 객체(예: DB 연결, 네트워크 자원 등)에 직접 접근이 위험하거나, 권한 관리가 필요할 수 있다.
예를 들어, “관리자 권한”이 필요한 기능을, 모든 클라이언트가 직접 호출하기에 앞서, 프록시에서 인증이나 권한 확인 로직을 수행하도록 만들 수 있다.
public interface IAdminService
{
void PerformSensitiveOperation();
}
// 실제 서비스 (RealSubject)
public class AdminService : IAdminService
{
public void PerformSensitiveOperation()
{
Console.WriteLine("중요한 작업을 수행합니다.");
}
}
// 프록시 (Proxy) - 권한 체크 추가
public class AdminServiceProxy : IAdminService
{
private readonly IAdminService _realService;
private readonly string _userRole;
public AdminServiceProxy(string userRole)
{
_realService = new AdminService();
_userRole = userRole;
}
public void PerformSensitiveOperation()
{
if (_userRole != "Admin")
{
Console.WriteLine("권한이 없습니다!");
return;
}
_realService.PerformSensitiveOperation();
}
}
// 사용 예제
class Program
{
static void Main()
{
IAdminService proxy1 = new AdminServiceProxy("Admin");
proxy1.PerformSensitiveOperation(); // 성공
IAdminService proxy2 = new AdminServiceProxy("User");
proxy2.PerformSensitiveOperation(); // 실패
}
}
어떤 객체(예: 대용량 이미지, 네트워크 연결, DB 쿼리)의 생성 비용이 매우 클 때, 필요한 시점까지 실제 객체를 만들지 않고, 프록시로 대체해 둘 수 있다.
예를 들어, “가상 프록시(Virtual Proxy)”를 사용하면, 클라이언트가 실제로 접근하기 전까지 원본 객체를 생성하지 않음으로써 초기 비용을 줄일 수 있다.
public interface IImage
{
void Display();
}
// 실제 이미지 객체 (RealSubject)
public class HighResolutionImage : IImage
{
private readonly string _fileName;
public HighResolutionImage(string fileName)
{
_fileName = fileName;
LoadFromDisk();
}
private void LoadFromDisk()
{
Console.WriteLine($"이미지 {_fileName}를 디스크에서 로딩 중...");
}
public void Display()
{
Console.WriteLine($"이미지 {_fileName} 표시 중");
}
}
// 프록시 (Proxy) - 실제 객체를 필요할 때만 생성
public class ImageProxy : IImage
{
private HighResolutionImage _realImage;
private readonly string _fileName;
public ImageProxy(string fileName)
{
_fileName = fileName;
}
public void Display()
{
if (_realImage == null)
{
_realImage = new HighResolutionImage(_fileName);
}
_realImage.Display();
}
}
// 사용 예제
class Program
{
static void Main()
{
IImage image = new ImageProxy("photo.jpg");
Console.WriteLine("이미지를 아직 로드하지 않음");
image.Display(); // 여기서야 비로소 이미지가 로드됨
image.Display(); // 두 번째 호출에서는 로딩 없이 바로 표시
}
}
메서드 호출이 일어날 때마다 프록시가 중간에 개입하여, 로깅이나 사용 통계를 남길 수 있다.
예를 들어, 네트워크 호출 전/후에 로그를 찍거나, 요청 카운트를 세는 등 부수적인 기능을 쉽게 추가할 수 있다.
public class LoggingProxy : ISubject
{
private readonly ISubject _realSubject;
public LoggingProxy(ISubject realSubject)
{
_realSubject = realSubject;
}
public void Request()
{
Console.WriteLine("[LOG] 메서드 호출: Request()");
_realSubject.Request();
Console.WriteLine("[LOG] 메서드 호출 완료.");
}
}
public class RemoteServiceProxy : IRemoteService
{
private readonly HttpClient _client = new HttpClient();
private readonly string _serverUrl;
public RemoteServiceProxy(string serverUrl)
{
_serverUrl = serverUrl;
}
public async Task<string> GetDataAsync()
{
Console.WriteLine($"서버 {_serverUrl}에 요청 전송...");
string response = await _client.GetStringAsync(_serverUrl);
Console.WriteLine("서버 응답 받음");
return response;
}
}
원격 객체(서버)에 실제로 연결하고 싶을 때, Proxy가 마치 로컬 객체인 것처럼 보여주고, 내부적으로는 원격 RPC를 수행할 수도 있다(“원격 프록시(Remote Proxy)”).
내부적으로는 RPC, Socket 통신 등을 통해 원격 객체를 호출하지만, 클라이언트 입장에서는 지역(Local) 객체처럼 보이게 된다.
이처럼 프록시 패턴은 “실제 객체에 대한 간접 참조 레이어”를 만들어서, 접근 제어, 리소스 지연 로딩, 로깅 등 다양한 부가 기능을 구현하기 유리하다.
클라이언트는 원본 객체(RealSubject)를 직접 접근하지 않고, 프록시(Proxy)를 통해 접근한다.
프록시(Proxy)는 원본 객체를 감싸고, 요청을 가로채거나(Intercept), 위임(Delegate), 또는 추가 로직을 실행할 수 있다.
프록시는 동일한 인터페이스(Interface)를 구현하여, 클라이언트가 원본 객체와 동일한 방식으로 사용할 수 있도록 보장한다.
접근 제어, 성능 최적화(지연 로딩, 캐싱), 로깅, 네트워크 호출 등을 간편하게 처리 가능하다.
// 클라이언트가 사용하고자 하는 공통 인터페이스
public interface ISubject
{
void Request();
}
// 실제 객체(RealSubject) - 무거운 연산 또는 외부 자원 연결 등이 있을 수 있음
public class RealSubject : ISubject
{
public void Request()
{
Console.WriteLine("RealSubject: 실제 요청을 처리합니다.");
}
}
ISubject는 클라이언트가 호출하는 “공통 인터페이스”이다.
RealSubject는 실제 구현 객체로, 클라이언트가 원하는 핵심 로직을 제공한다.
public class Proxy : ISubject
{
private RealSubject _realSubject;
public void Request()
{
// 1) 접근 제어, 로깅, 권한 확인 등 부가 로직
Console.WriteLine("Proxy: 접근 전 로깅 로직을 수행합니다.");
// 2) 실제 객체를 필요할 때만 생성(지연 로딩)
if (_realSubject == null)
{
_realSubject = new RealSubject();
}
// 3) 실제 객체에게 요청을 위임
_realSubject.Request();
// 4) 접근 후 후처리 로직 (예: 캐싱, 통계 등)
Console.WriteLine("Proxy: 요청 후 처리 로직을 수행합니다.");
}
}
Proxy는 ISubject를 구현하고, 내부적으로 RealSubject를 참조한다.
Request()가 호출될 때, 아직 RealSubject가 없다면 그때 생성(또는 연결)하고, 그 뒤에 실제 Request()를 호출한다.
호출 전/후에 추가 로직(로그 기록, 권한 검사 등)을 넣을 수 있다.
class Program
{
static void Main()
{
// 1) 클라이언트는 'ISubject' 인터페이스만 알고 있음
ISubject proxy = new Proxy();
// 2) 내부적으로는 실제 RealSubject가 필요할 때 생성되고, 요청이 전달됨
proxy.Request();
proxy.Request(); // 두 번째 요청 시엔 이미 생성된 RealSubject를 재사용
}
}
Proxy: 접근 전 로깅 로직을 수행합니다.
RealSubject: 실제 요청을 처리합니다.
Proxy: 요청 후 처리 로직을 수행합니다.
Proxy: 접근 전 로깅 로직을 수행합니다.
RealSubject: 실제 요청을 처리합니다.
Proxy: 요청 후 처리 로직을 수행합니다.
클라이언트는 ISubject에 대해 호출만 했지만, 내부적으로는 프록시가 로깅, 실제 객체 생성, 요청 위임 등을 처리했다.
만약 RealSubject가 무거운 초기화가 필요한 객체라면, 첫 번째 요청 시에만 생성이 이뤄지고, 그 이후에는 재사용될 수 있다.
Unreal Engine에서의 RPC(Remote Procedure Call)는 네트워크를 통해 함수 호출을 원격으로 실행할 수 있게 해주는 메커니즘입니다.
이를 통해 클라이언트와 서버가 서로 다른 머신에서 실행되더라도, 마치 로컬에서 함수 호출을 하는 것처럼 쉽게 통신할 수 있습니다.
RPC의 역할
Unreal의 RPC는 주로 네트워크 게임에서 클라이언트와 서버 간의 데이터 동기화, 명령 전달, 상태 변경 등을 위해 사용된다.
예를 들어, 플레이어의 행동(공격, 이동, 상호작용 등)이 서버에 전달되어 게임 상태를 업데이트하거나, 서버에서 결정된 결과를 클라이언트에 알려주는 경우에 사용된다.
RPC 사용 방법
Unreal Engine에서는 RPC를 구현할 때 UFUNCTION 매크로를 사용한다.
Server RPC: 클라이언트에서 호출되지만, 실제로는 서버에서 실행됩니다. 예를 들어, 플레이어가 어떤 행동을 할 때 서버에 이를 알리기 위해 사용된다.
UFUNCTION(Server, Reliable, WithValidation)
void Server_DoAction();
Client RPC: 서버에서 호출되지만, 실제로는 특정 클라이언트에서 실행됩니다. 예를 들어, 서버가 클라이언트에게 애니메이션이나 효과를 재생하도록 명령할 때 사용됩니다.
UFUNCTION(Client, Reliable)
void Client_PlayEffect();
이처럼 Unreal의 RPC는 함수의 호출 위치(클라이언트 혹은 서버)를 지정하고, 네트워크의 신뢰성과 유효성 검사를 위한 옵션을 설정할 수 있게 해준다.
RPC 호출 흐름
클라이언트 측: 클라이언트가 Server RPC 함수를 호출하면, 해당 호출은 로컬에서는 바로 실행되지 않고, 네트워크 패킷으로 패키징되어 서버로 전송된다.
서버 측: 서버는 수신한 패킷을 분석하여 해당 함수를 호출하고, 그 결과를 다시 클라이언트에 전달하거나, 내부 상태를 업데이트한다.
어떤 디자인 패턴인가?
Unreal Engine의 RPC는 프록시(Proxy) 패턴의 한 형태로 볼 수 있다.
프록시 패턴의 핵심:
클라이언트는 원격 객체를 로컬 객체처럼 사용하게 하며, 실제 호출은 프록시(스텁)가 대신하여 원격 객체에 전달한다.
Unreal의 경우, UFUNCTION 매크로로 지정된 RPC 함수는 클라이언트 스텁(Client Stub) 또는 서버 스텁(Server Stub) 역할을 하며, 네트워크 통신의 세부사항(패킷화, 전송, 역직렬화 등)을 캡슐화한다.
RPC와 프록시 패턴의 연관성:
클라이언트가 함수 호출을 할 때 실제로는 로컬에서 호출하는 것처럼 보이지만, 내부적으로는 네트워크를 통해 원격 서버의 객체에 그 호출을 위임하는 구조를 띤다.
이러한 방식은 프록시 패턴의 특징인 “대리자 역할”을 수행하며, 클라이언트는 원격 호출의 복잡성을 전혀 인지하지 않고, 마치 로컬 함수를 호출하는 것처럼 코드를 작성할 수 있게 된다.
요약
Unreal RPC: 네트워크 상에서 클라이언트와 서버 간에 원격 함수 호출을 가능하게 하는 메커니즘으로, UFUNCTION 매크로를 통해 쉽게 설정할 수 있다.
디자인 패턴: Unreal의 RPC 시스템은 프록시 패턴의 한 구현 형태입니다. 클라이언트와 서버 각각의 스텁이 원격 호출을 추상화하여, 호출자는 로컬 호출처럼 사용하면서 내부의 네트워크 통신은 감춰진다.
이렇게 Unreal Engine에서의 RPC는 복잡한 네트워크 통신을 단순화하고, 클라이언트와 서버 간의 원활한 상호작용을 가능하게 하는 중요한 메커니즘이며, 프록시 패턴의 개념을 기반으로 하고 있다고 할 수 있다.