13. 프록시 패턴 (Proxy)
진짜 객체 앞에 같은 약속을 따르는 대리인을 세워서, 부르는 쪽 모르게 접근을 조절하는 프록시 패턴을 알아봅니다.
한 줄 요약
진짜 객체 앞에 같은 약속을 따르는 대리인을 세워서, 부르는 쪽은 그대로 두고 진짜 객체에 가는 길을 조절하는 패턴이에요. 기억해 두기, 막기, 늦게 만들기, 기록 남기기 같은 일을 대리인이 맡아요.
문제: 같은 걸 몇 번이고 다시 묻는 코드
날씨 앱을 만든다고 해 볼게요. 날씨는 멀리 있는 서버에 물어봐야 알 수 있어요. 서버는 한 번 물을 때마다 느리고, 돈도 들어요.
- 첫 화면은 서울 날씨를 보여 줘요.
- 지도 화면은 서울과 부산 날씨를 보여 줘요.
가장 먼저 떠오르는 방법은 화면들이 서버에 바로 물어보는 거예요.
[날씨정보]를 규정하자:
[문자열]을 돌려주는 <오늘날씨>가 있어야 한다 ([문자열]인 '도시')
(참고: 멀리 있는 서버에 물어보는 서비스예요. 한 번 물을 때마다 느리고, 돈도 들어요)
[날씨정보]를 따르는 [날씨서버]를 설계하자:
'물어본횟수'를 [숫자]인 0으로 정하자
[문자열]을 돌려주는 <오늘날씨>를 만들자 ([문자열]인 '도시'):
'나'의 '물어본횟수'를 ('나'의 '물어본횟수' + 1)로 정하자
틀"(서버에 {'도시'} 날씨를 물어보는 중...)"을 출력하자
'도시'에 따라 나누자:
"서울" 인 경우:
"맑음"을 돌려주자
"부산" 인 경우:
"비"를 돌려주자
"모름"을 돌려주자
<첫화면>을 만들자 ([날씨정보]인 '날씨'):
틀"[첫 화면] 서울: {'날씨'의 <오늘날씨>("서울")}"을 출력하자
<지도화면>을 만들자 ([날씨정보]인 '날씨'):
틀"[지도] 서울: {'날씨'의 <오늘날씨>("서울")}, 부산: {'날씨'의 <오늘날씨>("부산")}"을 출력하자
'서버'를 새로운 [날씨서버]()로 정하자
<첫화면>('서버')를 실행하자
<지도화면>('서버')를 실행하자
<첫화면>('서버')를 실행하자
틀"서버에 물어본 횟수: {'서버'의 '물어본횟수'}"를 출력하자
실행 결과
(서버에 서울 날씨를 물어보는 중…)
[첫 화면] 서울: 맑음
(서버에 서울 날씨를 물어보는 중…)
(서버에 부산 날씨를 물어보는 중…)
[지도] 서울: 맑음, 부산: 비
(서버에 서울 날씨를 물어보는 중…)
[첫 화면] 서울: 맑음
서버에 물어본 횟수: 4
화면은 세 번 그렸는데, 서버에는 네 번이나 물어봤어요. 그중 세 번은 똑같은 “서울”이었어요.
- 같은 질문을 계속 다시 해요. 오늘 서울 날씨는 몇 번을 물어도 같은데, 화면을 그릴 때마다 서버에 다시 물어요. 느리고 돈도 들어요.
- 화면마다 기억해 두기를 넣으면 복사가 돼요.
<첫화면>과<지도화면>에 각자 “이미 물어봤으면 다시 묻지 말자”를 넣으면 같은 코드가 두 벌이고, 기억도 따로따로라서 서로 나눠 쓰지도 못해요. - 서버를 고칠 수는 없어요. 서버는 남이 만든 것일 수도 있고, 서버에 기억해 두는 일까지 넣으면 “날씨를 알려 준다”는 본래 일과 섞여요.
해결책: 같은 약속을 따르는 대리인 세우기
프록시 패턴은 서버 앞에 대리인(프록시)을 하나 세워요.
- 진짜 서버가 따르는 약속(
[날씨정보])이 있어요. 화면들은 이 약속만 알아요. - 프록시도 같은 약속을 따라요. 그래서 화면 입장에서는 프록시와 진짜 서버가 똑같아 보여요.
- 프록시는 안에 진짜 서버를 품고, 부탁이 오면 먼저 자기가 판단해요. 이미 물어본 도시라면 기억해 둔 답을 바로 주고, 처음 듣는 도시만 진짜 서버에 물어요.
- 화면들은 진짜 서버 대신 프록시를 받아요. 화면 코드는 한 글자도 안 바뀌어요.
비유: 연예인의 매니저
방송국에서 연예인에게 섭외 전화를 하면 매니저가 받아요. 매니저도 “연예인 쪽 연락처”라서, 거는 쪽은 똑같이 전화를 걸 뿐이에요. 매니저는 이미 답한 질문(“다음 주 일정 비어요?”)은 바로 대답하고, 이상한 연락은 막고, 꼭 필요한 것만 연예인에게 전해요. 매니저가 바로 “프록시”예요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 약속 | [날씨정보] (규정) |
진짜든 대리인이든 따라야 하는 모양이에요 |
| 진짜 객체 | [날씨서버] |
실제로 일을 해요. 느리고 비싸요 |
| 프록시(대리인) | [날씨캐시] |
같은 약속을 따르고, 진짜 객체에 가는 길을 조절해요 |
| 쓰는 쪽 | <첫화면>, <지도화면> |
약속만 알아요. 프록시인지 진짜인지 몰라요 |
하리로 만들기
같은 날씨 앱에 프록시를 세워 볼게요. 서버와 화면 코드는 그대로예요.
(참고: 1단계 — 날씨를 알려 주는 쪽이라면 지킬 약속. 프록시도 이 약속을 따라요)
[날씨정보]를 규정하자:
[문자열]을 돌려주는 <오늘날씨>가 있어야 한다 ([문자열]인 '도시')
(참고: 멀리 있는 서버에 물어보는 서비스예요. 한 번 물을 때마다 느리고, 돈도 들어요)
[날씨정보]를 따르는 [날씨서버]를 설계하자:
'물어본횟수'를 [숫자]인 0으로 정하자
[문자열]을 돌려주는 <오늘날씨>를 만들자 ([문자열]인 '도시'):
'나'의 '물어본횟수'를 ('나'의 '물어본횟수' + 1)로 정하자
틀"(서버에 {'도시'} 날씨를 물어보는 중...)"을 출력하자
'도시'에 따라 나누자:
"서울" 인 경우:
"맑음"을 돌려주자
"부산" 인 경우:
"비"를 돌려주자
"모름"을 돌려주자
(참고: 2단계 — 진짜 서버 앞에 서서, 같은 약속으로 대신 답해 주는 프록시예요)
[날씨정보]를 따르는 [날씨캐시]를 설계하자:
'서버'를 [날씨정보]인 비어있음으로 정하여 숨기자
'기억'을 {}로 정하여 숨기자
처음 만들어질 때 ([날씨정보]인 '서버') 다음과 같이 하자:
'나'의 '서버'를 '서버'로 정하자
[문자열]을 돌려주는 <오늘날씨>를 만들자 ([문자열]인 '도시'):
(참고: 3단계 — 이미 물어본 도시면 서버에 가지 않고 기억해 둔 답을 줘요)
만약 ('나'의 '기억'의 <포함확인>('도시')) 라면:
('나'의 '기억'의 '도시')를 돌려주자
'답'을 '나'의 '서버'의 <오늘날씨>('도시')로 정하자
'나'의 '기억'의 '도시'를 '답'으로 정하자
'답'을 돌려주자
(참고: 화면 코드는 한 글자도 안 바뀌어요. [날씨정보]만 알고 있으니까요)
<첫화면>을 만들자 ([날씨정보]인 '날씨'):
틀"[첫 화면] 서울: {'날씨'의 <오늘날씨>("서울")}"을 출력하자
<지도화면>을 만들자 ([날씨정보]인 '날씨'):
틀"[지도] 서울: {'날씨'의 <오늘날씨>("서울")}, 부산: {'날씨'의 <오늘날씨>("부산")}"을 출력하자
'서버'를 새로운 [날씨서버]()로 정하자
'날씨'를 [날씨정보]인 새로운 [날씨캐시]('서버')로 정하자
<첫화면>('날씨')를 실행하자
<지도화면>('날씨')를 실행하자
<첫화면>('날씨')를 실행하자
틀"서버에 물어본 횟수: {'서버'의 '물어본횟수'}"를 출력하자
실행 결과
(서버에 서울 날씨를 물어보는 중…)
[첫 화면] 서울: 맑음
(서버에 부산 날씨를 물어보는 중…)
[지도] 서울: 맑음, 부산: 비
[첫 화면] 서울: 맑음
서버에 물어본 횟수: 2
화면은 똑같이 세 번 그렸고 보이는 날씨도 같아요. 그런데 서버에는 두 번만 물어봤어요. 서울과 부산을 한 번씩이요.
단계별로 다시 보기
- 1단계 — 같은 약속.
[날씨캐시]도[날씨정보]를 따라요. 그래서[날씨정보]인 '날씨'를 받는 화면 함수에 그대로 넘길 수 있어요. - 2단계 — 진짜를 품기. 프록시는 진짜 서버를
'서버'에 품고 있어요. 진짜 서버가 할 일을 다시 만들지 않고, 필요할 때 맡겨요. - 3단계 — 가기 전에 판단하기.
<오늘날씨>는 먼저'기억'이라는 사전에 그 도시가 있는지<포함확인>으로 봐요. 있으면 바로 돌려주고, 없을 때만 서버에 물어 그 답을 기억해 둬요. - 바꾼 곳은 한 줄. 화면 코드도, 서버 코드도 그대로예요. 바뀐 건 맨 아래에서 화면에 넘기는 객체를
'서버'에서'날씨'(프록시)로 바꾼 것뿐이에요.
프록시는 기억해 두기만 하나요?
아니에요. “진짜 객체에 가기 전에 무언가를 한다”면 모두 프록시예요. 자주 쓰는 종류는 이래요.
기억하는 프록시: 이 예제처럼 같은 답을 다시 묻지 않아요.
지키는 프록시: 권한이 없으면 진짜 객체에 가지 못하게 막아요.
늦게 만드는 프록시: 만드는 데 오래 걸리는 진짜 객체를, 처음 부탁이 올 때에야 만들어요.
기록하는 프록시: 누가 언제 무엇을 부탁했는지 남겨요.
새 프록시를 더해 보면
“로그인한 사람만 날씨를 볼 수 있게” 해 달라는 요청이 왔다고 해 볼게요.
- 앞의 방식: 날씨를 묻는 모든 화면에 “로그인했나?“를 넣거나, 서버를 고쳐야 해요.
- 프록시 패턴:
[날씨정보]를 따르는 지키는 프록시를 하나 만들어서 앞에 세우면 돼요. 기억하는 프록시 앞에 또 세울 수도 있어요. 같은 약속을 따르니까 겹겹이 세워도 돼요.
직접 해 보기
위 코드에[로그인확인]프록시를 더해 보세요.[날씨정보]를 따르고, 안에 다른[날씨정보]하나와'로그인함'(참/거짓)을 품어요.<오늘날씨>는 로그인하지 않았으면 서버에 가지 않고"로그인이 필요해요"를 돌려주고, 로그인했으면 안에 품은 것에게 맡겨요.새로운 [로그인확인](새로운 [날씨캐시]('서버'), 거짓)으로 만들어서 첫 화면에 넘겨 보세요. 서버에 물어본 횟수는 0이어야 해요.
언제 쓰면 좋을까요?
- 진짜 객체가 느리거나 비싸서, 같은 부탁을 다시 하지 않게 하고 싶을 때
- 진짜 객체에 가기 전에 권한을 확인하거나, 부탁을 기록하고 싶을 때
- 진짜 객체를 만드는 데 오래 걸려서, 정말 필요할 때까지 미루고 싶을 때
- 진짜 객체를 고칠 수 없거나, 그 일과 섞고 싶지 않을 때
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 쓰는 쪽과 진짜 객체를 고치지 않아요 | 한 번 더 거쳐 가서, 따라 읽을 때 한 단계가 더 있어요 |
| 기억, 막기, 늦게 만들기, 기록을 한곳에 모아요 | 기억하는 프록시는 진짜 값이 바뀌어도 옛 답을 줄 수 있어요 (언제 잊을지 정해야 해요) |
| 같은 약속이라 겹겹이 세울 수도 있어요 | 프록시가 하는 일이 겉으로 안 보여서, 왜 느린지·왜 막혔는지 찾기 어려울 수 있어요 |
너무 이른 패턴은 독이에요.
진짜 객체가 빠르고, 막을 것도, 기록할 것도 없다면 그냥 진짜 객체를 쓰세요. “같은 걸 또 물어서 느려요”, “아무나 부르면 안 돼요” 같은 이유가 생길 때 프록시를 세워도 늦지 않아요.