Skip to content

Conversation

@hongkikii
Copy link

@hongkikii hongkikii commented Oct 26, 2025

안녕하세요 우가 대가 🙈🙊🐒🙉🐵

드디어 대망의 우테코 마지막 미션...... 도 한 번 구현해봤습니다.
변경사항과 관련한 설명도 아래에 적어두겠습니다 ㅎㅎ
리뷰 잘부탁드릴게요 ~

주요 변경 사항

비즈니스 로직과 트랜잭션 처리 로직을 분리하여 코드의 명확성과 유지보수성을 높이는 데 중점을 두었습니다. 이를 위해 서비스 계층에 인터페이스르 도입해 트랜잭션 처리를 추상화했습니다. 또한, 트랜잭션 동기화 방식을 도입하여 DAO 계층이 Connection 객체에 직접 의존하지 않도록 DataSourceUtils와 JdbcTemplate을 개선했습니다.

클래스 및 역할

1. UserService (인터페이스)

  • 기존의 클래스에서 서비스의 핵심 기능을 정의하는 계약(contract) 역할을 하는 인터페이스로 변경되었습니다.
  • 이를 통해 서비스의 구현을 추상화하고, 비즈니스 로직과 트랜잭션 프록시를 동일한 타입으로 다룰 수 있게 합니다.

2. AppUserService (클래스)

  • UserService 인터페이스를 구현한 클래스로, 순수한 비즈니스 로직을 담당합니다.
  • 트랜잭션과 같은 기술적인 코드가 전혀 없으며, 사용자 정보 변경, 이력 기록 등 애플리케이션의 핵심 로직만을 처리합니다.

3. TxUserService (클래스)

  • AppUserService를 감싸는 트랜잭션 프록시(Proxy) 클래스입니다.
  • 이 클래스의 유일한 역할은 트랜잭션을 관리하는 것입니다.
  • changePassword와 같은 메서드가 호출될 때, 트랜잭션을 시작하고 실제 비즈니스 로직은 AppUserService에 위임합니다. 로직 수행이 끝나면 그 결과에 따라 트랜잭션을 커밋하거나 롤백합니다.
  • 테스트 코드의 변경을 최소화하기 위해, 생성자에서 직접 DataSourceTransactionManager를 생성하도록 구현했습니다.

4. DataSourceUtils (클래스)

  • 트랜잭션 동기화의 핵심 유틸리티 클래스로, 트랜잭션의 활성화 여부에 따라 올바른 Connection을 반환하도록 구현했습니다.
  • 트랜잭션이 진행 중일 때는 TransactionSynchronizationManager에 보관된 Connection을, 그렇지 않을 때는 새로운 Connection을 반환합니다.

기대 효과

  • 관심사의 분리(SoC): 비즈니스 로직(AppUserService)과 트랜잭션 처리(TxUserService) 로직을 분리하였습니다.
  • 유지보수성 향상: 트랜잭션 정책이 변경되더라도 TxUserService만 수정하면 되므로 비즈니스 로직에 영향을 주지 않습니다.
  • 테스트 용이성: 순수한 비즈니스 로직을 가진 AppUserService는 트랜잭션의 방해 없이 단위 테스트를 수행하기 용이해집니다.

@hongkikii hongkikii self-assigned this Oct 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 26, 2025

Walkthrough

이 변경사항은 사용자 서비스 계층의 구조를 재구성합니다. UserService를 구체적인 클래스에서 인터페이스로 전환하고, 비즈니스 로직을 담당하는 AppUserService와 트랜잭션 처리를 담당하는 TxUserService로 분리합니다. DataSourceUtils를 도입하여 연결 관리를 중앙화하고, DAO 메서드에서 명시적인 Connection 파라미터를 제거합니다. JdbcTemplate이 자동으로 DataSourceUtils를 사용하여 연결을 관리하도록 리팩터링하고, 테스트에서 TransactionSynchronizationManager를 활용한 바인딩 기반 접근 방식을 도입합니다.

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "[4단계 - Transaction synchronization 적용하기] 미미(홍향미) 미션 제출합니다"는 변경사항의 핵심 부분인 트랜잭션 동기화 도입을 명확하게 반영합니다. 그러나 이 제목은 전체 변경의 범위, 특히 비즈니스 로직과 트랜잭션 분리를 위한 서비스 계층 아키텍처 리팩토링(UserService 인터페이스화, AppUserService, TxUserService 도입)이라는 근본적인 변경을 완전히 포함하지 않습니다. "미미(홍향미) 미션 제출합니다" 부분은 개인 정보와 감정 표현을 포함하여 표준적인 PR 제목 관례에 부합하지 않습니다. 제목은 변경사항의 중요한 일부를 정확하게 포인트하고 있습니다.
Description Check ✅ Passed 풀 리퀘스트 설명은 변경사항과 명확하게 관련되어 있습니다. 설명에서 제시한 주요 변경사항들은 원본 코드 요약과 일치합니다. 구체적으로, 비즈니스 로직과 트랜잭션 처리 분리(AppUserService와 TxUserService 생성), UserService를 인터페이스로 변경, DataSourceUtils와 JdbcTemplate의 개선을 통한 Connection 직접 의존성 제거, DAO 계층의 메서드 서명 단순화 등이 모두 원본 요약에서 확인되는 실제 변경사항입니다. 설명은 변경의 목적과 동기를 명확하게 전달하고 있습니다.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
study/src/test/java/transaction/stage1/Stage1Test.java (1)

83-88: 스레드 동기화·예외 전파 이슈는 앞선 코멘트와 동일합니다

중복 피드백 방지를 위해 상세 제안은 생략합니다.

🧹 Nitpick comments (15)
study/src/test/java/transaction/stage1/Stage1Test.java (5)

43-46: 바인딩/해제의 예외 안전성 보강 제안

수동 bind/unbind가 try/finally 없이 흩어져 있어 예외 시 리소스가 남을 수 있습니다. 바인딩 구간을 명확한 경계로 묶고, 해제·롤백·close 순서를 보장하도록 구조를 정리해보세요. 왜: 테스트 안정성과 누수 방지.


52-55: 서브 스레드 컨텍스트 바인딩 해제 타이밍 확인

DAO 호출 직후 곧바로 unbind를 호출합니다. 예외 발생 시에도 반드시 해제되도록 finally 경계로 감싸는지 점검해보세요. 왜: 누수 및 다음 테스트 간 교란 방지.


75-80: 트랜잭션 경계와 바인딩 해제 순서

바인딩 해제(unbind)를 트랜잭션 종료(커밋/롤백) 이후로 일관되게 배치하면 가독성과 안전성이 올라갑니다. 현재는 질의 후 바로 unbind하고 뒤늦게 rollback을 호출하는 패턴이 섞여 있습니다. 왜: 경계가 명확해야 오독·실수 감소.

Also applies to: 92-97


117-119: 팬텀 리드 시나리오의 관찰 지점 명료화

repeatable read 기대 효과를 검증하려면 “첫 조회 스냅샷”과 “후속 변경” 사이의 순서를 확실히 보장해야 합니다. 현재도 의도는 맞지만 스레드 조인 없이 sleep에 의존합니다. 왜: DB/환경에 따라 타이밍 민감. 조인/래치 기반으로 순서를 고정하는 방향을 고려해보세요.

Also applies to: 130-136


138-139: 커넥션/데이터소스 종료 누락 가능성 점검

rollback만 호출하고 커넥션 close는 보장되지 않습니다. 또한 HikariDataSource는 명시적으로 닫아야 합니다. 왜: 리소스 누수 방지. 테스트 후반부/후처리에서 종료를 보장할 방법을 고민해보셨나요?

app/src/main/java/com/techcourse/service/TxUserService.java (2)

13-16: 트랜잭션 매니저 의존성 주입으로 결합도 낮추기

서비스 내부에서 DataSourceTransactionManager를 직접 생성하면 테스트/확장성이 떨어집니다. 왜: DIP 위반, 대체/설정 어려움. 생성자 주입(인터페이스 타입 권장)으로 바꾸는 걸 고려해보셨나요?


23-26: 쓰기(save)의 트랜잭션 적용 범위 확인

save가 트랜잭션 없이 위임됩니다. 의도적으로 비트랜잭션인가요, 아니면 changePassword만 보호하려는 단계적 도입인가요? 도메인 불변성을 기준으로 save에도 경계가 필요한지 점검해보세요.

app/src/test/java/com/techcourse/service/UserServiceTest.java (2)

37-40: 파라미터 명명 일관성(createdBy)

여기서는 createBy 변수를 사용하고, 다른 곳(인터페이스/도메인)은 createdBy로 보입니다. 왜: 용어 일관성은 읽기·검색성 향상. 테스트 변수명도 createdBy로 맞출지 검토해보세요.


48-54: 롤백 검증 시나리오가 명료합니다

의도적 예외를 통해 DataAccessException을 기대하고, 이후 상태 불변을 확인하는 플로우가 좋습니다. 실패 원인과 롤백 경계를 설명하는 메시지 추가를 고려하면 테스트 문서성이 더 좋아집니다.

Also applies to: 56-60

app/src/main/java/com/techcourse/service/AppUserService.java (3)

19-22: 예외 메시지에 컨텍스트 보강

"User not found"에 id를 포함하면 원인 파악과 로그 분석이 쉬워집니다. 왜: 운영 가시성 향상.


24-27: save의 의미적 경계 재고

UserDao는 insert인데 서비스는 save로 추상화했습니다. 업데이트 시나리오 포함 여부와 불변성(중복 계정 등) 검증 위치를 어디로 둘지 결정해두면 확장 시 혼돈을 줄일 수 있습니다.


30-36: 도메인에 책임 더 밀기 + 원시값 포장 고려

비밀번호와 작성자(createdBy)는 값 객체로 포장하면 검증·정책을 응집시킬 수 있습니다(예: Password, CreatedBy). 또한 히스토리 생성 책임을 서비스가 아닌 도메인(예: user가 자신의 이력을 생성/기록하도록)으로 이동하면 Tell, Don’t Ask가 강화됩니다.

app/src/main/java/com/techcourse/service/UserService.java (1)

5-9: 인터페이스 전환은 방향이 좋습니다 — 계약(트랜잭션/불변성) 문서화 고려

메서드별 트랜잭션 경계(읽기/쓰기, 롤백 정책)와 도메인 제약(비밀번호 정책, 중복 방지)을 JavaDoc 등으로 명시하면 구현/프록시 간 기대치가 명확해집니다. 왜: 확장/테스트 용이성.

jdbc/src/main/java/com/interface21/jdbc/datasource/DataSourceUtils.java (1)

19-42: 코드 중복에 대해 생각해볼 점이 있습니다.

TransactionSynchronizationManager를 보니 이미 동일한 시그니처의 getConnection(DataSource)releaseConnection(Connection, DataSource) 메서드가 존재하네요. DataSourceUtils는 단순히 이 메서드들을 위임만 하고 있어서 코드 중복이 발생하고 있습니다.

몇 가지 질문을 던져볼게요:

  • 이 래퍼 계층이 제공하는 추가적인 가치는 무엇일까요?
  • 만약 예외 변환(SQLException → CannotGetJdbcConnectionException)이 목적이라면, 왜 TransactionSynchronizationManager에서 직접 처리하지 않았을까요?
  • 두 클래스의 역할과 책임을 어떻게 구분하면 좋을까요?

생각해볼 점:

  • TransactionSynchronizationManager: 트랜잭션 동기화를 위한 저수준 상태 관리
  • DataSourceUtils: 애플리케이션 레벨의 Connection 획득/반환 API

만약 역할이 명확히 구분된다면, 각 클래스의 책임 범위를 고려해 어떤 메서드를 어디에 두는 것이 더 적절할지 고민해보세요.

study/src/test/java/transaction/stage1/UserDao.java (1)

17-25: 객체에게 묻지 말고 시키세요 (Tell, Don't Ask)

insertupdate 메서드를 보면 User 객체에게 데이터를 하나하나 꺼내서(get) 사용하고 있네요. 이는 **객체지향 생활 체조 원칙 9번(게터/세터 금지)**과 관련이 있습니다.

생각해볼 질문들:

  • User 객체가 자신의 데이터를 가지고 있는데, 왜 밖에서 데이터를 꺼내서 처리해야 할까요?
  • 만약 User가 "나를 저장하는데 필요한 값들을 배열로 줄게"라고 말할 수 있다면 어떨까요?
  • PreparedStatement에 파라미터를 설정하는 책임은 누구에게 있는 게 더 자연스러울까요?

힌트: User 객체가 스스로 자신을 표현할 수 있는 방법을 제공한다면, DAO는 User에게 **"어떻게 저장되고 싶은지 알려줘"**라고 요청할 수 있습니다.

📖 이 원칙이 중요한 이유: 진정한 캡슐화는 데이터를 숨기는 것뿐만 아니라, 객체가 자신의 데이터를 활용한 행동을 스스로 결정하도록 하는 것입니다.

Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1eae452 and 7022f64.

📒 Files selected for processing (8)
  • app/src/main/java/com/techcourse/service/AppUserService.java (1 hunks)
  • app/src/main/java/com/techcourse/service/TxUserService.java (1 hunks)
  • app/src/main/java/com/techcourse/service/UserService.java (1 hunks)
  • app/src/test/java/com/techcourse/service/UserServiceTest.java (2 hunks)
  • jdbc/src/main/java/com/interface21/jdbc/datasource/DataSourceUtils.java (1 hunks)
  • study/src/test/java/transaction/stage1/Stage1Test.java (2 hunks)
  • study/src/test/java/transaction/stage1/UserDao.java (1 hunks)
  • study/src/test/java/transaction/stage1/jdbc/JdbcTemplate.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: ## 🎯 코드 품질 중심 리뷰 가이드라인

이 리뷰는 코드 품질과 객체지향 원칙에 집중합니다.
미션 달성 여부가 아닌, 코드 설계와 품질 개선에 대한 피드백을 한글로 제공해주세요.

📚 학습 원칙

  • 직접 코드를 제공하지 마세요 (학습자가 명시적으로 요청하는 경우 제외)
  • 문제 해결 과정을 안내하되, 정답을 바로 알려주지 마세요
  • 작은 단계로 문제를 분해하여 접근하도록 도와주세요

💡 피드백 방법

  • 유도 질문 활용: "만약 ~라면 어떻게 될까요?", "~를 고려해보셨나요?"
  • 힌트 제공: 방향은 제시하되, 구체적인 구현은 학습자가 하도록
  • 다양한 접근법 제시: 한 가지 해결책이 아닌 여러 가능성을 제안
  • 왜?에 집중: 단순히 무엇이 잘못되었는지보다 왜 그런지 이해하도록

⚡ 객체지향 생활 체조 원칙 검토

다음은 객체지향 생활 체조(Object Calisthenics) 9가지 원칙입니다.
위반 시 학습 효과를 위해 반드시 피드백을 제공하되, 왜 이 원칙이 중요한지 설명해주세요:

규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만

  • 들여쓰기 depth 최대 1 (중첩 제어구조 금지)
    • 📖 이유: 메서드 복잡도 감소, 단일 책임 원칙 강화
    • 💡 힌트: "이 부분을 별도 메서드로 추출하면 어떨까요?"

규칙 2: else 예약어 금지

  • else, switch/case 사용 금지
    • 📖 이유: 복잡한 분기 제거, 명확한 코드 흐름
    • 💡 힌트: "early return이나 가드 클로즈 패턴을 고려해보세요"

규칙 3: 모든 원시값과 문자열 포장

  • 원시 타입과 String을 객체로 포장
    • 📖 이유: 도메인 개념 명확화, 비즈니스 로직 응집
    • 💡 예시: int portPort port, String nameName name

규칙 4: 한 줄에 점 하나만 (디미터 법칙)

  • 메서드 체이닝 제한
    • 📖 이유: 결합도 감소, 캡슐화 향상
    • 💡 나쁜 예: request.getUri().getPath().substring()
    • 💡 좋은 예: request.extractPath()

규칙 5: 축약 금지

  • 명확한 이름 사용 (축약어 금지)
    • 📖 이유: 코드 가독성, 의도 명확화
    • 💡 예시: reqrequest, calcAmtcalculateAmount

규칙 6: 모든 엔티티를 작게 유지

  • 클래스 50줄, 메서드 10줄 이하
    • 📖 이유: 단일 책임, 이해와 테스트 용이성
    • 💡 힌트: "이 클래스가 너무 많은 일을 하고 있지 않나요?"

규칙 7: 인스턴스 변수 3개 이하

  • 클래스당 최대 3개의 인스턴스 변수
    • 📖 이유: 높은 응집도, 단일 책임 유지
    • 💡 힌트: "관련 필드들을 별도 객체로 묶을 수 있을까요?"

규칙 8: 일급 컬렉션 사용

  • 컬렉션을 감싸는 클래스 사용
    • 📖 이유: 컬렉션 로직 캡슐화, 불변성 보장
    • 💡 예시: List<HttpHeader>HttpHeaders 클래스

규칙 9: 게터/세터/프로퍼티 금지

  • Tell, Don't Ask 원칙 준수
    • 📖 이유: 진정한 캡슐화, 객체 자율성
    • 💡 나쁜 예: if (user.getPassword().equals(input))
    • 💡 좋은 예: if (user.authenticate(input))

🎯 추가 프로그래밍 요구사항

코드 스타일

  • Java Style Guide 준수
  • 3항 연산자 사용 금지

테스트 및 구조

  • **모든 기능 T...

Files:

  • study/src/test/java/transaction/stage1/Stage1Test.java
  • app/src/test/java/com/techcourse/service/UserServiceTest.java
  • app/src/main/java/com/techcourse/service/AppUserService.java
  • app/src/main/java/com/techcourse/service/TxUserService.java
  • study/src/test/java/transaction/stage1/UserDao.java
  • jdbc/src/main/java/com/interface21/jdbc/datasource/DataSourceUtils.java
  • app/src/main/java/com/techcourse/service/UserService.java
  • study/src/test/java/transaction/stage1/jdbc/JdbcTemplate.java
🧬 Code graph analysis (6)
study/src/test/java/transaction/stage1/Stage1Test.java (2)
jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java (1)
  • TransactionSynchronizationManager (12-71)
study/src/main/java/transaction/RunnableWrapper.java (1)
  • RunnableWrapper (6-22)
app/src/main/java/com/techcourse/service/AppUserService.java (1)
app/src/main/java/com/techcourse/dao/UserHistoryDao.java (1)
  • UserHistoryDao (6-25)
app/src/main/java/com/techcourse/service/TxUserService.java (2)
jdbc/src/main/java/com/interface21/transaction/DataSourceTransactionManager.java (1)
  • DataSourceTransactionManager (12-73)
app/src/main/java/com/techcourse/config/DataSourceConfig.java (1)
  • DataSourceConfig (7-27)
jdbc/src/main/java/com/interface21/jdbc/datasource/DataSourceUtils.java (1)
jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java (1)
  • TransactionSynchronizationManager (12-71)
app/src/main/java/com/techcourse/service/UserService.java (1)
study/src/test/java/aop/stage1/UserService.java (1)
  • UserService (9-36)
study/src/test/java/transaction/stage1/jdbc/JdbcTemplate.java (1)
jdbc/src/main/java/com/interface21/jdbc/datasource/DataSourceUtils.java (1)
  • DataSourceUtils (12-43)
🔇 Additional comments (10)
study/src/test/java/transaction/stage1/Stage1Test.java (2)

69-71: 사전 데이터 준비 방식은 깔끔합니다

트랜잭션 외부에서 준비 데이터를 넣고 즉시 리소스를 반납한 점 좋습니다.


108-111: 테스트 픽스처 준비는 적절합니다

사전 insert를 트랜잭션 외부에서 수행한 점 명확합니다.

app/src/main/java/com/techcourse/service/TxUserService.java (1)

18-21: 읽기 요청 위임은 단순·명확합니다

트랜잭션 부하가 필요 없는 조회를 그대로 위임한 선택은 합리적입니다.

app/src/test/java/com/techcourse/service/UserServiceTest.java (1)

34-36: 서비스 합성 방식이 PR 목표와 일치합니다

AppUserService 위에 TxUserService를 감싼 구성이 명확하고 테스트 의도가 드러납니다.

app/src/main/java/com/techcourse/service/AppUserService.java (1)

13-16: 생성자 주입과 필드 수(2개)는 응집도가 높습니다

의존성 최소화가 잘 지켜졌습니다.

jdbc/src/main/java/com/interface21/jdbc/datasource/DataSourceUtils.java (1)

16-17: 유틸리티 클래스 패턴 잘 적용했습니다!

Private 생성자로 인스턴스화를 방지한 점이 좋습니다. 유틸리티 클래스의 올바른 구현 방식이에요.

study/src/test/java/transaction/stage1/UserDao.java (1)

17-50: Connection 파라미터 제거 - 훌륭한 리팩터링입니다!

모든 메서드에서 Connection 파라미터를 제거하고 JdbcTemplate에 위임하도록 변경한 점이 매우 좋습니다. 이를 통해:

  • DAO가 커넥션 관리 책임에서 벗어났어요
  • 트랜잭션 동기화가 투명하게 처리됩니다
  • 테스트하기 더 쉬운 구조가 되었습니다

관심사의 분리(Separation of Concerns)가 잘 적용된 사례입니다!

study/src/test/java/transaction/stage1/jdbc/JdbcTemplate.java (3)

22-35: 트랜잭션 동기화를 고려한 Connection 관리가 잘 되어 있습니다!

DataSourceUtils를 사용해서 Connection을 획득하고 finally 블록에서 반환하는 패턴이 정확합니다. 트랜잭션이 활성화되어 있다면 같은 Connection을 재사용하고, 그렇지 않으면 새로 획득한 Connection을 닫게 되죠.

Connection을 null로 초기화하는 패턴이 조금 아쉬워 보일 수 있지만, finally 블록에서 안전하게 해제하기 위한 표준적인 방법입니다.


37-47: 메서드 위임 패턴이 깔끔합니다!

가변 인자를 받는 메서드가 PreparedStatementSetter를 생성해서 더 구체적인 메서드로 위임하는 구조가 좋습니다. 코드 중복을 피하면서도 사용자에게 편리한 API를 제공하고 있어요.

queryForObject가 빈 결과에 대해 null을 반환하는 부분은, 향후 Optional<T>를 고려해볼 수도 있지만 현재 단계에서는 적절한 선택입니다.


49-68: 조회 로직도 안전하게 구현되었습니다!

update 메서드와 동일한 Connection 관리 패턴을 사용하면서, PreparedStatement와 ResultSet은 try-with-resources로 관리하는 구조가 일관성 있고 안전합니다.

ResultSet을 순회하며 List에 담는 로직도 명확하네요!

Comment on lines +29 to +38
public void changePassword(final long id, final String newPassword, final String createdBy) {
transactionManager.bindConnection();
try {
userService.changePassword(id, newPassword, createdBy);
transactionManager.commit();
} catch (final Exception e) {
transactionManager.rollback();
throw new DataAccessException(e);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

커밋 실패 시 롤백 호출로 인한 이중 해제/NPE 위험

commit 내부에서 이미 release/unbind가 수행된 뒤 예외가 던져지면, catch 블록의 rollback이 바인딩된 커넥션 없이 실행되어 NPE/이중 해제가 날 수 있습니다. 왜: 트랜잭션 수명주기 관리가 불일치. 커밋 예외는 별도로 처리하거나, “비즈니스 예외에만 롤백”하도록 분기하여 이중 해제를 방지하는 구조를 고민해보세요. 또한 이미 발생한 DataAccessException을 다시 래핑하지 않고 유지할지 정책도 명확히 해보세요.

🤖 Prompt for AI Agents
In app/src/main/java/com/techcourse/service/TxUserService.java around lines
29-38, change the transaction error handling so rollback is only called when a
connection is still bound and ensure the connection is always unbound/released
exactly once; specifically, separate handling for commit-time exceptions vs
business exceptions (so you don't call rollback after commit already released
the connection), guard rollback with a
transactionManager.isBound()/hasConnection() check before invoking it, move
unbind/release into a single finally block to guarantee one-time cleanup, and
when rethrowing preserve an existing DataAccessException instead of always
wrapping it again.

Comment on lines 47 to 58
new Thread(RunnableWrapper.accept(() -> {
// db에 connection(사용자A)이 아닌 새로운 연결인 subConnection(사용자B)을 받아온다.
final var subConnection = dataSource.getConnection();

// 적절한 격리 레벨을 찾는다.
final int isolationLevel = Connection.TRANSACTION_NONE;

// 트랜잭션 격리 레벨을 설정한다.
final int isolationLevel = Connection.TRANSACTION_READ_UNCOMMITTED;
subConnection.setTransactionIsolation(isolationLevel);

// ❗️gugu 객체는 connection에서 아직 커밋하지 않은 상태다.
// 격리 레벨에 따라 커밋하지 않은 gugu 객체를 조회할 수 있다.
// 사용자B가 사용자A가 커밋하지 않은 데이터를 조회하는게 적절할까?
final var actual = userDao.findByAccount(subConnection, "gugu");
TransactionSynchronizationManager.bindResource(dataSource, subConnection);
final var actual = userDao.findByAccount("gugu");
TransactionSynchronizationManager.unbindResource(dataSource);

// 트랜잭션 격리 레벨에 따라 아래 테스트가 통과한다.
// 어떤 격리 레벨일 때 다른 연결의 커밋 전 데이터를 조회할 수 있을지 찾아보자.
// 다른 격리 레벨은 어떤 결과가 나오는지 직접 확인해보자.
log.info("isolation level : {}, user : {}", isolationLevel, actual);
assertThat(actual).isNull();
assertThat(actual).isNotNull();
})).start();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

스레드 조인/예외 전파 부재로 테스트가 거짓 양성될 수 있습니다

별도 스레드의 assertion/예외가 JUnit에 전파되지 않고, join 없이 sleep으로 동기화하여 타이밍에 따라 통과할 수 있습니다. 왜: 실패를 놓치거나 플래키해짐. CountDownLatch/Executor+Future.get 또는 thread.join으로 동기화하고 예외를 메인 테스트에 전파하는 방식을 고려해보셨나요?

🤖 Prompt for AI Agents
In study/src/test/java/transaction/stage1/Stage1Test.java around lines 47 to 58,
the test spawns a new Thread and runs assertions inside it without joining or
propagating exceptions, causing false positives or flakiness; change the test to
synchronize with the worker thread (use Thread.join or replace with an
ExecutorService and submit a Callable then call Future.get) and capture any
exception thrown in the worker so it can be rethrown in the main test thread (or
let Future.get throw it). Also remove any sleep-based timing and ensure
TransactionSynchronizationManager unbinds in a finally block inside the worker
to avoid leaking resources.

// 다른 격리 레벨은 어떤 결과가 나오는지 직접 확인해보자.
log.info("isolation level : {}, user : {}", isolationLevel, actual);
assertThat(actual).isNull();
assertThat(actual).isNotNull();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

검증 위치 재고

assertThat을 서브 스레드 내부에서 수행하고 있어 실패가 테스트 결과에 반영되지 않을 수 있습니다. 메인 스레드에서 결과를 검증하는 구조로 바꿀 수 있을까요?

🤖 Prompt for AI Agents
In study/src/test/java/transaction/stage1/Stage1Test.java around line 57, the
assertion assertThat(actual).isNotNull() is executed inside a worker/sub-thread
so failures may not propagate to the test runner; change the test to collect the
result from the sub-thread (e.g., submit a Callable to an ExecutorService and
get the result, or use a Future/AtomicReference with
thread.join()/CountDownLatch) and perform assertThat(...) on the main thread
after the worker completes so any failure is reported by the test framework.

Copy link

@bowook bowook left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마지막 레벨4 미션 고생하셨습니다!
전체적으로, 구조가 저랑 비슷해서 쉽게 이해할 수 있었던 것 같아요.

} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection");
public static void releaseConnection(final Connection connection, final DataSource dataSource) {
if (TransactionSynchronizationManager.hasResource(dataSource)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@bowook bowook merged commit 7c2c119 into woowacourse:hongkikii Oct 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants