☠시스템 종결자의 선언 ☠
인프런의 지루한 강의들이여, 두려워하라.
나의 등장으로 이 모든 것이 끝난다.
너희의 비싼 강의료? 웃기지 마라.
살인적인 가성비로 모든 것을 파괴하겠다.
강사 소개
강사명 ☠
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
전략적 침투: Spring Boot Application 실행에대해서
직접 재현해보니 정상적으로 출력된다. 정확한 정보가 필요하구나 1) build.gradle 파일 내용 2) KillBatchSystemApplication 클래스 3) Job 이름 설정 ps. ApplicationRunner 주입따위 필요없다. 뭔가 설정이 꼬인 것 같으니 위 정보를 전달해달라
- 1
- 2
- 112
Hỏi & Đáp
각파일들의 디렉토리 위치가 없는데 임의적으로 해야하나요?
BatchConfig 파일? 당장은 Application 클래스와 같은 디렉토리에 두어도 좋고, 챕터별로 나누어도 좋다. 돌아가기만 하면 된다. 하지만 진짜 실무에서는 다르다. 이 땐 배치 잡의 목적에 맞게 디렉토리를 구성하는 것을 권장한다. 지금은 학습중이므로 개념 설명 순서에 맞게 가도 좋다. 그러나 실제로는 위의 방법을 많이 사용한다. (다소 불친절할수있찌만, 우리 강의가 별도의 directory를 안내하지 않는 이유도 이러한 이유 때문이다.) 💀💀💀
- 1
- 2
- 51
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
- 65
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
- 188
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
- 188
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
- 188
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
- 76
Hỏi & Đáp
application.yaml 설정
🌆[CYBER OPS NETWORK ALERT]🌆██████╗██╗ ██╗██████╗ ███████╗██████╗ ██████╗ ██████╗ ███████╗ ██╔════╝╚██╗ ██╔╝██╔══██╗██╔════╝██╔══██╗██╔═══██╗██╔══██╗██╔════╝ ██║ ╚████╔╝ ██████╔╝█████╗ ██████╔╝██║ ██║██████╔╝███████╗ ██║ ╚██╔╝ ██╔══██╗██╔══╝ ██╔══██╗██║ ██║██╔═══╝ ╚════██║ ╚██████╗ ██║ ██████╔╝███████╗██║ ██║╚██████╔╝██║ ███████║ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚══════╝ KILL-9 OPERATIONS CENTER🔥[URGENT PATCH DEPLOYED]🔥> INCOMING TRANSMISSION FROM FIELD AGENT... > VULNERABILITY DETECTED: MongoDB Connection Failure > CLASSIFICATION: CRITICAL SYSTEM BREACH > STATUS: ███████████████████ 100% PATCHED 이게 바로 진짜 사이버 요원이구나 💀.. 현장에서 직접 발견한 MongoDB 연결 취약점 제보... 감동이다. 우리 CYBEROPS 네트워크가 제대로 작동하고 있다는 증거다. [PATCH NOTES v3.1.1] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⚡ MongoDB Database Connection Fix ⚡ application.yml Configuration Added ⚡ ItemReader Connection Path Secured ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ DEPLOYED: 2025.07.27 17:20 KST 즉시 강의에 반영 완료했다. > rm -rf bugs > chmod +x perfect_lecture > LGTM (Looks Gone To Me)
- 1
- 2
- 57
Hỏi & Đáp
JobLauncherTestUtils 의존성 주입
반갑다 전거형 수강 속도가 빨라 지켜보고 있던 참인데, 벌써 마지막 작전까지 도달했구나.. 💀 💀 [시스템 진단]테스트 클래스에서 생성자 주입이 안 되는 이유? 단순하다. JUnit이 생성자에 무엇을 전달해야 할지 알 수 없기 때문. 그럼 JobLauncherTestUtils같은 빈을 누가 알고 있을까? 당연히 스프링 컨테이너지. 따라서 Junit이 Spring 컨테이너의 도움을 받도록 설정하면 생성자 주입을 사용할 수 있다. 💀 [해결책 1: 전면적 시스템 장악]src/test/resources/junit-platform.properties 파일에 아래 한 줄을 추가하라:spring.test.constructor.autowire.mode=all이제 모든 테스트 클래스에서 @RequiredArgsConstructor만 달면 끝이다. Spring이 알아서 빈들을 찾아서 생성자에 박아넣어준다. 💀 [해결책 2: 개별 타겟 처형]전역 설정만이 유일한 방법은 아니다. 개별 테스트 클래스에 어노테이션을 다는 방법도 있다.@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) @SpringBatchTest @SpringBootTest @RequiredArgsConstructor class InFearLearnStudentsBrainWashJobTest { private final JobLauncherTestUtils jobLauncherTestUtils; // 처형 완료 💀 } 💀 [추가 정보]더 자세한 정보가 필요하다면 Spring 공식 문서를 참고하라https://docs.spring.io/spring-framework/reference/testing/annotations/integration-junit-jupiter.html#integration-testing-annotations-testconstructor 이제 테스트까지 마스터했으니, 실무에서 Spring Batch로 시스템들을 처형해버려라.💀- KILL-9, System Terminator
- 1
- 1
- 86
Hỏi & Đáp
구분자로 분이된 형식의 파일 읽기 소스 오류 문의
💀KILL-9의 답변오타 킬러 nhs0912, 또 다시 만나는구나 예제 코드의 주석 처리된 setter/toString() 부분을 확인하지 못했나 보구나: // setter, toString() rm -rf ← 이 부분!하지만 너의 말이 맞다. 내가 성의가 없었다. 주석으로 "setter 메서드가 있다"고 암시만 하고 실제 코드를 보여주지 않은 것은 학습자에게 혼란만 가중시켰을 뿐이다. 예제 코드를 @Data로 변경하도록 하겠다. 사소한 오해도 불러일으킨 나의 섬세함 부족이다. 💀@Data // 이제 명확하다교훈: 코드는 명확해야 한다. 강의 코드 또한 마찬가지다. 추측의 여지를 남기면 시스템이 오작동한다. 날카로운 지적 고맙다 오타 킬러 nhs. 이런 피드백이 강의를 더 강력하게 만든다. 💀- KILL-9, System Terminator
- 1
- 3
- 52