461. Spy (스파이) - 스텁 역할 + 호출 정보 기록
핵심 인사이트 (3줄 요약)
- 본질: 스파이(Spy) 객체는 테스트 더블(Test Double) 중에서 진짜 객체처럼 몰래 행동하거나 스텁(Stub)처럼 가짜 값을 뱉으면서도, 등 뒤로는 "내가 몇 번 호출되었는지, 어떤 파라미터를 들고 내게 찾아왔는지"의 모든 은밀한 행적(Log/History)을 몰래 수첩에 기록하여 테스트에게 밀고하는 감시자다.
- 가치:
이메일 발송이나결제 요청처럼 결괏값(상태)이 밖으로 튀어나오지 않고 허공으로 증발해 버리는(Side-effect) 파괴적인 함수를 검증해야 할 때, **"진짜로 그 함수가 불리긴 한 거야? 무슨 값 넣어서 불렀어?"라는 행위(Behavior)의 진실 여부를 추적할 수 있는 유일한 블랙박스(CCTV)**를 제공한다.- 융합: 객체의 상태(State)가 아니라 행위(Behavior)를 검증한다는 측면에서 다음 장의
Mock(목)과 강력하게 융합되지만, Mock이 대본을 깐깐하게 강제하는 '감독관'이라면 Spy는 진짜 객체의 기능 일부를 그대로 살려두면서 살짝 기록만 훔쳐보는 '부분적 위장(Partial Mocking)'이라는 고도의 유연한 첩보 아키텍처를 자랑한다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: Spy(스파이)는 현실 세계의 간첩과 똑같다. 평소에는 일반 시민(진짜 객체)처럼 굴거나 주어진 대답(Stub)만 충실히 한다. 하지만 속으로는 자기를 부른 놈을 감시한다.
orderService.order()를 실행했을 때, 테스트 코드는 결과가 없다(void). 그래서 Spy로 심어둔EmailSender에게 조용히 묻는다. "야 스파이, 너 아까 불렸어? 파라미터로 'kim@mail.com' 들고 온 거 맞지?" 스파이는 수첩을 까서 증거를 제출한다. -
필요성: 함수를 짰다.
public int add(1, 2)는3이 리턴되니까, 테스트 코드에서assertEquals(3, result)로 확인(상태 검증)하면 끝이다. 그런데 리턴값이 없는 허공의 메아리(void) 함수는 어떻게 검증할 것인가? 예를 들어pushNotification()(푸시 알람 쏘기) 함수가 있다. 로직을 탔는데 화면엔 아무 변화가 없다. 진짜 알람이 나갔는지 테스트 코드 입장에서는 장님이다. 이때 푸시 알람 객체 자리에 '스파이(Spy)'를 꽂아두면, "네! 저 방금 1번 호출됐습니다!"라고 자백을 받아냄으로써 로직이 빵꾸 나지 않고 정상적으로 흘렀음을 완벽하게 증명해 낼 수 있다. -
💡 비유: Spy는 레스토랑의 **'비밀 미스터리 쇼퍼(손님 위장 알바)'**와 같습니다. 매니저(테스트)가 주방장(로직)을 칭찬해야 할지 혼내야 할지 알고 싶습니다. 그래서 비밀 알바생(스파이)을 손님으로 투입시킵니다. 주방장은 알바생을 진짜 손님인 줄 알고 요리(함수 호출)를 내어줍니다. 알바생은 요리를 먹으면서(스텁 역할) 몰래 수첩에 "주방장이 소금을 2스푼 넣음, 요리 나오는 데 10분 걸림"을 싹 다 기록해서 나중에 매니저에게 넘깁니다(행위 검증). 주방장은 자기가 감시당한 줄도 모릅니다.
-
등장 배경 및 발전 과정:
- Void 함수의 저주: 초기 TDD 시절, 리턴값이 있는 함수는 테스트하기 쉬웠지만, DB 저장(
save), 메일 발송(send) 같은 Void 함수는 검증할 길이 없어 눈으로 콘솔 로그(Print)를 찍어보며 노가다를 했다. - 수작업 카운터 변수 주입: 화가 난 개발자들이 가짜 클래스를 만들고 내부에
int callCount = 0;이라는 변수를 둬서, 함수가 불릴 때마다callCount++를 시키는 수동 스파이 코드를 짜기 시작했다. - Mockito @Spy 어노테이션 대통일 (현재): 이런 뻔한 첩보 활동을 손으로 짜지 마라.
Mockito의@Spy하나만 딱지 붙여 놓으면, 진짜 객체의 능력을 100% 보존하면서 프레임워크가 뒤에서 몰래 통화 기록(Call History)을 빼돌려주는 최첨단 도청 장치가 완성되었다.
- Void 함수의 저주: 초기 TDD 시절, 리턴값이 있는 함수는 테스트하기 쉬웠지만, DB 저장(
-
📢 섹션 요약 비유: 스파이는 전투기 파일럿 연습 조종석의 **'기록용 블랙박스'**입니다. 파일럿(로직)이 연습을 끝냈을 때 비행기가 터졌냐 안 터졌냐(결괏값)도 중요하지만, "위험한 순간에 회피 버튼을 몇 번 눌렀고, 조종간을 몇 도 꺾었는가?"라는 보이지 않는 행위(Behavior)의 흔적을 싹 다 메모리 카드(스파이)에 저장해 두어야만 나중에 교관(Assert)이 완벽하게 채점할 수 있습니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
1. Spy의 동작 아키텍처 (Mockito @Spy 구조)
스파이는 Stub과 Mock의 교집합에 있으면서도 자신만의 독특한 성격을 지닌다.
[ Spy를 이용한 행위 추적의 정석 ]
// 1. 진짜 객체를 본뜬 Spy(스파이) 요원을 침투시킨다.
// (진짜 ArrayList의 기능은 100% 살아있으면서 뒤로 기록만 훔친다)
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);
// 2. 비즈니스 로직 실행 (주방장이 스파이에게 요리를 건넴)
spyList.add("kim");
spyList.add("lee");
// 3. Assert (행위 검증 - Behavior Verification)
// 스파이의 뒷주머니(수첩)를 뒤져서, 진짜로 "kim"이라는 글자를 넣고 'add'가 1번 불렸는지 취조한다.
verify(spyList, times(1)).add("kim");
verify(spyList, times(1)).add("lee");
// 4. 상태도 진짜로 변해있음 (이게 Mock과 다른 점!)
assertEquals(2, spyList.size()); // 스파이는 진짜 기능을 수행했으므로 사이즈가 2가 되어 있음.
- 부분적 위장 (Partial Mocking): 위 코드에서
spyList는 겉으로는 진짜(Real) 깡패 집단(ArrayList) 소속이다. 그래서 실제로 값이 2개 들어갔다(상태 변경). 하지만 뒤로는 프레임워크와 결탁하여 "누가 나한테 무슨 파라미터를 넣었는지" 싹 다 기록해 버린다. 즉, "진짜 로직을 태우면서도, 특정 행위만 도청하고 싶을 때" 쓰는 극강의 유연한 무기다.
2. Stub과 Spy의 혼종 (스파이를 스텁처럼 쓰기)
스파이는 자기가 진짜인 척하지만, 원한다면 언제든 특정 질문에는 "거짓말(Stub)"을 하도록 훈련시킬 수 있다.
// 진짜 기능은 다 살아있지만, size()를 물어볼 때만 무조건 "100"이라고 거짓말을 해라! (Stubbing)
doReturn(100).when(spyList).size();
spyList.add("one"); // 진짜 로직 수행 (1개 들어감)
System.out.println(spyList.size()); // "100" 출력 (스파이의 거짓말)
- 📢 섹션 요약 비유: 이 첩보전은 영화 **'무간도'**와 같습니다. 스파이(경찰)는 조폭 조작에 잠입해 진짜 조폭처럼 싸움도 하고 돈도 수금합니다(진짜 객체 기능 수행). 하지만 형사반장(테스트 코드)이 "조폭 두목이 오늘 어디 갔어?"라고 물으면 몰래 정보를 넘깁니다(행위 검증). 심지어 반장이 시키면 "두목님, 오늘 경찰 안 깔렸습니다"라고 조폭들에게 거짓말(Stub 기능)까지 섞어서 할 수 있는 완벽한 위장 요원입니다.
Ⅲ. 융합 비교 및 다각도 분석
1. Mock (목) vs Spy (스파이) 결정적 차이 (★ 면접 핵심)
둘 다 행위 검증(Behavior Verification)을 하지만 철학이 완전히 다르다.
| 척도 | Mock (가짜 인형) | Spy (위장 스파이) |
|---|---|---|
| 기본 뼈대 | 완전한 가짜 껍데기. 아무 기능도 안 함. | **진짜 객체(Real Object)**의 복제본. |
| 기본 행동 | 내가 "이거 해!"라고 대본(Stub)을 안 짜주면 함수 불렀을 때 무조건 null 뱉음 (멍청함). | 대본을 안 짜줘도 알아서 진짜 원본 객체의 로직을 돌려서 진짜 값을 뱉음 (스마트함). |
| 위장술의 한계 | 그냥 가상 현실 세트장 100%. (전체 목킹) | 현실의 집에서 방 하나만 가상현실로 꾸밈 (부분 목킹, Partial Mocking). |
| 사용하는 때 | DB나 외부 API 등 진짜로 부르면 돈이 나가거나 너무 느려서 "절대 진짜 로직을 태우면 안 될 때". | 레거시 코드를 뜯어고치기 무서워서, "진짜 로직을 그대로 굴리면서 통계(기록)만 살짝 빼내고 싶을 때". |
과목 융합 관점
-
소프트웨어 공학 (레거시 코드 리팩토링): 신규 프로젝트(TDD)를 할 때는 100% Mock을 쓰는 것이 깔끔하다. 하지만 10년 된 썩은 스파게티 레거시 코드를 고칠 때는? 의존성이 너무 복잡하게 얽혀있어 Mock으로 가짜 대본을 다 짜려면 1,000줄이 넘어가 포기하게 된다. 이때 Spy가 레거시의 구원자로 등판한다. 레거시 객체의 진짜 더러운 로직을 그대로 굴려버리고(대본 짤 필요 없음), 내가 확인하고 싶은 딱 하나의 "결제 함수가 호출되긴 했나?" 흔적만 스파이로 빼내서 검증하는, 더러운 코드를 부수지 않고 묶어두는 훌륭한 과도기적 아키텍처 툴이다.
-
클린 아키텍처 (안티패턴 경계): 아키텍트는 개발자들이
@Spy를 남발하면 칼을 빼들어야 한다. 진짜 객체를 띄우고 일부만 훔쳐보는 행위(Partial Mock)는, "이 객체가 한 가지 일만 하지 않고(SRP 위반) 너무 많은 일을 하고 있어서, 억지로 스파이를 심어 쪼개 보겠다"는 설계의 실패를 기만하는 행위일 확률이 매우 높다. 무분별한 스파이 남용은 코드 결합도 붕괴의 적신호다. -
📢 섹션 요약 비유: Mock은 드라마 세트장의 **'가짜 나무 합판 문'**입니다. 문을 열어도 뒤에 진짜 방이 없고 낭떠러지(null)입니다. 내가 대본(Stub)을 안 짜주면 덜렁거립니다. 반면 Spy는 진짜 우리 집의 **'진짜 강철 문'**입니다. 문을 열면 내 방이 나옵니다. 단지 문 손잡이에 찰칵! 하고 도청 장치(Spy) 하나만 살짝 붙여둬서 누가 몇 번 열었는지만 카운트하는 것입니다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — 부수 효과(Side-effect)로 가득 찬 알림 서버의 맹점: 주문이 떨어지면 고객에게 이메일 1통, 카카오톡 1통, 문자 1통 총 3개의 알림을 동시에 쏘는 짬뽕 함수를 만들었다. 리턴값이 없는
void함수다. 개발자는 테스트 코드에 그냥order.sendAllAlerts()한 줄만 적어놓고 초록불(Pass)이 뜨자 "와! 에러 안 났다!"하고 배포했다. 하지만 카카오톡 API 설정이 빠져있어 실제론 이메일 1개만 나가고 알림 2개가 누락되었지만, 에러를 뿜지 않고(조용한 실패) 그냥 묻혀버렸다. 고객의 클레임이 터졌다.- 아키텍트의 해결책: Void 함수의 행위 검증(Behavior Verification) 부재가 낳은 비극이다. 에러가 안 난다고 일이 잘된 것이 아니다. 아키텍트는 3개의 알림 객체 자리에 3명의 **스파이(Spy) 또는 목(Mock)**을 쑤셔 넣도록 강제해야 한다. 그리고 마지막 Assert 라인에
verify(spyKakao, times(1)).send();,verify(spySms, times(1)).send();라고 깐깐하게 호통을 치는 감시 코드를 박아야 한다. 그래야 카카오 로직이 0번 불렸을 때 "야! 왜 1번 안 불렀어!"라며 테스트가 피를 토하고 터져서 버그를 막아낸다.
- 아키텍트의 해결책: Void 함수의 행위 검증(Behavior Verification) 부재가 낳은 비극이다. 에러가 안 난다고 일이 잘된 것이 아니다. 아키텍트는 3개의 알림 객체 자리에 3명의 **스파이(Spy) 또는 목(Mock)**을 쑤셔 넣도록 강제해야 한다. 그리고 마지막 Assert 라인에
-
시나리오 — 너무 무거운 진짜 로직을 스파이(Spy)로 부분 마취시키기: 외부 PG사에서 결제 암호화 해시(Hash)를 만들어주는
CryptoUtil이라는 거대한 객체가 있다. 테스트를 하려는데 이 해시 함수가 CPU를 너무 먹어서 1초씩 걸린다. 그런데 나는 이 객체 안에 있는 단순한validateLength()함수만 테스트하고 싶다. 아예 Mock(가짜)으로 덮어버리자니, 진짜 암호화 로직을 아예 못 쓰게 되어 곤란하다.- 아키텍트의 해결책: 스파이(Spy)의 부분 목킹(Partial Mocking) 흑마법을 쓸 최적의 타이밍이다.
CryptoUtil을 통째로 스파이(@Spy)로 만든다. 그리고 1초씩 걸리는 지옥의 해시 함수에만 핀셋으로 마취 주사를 놓는다.doReturn("가짜해시값").when(spyCrypto).generateHash();이렇게 세팅하면, 내가 검증하고 싶은validateLength()는 진짜 오리지널 로직을 타서 완벽히 테스트가 돌면서도, 무거운generateHash()가 불리는 순간엔 0.001초 만에 스파이가 거짓말(가짜해시값)을 뱉어주어 성능과 진짜 로직 두 마리 토끼를 우아하게 다 잡아낼 수 있다.
- 아키텍트의 해결책: 스파이(Spy)의 부분 목킹(Partial Mocking) 흑마법을 쓸 최적의 타이밍이다.
도입 체크리스트
- 비즈니스적: "내가 이 함수가 진짜로 몇 번 불렸는지 감시하는 게, 우리 비즈니스에서 의미가 있는 짓인가?" 이메일 발송은 2번 부르면 고객에게 스팸 메일 2개가 날아가니 무조건
verify(times(1))로 1번 불렸는지 감시(Spy)해야 한다. 하지만DB.findId()처럼 단순히 값을 읽어오는 로직은 1번 부르든 5번 부르든 멱등성(Idempotency)이 보장되어 상관없다. 이런 단순 조회 로직에 무지성으로 스파이를 달아 "정확히 2번 불렀니?"라고 검증하는 것은 깨지기 쉬운 쓰레기 코드를 양산하는 짓이다. 읽기는 Stub으로, 파괴적 쓰기(이메일, 결제)는 Spy/Mock으로 찢어 써라. - 기술적: Mockito의
when().thenReturn()과doReturn().when()의 함정 피하기. 진짜 객체를 베이스로 하는 Spy는, 앞서 말한 스텁 세팅 문법에 치명적 차이가 있다. 스파이에게when(spyList.get(0)).thenReturn("가짜");라고 지시하면, 내부적으로 진짜get(0)코드를 한 번 타버리면서 뻗어버리는(IndexOutOfBoundsException) 재앙이 벌어진다(진짜 객체이기 때문). 스파이를 마취시킬 땐 무조건 진짜 로직을 스킵하는doReturn("가짜").when(spyList).get(0);문법을 강제해야 폭발을 막을 수 있다.
안티패턴
-
과도한 감시 국가 (Big Brother Anti-pattern): 개발자가
verify()뽕에 취해서, 메인 함수 안에 있는a(),b(),c(),d()4개의 함수가 순서대로 1번씩 딱딱 불렸는지 모든 줄에 스파이를 달아(InOrder Verify) 완벽한 통제 국가를 만들어놨다. 나중에 로직이 개선되어b()를 안 부르게 최적화되었는데, 비즈니스는 멀쩡함에도 감시 스파이들이 "왜 b 안 불렀어!"라며 1,000개의 테스트를 다 터뜨려버린다. 행위 검증(Spy)은 내부 구현과 너무 강하게 결합되므로, 진짜 중요한 '외부 통신' 한두 개만 핀셋으로 감시해야 한다. -
📢 섹션 요약 비유: 스파이(Spy) 남용은 자식 방에 **'CCTV 10대 설치하기'**와 같습니다. 수학 100점(상태 검증) 맞은 애한테, "너 왜 책상에 안 앉고 침대에서 문제 풀었어!(행위 검증)"라고 트집 잡는 격입니다. 결과만 좋으면 과정은 자유롭게 놔둬야 리팩토링(창의성)이 살아납니다. 스파이는 '이메일 발송'이나 '돈 출금'처럼 한 번 실수하면 경찰서 가는(치명적 부수 효과) 진짜 무서운 외출에만 몰래 붙여야 하는 조심스러운 미행입니다.
Ⅴ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 메일 발송, 알림 등 Void 함수 검증 불가 환경 (AS-IS) | Spy(스파이) 주입을 통한 은밀한 행위 검증 도입 (TO-BE) | 개선 효과 |
|---|---|---|---|
| 정량 | 알림 누락 버그 발생 후 고객 클레임(CS)으로만 인지 | verify(spy).send() 실패로 릴리즈 전 즉각(0.1초) 감지 | 보이지 않는 부수 효과(Side-effect) 유실 버그 발생률 0% 달성 |
| 정량 | 레거시 코드 테스트 짜려고 코드 1만 줄을 갈아엎어야 함 | 진짜 로직 살리며 부분 마취(Partial Mock)로 1일 내 테스트 구축 | 레거시(Legacy) 시스템의 테스트 커버리지 확보 속도 10배 상승 |
| 정성 | "이 함수가 끝까지 잘 돈 게 맞을까?"라는 막연한 불안감 | "정확히 결제 모듈을 1번만 찔렀다"는 100% 논리적 확신 | 아키텍트의 의도(설계 로직 흐름)가 코드로 완벽하게 강제 및 문서화 |
미래 전망
- AI 로그 헌팅(Log Hunting)과의 경계 약화: 현재 단위 테스트에서는 스파이(Spy)를 꽂아 내부 함수의 호출 횟수를 기계적으로 셌다. 미래의 클라우드 관측성(Observability) 시대에는, 개발자가 굳이 코드 안에 번거롭게 스파이 객체를 주입하지 않아도, Datadog이나 OpenTelemetry에 심어진 초거대 AI가 런타임에 뿜어져 나오는 수천만 줄의 로그를 1초 만에 스캔하여 "어? 고객이 결제를 눌렀는데 이메일 큐(Queue)로 날아가는 로그가 안 찍혔네? 삐용삐용!" 하고 밖에서 거대하게 감시(Spying)하는 인프라 레벨의 행위 검증으로 진화하고 있다.
- 함수형 프로그래밍(Functional Programming)에 의한 소멸: 스파이(Spy)가 필요한 근본적 이유는 객체지향 프로그래밍(OOP)이 '상태를 변경'하고 허공으로 날아가 버리는(Side-effect) 부수 효과를 좋아하기 때문이다. 하지만 하스켈(Haskell)이나 모던 코틀린/스칼라 같은 순수 함수형 패러다임이 대세가 되면서, "모든 함수는 입력값에 대해 부수 효과 없이 투명한 출력값(순수 함수)만을 뱉어야 한다"는 사상이 지배하고 있다. 출력이 투명하면 상태 검증(assertEquals)만 하면 끝난다. 즉, 더러운 행위를 감시해야 하는 스파이(Spy) 객체 자체의 존재 이유가 미래에는 철학적으로 소멸할 수도 있다.
참고 표준
- Mockito의 @Spy 어노테이션: 전 세계 자바 진영에서 "얘는 진짜 놈인데, 몰래 수첩(Call History)만 들려 보낼게"를 단 4글자로 해결해 버린, 현대 단위 테스팅의 가장 위대하고 간결한 마법의 딱지.
- Behavior Verification (행위 검증): 결괏값 5가 맞느냐를 따지는 것이 아니라, "목표를 향해 가는 궤적(로직의 순서와 횟수)이 내가 설계한 대로 정확히 일치하는가?"를 검열하는 소프트웨어 테스팅의 양대 철학 중 하나.
스파이(Spy) 테스트 더블은 소프트웨어 공학이 '결과 지상주의'의 오만함을 버리고, '과정(Process)의 정당성'에 집착하기 시작한 철학적 분기점이다. 객체지향의 세계에서는 단순히 수학 공식처럼 1+1=2가 나오는 것이 끝이 아니다. 수만 명의 트래픽이 오가는 혼돈의 서버 안에서, 그 2라는 결과를 내기 위해 다른 마이크로서비스의 옆구리를 몇 번 찔렀고, 쓸데없이 DB 문을 두 번 세 번 열어재끼며 인프라를 박살 내지는 않았는지, 그 은밀한 비즈니스 로직의 그림자를 완벽하게 추적해 내는 현미경이 필요하다. 기술사는 겉으로 돌아가는 초록불(Pass)에 속지 않고, 스파이를 침투시켜 보이지 않는 시스템의 장기(內臟)들이 내가 지휘한 오케스트라 악보(Architecture)대로 한 치의 오차 없이 호흡하고 있는지 냉혹하게 감청하는 지휘관이 되어야 한다.
- 📢 섹션 요약 비유: 스파이(Spy)는 경찰이 뿌려둔 **'형광 도료 지폐'**와 같습니다. 겉보기엔 진짜 5만 원짜리 지폐(진짜 객체)라 도둑이 훔쳐서 빵도 사 먹고 피자도 사 먹습니다(진짜 기능 수행). 하지만 지폐에는 눈에 보이지 않는 형광 물질(Spy)이 묻어 있어서, 나중에 자외선을 쫙 쏘면(verify 검증) 그 지폐가 오늘 하루 종일 빵집과 피자집(어느 함수)을 거쳐서 여기까지 흘러왔는지 범인의 모든 행적(Behavior)이 백일하에 적나라하게 드러나는 완벽한 잠복 수사입니다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| 스텁 (Stub) | 스파이의 하위 호환 로봇. 스텁은 그냥 "100원을 리턴해"라는 대답만 앵무새처럼 하고 끝이지만, 스파이는 대답도 하면서 뒤로 수첩에 행적을 적는 상위 호환 위장 요원이다. (이전 장 460번) |
| 목 (Mock) | 스파이와 함께 행위 검증(Behavior Verify)을 담당하는 쌍두마차. 단, Mock은 뼈대 자체가 가짜 합판 세트장이라면, 스파이는 진짜 건물에 도청 장치만 살짝 붙여놓은 것. (다음 장 462번) |
| 부수 효과 (Side-Effect) | 스파이가 존재하는 이유의 알파요 오메가. 화면에 출력되지 않고 DB나 이메일 큐(Queue)로 날아가 버려서 눈에 보이지 않는 결과물. 이것을 스파이의 도청을 통해서만 잡아낼 수 있다. |
| 부분 목킹 (Partial Mocking) | 진짜 객체 기능 90%는 살려두고, 딱 10%의 함수만 마취 총을 쏴서 가짜 대답(Stub)을 하게 만드는, 오직 스파이(Spy)만이 할 수 있는 절묘한 수술 기법. |
| 순수 함수 (Pure Function) | "같은 값을 넣으면 무조건 같은 값만 나오고, 밖의 세상(DB, 상태)을 절대 건드리지 않는 함수". 이 함수로만 세상을 짜면 스파이 같은 추잡한 감시자는 영원히 은퇴해도 된다. |
👶 어린이를 위한 3줄 비유 설명
- 심부름을 간 동생이 진짜로 마트에서 '우유'를 사 오긴 했어요(결괏값 확인). 그런데 동생이 오락실에 들렀다가 갔는지 바로 갔는지 너무 궁금해요.
- 그래서 동생 주머니에 아무도 모르게 **'만보계(걸음 수 측정기)'와 '비밀 GPS'**를 살짝 넣어두었어요(진짜 동생 기능은 100% 작동함).
- 동생이 심부름을 마치고 집에 돌아왔을 때, 내가 주머니에서 기계를 쏙 빼서 "너 오락실 정확히 1번 들렀다 왔지!" 하고 족집게처럼 행동을 알아맞히는 가짜 첩보 요원을 **'스파이(Spy)'**라고 한답니다!