안녕하세요! 그로스로그 입니다!😊 🌱
외부 API를 끊어내고 코드만 검증하는 법, 틀린 문제 옆에 이유를 붙여둔 오답노트, 그리고 한 학기를 반성과 성장으로 나눠 적은 회고. 3기 5회차 세 편입니다.
3기 5회차, 2025년 5월 하순입니다. 기말을 앞둔 시기였어요. 이번 회차에는 제대로 됐는지 확인하는 방법을 다룬 세 편이 모였습니다.
01
🧪 Flutter에서 의존성 주입과 Mockito로 유닛 테스트 작성하기
박상훈 님
Flutter에서 외부 API를 호출하는 코드를 테스트하는 방법을 정리한 글입니다.
문제 제기가 구체적이에요. "외부 API 호출(http)이 있는 경우 테스트가 쉽지 않은 경험이 있어서"요.
해법은 두 단계입니다. API를 인터페이스로 추상화해 주입하고, Mockito로 가짜 구현을 끼워 넣는 것이요.
의존성 추가부터 코드 생성 명령까지, 따라 할 수 있게 순서대로 적었습니다.
조금 더 들어가 보면
🚧 왜 테스트가 어려운가
시작이 문제 인식입니다.
하지만 외부 API 호출(http)이 있는 경우 테스트가 쉽지 않은 경험이 있어서...
왜 어려운지를 풀어보면요.
테스트는 같은 입력에 항상 같은 결과가 나와야 합니다. 그런데 진짜 서버를 부르면 그게 깨져요. 서버가 내려가 있으면 실패하고, 인터넷이 없으면 실패하고, 서버 데이터가 바뀌면 또 결과가 달라집니다.
내 코드는 멀쩡한데 테스트가 실패하는 상황이죠. 그러면 테스트를 믿을 수 없게 되고, 결국 안 돌리게 됩니다.
그리고 느립니다. API 하나에 0.5초씩만 걸려도 테스트 백 개면 1분이에요. 코드를 고칠 때마다 1분을 기다리면 아무도 안 돌립니다.
🔌 사이에 인터페이스를 하나 두기
해법의 첫 단계가 추상화입니다.
abstract class IRestApi {
Future doPostWithUser(String apiId, User user, dynamic body);
}
그리고 진짜 구현을 따로 만듭니다.
class RestApiWrapper implements IRestApi {
final http.Client client;
RestApiWrapper(this.client);
@override
Future doPostWithUser(String apiId, User user, body) async {
final response = await client.post(Uri.parse('https://example.com/$apiId'), ...);
// ...
}
}
여기서 중요한 건 서비스 쪽입니다.
class TestService {
final IRestApi api;
TestService(this.api); // 밖에서 넣어준다
Future sendData(User user, dynamic body) {
return api.doPostWithUser('homeappliance', user, body);
}
}
TestService가 RestApiWrapper를 직접 만들지 않습니다. 생성자로 받아요. 이게 의존성 주입입니다.
차이가 뭘까요. TestService 안에서 RestApiWrapper()를 만들었다면, 밖에서 바꿔 끼울 방법이 없습니다. 항상 진짜 서버를 부르게 되죠.
생성자로 받으면 무엇을 넣을지 부르는 쪽이 정합니다. 실제 앱에서는 진짜를 넣고, 테스트에서는 가짜를 넣는 거예요.
그리고 타입이 IRestApi라는 것. 인터페이스만 알고 구현은 모릅니다. IRestApi를 따르기만 하면 뭐든 들어갈 수 있어요.
🤖 가짜를 만들어주는 도구
두 번째 단계가 Mockito입니다. 가짜 구현을 손으로 짜지 않고 자동으로 만들게 하는 것이죠.
@GenerateMocks([IRestApi])
void main() {}
이렇게 적어두고 명령 하나를 실행합니다.
flutter pub run build_runner build
어노테이션을 보고 코드를 생성하는 방식입니다. MockIRestApi 클래스가 자동으로 만들어져요.
이게 왜 필요할까요. 인터페이스에 메서드가 열 개면 가짜도 열 개를 다 구현해야 합니다. 테스트에서 쓰는 건 하나뿐인데도요. 그걸 손으로 쓰면 인터페이스가 바뀔 때마다 또 고쳐야 하고요.
생성기가 만들어주면 인터페이스만 고치고 명령을 다시 돌리면 끝입니다.
✅ 세 줄로 확인한다
실제 테스트 코드에서 핵심이 세 줄입니다.
when(mockApi.doPostWithUser('homeappliance', user, body))
.thenAnswer((_) async => mockResponse); // ① 이렇게 부르면 이걸 돌려줘라
final result = await service.sendData(user, body);
expect(result, mockResponse); // ② 결과가 맞는가
verify(mockApi.doPostWithUser('homeappliance', user, body)).called(1); // ③ 정확히 한 번 불렀는가
세 줄이 각각 다른 걸 확인합니다.
when은 준비입니다. 가짜 API가 어떻게 반응할지 미리 정해두는 것요. 서버 없이도 원하는 응답을 만들 수 있죠.
expect는 결과 확인입니다. 서비스가 돌려준 값이 기대한 것과 같은지요.
verify는 호출 확인입니다. 그리고 이게 특히 유용해요. .called(1)은 정확히 한 번 불렀는지를 봅니다.
왜 이게 필요할까요. 결과가 맞아도 API를 두 번 불렀다면 문제입니다. 서버 부담도 있고, 결제처럼 두 번 부르면 안 되는 일도 있으니까요. expect만으로는 그걸 못 잡습니다.
글도 그 구조를 짚었습니다.
expect를 통해 예상하는 결과값을 인자로 넣어서 테스트 성공 여부 판단하는 구조.그리고 setUp 블록에서 매번 새 mock을 만드는 것도 중요합니다.
setUp(() {
mockApi = MockIRestApi();
service = TestService(mockApi);
});
테스트마다 새로 만들어야 앞 테스트의 호출 기록이 안 남습니다. verify(...).called(1)이 정확히 동작하려면 필요한 준비예요.
📝 원문에서는 이렇게 씁니다
글의 순서가 의존성 추가 → 인터페이스 → 구현 → 서비스 → mock 생성 → 테스트입니다.
그리고 각 코드 블록마다 파일 경로를 적었어요. "test/mock_api.dart (경로)"처럼요. Mockito는 파일 위치와 생성되는 파일 이름(mock_api.mocks.dart)이 연결되어 있어서, 이게 없으면 따라 하다 막힙니다.
pubspec.yaml의 버전까지 박아뒀습니다.
dev_dependencies: mockito: ^5.4.2 build_runner: ^2.4.6
dev_dependencies에 넣은 것도 정확합니다. 테스트 도구는 앱에 포함될 필요가 없으니까요.
큐레이터 노트
테스트를 위해 구조를 바꾼 기록이라 골랐습니다.
유닛 테스트를 배울 때 흔히 겪는 벽이 있습니다. 문법은 알겠는데 내 코드에는 못 쓰겠는 상황이요. 함수 안에서 직접 서버를 부르고, 직접 DB를 열고, 직접 파일을 읽으면 테스트할 방법이 없거든요.
이 글은 그 지점을 정면으로 다룹니다. 테스트를 쓰려면 먼저 코드를 테스트 가능한 모양으로 바꿔야 한다는 것요. 인터페이스를 하나 두고, 밖에서 넣어주게 하는 것이 그 방법입니다.
그리고 그렇게 바꾸면 테스트만 좋아지는 게 아닙니다. 구현을 갈아끼울 수 있는 구조가 되니까요. 6회차 큐레이션에서 소개해드릴 이야기와도 이어지는 부분입니다.
verify(...).called(1)까지 쓴 것도 눈에 띕니다. 결과만 보는 게 아니라 몇 번 불렀는지까지 확인하는 것. 테스트를 한 단계 더 쓰는 방식입니다.
02
📝 파이썬 스터디 후 오답노트 2
윤인희 님
파이썬 스터디에서 틀린 문제를 정리한 오답노트입니다. 두 번째 편이에요.
형식이 아주 단순합니다. 문제 → 답 →
----> 뒤에 왜 그런지요.그런데 그 화살표 뒤가 이 노트의 전부입니다. 답만 적은 게 아니라 헷갈린 지점을 정확히 짚었어요.
"헷갈리지 않기!" 같은 자기 다짐도 그대로 적혀 있습니다.
조금 더 들어가 보면
↔️ 비슷한 둘을 갈라놓기
첫 항목부터 대비 구조입니다.
틀린 이유가 "둘을 헷갈려서"라는 걸 알고, 둘을 나란히 적었습니다.
오답노트에서 이게 중요합니다. 답만 적으면 "코랩"만 남는데, 다음에 또 주피터와 헷갈리거든요. 둘을 붙여 적어야 구분이 됩니다.
random.randint 항목도 같은 방식입니다.
"사이"와 "또는"의 차이를 짚었어요.
이건 실제로 자주 틀리는 자리입니다. random.random()은 0과 1 사이의 실수를 주고, random.randint(0,1)은 0 아니면 1을 줍니다. 이름이 비슷한데 결과가 완전히 달라요.
그리고 randint는 양쪽 끝을 다 포함합니다. 대부분의 언어에서 범위 함수는 끝을 안 포함하는데, 파이썬의 randint는 포함해요. range(0,2)와 randint(0,1)이 같은 결과를 내는 이유입니다.
📦 담는 관계를 정리하기
두 번째 항목이 이 노트에서 가장 실용적입니다.
프레임워크는 약간 다른 종류..
세 단계를 한 줄로 이어 적었습니다. 모듈 → 패키지 → 라이브러리요.
이 셋은 일상적으로 섞어 쓰는 말이라 구분이 흐려집니다. 그런데 담는 크기 순으로 늘어놓으면 관계가 한눈에 들어와요.
그리고 뒤에 붙인 한 줄이 좋습니다.
크기 순서에 안 들어간다는 걸 알아챈 겁니다. 프레임워크는 라이브러리보다 큰 게 아니라 성격이 다르거든요. 라이브러리는 내가 부르는 것이고, 프레임워크는 나를 부르는 것입니다.
"약간 다른 종류"라고만 적고 넘어간 것도 정직합니다. 정확히 뭐가 다른지는 아직 모르지만 같은 줄에 두면 안 된다는 것까지는 안 거니까요.
딕셔너리 항목도 담는 관계입니다.
답: 2. 항목(item)
키와 값을 묶은 한 쌍이 항목이라는 것. 선택지 넷을 다 적어두고 답을 골랐어요. 오답 선택지가 뭐였는지도 남습니다.
🔢 계산해서 답을 남기기
문자열 정렬 문제 풀이가 짧지만 명확합니다.
----> 20칸 오른쪽으로 정렬, 3.141 5자리, 앞이니까 앞 15개 공백.
계산 과정을 적었습니다. 20에서 5를 빼서 15요.
답만 "15"라고 적어두면 다음에 숫자가 바뀌었을 때 못 풉니다. "전체 폭 빼기 내용 길이"라는 방법이 남아야 하죠.
3.141이 5자리라는 것도 짚어뒀습니다. 소수점도 한 칸을 차지하니까요. 이런 문제에서 실수가 나오는 자리가 대개 여기입니다.
마지막 O/X 문항도 짧게 갈랐습니다.
우리말 용어와 영어 용어를 붙여둔 것입니다. 시험에서는 우리말로 나오고 코드에서는 영어로 쓰니, 둘을 같이 외워야 하거든요.
📝 원문에서는 이렇게 씁니다
이 노트에는 설명하는 문장이 없습니다. 전부 ----> 뒤의 한두 줄이에요.
그런데 그 한두 줄이 답이 아니라 이유입니다. "코랩"이 아니라 "클라우드 기반은 코랩, 웹 기반은 주피터"고, "15개"가 아니라 "20칸에서 5자리 빼면 15"입니다.
틀린 문제를 다시 보는 이유가 답을 외우려는 게 아니라는 걸 알고 쓴 노트예요.
그리고 제목이 「오답노트2」입니다. 앞선 편이 있고, 스터디가 이어지는 동안 계속 쌓아가고 있다는 뜻이죠.
큐레이터 노트
틀린 것만 모은 기록이라 골랐습니다.
공부 기록에는 배운 것을 정리한 노트가 많습니다. 그런데 틀린 것만 모은 노트는 성격이 다릅니다. 분량은 적은데 밀도가 높아요. 이미 아는 건 안 적혀 있으니까요.
그리고 여기 적힌 것들이 다음에도 틀릴 것들입니다. 헷갈리는 데는 이유가 있거든요. 이름이 비슷하거나, 담는 관계가 애매하거나, 한 칸 차이로 답이 갈리거나요.
그래서 오답노트는 시간이 지날수록 값어치가 올라갑니다. 내 약한 자리의 목록이니까요.
무엇보다 "헷갈리지 않기!"라는 한 줄. 남에게 하는 말이 아니라 자기한테 하는 말입니다. 이런 문장이 남아 있는 노트가 나중에 더 잘 읽힙니다.
03
🎓 방통대 컴퓨터과학과 첫 학기 회고
조민재 님
첫 학기를 마치고 쓴 회고입니다.
앞부분은 과목별 정리예요. 여섯 과목 각각을 강의 · 중간 평가 · 기말 시험 세 항목으로 나눠 적었습니다.
뒷부분이 이 글의 중심입니다. 「반성하는 점」과 「성장한 점」으로 나뉘어 있어요.
그리고 반성 쪽이 성적 이야기가 아닙니다.
3기 1·2회차에서도 이분의 알고리즘 정리를 소개해드렸습니다. 빅오와 빈도수 세기 패턴이요.
조금 더 들어가 보면
📐 세 칸으로 나눠 적기
과목별 정리의 형식이 일정합니다. 강의 · 중간 평가 · 기말 시험이요.
이 세 칸이 좋은 이유가 있습니다. 각각 대비 방법이 다르거든요.
기말 시험 : (…) 강의 연습문제, 교재 연습문제, 워크북 연습문제, 기출문제 위주로 숙지하면 만점은 아니더라도 높은 점수 받을 수 있음.
"의외로 분량 채우는 것이 까다로웠음" 같은 정보는 시간 배분에 바로 쓰입니다. 내용이 어려운 게 아니라 쓰는 데 시간이 걸린다는 뜻이니까요.
그리고 과목마다 무엇까지 봐야 하는지를 적었습니다. 「연습문제·워크북·기출로 충분」과 「강의와 교재 전체가 범위」가 갈려요.
알고리즘 — 강의 및 교재 전체가 시험 범위라서 공부할 양이 엄청남. 개념과 풀이 문제가 섞여 나오기 때문에
"벼락치기가 되는 과목"과 "안 되는 과목"을 구분한 것이 이 회고에서 가장 실용적입니다. 다음 학기 시간표를 짤 때 바로 쓰이는 정보예요.
⚠️ 예상이 빗나간 과목
한 과목에서는 준비가 부족했다고 적었습니다.
기출 중심 공부의 한계를 직접 겪은 기록입니다.
기출 위주는 효율이 좋은 방법이지만 출제 경향이 바뀌면 무너집니다. 그걸 시험장에서 알게 된 거예요.
그리고 대비책도 함께 적었습니다. "SQL 문법은 특히 잘 숙지하고 있어야 함"이라고요. 어느 부분이 약했는지까지 남긴 겁니다.
운영체제 쪽에는 이런 문장도 있습니다.
시간이 없었다면 개념 이해는 포기하고 문제만 암기했을 듯.
"틀어막음"과 "포기하고 암기했을 듯". 이상적인 방법이 아니라 실제로 한 것을 적었어요.
🪞 성적이 아니라 방식을 반성하기
「반성하는 점」이 이 회고의 중심입니다.
그리고 다음 학기 계획이 구체적입니다.
"열심히 하겠다"가 아니라 "강의 하나마다 이 네 가지를 한다"입니다. 단위와 항목이 정해져 있으니 실제로 지킬 수 있는지 확인도 되고요.
그런데 진짜 반성은 그다음에 나옵니다.
입학한 이상 좋은 점수를 받는 것도 중요하지만, 개발자로서 필요하기에 입학했다는 사실을 잊지 말자.
시험을 잘 못 봐서가 아니라, 시험만 보게 돼서 아쉽다는 것입니다.
방향이 반대입니다. 대개는 "더 잘 봤어야 했는데"로 가는데, 여기서는 "점수 말고 원래 목적"을 다시 꺼냈어요.
💪 공부하는 일 자체가 남긴 것
「성장한 점」의 시작이 이렇습니다.
내용 말고 행위요.
생전 처음 보는 개념들, 당최 무슨 말인지 하나도 이해가 가지 않는 것들을 붙잡고 꾸역꾸역 공부했다.
그리고 결론이 뜻밖입니다.
"놀랍게도"와 "오히려"가 붙어 있습니다. 예상과 반대였다는 뜻이에요.
마지막 문단이 그걸 정리합니다.
"당연한 논리를 체감하기는 쉽지 않다"요. 말로는 다 아는 것인데, 실제로 겪어야 아는 것이 있다는 이야기입니다.
📝 원문에서는 이렇게 씁니다
이 회고의 구조가 정보와 소회를 분리했습니다.
앞부분은 다음 사람에게 주는 정보입니다. 문체도 "~함", "~음"으로 짧게 끊어져 있어요. 뒷부분은 자기 이야기고, 문체가 "~했다"로 바뀝니다.
두 부분이 섞이지 않게 나눠둔 것이 읽기 좋습니다. 과목 정보만 필요한 사람은 앞만 읽으면 되니까요.
그리고 「반성하는 점」과 「성장한 점」을 제목으로 못박아둔 것. 회고를 쓰다 보면 반성 쪽으로만 기울기 쉬운데, 칸이 두 개면 좋았던 것도 찾아 적게 됩니다.
큐레이터 노트
반성의 방향이 다른 회고라 골랐습니다.
첫 학기 회고는 대개 성적과 난이도 이야기로 채워집니다. 이 글도 앞부분은 그렇고요. 그런데 뒤에서 "시험 준비에만 급급하게 되었다"로 갑니다.
이건 성적이 나빴다는 뜻이 아닙니다. 원래 하려던 것과 실제로 한 것이 달랐다는 반성이에요. 개발자로서 필요해서 입학했는데 문제 풀이만 했다는 것요.
그리고 「성장한 점」에 적은 자기 효능감 이야기. 공부 내용이 아니라 꾸역꾸역 해낸 경험 자체를 성과로 적었습니다.
1·2회차 큐레이션에서 소개해드린 이분의 알고리즘 글들이 효율을 따지는 글이었는데, 이 회고는 반대로 효율이 안 나오는 시간을 견딘 이야기입니다. 같은 사람의 기록에 두 결이 다 있는 게 좋았어요.
💡 세 편을 겹쳐 읽으며
세 편의 자리가 다릅니다. 앱 코드, 파이썬 문제, 한 학기요.
그런데 셋 다 맞는지 확인하는 장치를 만들었습니다.
확인하려면 끊어내야 한다
박상훈 님의 테스트가 가능해진 건 API를 끊어냈기 때문입니다. 서버가 붙어 있으면 실패해도 내 코드 탓인지 서버 탓인지 알 수 없거든요. 가짜를 끼워 넣어야 코드만 남습니다.
윤인희 님의 오답노트도 같은 구조입니다. 맞힌 문제를 다 빼고 틀린 것만 남겼어요. 그래야 약한 자리가 보입니다.
조민재 님의 회고도 그렇습니다. 「반성하는 점」과 「성장한 점」으로 칸을 나눴어요. 섞어놓으면 어느 쪽도 안 보이니까요.
확인하려면 먼저 나머지를 치워야 한다는 게 셋에서 공통으로 보입니다.
답보다 이유를 남긴다
기록 방식에서도 닮았습니다.
윤인희 님은 답만 적지 않고 ----> 뒤에 이유를 붙였습니다. "15개"가 아니라 "20칸에서 5자리를 빼서 15"요.
조민재 님은 "어려웠다"가 아니라 왜 어려웠는지를 적었습니다. 범위가 전체라서, 개념과 풀이가 섞여 나와서, 기출에 없던 스타일이 있어서요.
박상훈 님은 expect만 쓰지 않고 verify(...).called(1)을 붙였습니다. 결과만이 아니라 과정도 확인한 거예요.
답만 있으면 다음에 못 쓰고, 이유가 있으면 상황이 달라져도 쓸 수 있습니다.
예상이 빗나가는 자리
세 편 모두 예상과 달랐던 지점을 적었습니다.
박상훈 님은 "외부 API 호출이 있는 경우 테스트가 쉽지 않은 경험"에서 출발했습니다. 테스트를 쓰려다 막힌 경험이요.
조민재 님은 기출을 다 봤는데 "기출에 없었던 스타일의 문제"를 만났고, 고통스러울 줄 알았던 공부에서 "오히려 정신적으로 건강해짐"을 느꼈습니다.
윤인희 님의 오답노트 자체가 예상이 빗나간 목록이고요.
빗나간 자리를 적어두는 게 확인의 마지막 단계입니다. 맞은 것만 기록하면 다음에 같은 데서 또 빗나가니까요.
이번 회차에서 가져갈 것
1. 테스트가 안 되면 구조부터 보기.
함수 안에서 직접 서버를 부르면 테스트할 방법이 없습니다. 인터페이스로 빼고 밖에서 넣어주면 가짜를 끼울 수 있습니다.
2. verify(...).called(1)까지 쓰기.
결과가 맞아도 두 번 불렀으면 문제일 수 있습니다. 결제나 알림처럼 중복이 치명적인 곳에서는 호출 횟수도 확인하세요.
3. 오답노트에는 답 말고 갈림길을 적기.
"코랩"이 아니라 "클라우드 기반은 코랩, 웹 기반은 주피터"요. 헷갈린 상대를 함께 적어야 다시 안 헷갈립니다.
4. 과목을 벼락치기 가능/불가능으로 나눠두기.
범위가 연습문제와 기출로 좁혀지는 과목과 교재 전체인 과목은 준비 시작 시점이 다릅니다.
5. 회고를 두 칸으로 나눠 쓰기.
반성만 적으면 기록이 어두워지고, 성과만 적으면 다음에 쓸 게 없습니다. 칸이 두 개면 양쪽을 다 찾게 됩니다.
3기 5회차는 2025년 5월 하순이었습니다. 기말을 앞두고 각자 마무리를 준비하던 때예요.
Flutter 테스트 코드와 파이썬 오답노트와 첫 학기 회고라는 서로 다른 자리에서, 세 분이 나란히 확인하는 장치를 만들었습니다.
성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 지난 기수의 성장일지를 다시 읽으면서, 그때 소개해드리지 못한 글을 하나씩 꺼내고 있습니다.
좋은 글 남겨주신 박상훈 님, 윤인희 님, 조민재 님께 감사드립니다. 그리고 3기를 함께 채워주신 모든 멤버분들께도요! 😊
다음 큐레이션으로 다시 찾아오겠습니다! 🌱
- 에디터 · 성장일지 큐레이터 -
※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.