구현과 시뮬레이션 — 코드가 길어질 때 실수를 줄이는 법
알고리즘은 쉬운데 코드가 100줄이 넘어가면서 버그가 생깁니다. 어떻게 하면 길고 복잡한 구현을 깔끔하게 작성할 수 있을까요?
구현/시뮬레이션 문제란
구현 문제 는 특별한 알고리즘 지식보다 정확한 코드 작성 능력 을 요구합니다. 조건이 많고 코드가 길어지면서 실수하기 쉬운 것이 특징입니다.
시뮬레이션 문제 는 주어진 규칙대로 상태를 단계별로 변화시키는 문제입니다. 게임 진행, 로봇 이동, 물리 시뮬레이션 등이 여기 속합니다.
왜 어려운가
- 알고리즘 자체는 단순하지만 조건 분기가 많습니다
- 코드가 길어지면서 ** 오타, 인덱스 오류 **가 발생합니다
- 경계 조건(가장자리, 빈 입력)을 놓치기 쉽습니다
- 디버깅이 어렵습니다 — 어디서 잘못됐는지 찾기 힘듭니다
좌표계 통일
2차원 격자 문제에서 가장 먼저 할 일은 좌표계를 통일하는 것입니다.
// 배열 기준 좌표계 (가장 일반적)
// grid[row][col] = grid[r][c]
// r이 증가하면 아래로, c가 증가하면 오른쪽으로
// 4방향 이동 배열 (상, 하, 좌, 우)
int[] dr = {-1, 1, 0, 0};
int[] dc = {0, 0, -1, 1};
// 8방향 이동 배열 (상, 하, 좌, 우 + 대각선 4개)
int[] dr8 = {-1, -1, -1, 0, 0, 1, 1, 1};
int[] dc8 = {-1, 0, 1, -1, 1, -1, 0, 1};
방향과 회전
// 방향: 0=북, 1=동, 2=남, 3=서
int[] dr = {-1, 0, 1, 0}; // 북, 동, 남, 서 순서
int[] dc = {0, 1, 0, -1};
// 오른쪽 회전: dir = (dir + 1) % 4
// 왼쪽 회전: dir = (dir + 3) % 4 (= dir - 1 + 4를 mod 4)
// 반대 방향: dir = (dir + 2) % 4
int direction = 0; // 북쪽 시작
// 오른쪽으로 90도 회전
direction = (direction + 1) % 4;
// 현재 방향으로 한 칸 이동
int nr = r + dr[direction];
int nc = c + dc[direction];
경계 조건 처리
// 범위 체크 — 함수로 분리
private boolean inBounds(int r, int c, int rows, int cols) {
return r >= 0 && r < rows && c >= 0 && c < cols;
}
// 사용
int nr = r + dr[d];
int nc = c + dc[d];
if (inBounds(nr, nc, rows, cols) && grid[nr][nc] != WALL) {
// 이동 가능
}
패딩(padding) 기법
경계 체크를 줄이기 위해 배열 주위에 벽을 둘러싸는 기법입니다.
// 원래 크기가 N×M이면, (N+2)×(M+2)로 만들고 테두리를 벽으로
int[][] grid = new int[N + 2][M + 2];
// 테두리를 벽으로 초기화
for (int i = 0; i < N + 2; i++) {
grid[i][0] = grid[i][M + 1] = WALL;
}
for (int j = 0; j < M + 2; j++) {
grid[0][j] = grid[N + 1][j] = WALL;
}
// 실제 데이터는 (1, 1) ~ (N, M)에 저장
// → inBounds 체크 없이 바로 grid[nr][nc] != WALL만 확인하면 됨
현재 상태와 다음 상태 분리
시뮬레이션에서 가장 흔한 실수는 현재 턴의 변경이 같은 턴의 다른 계산에 영향을 주는 것입니다.
// 나쁜 예 — 같은 배열을 즉시 수정
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
// grid[r][c]를 보고 결정하는데, 앞의 셀이 이미 변경됨!
grid[r][c] = computeNext(grid, r, c);
}
}
// 좋은 예 — 별도 배열에 기록 후 한꺼번에 반영
int[][] next = new int[rows][cols];
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
next[r][c] = computeNext(grid, r, c);
}
}
// 한꺼번에 복사
for (int r = 0; r < rows; r++) {
System.arraycopy(next[r], 0, grid[r], 0, cols);
}
코드 모듈화 전략
반복되는 로직을 함수로
// 나쁜 예 — 같은 코드가 여러 곳에 복붙
if (direction == 0) {
nr = r - 1; nc = c;
if (nr >= 0 && grid[nr][nc] != 1) { r = nr; }
} else if (direction == 1) {
nr = r; nc = c + 1;
if (nc < cols && grid[nr][nc] != 1) { c = nc; }
}
// ... 반복
// 좋은 예 — 방향 배열 + 함수
private int[] move(int r, int c, int dir, int[][] grid) {
int nr = r + dr[dir];
int nc = c + dc[dir];
if (inBounds(nr, nc, grid.length, grid[0].length) && grid[nr][nc] != WALL) {
return new int[]{nr, nc};
}
return new int[]{r, c}; // 이동 불가 시 제자리
}
단계별 분리
// 시뮬레이션의 각 턴을 단계별로 분리
public void simulate() {
for (int turn = 0; turn < maxTurns; turn++) {
moveObjects(); // 1단계: 이동
checkCollisions(); // 2단계: 충돌 검사
applyEffects(); // 3단계: 효과 적용
removeDestroyed(); // 4단계: 제거
}
}
상태 클래스 활용
// 여러 값을 묶어서 관리
class Robot {
int r, c, dir;
boolean alive;
Robot(int r, int c, int dir) {
this.r = r; this.c = c; this.dir = dir;
this.alive = true;
}
void turnRight() { dir = (dir + 1) % 4; }
void turnLeft() { dir = (dir + 3) % 4; }
void moveForward(int[][] grid) {
int nr = r + dr[dir];
int nc = c + dc[dir];
if (inBounds(nr, nc, grid.length, grid[0].length)) {
r = nr; c = nc;
}
}
}
디버깅 전략
격자 상태 출력
private void printGrid(int[][] grid, int robotR, int robotC) {
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (r == robotR && c == robotC) {
System.out.print("R ");
} else {
System.out.print(grid[r][c] + " ");
}
}
System.out.println();
}
System.out.println("---");
}
작은 예제부터 확인
// 큰 입력을 바로 테스트하지 말고
// 2×2, 3×3 같은 작은 격자에서 먼저 확인
int[][] smallGrid = {
{0, 0, 0},
{0, 1, 0},
{0, 0, 0}
};
// 예상 결과와 비교
자주 나오는 시뮬레이션 패턴
뱀 게임
// 뱀의 몸체를 Deque로 관리
Deque<int[]> snake = new ArrayDeque<>();
snake.offerFirst(new int[]{0, 0}); // 머리
Set<String> body = new HashSet<>();
body.add("0,0");
// 이동: 머리를 추가하고, 사과가 없으면 꼬리 제거
int[] head = {snake.peekFirst()[0] + dr[dir], snake.peekFirst()[1] + dc[dir]};
snake.offerFirst(head);
body.add(head[0] + "," + head[1]);
if (!hasApple(head)) {
int[] tail = snake.pollLast();
body.remove(tail[0] + "," + tail[1]);
}
톱니바퀴
// 톱니바퀴 회전 — 인접 톱니바퀴와 맞물림 체크
// 한 바퀴 회전 시 인접 톱니바퀴의 연쇄 회전 처리
void rotate(int[][] gears, int idx, int dir) {
int[] rotations = new int[4];
rotations[idx] = dir;
// 왼쪽 전파
for (int i = idx - 1; i >= 0; i--) {
if (gears[i][2] != gears[i + 1][6]) { // 맞닿은 극이 다르면
rotations[i] = -rotations[i + 1]; // 반대 방향 회전
} else break;
}
// 오른쪽 전파
for (int i = idx + 1; i < 4; i++) {
if (gears[i][6] != gears[i - 1][2]) {
rotations[i] = -rotations[i - 1];
} else break;
}
// 실제 회전 적용
for (int i = 0; i < 4; i++) {
if (rotations[i] != 0) applyRotation(gears[i], rotations[i]);
}
}
체크리스트
코드 제출 전에 확인해야 할 사항입니다.
□ 좌표계가 일관되는가? (행/열, x/y 혼동 없는지)
□ 경계 조건 — 0, N-1, 빈 배열을 처리하는가?
□ 방향 회전 — (dir + 1) % 4가 맞는지, 방향 배열과 일치하는지?
□ 현재/다음 상태 — 같은 턴의 변경이 서로 영향을 주지 않는지?
□ 오프 바이 원 — for문의 시작/끝이 정확한지?
□ 자료형 오버플로 — int 범위를 넘지 않는지?
주의할 점
문제를 읽자마자 코딩을 시작하는 실수
구현 문제는 설계 없이 바로 코딩하면 중간에 구조가 꼬여 전체를 다시 작성하게 됩니다. 입출력 형식, 상태 표현, 이동/변환 규칙을 먼저 정리한 뒤 코딩을 시작해야 합니다.
방향 배열(dx, dy)에서 인덱스 실수
4방향/8방향 이동에서 dx, dy 배열의 순서를 잘못 설정하면 대각선으로 가야 할 곳을 직진합니다. 디버깅이 어려우므로 항상 테스트 케이스로 검증해야 합니다.
정리
- ** 좌표계를 통일 **하고, 방향 배열
dr/dc를 사용하여 이동을 간결하게 처리합니다 - ** 경계 체크를 함수로** 분리하거나, 패딩 기법으로 제거합니다
- ** 현재 상태와 다음 상태를 분리 **하여, 같은 턴의 변경이 간섭하지 않게 합니다
- 반복되는 로직은 ** 함수로 분리 하고, 상태는 ** 클래스로 묶어 관리합니다
- ** 작은 예제 **부터 격자 상태를 출력하며 단계별로 검증합니다
- 제출 전 좌표, 경계, 방향, 오프바이원, 오버플로를 반드시 점검합니다
댓글 로딩 중...