실사용 영상
--------------
이전 글에서는 VS Code 확장 프로그램이 현재 풀이 파일과 테스트 파일을 찾고, debugpy를 실행하는 과정까지 살펴봤다.
이번에는 그 뒤에서 실제로 Solution을 실행하는 Python 런타임을 정리해보려고 한다.
처음에는 테스트 파일을 읽고 Solution 메서드를 호출하면 끝날 것 같았다.
하지만 실제 LeetCode 코드를 실행하려면 다음 문제들을 처리해야 했다.
입력 형식이 일정하지 않음 Solution 안에 메서드가 여러 개일 수 있음 ListNode와 TreeNode가 로컬에 없음 타입 어노테이션이 없는 풀이도 있음 반환값 없이 입력을 수정하는 문제도 있음 사용자 코드가 무한 루프에 빠질 수 있음
이 작업은 leetcode_debug_runtime.py에서 처리한다.
테스트 케이스 파싱
테스트 파일은 LeetCode의 입력 형식을 최대한 그대로 사용할 수 있도록 했다.
1Input: nums = [2,7,11,15], target = 9 2Output: [0,1]
처음에는 Input: 뒤의 문자열을 쉼표로 나누면 된다고 생각했다.
하지만 리스트 안에도 쉼표가 들어간다.
nums = [2,7,11,15], target = 9
단순히 split(",")을 사용하면 리스트의 값까지 각각 나뉘어버린다.
그래서 문자열을 한 글자씩 읽으면서 괄호의 깊이와 따옴표 상태를 확인했다.
괄호 바깥의 쉼표 → 매개변수 구분 괄호 안의 쉼표 → 리스트나 딕셔너리 내부 값
여러 줄 입력도 처리한다.
1Input: 2grid = [ 3 ["1","1","0"], 4 ["0","1","0"], 5 ["1","0","1"] 6] 7Output: 3
입력값은 먼저 JSON으로 해석하고, 실패하면 Python의 ast.literal_eval()을 사용한다. LeetCode에서 사용하는 null, true, false도 각각 None, True, False로 변환한다.
여러 테스트 케이스는 ---로 구분한다.
1Input: nums = [2,7,11,15], target = 9 2Output: [0,1] 3--- 4Input: nums = [3,2,4], target = 6 5Output: [1,2]
실행할 메서드 찾기
처음에는 Solution 클래스의 public 메서드가 하나라고 가정했다.
1class Solution: 2 def twoSum(self, nums, target): 3 ...
이 경우에는 twoSum()을 실행하면 된다.
그런데 실제 풀이에서는 보조 메서드를 자주 사용한다.
1class Solution: 2 def dfs(self, node): 3 ... 4 5 def maxDepth(self, root): 6 return self.dfs(root)
이제 dfs()와 maxDepth() 중 무엇이 실제 문제의 진입점인지 찾아야 한다.
현재 런타임은 다음 정보를 이용해 실행할 메서드를 추론한다.
테스트 케이스의 인자 개수 테스트 케이스에 작성된 인자 이름 메서드의 매개변수 다른 메서드에서 호출되는 메서드인지
이를 위해 Python의 inspect로 메서드 시그니처를 확인하고, ast로 self.dfs()와 같은 메서드 호출 관계를 분석한다.
즉 입력 형태와 가장 잘 맞으면서 다른 메서드의 보조 함수가 아닌 메서드를 진입점으로 선택한다.
LeetCode 전용 자료형
LeetCode에서는 다음 코드가 그대로 동작한다.
1class Solution: 2 def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]: 3 ...
하지만 로컬 Python에는 ListNode가 정의되어 있지 않다.
그래서 런타임 안에 LeetCode가 제공하는 대표적인 자료형을 직접 구현했다.
ListNode TreeNode Node NestedInteger Employee Interval Point ArrayReader BinaryMatrix MountainArray
예를 들어 테스트 파일에 다음 입력을 작성하면
Input: head = [1,2,3,4,5]
메서드의 타입 정보를 확인한 뒤 실제 연결 리스트로 변환한다.
[1,2,3,4,5] → ListNode(1) └─ ListNode(2) └─ ListNode(3) └─ ListNode(4) └─ ListNode(5)
메서드 실행이 끝나면 결과를 다시 일반 리스트로 바꾼다.
ListNode(3) → ListNode(4) → ListNode(5) → [3,4,5]
트리나 그래프도 같은 방식으로 입력을 객체로 변환하고, 반환값을 비교 가능한 형태로 직렬화한다.
타입 정보가 없는 코드
모든 풀이에 타입 어노테이션이 남아 있는 것은 아니다.
1class Solution: 2 def maxDepth(self, root): 3 if not root: 4 return 0 5 6 return 1 + max( 7 self.maxDepth(root.left), 8 self.maxDepth(root.right) 9 )
여기서는 root가 TreeNode라는 정보가 직접 작성되어 있지 않다.
그래서 매개변수 이름과 코드에서 사용하는 속성을 이용해 자료형을 추론한다.
root.left, root.right 사용 → TreeNode head.next 사용 → ListNode node.children 사용 → Node
처음에는 타입 어노테이션만 확인했지만, 실제로 코드를 붙여 넣어보니 타입 정보가 제거된 풀이도 많았다. 이후 AST를 이용해 매개변수의 사용 방식을 분석하는 기능을 추가했다.
반환값이 없는 문제
LeetCode에는 값을 반환하지 않고 입력을 직접 수정하는 문제가 있다.
1class Solution: 2 def rotate(self, matrix: list[list[int]]) -> None: 3 ...
이 메서드의 반환값은 None이다.
반환값만 정답과 비교하면 실제로 matrix가 올바르게 변경되어도 결과는 항상 null이 된다.
그래서 반환값이 None이면 변경된 입력을 결과로 사용한다.
반환값이 있음 → 반환값 비교 반환값이 None → 변경된 board, matrix, nums 등의 입력 비교
이를 통해 rotate(matrix), solve(board), merge(nums1, m, nums2, n) 같은 문제도 실행할 수 있게 되었다.
무한 루프 처리
디버깅 중인 코드가 무한 루프에 빠지는 경우도 있다.
while True: ...
한 테스트가 멈추면 뒤의 테스트도 실행할 수 없기 때문에 각 테스트 케이스에 기본 5초 제한을 추가했다.
Case 1: INFINITE LOOP stopped after 5s of CPU time
제한 시간은 LEETCODE_DEBUG_TIMEOUT_SECONDS 환경 변수로 변경할 수 있고, 0을 설정하면 제한을 끌 수 있다.
결과 비교
메서드 실행이 끝나면 실제 결과를 테스트 파일의 Output:과 비교한다.
Case 1: PASS expected: [7,0,8] actual: [7,0,8]
결과가 다르면 FAIL로 표시한다.
Case 1: FAIL expected: [7,0,8] actual: [8,0,7]
Output:을 생략하면 정답을 비교하지 않고 실제 실행 결과만 출력한다.
만들면서 추가된 기능
프로젝트는 처음부터 현재 구조였던 것은 아니다.
초기 버전은 현재 Python 파일과 같은 이름의 테스트 파일을 읽고 Solution 메서드를 실행하는 정도였다.
실제 문제를 실행하면서 부족한 기능을 하나씩 추가했다.
초기 버전 - 테스트 파일 읽기 - Solution 메서드 실행 - LeetCode 자료형 변환 이후 개선 - 보조 메서드가 있는 Solution 처리 - 타입이 없는 ListNode와 TreeNode 추론 - 여러 줄 입력 지원 - Python 버전 검사 - 무한 루프 제한 - 입력을 직접 수정하는 문제 지원 - F5 디버깅 지원
실제로 실패했던 코드 형태는 test_leetcode_debug_runtime.py의 테스트로 추가했다.
결론
처음에는 LeetCode 코드를 로컬에서 실행하는 일이 단순히 입력값을 넣고 메서드를 호출하는 정도라고 생각했다.
하지만 실제로는 LeetCode가 뒤에서 제공하던 실행 환경을 일부 다시 만들어야 했다.
테스트 케이스 파싱 → Solution 메서드 추론 → 입력 자료형 변환 → 사용자 코드 실행 → 반환값 직렬화 → 예상 결과와 비교
결국 이 프로젝트는 VS Code 확장 프로그램과 작은 LeetCode 호환 런타임을 결합한 구조다.
VS Code의 debugpy가 중단점과 변수 확인을 담당하고, Python 런타임은 LeetCode 코드를 일반적인 Python 프로그램처럼 실행할 수 있도록 준비한다.
모든 LeetCode 문제를 완전히 재현하는 것은 아니다. isBadVersion, knows, read4처럼 LeetCode가 숨겨서 제공하는 전역 API는 아직 자동으로 구현하지 않는다.
하지만 일반적인 배열, 문자열, 연결 리스트, 트리 문제라면 다음 과정만으로 VS Code에서 디버깅할 수 있다.
풀이 파일 작성 → 같은 이름의 테스트 파일 작성 → 중단점 설정 → F5
처음에는 print()를 조금 덜 사용하기 위해 시작했지만, 만들고 나니 VS Code 확장 프로그램과 Python 디버거가 연결되는 방식, 동적 코드 실행, AST 분석과 타입 변환까지 함께 공부할 수 있었던 프로젝트였다.