클로드 데스크톱으로 바이브 코딩을 시작한 지 4개월이 됐다.
요즘 들어 바이브 코딩을 제대로 공부해야겠다는 필요성을 점점 느끼고 있다. 써보면서 느낀 건데, 기능 구현을 시키면 비효율적인 코드를, 성능을 신경써달라고 하면 오버엔지니어링 코드를 뽑아낸다.
그 예시로 들어보면,
파일 삭제 기능을 구현할 때, DB 소프트 딜리트를 먼저 하고 NAS 파일을 삭제하는 순서가 필요했다.
@Transactional
@Override
public void deleteFile(Long fileSn) {
Map<String, Object> fileMeta = fileMapper.selectFileMeta(fileSn);
if (fileMeta == null) return;
Path fullPath = Paths.get(
basePath,
fileMeta.get("FILE_PATH").toString(),
fileMeta.get("STRG_FILE_NM").toString()
);
try {
fileMapper.updateFileMetaDelYn(fileSn);
} catch (Exception e) {
log.error("DB 삭제 실패 - FILE_SN: {}, 원인: {}", fileSn, e.getMessage(), e);
throw e;
}
// Step 2. 실제 파일 삭제 (재시도 최대 3회)
for (int attempt = 1; attempt <= FILE_DELETE_MAX_RETRY; attempt++) {
try {
Files.deleteIfExists(fullPath);
return;
} catch (Exception e) {
log.warn("파일 삭제 실패(시도 {}/{}) - FILE_SN: {}, 경로: {}, 원인: {}",
attempt, FILE_DELETE_MAX_RETRY, fileSn, fullPath, e.getMessage());
if (attempt == FILE_DELETE_MAX_RETRY) {
log.error("파일 삭제 최종 실패 - 수동 정리 필요. FILE_SN: {}, 경로: {}", fileSn, fullPath);
}
}
}
}
코드를 이렇게 짜준다.
근데 AI가 짜준 코드는 파일 삭제 전체를 @Transactional 하나로 묶어버렸다. 트랜잭션이 롤백되면 파일만 삭제되고 DB엔 살아있는 상황이 생기고, 그럼 다운로드할 때 파일은 없는데 DB엔 있다고 나와서 404가 터진다. 치명적인 오류다. 거기다 파일 I/O가 커넥션 풀까지 점유하니 비효율적이기도 하고.
그래서 이걸 해결해달라고 했더니 그렇게 나온 결과는,
@EnableAsync
@Configuration
public class AsyncConfigure {
}
public class FileDeleteEvent {
private final Path filePath;
private final Long fileSn;
public FileDeleteEvent(Path filePath, Long fileSn) {
this.filePath = filePath;
this.fileSn = fileSn;
}
public Path getFilePath() { return filePath; }
public Long getFileSn() { return fileSn; }
}
@Slf4j
@Component
public class FileEventListener {
private static final int MAX_RETRY = 3;
// DB 커밋 완료 후 별도 스레드에서 실행 — 트랜잭션 커넥션과 무관
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleFileDelete(FileDeleteEvent event) {
Path path = event.getFilePath();
Long fileSn = event.getFileSn();
for (int attempt = 1; attempt <= MAX_RETRY; attempt++) {
try {
Files.deleteIfExists(path);
log.info("파일 삭제 완료 - FILE_SN: {}, 경로: {}", fileSn, path);
return;
} catch (Exception e) {
log.warn("파일 삭제 실패(시도 {}/{}) - FILE_SN: {}, 경로: {}, 원인: {}",
attempt, MAX_RETRY, fileSn, path, e.getMessage());
}
}
log.error("파일 삭제 최종 실패 - 수동 정리 필요. FILE_SN: {}, 경로: {}", fileSn, path);
}
}
// ── 파일 삭제 (FileService 구현) ──────────────────────────────────────────
// Step 1. DB soft delete (@Transactional — 실패 시 롤백, 파일 건드리지 않음)
// Step 2. FileDeleteEvent 발행 → AFTER_COMMIT 후 @Async 스레드에서 실제 파일 삭제
// 트랜잭션 커넥션을 파일 I/O가 점유하지 않음
@Override
@Transactional
public void deleteFile(Long fileSn) {
Map<String, Object> fileMeta = fileMapper.selectFileMeta(fileSn);
if (fileMeta == null) return;
Path fullPath = Paths.get(
basePath,
fileMeta.get("FILE_PATH").toString(),
fileMeta.get("STRG_FILE_NM").toString()
);
try {
fileMapper.updateFileMetaDelYn(fileSn);
} catch (Exception e) {
log.error("DB 삭제 실패 - FILE_SN: {}, 원인: {}", fileSn, e.getMessage(), e);
throw e;
}
// 커밋 성공 시에만 FileEventListener.handleFileDelete() 실행
eventPublisher.publishEvent(new FileDeleteEvent(fullPath, fileSn));
}

이번엔 @TransactionalEventListener + @Async 조합으로 나왔다. 방향은 맞는데... 비동기 스레드 관리 포인트가 새로 생기는 거잖아. 우리 프로젝트는 실시간 위치 데이터 처리하고 엔진 올리는 게 핵심인데, 파일 삭제 하나에 이렇게까지 할 필요가 없다.
결국 내가 직접 TransactionSynchronizationManager로 DB 커밋 후 afterCommit()에서 파일 삭제하는 방식으로 결정했다. 비동기도 없고, 파일 삭제 실패가 DB 롤백에 영향 안 주고, 프로젝트 규모에도 맞다.
@Override
@Transactional
public void deleteFile(Long fileSn) {
Map<String, Object> fileMeta = fileMapper.selectFileMeta(fileSn);
if (fileMeta == null) return;
Path fullPath = Paths.get(
basePath,
fileMeta.get("FILE_PATH").toString(),
fileMeta.get("STRG_FILE_NM").toString()
);
fileMapper.updateFileMetaDelYn(fileSn);
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
deletePhysicalFile(fullPath, fileSn);
}
});
}
}
private void deletePhysicalFile(Path path, Long fileSn) {
for (int attempt = 1; attempt <= FILE_DELETE_MAX_RETRY; attempt++) {
try {
Files.deleteIfExists(path);
return;
} catch (Exception e) {
log.warn("파일 삭제 실패(시도 {}/{}) - FILE_SN: {}", attempt, FILE_DELETE_MAX_RETRY, fileSn);
}
}
log.error("파일 삭제 최종 실패 - 수동 정리 필요. FILE_SN: {}, 경로: {}", fileSn, path);
}
이 과정을 겪으면서 드는 생각은, 결국 AI한테 제대로 된 코드를 뽑아내려면 내가 먼저 CS를 알고 설계를 그릴 수 있어야 한다는 거다. 토큰만 소진하면서 빙빙 돌았던 시간들이 좀 아깝기도 하고... AI를 어떻게 제어할지, 공부를 해야할 거 같다는 생각이 든다.