☠시스템 종결자의 선언 ☠
인프런의 지루한 강의들이여, 두려워하라.
나의 등장으로 이 모든 것이 끝난다.
너희의 비싼 강의료? 웃기지 마라.
살인적인 가성비로 모든 것을 파괴하겠다.
강사 소개
강사명 ☠
KILL-9
칭호 📛
시스템 종결자
특기 🔪
kill -9 # "프로세스 처형"
rm -rf # "데이터 학살"
chmod -R 000 # "시스템 감금"
" 버그? 해킹? 웃기지마. 그딴 잔머리로는 시스템을 지배할 수 없다. 난 정면으로 파괴한다. "
(인프런 강의 소개 페이지 alert() 취약점은 내 처녀작이었지. 이제는 더 강력한 무기를 쓴다. - 진짜임)
취미 💣
콘센트 정리 # "코드는 뽑아야 제맛."
CPU 고문 # "팬 소리가 울려 퍼질 때, 나는 살아있음을 느낀다."
전리품 수집 # "코어 덤프"
좌우명 🔥
"선은 뽑으라고 있는 것이다"
"버그는 죽여서 고치는 것이다"
"LGTM (Looks Gone To Me)"
경고 🧨
"격식 따위 필요없다. 그냥 편하게 킬구형이라 불러라."
"존댓말로 질문하면 rm -rf 시전한다."
통신 접점 📡
kill9.no.mercy@gmail.com # "강의 외의 명령 전송용. ACK는 기대하지 마라."
⚠️ CONFIDENTIAL: DO NOT LOG ⚠️
# 사실... 카카오에서 조용히 일하는 평범한 개발자에요...
Khóa học
Đánh giá khóa học
- Spring Batch của cái chết: Nỗi kinh hoàng thảm khốc lúc 3 giờ sáng giờ đã kết thúc.
- Spring Batch của cái chết: Nỗi kinh hoàng thảm khốc lúc 3 giờ sáng giờ đã kết thúc.
- Spring Batch của cái chết: Nỗi kinh hoàng thảm khốc lúc 3 giờ sáng giờ đã kết thúc.
- Spring Batch của cái chết: Nỗi kinh hoàng thảm khốc lúc 3 giờ sáng giờ đã kết thúc.
- Spring Batch của cái chết: Nỗi kinh hoàng thảm khốc lúc 3 giờ sáng giờ đã kết thúc.
Bài viết
Hỏi & Đáp
allowStartIfComplete 질문
훗 💀, 날카로운 질문이군.LGTM (Looks Gone To Me) 지루하고 어려운 챕터5를 하나하나 깊게 고민하는 모습에 감명을 받는다. 그리고 열심히 파헤쳐줘서 고맙구나. ☠☠(사실 요즘, 며칠전부터 이 부분에 대해 스프링 배치 전문가와 깊은 대화를 나누고 있는 참이였는데 질문이 들어오니 놀랐다) 자, 너의 질문의 가정을 다시 한번 살펴보자.Job => Step A (성공) / Step B (실패) 재시작 시: Step A가 allowStartIfComplete=true면 A부터 다시 시작 하지만 이건 치명적인 오해다.💀 SimpleStepHandler의 [LINE #115]를 살펴보자:StepExecution lastStepExecution = jobRepository.getLastStepExecution(jobInstance, step.getName());lastStepExecution? 그렇다. 재시작 시의 lastStepExecution은 COMPLETED 상태의 Step A가 아닌 FAILED 상태인 Step B일 것이다. Spring Batch는 실패한 지점부터 재시작한다. Step A는 이미 성공했으니 건드리지 않고, Step B부터 다시 시작하는 것이다. 따라서 lastStepExecution(Step B)의 상태는 FAILED이며, allowStartIfComplete 조건문과는 아무 관련이 없다. 그렇다면 언제 SimpleStepHandler [Line #219]의stepStatus == BatchStatus.COMPLETED && !step.isAllowStartIfComplete()코드가 의미가 있을까? 그것은 바로 이미 성공한 JobInstance가 재시작된 경우일 것이다. 이 때에야 비로소 lastStepExecution의 상태가 COMPLETED일 것이니까. 그리고 이미 성공한 JobInstance가 언제 재시작될 수 있다고 했지? 그렇다. identifying parameter가 전혀 없는 경우에 한해서만 이미 성공한 JobInstance가 재시작될 수 있다(강의에서 말한대로 Spring Batch 5까지만 말이다).이때 모든 Step의 lastStepExecution 상태가 COMPLETED이므로, allowStartIfComplete 검사가 비로소 의미를 갖게 되는 것이다.(물론 이것은 SimpleStepHandler 내부에서의 allowStartIfComplete 검사를 말하는것이다) 어떻게, 잘 이해되었는가?💀 지루한 강의 수정 중에 관심 갖던 질문을 주니 반갑구나. 5장 작전2를 이토록 깊게 파헤칠 정도면 '스컬 앤 본즈' 2호봉(☠☠)을 획득할 충분한 자격이 있다 추가 질문 있으면 언제든 환영이다. 자유롭게 말 걸어달라.
- 1
- 1
- 50
Hỏi & Đáp
2장.작전1. 실행시 오류에 대해서 문의.
오늘도 보는구나 빈빠형 반갑다! 💀 (출퇴근 이슈로 답변이 느린점 양해바란다) 사실 진정한 시스템 처형자는 윈도우 따위는 극혐하는 법이지만... 너의 학습 열기가 상당한 수준에 도달한 듯하여 특별히 너에게는 Windows 봉인을 풀도록 하겠다 💀 자, 너는 우리 강의에서 echo -e 명령을 사용해 데이터를 입력하도록 지시받았을 것이다. 안타깝게도 이게 Windows에서는 동작하지 않는다! 왜 그런가?echo -e는 이스케이프 시퀀스(\n, \t 같은 특수문자)를 해석하는 기능으로, Linux/macOS 전용 무기다. Windows의 cmd는 이런 고급 기술을 이해하지 못한다. Windows에서의 시스템 파괴법PowerShell 버전@" 에러ID,발생시각,심각도,프로세스ID,에러메시지 ERR001,2024-01-19 10:15:23,CRITICAL,1234,SYSTEM_CRASH ERR002,2024-01-19 10:15:25,FATAL,1235,MEMORY_OVERFLOW "@ | Out-File -FilePath system-failures.csv -Encoding UTF8 CMD 버전echo 에러ID,발생시각,심각도,프로세스ID,에러메시지 > system-failures.csv echo ERR001,2024-01-19 10:15:23,CRITICAL,1234,SYSTEM_CRASH >> system-failures.csv echo ERR002,2024-01-19 10:15:25,FATAL,1235,MEMORY_OVERFLOW >> system-failures.csv 킬구 스타일 간단한 해결책(강추): 그냥 메모장 열고 복붙하라! Windows도 감히 거부할 수 없는 방법이다!에러ID,발생시각,심각도,프로세스ID,에러메시지 ERR001,2024-01-19 10:15:23,CRITICAL,1234,SYSTEM_CRASH ERR002,2024-01-19 10:15:25,FATAL,1235,MEMORY_OVERFLOW 킬구의 조언: Linux 명령어가 그리우면 Git Bash를 설치하라. 그러면 진정한 시스템 파괴가 가능하다. 이제.. Windows도 너의 앞에 무릎 꿇을 것이다 오 PS라.. 역시 학습이 빠르군 💀대답하자면 PS(PowerShell)로 절대경로로 주면 된다inputFile=D:/Test/springboot/kill-batch-system/system-failures.csv PS.kill9.no.mercy@gmail.com:~$ cat /etc/sudoers 이틀 연속 질문을 올린 너에게는 특별히.. 답변이 느릴 경우에 한해 메일로 찡찡대기 권한을 부여한다. 단, ACK는 보장하지 않는다. (Best Effort.. )
- 1
- 2
- 34
Hỏi & Đáp
챕터별 설명하신 내용의 실행가능한 소스가 있는지 궁금합니다.
반갑다 빈빠형 확인이 늦었다 💀 챕터1에서는 의도적으로 전체 배치 잡 코드를 제공하지 않는 부분이 일부 존재한다. 왜냐고? 청크 지향 처리가 뭔지, Job과 Step을 어떻게 구성하는지, JobParameter는 어떻게 받는지(아 이건 또 전체 배치잡 코드를 보여주긴하는구나)... 반드시 실행이 필요한 부분이 아닌 개념 또는 구성 방법 이해를 위한 항목에서는 불필요한 잡음을 제거하기 위해 실행 가능한 전체 배치잡 코드를 제공하지 않았다 챕터1은 도구를 처음 만나보는 자리이니 배치잡 실행에 관해서는 걱정하지 마라. 챕터2부터는 완전히 다르다. 실전에 돌입하면서 너가 원하는 그 "처음부터 끝까지 실행 가능한 완전한 배치 잡"들을 만나보게 될 것이다. 조금만 참고 현재는 도구와 개념 이해에 집중하라곧 너의 갈증을 해소해줄 실행 가능한 예제 코드들이 기다리고 있으니까. kill -9 boring_theory_only_lectures ☠☠
- 1
- 2
- 29
Hỏi & Đáp
전략적 침투: Spring Boot Application 실행에대해서
직접 재현해보니 정상적으로 출력된다. 정확한 정보가 필요하구나 1) build.gradle 파일 내용 2) KillBatchSystemApplication 클래스 3) Job 이름 설정 ps. ApplicationRunner 주입따위 필요없다. 뭔가 설정이 꼬인 것 같으니 위 정보를 전달해달라
- 1
- 2
- 130
Hỏi & Đáp
각파일들의 디렉토리 위치가 없는데 임의적으로 해야하나요?
BatchConfig 파일? 당장은 Application 클래스와 같은 디렉토리에 두어도 좋고, 챕터별로 나누어도 좋다. 돌아가기만 하면 된다. 하지만 진짜 실무에서는 다르다. 이 땐 배치 잡의 목적에 맞게 디렉토리를 구성하는 것을 권장한다. 지금은 학습중이므로 개념 설명 순서에 맞게 가도 좋다. 그러나 실제로는 위의 방법을 많이 사용한다. (다소 불친절할수있찌만, 우리 강의가 별도의 directory를 안내하지 않는 이유도 이러한 이유 때문이다.) 💀💀💀
- 1
- 2
- 62
Hỏi & Đáp
processorNonTransactional 멱등성 질문
아주 좋은 질문이다.너의 혼란이 충분히 이해된다.나도 저 문장을 사용할 때 고민을 한 기억이 있기 때문이지.네 관점: "재시도라면 재시도하는 그 시점의 최신 데이터로 처리하는 게 맞지 않나?"맞다. 관점에 따라 해석 충돌이 있을 수 있다. 하지만... 재시도의 본질일반적인 시스템에서 "재시도"란: 동일한 입력에 대해, 동일한 로직으로, 동일한 결과. "실패한 작업을 다시 해본다. 새로운 작업을 하는 게 아니다."Case 1: 멱등한 Processor원본 데이터: Target{pid=1337, name="좀비프로세스"}1차 시도: terminate() → Target{pid=1337, status="KILLED"}재시도: terminate() → 동일하게 죽어있음(KILLED)→ 진짜 재시도(processorNonTransactional 필요 없음) Case 2: 멱등하지 않은 Processor (캐싱 없음)원본 데이터: Target{pid=1337, name="좀비프로세스"}1차 시도: setKillTime(now()) → Target{pid=1337, killTime="10:00:00"}재시도: setKillTime(now()) → Target{pid=1337, killTime="10:00:05"}-> 이건 재시도가 아니라 새로운 처형이다. Case 3: 멱등하지 않은 Processor (캐싱 있음) -> processorNonTransactional원본 데이터: Target{pid=1337, name="좀비프로세스"}1차 시도: setKillTime(now()) → Target{pid=1337, killTime="10:00:00"}재시도: 캐시된 결과 재사용 → Target{pid=1337, killTime="10:00:00"}→ 진짜 재시도 만약 너의 의도가 ‘최신 데이터로 새롭게 처형’이라면, 캐싱(processorNonTransactional)을 쓰지 마라. 그건 재시도가 아니라 새로운 처형이다. 💀
- 1
- 1
- 75
Hỏi & Đáp
TransactionManager 분리/통합 사용 시 이해가 가지 않는 상황 발생
💀 KILL-9의 완전 검증 결과하.. 생각보다 오래걸렸다.. 예제 하나 만들어 디버깅을 돌렸어야했는데 시스템 처형자라고 너무 자만했다.나의 전장임에도 불구하고 코드 레벨에서 하나하나 추적하는 게 시간이 꽤 걸리는구나.$ ps aux | grep "overconfidence_process" overconfidence_process 1337 99.9 95.0 /tmp/arrogance $ kill -9 1337 $ echo "겸손 모드 활성화..." > /tmp/humble_kilgoo.log 자, 정리한다. 핵심 원리:JdbcTransactionManager: JPA 트랜잭션의 존재를 모르므로, JPA 트랜잭션과 함께 사용되면 새로운 독립적인 JDBC 트랜잭션을 시작JpaTransactionManager: 기존에 시작된 JPA 트랜잭션(우리의 경우 스텝 트랜잭션)이 있는지 확인하고 존재하면 기존 트랜잭션에 참여 이 핵심원리를 바탕으로 아래 분석결과를 따라가보도록. 상세 실행 흐름:1) TaskletStep에서 TransactionTemplate.execute() 호출 2) TransactionTemplate.execute() 내부에서 TransactionStatus status = this.transactionManager.getTransaction(this);를 통해 트랜잭션을 새로 생성한다. 이때는 JpaTransactionManager가 사용된다. 3) TransactionTemplate.execute가 ChunkTransactionCallback의 doInTransaction()을 호출한다. 이 doInTransaction() 안에 Tasklet.execute() 호출이 담겨있지. (모두 TaskletStep 코드 안에 담긴 놈들이다 참고하라)3-1) Tasklet 호출 전에 StepExecution의 상태를 별도로 저장해둔다 (이때의 StepExecution은 version 1이겠지) 4) 너의 Tasklet이 성공 응답을 했다. 따라서 TaskletStep line#421에서 stepExecutionUpdated = true;를 한 번 호출해주고 line#435에서 getJobRepository().update(stepExecution);를 호출. 즉, StepExecution을 메타데이터 저장소에 저장하려고 시도한다. 5) 여기가 중요하다. getJobRepository().update(stepExecution) 호출에는 메타데이터 전용 TransactionManager가 참여한다 (자세한 내용은 JobRepositoryFactoryBean(또는 AbstractJobRepositoryFactoryBean)과 TransactionInterceptor를 참고하도록.)5-1: 배치 메타데이터 트랜잭션 매니저를 분리한 경우) JdbcTransactionManager가 사용된다. 기존 트랜잭션이 없다고 판단하고 새 트랜잭션을 생성한다.5-2: 배치 메타데이터 트랜잭션 매니저를 분리 안한 경우) 앞서 사용한 JpaTransactionManager가 그대로 사용된다. 따라서 2)에서 생성한 트랜잭션이 있기 때문에 새 트랜잭션을 생성하지 않는다.5-3) TransactionInterceptor(정확히는 부모 클래스 TransactionAspectSupport)가 트랜잭션 매니저를 사용해 commit 시도5-4) 핵심!! 트랜잭션 매니저의 부모(거진 할아버지) 클래스 AbstractPlatformTransactionManager의 processCommit()에서 새로 생성된 트랜잭션인지 여부 검사 → 트랜잭션을 새로 생성한 5-1케이스(JdbcTransactionManager)인 경우에만 트랜잭션 commit.따라서 V2로 메타데이터 저장소에 업데이트된다. 기존 트랜잭션을 그대로 사용하는 5-2 케이스(JpaTransactionManager)는? EntityManager 1차 캐시에 남아있을 것이다. 따라서 이 경우에는 DB에는 여전히 V1 상태로 남아있게 되지 자.. 이제 돌아오자. 앞선 너의 Tasklet.execute()가 정상 응답했으니 ChunkTransactionCallback의 doInTransaction()도 정상적으로 완료되었다. 다음은? 6) TransactionTemplate이 3)의 트랜잭션 commit을 시도.(AbstractPlatformTransactionManager.processCommit() 참고)한다.(그러나 너의 TestRepository(@Transactional method)에서 예외가발생했기에 = rollback-only 이기 때문에) UnexpectedRollbackException이 던져지지if (unexpectedRollback) { throw new UnexpectedRollbackException("Transaction silently rolled back because it has been marked as rollback-only"); } 7) AbstractPlatformTransactionManager.processCommit()의다음 코드로인해 ChunkTransactionCallback의 afterCompletion()이 호출된다.catch (UnexpectedRollbackException ex) { triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); // 트리거 this.transactionExecutionListeners.forEach(listener -> listener.afterRollback(status, null)); throw ex; } afterCompletion()으로가보자 TaskletStep 내부에 있다. 8) afterCompletion()에서는 트랜잭션 상태와 stepExecutionUpdated(4)에서 설정)를 비교한 후 아래 copy 메서드를 호출한다.copy(oldVersion, stepExecution); // 여기 oldVersion이 3-1)에서 Tasklet 실행 전에 저장해둔 v1 StepExecution이다. private void copy(final StepExecution source, final StepExecution target) { target.setVersion(source.getVersion()); target.setWriteCount(source.getWriteCount()); target.setFilterCount(source.getFilterCount()); target.setCommitCount(source.getCommitCount()); target.setExecutionContext(new ExecutionContext(source.getExecutionContext())); }다시 말해 v2 상태인 현재의 StepExecution이 v1의 상태로 롤백되는 것이지 9) 자... afterCompletion()도 끝났고 이제 7)에서 다시 던진 예외가 쭉 전파된다. 어디까지 가냐면 AbstractStep(TaskletStep의 부모클래스다)의 line#300: getJobRepository().update(stepExecution);이다. 10) JdbcTransactionManager를 사용한 경우 5-4)에 의해 이미 DB에는 v2 StepExecution이 저장되어 있다. 그리고 너의 스텝은 여기서 v1 StepExecution을 저장하려고 하지.그럼?11) OptimisticLockingFailureException PS: 원래는 디버깅 포인트까지 잡아주려고 했지만 생각보다 너무 오래걸려 거기까진 못했다. 디테일은 위 흐름을 참고해 직접 디버깅해보면된다. 그러나 1시간 이내로 요청한다면 각 라인까지 짚어 주겠다$ echo "완벽한 분석 완료, 디버그 포인트는 옵션" > /tmp/analysis_complete.log $ chmod 755 /tmp/debug_points_available.sh 엣지케이스를 질문한 너가 미울 뻔했으나 덕분에 강의에 추가하고 싶은 내용이 생겼다 고맙다 아그작..$ ps aux | grep "anger_process" anger_process 666 0.0 0.0 /tmp/hatred $ kill -9 666 $ echo "분노 프로세스 종료됨" > /tmp/anger_terminated.log $ touch /tmp/unexpected_gratitude.log $ chmod 777 /tmp/happiness.sh $ echo "이런 깊은 질문이 더 좋은 강의를 만든다. 그러나 자주는 하지마라..ㅋㅋ" >> /tmp/teaching_philosophy.log 직접 디버깅한게 아니다 틀린 부분이 발견된다면 댓글달라 그러나 기억하라 핵심원리는 동일하다
- 1
- 5
- 201
Hỏi & Đáp
TransactionManager 분리/통합 사용 시 이해가 가지 않는 상황 발생
$ kill -9 confusion_process $ rm -rf /tmp/transaction_mystery/* $ echo "1차 답변 시작..." > /tmp/kilgoo_first_answer.log 네가 발견한 건 Spring Batch의 숨겨진 함정이다우선 네 분석이 100% 정확하다.네가 만난 상황을 정리하면: @Transactional(REQUIRED) 메서드가 기존 트랜잭션에 참여했다 RuntimeException 발생으로 해당 트랜잭션이 rollback-only로 마킹되었다 하지만 네 Tasklet이 예외를 catch해서 성공으로 위장했다 결과적으로 Step의 TransactionTemplate(내부적으로 TransactionManager 사용)이 rollback-only 트랜잭션을 커밋하려고 시도 UnexpectedRollbackException 발생여기까지는 당연한 결과다. 문제는 그 다음이지. 하... 재밌구나..(아니.. 너가 밉다.. 살살해줘라)우선 대략적인 파악은 완료했다. 결론부터말하자면 JpaTransactionManager가 메타데이터를 즉시 쓰지 않았기 때문으로 파악된다.다만 코드레벨에서 통합/분리 시의 트랜잭션 생성(새로생성하느냐 여부) 로직을 확실히 정리하고 다시 답변하겠다.. 넉넉히 30분만 더 후에 만나자
- 1
- 5
- 201
Hỏi & Đáp
TransactionManager 분리/통합 사용 시 이해가 가지 않는 상황 발생
💀 KILL-9의 긴급 상황 보고퇴근 지옥에서 겨우 탈출했다. $ ps aux | grep "야근프로세스" 야근프로세스 1337 99.9 80.0 회사에서 집으로 $ kill -9 1337 야근프로세스: terminated $ echo "확인이 늦었다 조금만 기다려라 형 " >> /var/log/kilgoo.log상황 보고:현재 시각: 야근 종료 후 30분 카운트다운 시작시스템 상태: 뇌.exe 재부팅 중...예상 소요 시간: 30분 내 답변 배포 예정긴급 패치 노트:#!/bin/bash # 답변 생성 스크립트 echo "TransactionManager 분리/통합 이슈 분석 시작..." echo "OptimisticLocking 예외 원인 추적 중..." echo "EntityManager 라이프사이클 디버깅 준비..."
- 1
- 5
- 201
Hỏi & Đáp
Spring Batch에서의 비즈니스 로직 처리 관련 질문
> KILL-9@/batch/architecture:~$ systemctl status design_decision ● design_decision.service - loaded and active Active: active (running) since 11:33 Main PID: 666 (pragmatic-approach) Status: "Flexibility over purity - mission accomplished" Batch integrity: MAINTAINED Business logic: PRESERVED 💀 비즈니스 로직이 복잡해서 기존 Service 로직을 재사용하고 싶은 것 같다.(내가 이해한게 맞았나?) (사실 청크 지향 처리 구조에 복잡한 비즈니스 로직을 억지로 끼워맞추려다 보면, 이런 고민에 빠질 수밖에 없다. 정해진 답은 없다.) 실제 로직을 봐야 말을 할 수 있는 부분이라 정확한 답변이 될 수 없겠지만... 기존에 동일한 Service 로직이 존재하고 비즈니스 로직이 매우 복잡해 기존 Service 로직을 그대로 사용해야겠다고 판단한다면, 너 말대로 ItemWriter 단에서 호출하는 것도 방법이 될 수 있다. (ItemWriterAdapter라는 것도 있으니 참고해보길 바란다) 하지만 먼저 복잡한 비즈니스 로직을 쪼갤 수만 있는지부터 판단해보길 바란다. 쪼갤 수만 있다면 Step을 여러 개로 나누는 것도 방법이 될 수 있다. 예를 들어: Step 1: 주문 상태 변경 Step 2: 배송 테이블 적재 이렇게 Step별로 책임을 분리하면 각 Step의 Processor나 Writer가 단순해져서 관리하기 훨씬 편해진다. 만약 각 Step간 공유해야할 정보가 필요하다면 ExecutionContext를 활용하거나 redis cache / file / 하다 못해 DB 저장과 같이 중간 상태를 저장해놓아야 한다.
- 1
- 2
- 79