[DB 1] Transaction with Spring

oh_eolΒ·2024λ…„ 4μ›” 10일
0

μŠ€ν”„λ§ DB

λͺ©λ‘ 보기
4/4

μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ ꡬ쑰

  • ν”„λ ˆμ  ν…Œμ΄μ…˜ 계측
    • UI와 κ΄€λ ¨λœ 처리 λ‹΄λ‹Ή
    • μ›Ή μš”μ²­κ³Ό 응닡
    • μ‚¬μš©μž μš”μ²­μ„ 검증
    • μ£Ό μ‚¬μš© 기술: μ„œλΈ”λ¦Ώκ³Ό HTTP 같은 μ›Ή 기술, μŠ€ν”„λ§ MVC
  • μ„œλΉ„μŠ€ 계측
    • λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ λ‹΄λ‹Ή
    • μ£Ό μ‚¬μš© 기술: 가급적 νŠΉμ • κΈ°μˆ μ— μ˜μ‘΄ν•˜μ§€ μ•Šκ³ , 순수 μžλ°” μ½”λ“œλ‘œ μž‘μ„±
  • 데이터 μ ‘κ·Ό 계측
    • μ‹€μ œ λ°μ΄ν„°λ² μ΄μŠ€μ— μ ‘κ·Όν•˜λŠ” μ½”λ“œ
    • μ£Ό μ‚¬μš© 기술: JDBC, JPA, File, Redis, Mongo ...

μ—¬κΈ°μ„œ κ°€μž₯ μ€‘μš”ν•œ 곳은, λ°”λ‘œ 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 λ“€μ–΄μžˆλŠ” μ„œλΉ„μŠ€ 계측이닀!!

  • μ„œλΉ„μŠ€μ˜ κ΄€μ μ—μ„œ 봀을 λ•Œ, ControllerλŠ” UI와 κ΄€λ ¨λœ κΈ°μˆ λ‘œλΆ€ν„°, RepositoryλŠ” DB와 κ΄€λ ¨λœ κΈ°μˆ λ‘œλΆ€ν„° λ³΄ν˜Έν•΄μ£ΌλŠ” μΏ μ…˜ 같은 μ‘΄μž¬μ΄λ‹€.
  • 이λ₯Ό 톡해 μ„œλΉ„μŠ€λŠ” νŠΉμ • κΈ°μˆ μ— 쒅속적이지 μ•Šκ³  μ΅œλŒ€ν•œ μˆœμˆ˜ν•˜κ²Œ μœ μ§€ν•˜λ©° κ°œλ°œν•  수 μžˆλ‹€.
  • λ”°λΌμ„œ μ„œλΉ„μŠ€λŠ” 가급적 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직만 κ΅¬ν˜„ν•˜κ³  νŠΉμ • κ΅¬ν˜„ κΈ°μˆ μ— 직접 μ˜μ‘΄ν•˜μ§€ μ•ŠλŠ”λ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄ ν–₯ν›„ κ΅¬ν˜„ 기술이 변경될 λ•Œ λ³€κ²½μ˜ 영ν–₯ λ²”μœ„λ₯Ό μ΅œμ†Œν™” ν•  수 μžˆλ‹€.

κΈ°μ‘΄ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ 문제점과 ν•΄κ²°

μ§€κΈˆκΉŒμ§€ μš°λ¦¬κ°€ κ°œλ°œν•œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ λ¬Έμ œμ μ€ 크게 3가지이닀.

νŠΈλžœμž­μ…˜ 문제

  • JDBC κ΅¬ν˜„ 기술이 μ„œλΉ„μŠ€ 계측에 λˆ„μˆ˜λ˜λŠ” 문제
    • νŠΈλžœμž­μ…˜μ„ μ μš©ν•˜κΈ° μœ„ν•΄ JDBC κ΅¬ν˜„ 기술이 μ„œλΉ„μŠ€ 계측에 λˆ„μˆ˜λ˜μ—ˆλ‹€.
    • μ„œλΉ„μŠ€ 계측은 μˆœμˆ˜ν•΄μ•Ό ν•œλ‹€. κ΅¬ν˜„ κΈ°μˆ μ„ 변경해도 μ„œλΉ„μŠ€ 계측 μ½”λ“œλŠ” μ΅œλŒ€ν•œ μœ μ§€ν•  수 μžˆμ–΄μ•Ό ν•œλ‹€. (변화에 λŒ€μ‘)
      κ·Έλž˜μ„œ 데이터 μ ‘κ·Ό 계측에 JDBC μ½”λ“œλ₯Ό λ‹€ λͺ°μ•„λ‘λŠ” 것이닀.
      λ¬Όλ‘  데이터 μ ‘κ·Ό κ³„μΈ΅μ˜ κ΅¬ν˜„ 기술이 변경될 μˆ˜λ„ μžˆμœΌλ‹ˆ 데이터 μ ‘κ·Ό 계측은 μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ œκ³΅ν•˜λŠ” 것이 μ’‹λ‹€.
    • μ„œλΉ„μŠ€ 계측은 νŠΉμ • κΈ°μˆ μ— μ’…μ†λ˜μ§€ μ•Šμ•„μ•Ό ν•œλ‹€. μ§€κΈˆκΉŒμ§€ κ·Έλ ‡κ²Œ λ…Έλ ₯ν•΄μ„œ 데이터 μ ‘κ·Ό κ³„μΈ΅μœΌλ‘œ JDBC κ΄€λ ¨ μ½”λ“œλ₯Ό λͺ¨μ•˜λŠ”데, νŠΈλžœμž­μ…˜μ„ μ μš©ν•˜λ©΄μ„œ κ²°κ΅­ μ„œλΉ„μŠ€ 계측에 JDBC κ΅¬ν˜„ 기술의 λˆ„μˆ˜κ°€ λ°œμƒν–ˆλ‹€.
  • νŠΈλžœμž­μ…˜ 동기화 문제
    • 같은 νŠΈλžœμž­μ…˜μ„ μœ μ§€ν•˜κΈ° μœ„ν•΄ 컀λ„₯μ…˜μ„ νŒŒλΌλ―Έν„°λ‘œ λ„˜κ²¨μ•Ό ν•œλ‹€.
    • μ΄λ•Œ νŒŒμƒλ˜λŠ” λ¬Έμ œλ“€λ„ μžˆλ‹€. λ˜‘κ°™μ€ κΈ°λŠ₯도 νŠΈλžœμž­μ…˜μš© κΈ°λŠ₯κ³Ό νŠΈλžœμž­μ…˜μ„ μœ μ§€ν•˜μ§€ μ•Šμ•„λ„ λ˜λŠ” κΈ°λŠ₯으둜 뢄리해야 ν•œλ‹€.
  • νŠΈλžœμž­μ…˜ 적용 반볡 문제
    • νŠΈλžœμž­μ…˜ 적용 μ½”λ“œλ₯Ό 보면 반볡이 λ§Žλ‹€. try , catch , finally ...

μ˜ˆμ™Έ λˆ„μˆ˜ 문제

  • 데이터 μ ‘κ·Ό κ³„μΈ΅μ˜ JDBC κ΅¬ν˜„ 기술 μ˜ˆμ™Έκ°€ μ„œλΉ„μŠ€ κ³„μΈ΅μœΌλ‘œ μ „νŒŒλœλ‹€.
  • SQLException 은 체크 μ˜ˆμ™Έμ΄κΈ° λ•Œλ¬Έμ— 데이터 μ ‘κ·Ό 계측을 ν˜ΈμΆœν•œ μ„œλΉ„μŠ€ κ³„μΈ΅μ—μ„œ ν•΄λ‹Ή μ˜ˆμ™Έλ₯Ό μž‘μ•„μ„œ μ²˜λ¦¬ν•˜κ±°λ‚˜ λͺ…μ‹œμ μœΌλ‘œ throws λ₯Ό ν†΅ν•΄μ„œ λ‹€μ‹œ λ°–μœΌλ‘œ λ˜μ Έμ•Όν•œλ‹€.
  • SQLException 은 JDBC μ „μš© κΈ°μˆ μ΄λ‹€. ν–₯ν›„ JPAλ‚˜ λ‹€λ₯Έ 데이터 μ ‘κ·Ό κΈ°μˆ μ„ μ‚¬μš©ν•˜λ©΄, 그에 λ§žλŠ” λ‹€λ₯Έ μ˜ˆμ™Έλ‘œ λ³€κ²½ν•΄μ•Ό ν•˜κ³ , κ²°κ΅­ μ„œλΉ„μŠ€ μ½”λ“œλ„ μˆ˜μ •ν•΄μ•Ό ν•œλ‹€.

JDBC 반볡 문제

  • μ§€κΈˆκΉŒμ§€ μž‘μ„±ν•œ MemberRepository μ½”λ“œλŠ” μˆœμˆ˜ν•œ JDBCλ₯Ό μ‚¬μš©ν–ˆλ‹€.
  • 이 μ½”λ“œλ“€μ€ μœ μ‚¬ν•œ μ½”λ“œμ˜ 반볡이 λ„ˆλ¬΄ λ§Žλ‹€.
    • try , catch , finally ...
    • 컀λ„₯μ…˜μ„ μ—΄κ³ , PreparedStatement λ₯Ό μ‚¬μš©ν•˜κ³ , κ²°κ³Όλ₯Ό λ§€ν•‘ν•˜κ³ ... μ‹€ν–‰ν•˜κ³ , 컀λ„₯μ…˜κ³Ό λ¦¬μ†ŒμŠ€λ₯Ό μ •λ¦¬ν•œλ‹€.

μŠ€ν”„λ§μ„ μ‚¬μš©ν•œ 문제 ν•΄κ²°

  • μŠ€ν”„λ§μ€ μ„œλΉ„μŠ€ 계측을 μˆœμˆ˜ν•˜κ²Œ μœ μ§€ν•˜λ©΄μ„œ, μ§€κΈˆκΉŒμ§€ μ΄μ•ΌκΈ°ν•œ λ¬Έμ œλ“€μ„ ν•΄κ²°ν•  수 μžˆλŠ” λ‹€μ–‘ν•œ 방법과 κΈ°μˆ λ“€μ„ μ œκ³΅ν•œλ‹€.
  • λ’€μ—μ„œλŠ” μŠ€ν”„λ§μ„ μ‚¬μš©ν•˜μ—¬ κΈ°μ‘΄ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ 가진 λ¬Έμ œλ“€μ„ ν•˜λ‚˜μ”© ν•΄κ²°ν•œλ‹€.

νŠΈλžœμž­μ…˜ 좔상화

ν˜„μž¬ μ„œλΉ„μŠ€ 계측은 νŠΈλžœμž­μ…˜μ„ μ‚¬μš©ν•˜κΈ° μœ„ν•΄μ„œ JDBC κΈ°μˆ μ— μ˜μ‘΄ν•˜κ³  μžˆλ‹€. ν–₯ν›„ JDBCμ—μ„œ JPA 같은 λ‹€λ₯Έ 데이터 μ ‘κ·Ό 기술둜 λ³€κ²½ν•˜λ©΄, μ„œλΉ„μŠ€ κ³„μΈ΅μ˜ νŠΈλžœμž­μ…˜ κ΄€λ ¨ μ½”λ“œλ„ λͺ¨λ‘ ν•¨κ»˜ μˆ˜μ •ν•΄μ•Ό ν•œλ‹€.

JDBC νŠΈλžœμž­μ…˜ 의쑴

JPA 기술둜 λ³€κ²½

이 문제λ₯Ό ν•΄κ²°ν•˜λ €λ©΄ νŠΈλžœμž­μ…˜ κΈ°λŠ₯을 μΆ”μƒν™”ν•˜λ©΄ λœλ‹€.
λ‹¨μˆœν•˜κ²Œ μƒκ°ν–ˆμ„ λ•Œ νŠΈλžœμž­μ…˜ 좔상화 μΈν„°νŽ˜μ΄μŠ€(TxManager)λ₯Ό λ§Œλ“€μ–΄μ„œ μ‚¬μš©ν•˜λ©΄ λœλ‹€.
그리고 μ•„λž˜ κ·Έλ¦Όκ³Ό 같이 TxManager μΈν„°νŽ˜μ΄μŠ€λ₯Ό 기반으둜 각각의 κΈ°μˆ μ— λ§žλŠ” κ΅¬ν˜„μ²΄λ₯Ό λ§Œλ“€λ©΄ λœλ‹€.

  • μ„œλΉ„μŠ€λŠ” νŠΉμ • νŠΈλžœμž­μ…˜ κΈ°μˆ μ— 직접 μ˜μ‘΄ν•˜λŠ” 것이 μ•„λ‹ˆλΌ, TxManager λΌλŠ” μΆ”μƒν™”λœ μΈν„°νŽ˜μ΄μŠ€μ— μ˜μ‘΄ν•œλ‹€. 이제 μ›ν•˜λŠ” κ΅¬ν˜„μ²΄λ₯Ό DIλ₯Ό ν†΅ν•΄μ„œ μ£Όμž…ν•˜λ©΄ λœλ‹€. 예λ₯Ό λ“€μ–΄μ„œ JDBC νŠΈλžœμž­μ…˜ κΈ°λŠ₯이 ν•„μš”ν•˜λ©΄
    JdbcTxManager λ₯Ό μ„œλΉ„μŠ€μ— μ£Όμž…ν•˜κ³ , JPA νŠΈλžœμž­μ…˜ κΈ°λŠ₯으둜 λ³€κ²½ν•΄μ•Ό ν•˜λ©΄ JpaTxManager λ₯Ό μ£Όμž…ν•˜λ©΄ λœλ‹€.
  • ν΄λΌμ΄μ–ΈνŠΈμΈ μ„œλΉ„μŠ€λŠ” μΈν„°νŽ˜μ΄μŠ€μ— μ˜μ‘΄ν•˜κ³  DIλ₯Ό μ‚¬μš©ν•œ 덕뢄에 OCP 원칙을 μ§€ν‚€κ²Œ λ˜μ—ˆλ‹€. 이제 νŠΈλžœμž­μ…˜μ„ μ‚¬μš©ν•˜λŠ” μ„œλΉ„μŠ€ μ½”λ“œλ₯Ό μ „ν˜€ λ³€κ²½ν•˜μ§€ μ•Šκ³ , νŠΈλžœμž­μ…˜ κΈ°μˆ μ„ 마음껏 λ³€κ²½ν•  수 μžˆλ‹€.
  • 그리고 μ΄λŠ” μŠ€ν”„λ§μ΄ 이미 λ§Œλ“€μ–΄μ„œ μ œκ³΅ν•˜λŠ” κΈ°λŠ₯이닀.
  • μš°λ¦¬λŠ” μŠ€ν”„λ§μ΄ μ œκ³΅ν•˜λŠ” νŠΈλžœμž­μ…˜ 좔상화 κΈ°μˆ μ„ μ‚¬μš©ν•˜λ©΄ 되고, 심지어 데이터 μ ‘κ·Ό κΈ°μˆ μ— λ”°λ₯Έ νŠΈλžœμž­μ…˜ κ΅¬ν˜„μ²΄λ„ λŒ€λΆ€λΆ„ λ§Œλ“€μ–΄λ‘μ–΄μ„œ κ°€μ Έλ‹€ μ‚¬μš©ν•˜κΈ°λ§Œ ν•˜λ©΄ λœλ‹€.
  • μŠ€ν”„λ§ νŠΈλžœμž­μ…˜ μΆ”μƒν™”μ˜ 핡심은 PlatformTransactionManager μΈν„°νŽ˜μ΄μŠ€μ΄λ‹€. μ½”λ“œλ₯Ό μ‚΄νŽ΄λ³΄λ©΄ λ‹€μŒκ³Ό κ°™λ‹€.
package org.springframework.transaction;
 public interface PlatformTransactionManager extends TransactionManager {
     TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
             throws TransactionException;
     void commit(TransactionStatus status) throws TransactionException;
     void rollback(TransactionStatus status) throws TransactionException;
}
  • getTransaction() : νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•œλ‹€.
  • commit() : νŠΈλžœμž­μ…˜μ„ μ»€λ°‹ν•œλ‹€.
  • rollback() : νŠΈλžœμž­μ…˜μ„ λ‘€λ°±ν•œλ‹€.

λ’€μ—μ„œλŠ” PlatformTransactionManager μΈν„°νŽ˜μ΄μŠ€μ™€ κ΅¬ν˜„μ²΄λ₯Ό ν¬ν•¨ν•΄μ„œ νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λ‘œ μ€„μ—¬μ„œ μ΄μ•ΌκΈ°ν•œλ‹€.


νŠΈλžœμž­μ…˜ 동기화

μŠ€ν”„λ§μ΄ μ œκ³΅ν•˜λŠ” νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λŠ” 크게 2가지 역할을 ν•œλ‹€.

  • νŠΈλžœμž­μ…˜ 좔상화
    • μ•žμ—μ„œ μ„€λͺ…ν–ˆλ‹€.
  • λ¦¬μ†ŒμŠ€ 동기화
    • νŠΈλžœμž­μ…˜μ„ μœ μ§€ν•˜λ €λ©΄ νŠΈλžœμž­μ…˜μ˜ μ‹œμž‘λΆ€ν„° λκΉŒμ§€ 같은 λ°μ΄ν„°λ² μ΄μŠ€ 컀λ„₯μ…˜μ„ μœ μ§€ν•΄μ•„ν•œλ‹€. κ²°κ΅­ 같은 컀λ„₯μ…˜μ„ 동기화(λ§žμΆ”μ–΄ μ‚¬μš©)ν•˜κΈ° μœ„ν•΄μ„œ μ΄μ „μ—λŠ” νŒŒλΌλ―Έν„°λ‘œ 컀λ„₯μ…˜μ„ μ „λ‹¬ν•˜λŠ” 방법을 μ‚¬μš©ν–ˆλ‹€.
    • νŒŒλΌλ―Έν„°λ‘œ 컀λ„₯μ…˜μ„ μ „λ‹¬ν•˜λŠ” 방법은 μ½”λ“œκ°€ μ§€μ €λΆ„ν•΄μ§€λŠ” 것은 물둠이고, 컀λ„₯μ…˜μ„ λ„˜κΈ°λŠ” λ©”μ„œλ“œμ™€ λ„˜κΈ°μ§€ μ•ŠλŠ” λ©”μ„œλ“œλ₯Ό μ€‘λ³΅ν•΄μ„œ λ§Œλ“€μ–΄μ•Ό ν•˜λŠ” λ“± μ—¬λŸ¬κ°€μ§€ 단점듀이 λ§Žλ‹€.

μŠ€ν”„λ§μ΄ μ œκ³΅ν•˜λŠ” νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €λŠ”, μ“°λ ˆλ“œ 둜컬( ThreadLocal )을 μ‚¬μš©ν•΄μ„œ 컀λ„₯μ…˜μ„ λ™κΈ°ν™”ν•œλ‹€. νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λŠ” λ‚΄λΆ€μ—μ„œ 이 νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €λ₯Ό μ‚¬μš©ν•œλ‹€.
νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €λŠ” μ“°λ ˆλ“œ λ‘œμ»¬μ„ μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ— λ©€ν‹°μ“°λ ˆλ“œ 상황에 μ•ˆμ „ν•˜κ²Œ 컀λ„₯μ…˜μ„ 동기화 ν•  수 μžˆλ‹€. λ”°λΌμ„œ 컀λ„₯μ…˜μ΄ ν•„μš”ν•˜λ©΄ νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €λ₯Ό 톡해 컀λ„₯μ…˜μ„ νšλ“ν•˜λ©΄ λœλ‹€. λ”°λΌμ„œ μ΄μ „μ²˜λŸΌ νŒŒλΌλ―Έν„°λ‘œ 컀λ„₯μ…˜μ„ μ „λ‹¬ν•˜μ§€ μ•Šμ•„λ„ λœλ‹€.

λ™μž‘ 방식

1. νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•˜λ €λ©΄ 컀λ„₯μ…˜μ΄ ν•„μš”ν•˜λ‹€. νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λŠ” λ°μ΄ν„°μ†ŒμŠ€λ₯Ό 톡해 컀λ„₯μ…˜μ„ λ§Œλ“€κ³  νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•œλ‹€.
2. νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λŠ” νŠΈλžœμž­μ…˜μ΄ μ‹œμž‘λœ 컀λ„₯μ…˜μ„ νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €μ— λ³΄κ΄€ν•œλ‹€.
3. λ¦¬ν¬μ§€ν† λ¦¬λŠ” νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €μ— λ³΄κ΄€λœ 컀λ„₯μ…˜μ„ κΊΌλ‚΄μ„œ μ‚¬μš©ν•œλ‹€. λ”°λΌμ„œ νŒŒλΌλ―Έν„°λ‘œ 컀λ„₯μ…˜μ„ μ „λ‹¬ν•˜μ§€ μ•Šμ•„λ„ λœλ‹€.
4. νŠΈλžœμž­μ…˜μ΄ μ’…λ£Œλ˜λ©΄ νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λŠ” νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €μ— λ³΄κ΄€λœ 컀λ„₯μ…˜μ„ 톡해 νŠΈλžœμž­μ…˜μ„ μ’…λ£Œν•˜κ³ , 컀λ„₯μ…˜λ„ λ‹«λŠ”λ‹€.


문제 ν•΄κ²° - 🧐 νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ € 적용

κΈ°μ‘΄ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ μ½”λ“œμ— νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λ₯Ό μ μš©ν•œλ‹€.

μ‹€μŠ΅

MemberRepositoryV3

package hello.jdbc.repository;

import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.support.JdbcUtils;

import javax.sql.DataSource;
import java.sql.*;
import java.util.NoSuchElementException;

/**
 *  νŠΈλžœμž­μ…˜ - νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ € μ‚¬μš©
 *  DatasourceUtils.getConnection()
 *  DatasourceUtils.releaseConnection()
 */
@Slf4j
public class MemberRepositoryV3 {

    private final DataSource dataSource;

    public MemberRepositoryV3(DataSource dataSource) {
        this.dataSource = dataSource;
    }


    public Member save(Member member) throws SQLException {
        String sql = "insert into member(member_id, money) values (?, ?)";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());

            pstmt.executeUpdate();
            return member;
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;    // μ˜ˆμ™Έ 던짐
        } finally {     // μ˜ˆμ™Έκ°€ λ°œμƒν•˜λ˜, ν•˜μ§€ μ•Šλ˜ 항상 μˆ˜ν–‰λ˜μ–΄μ•Ό ν•˜λŠ” λΆ€λΆ„(close)μ΄λ―€λ‘œ  finally 에 μž‘μ„±.
            // 컀λ„₯μ…˜(μ™ΈλΆ€ λ¦¬μ†ŒμŠ€)은 μ‹€μ œ TCP,IP 컀λ„₯μ…˜μ— κ±Έλ €μ„œ μ“°λŠ” κ²ƒμœΌλ‘œ μ•ˆ λ‹«μœΌλ©΄ 계속 μœ μ§€λ¨. μ—­μˆœμœΌλ‘œ λ‹«μ•„μ€˜μ•Ό 함
//            pstmt.close();
//            con.close()
			// μœ„μ—μ„œ μ˜ˆμ™Έκ°€ 터지면 λ‹«νžˆλŠ” 게 호좜 μžμ²΄κ°€ μ•ˆ 될 μˆ˜λ„ μžˆμ–΄μ„œ, close() λ©”μ„œλ“œλ₯Ό λ§Œλ“€μ–΄μ„œ try-catch 둜 λ™μž‘ν•˜κ²Œ 함
            close(con, pstmt, null);	// 
        }
    }

    public Member findById(String memberId) throws SQLException {
        String sql = "select * from member where member_id = ?";

        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);

            rs = pstmt.executeQuery();  // select λŠ” Query 문으둜. 이건 κ²°κ³Όλ₯Ό ResultSet(rs)에 λ‹΄μ•„μ„œ λ°˜ν™˜ν•΄μ€€λ‹€.

            if (rs.next()) {            // rs λŠ” 내뢀에 μ»€μ„œκ°™μ€ 게 μžˆμ–΄μ„œ, ν•œ 번 ν˜ΈμΆœμ„ ν•΄μ€˜μ•Ό μ‹€μ œ 데이터가 μžˆλŠ” κ³³λΆ€ν„° 싀행이 됨. next() λŠ” 첫 번째 데이터가 μžˆλŠ”μ§€λ₯Ό λ¬Όμ–΄λ΄μ„œ true λ©΄ 진행
                Member member = new Member();   // 멀버 객체 λ§Œλ“€μ–΄μ„œ μ €μž₯ν•΄μ£ΌκΈ°
                member.setMemberId(rs.getString("member_id"));
                member.setMoney(rs.getInt("money"));
                return member;
            } else {                    // false λ‚˜μ™€μ„œ~
                throw new NoSuchElementException("member not found memberId=" + memberId);  // μ˜ˆμ™Έλ₯Ό 던질 땐 λ©”μ‹œμ§€λ₯Ό 잘 λ„£λŠ” 게 μ’‹λ‹€. 문제 ν„°μ‘Œμ„ λ•Œ ν•΄κ²°ν•˜κΈ° μ’‹μŒ
            }

        } catch (SQLException e) {
            log.info("db error", e);
            throw e;
        } finally {
            close(con, pstmt, rs);
        }
    }

    public void update(String memberId, int money) throws SQLException {
        String sql = "update member set money=? where member_id=?";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
            int resultSize = pstmt.executeUpdate();
            log.info("resultSize={}", resultSize);
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

    public void delete(String memberId) throws SQLException {
        String sql = "delete from member where member_id=?";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

    private void close(Connection con, Statement stmt, ResultSet rs) {
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(stmt);
        // 주의! νŠΈλžœμž­μ…˜ 동기화λ₯Ό μ‚¬μš©ν•˜λ €λ©΄ DatasourceUtils λ₯Ό μ‚¬μš©ν•΄μ•Ό ν•œλ‹€.
        DataSourceUtils.releaseConnection(con, dataSource);
//        JdbcUtils.closeConnection(con);
    }

    private Connection getConnection() throws SQLException {
        // 주의! νŠΈλžœμž­μ…˜ 동기화λ₯Ό μ‚¬μš©ν•˜λ €λ©΄ DatasourceUtils λ₯Ό μ‚¬μš©ν•΄μ„œ νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €μ— λ³΄κ΄€λœ 컀λ„₯μ…˜μ„ κΊΌλ‚΄μ„œ μ‚¬μš©ν•œλ‹€.
        Connection con = DataSourceUtils.getConnection(dataSource);
        log.info("getConnection={}, class={}", con, con.getClass());
        return con;
    }
}

DataSourceUtils.getConnection()

  • getConnection() μ—μ„œ DataSourceUtils.getConnection() λ₯Ό μ‚¬μš©ν•˜λ„λ‘ λ³€κ²½λœ 뢀뢄을 특히 μ£Όμ˜ν•΄μ•Ό ν•œλ‹€.
  • DataSourceUtils.getConnection() λŠ” λ‹€μŒκ³Ό 같이 λ™μž‘ν•œλ‹€.
    • νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €κ°€ κ΄€λ¦¬ν•˜λŠ” 컀λ„₯μ…˜μ΄ 있으면 ν•΄λ‹Ή 컀λ„₯μ…˜μ„ λ°˜ν™˜ν•œλ‹€.
    • νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €κ°€ κ΄€λ¦¬ν•˜λŠ” 컀λ„₯μ…˜μ΄ μ—†λŠ” 경우 μƒˆλ‘œμš΄ 컀λ„₯μ…˜μ„ μƒμ„±ν•΄μ„œ λ°˜ν™˜ν•œλ‹€.

DataSourceUtils.releaseConnection()

  • close() μ—μ„œ DataSourceUtils.releaseConnection() λ₯Ό μ‚¬μš©ν•˜λ„λ‘ λ³€κ²½λœ 뢀뢄을 특히 μ£Όμ˜ν•΄μ•Ό ν•œλ‹€. 컀λ„₯μ…˜μ„ con.close() λ₯Ό μ‚¬μš©ν•΄μ„œ 직접 닫아버리면 컀λ„₯μ…˜μ΄ μœ μ§€λ˜μ§€ μ•ŠλŠ” λ¬Έμ œκ°€ λ°œμƒν•œλ‹€. 이 컀λ„₯μ…˜μ€ 이후 λ‘œμ§μ€ 물둠이고, νŠΈλžœμž­μ…˜μ„ μ’…λ£Œ(컀밋, λ‘€λ°±)ν•  λ•Œ κΉŒμ§€ μ‚΄μ•„μžˆμ–΄μ•Ό ν•œλ‹€.
  • DataSourceUtils.releaseConnection() 을 μ‚¬μš©ν•˜λ©΄ 컀λ„₯μ…˜μ„ λ°”λ‘œ λ‹«λŠ” 것이 μ•„λ‹ˆλ‹€.
    • νŠΈλžœμž­μ…˜μ„ μ‚¬μš©ν•˜κΈ° μœ„ν•΄ λ™κΈ°ν™”λœ 컀λ„₯μ…˜μ€ 컀λ„₯μ…˜μ„ 닫지 μ•Šκ³  κ·ΈλŒ€λ‘œ μœ μ§€ν•΄μ€€λ‹€.
    • νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €κ°€ κ΄€λ¦¬ν•˜λŠ” 컀λ„₯μ…˜μ΄ μ—†λŠ” 경우 ν•΄λ‹Ή 컀λ„₯μ…˜μ„ λ‹«λŠ”λ‹€.

MemberServiceV3_1

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV2;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

/**
 *  νŠΈλžœμž­μ…˜ - νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ € : μ˜μ‘΄κ΄€κ³„~ λ‹¨μΌμ±…μž„ 확립.
 */

@Slf4j
@RequiredArgsConstructor
public class MemberServiceV3_1 {

    //    private final DataSource dataSource;
    private final PlatformTransactionManager transactionManager;
    private final MemberRepositoryV3 memberRepository;

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {

        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            // λΉ„μ¦ˆλ‹ˆμŠ€ 둜직
            bizLogic(money, fromId, toId);
            // 성곡 μ‹œ 컀밋
            transactionManager.commit(status);

        } catch (Exception e) {
            // μ‹€νŒ¨ μ‹œ λ‘€λ°±
            transactionManager.rollback(status);
            throw new IllegalStateException(e);
        }   // 이제 release ν•  ν•„μš”κ°€ μ—†μŒ. νŠΈλžœμž­μ…˜μ΄ μ»€λ°‹λ˜κ±°λ‚˜ λ‘€λ°± 되면 λ‹€ λλ‚œ 것이기 λ•Œλ¬Έμ— νŠΈλžœμž­μ…˜μ΄ μ’…λ£Œλ¨.(더 이상 컀λ„₯μ…˜μ„ μ“Έ 일이 μ—†μŒ

    }

    private void bizLogic(int money, String fromId, String toId) throws SQLException {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(toId, toMember.getMoney() + money);
    }

    // μ˜ˆμ™Έ 상황 ν…ŒμŠ€νŠΈ ν•˜κΈ° μœ„ν•΄ id ex 인 경우 검증
    private void validation(Member toMember) {
        if (toMember.getMemberId().equals("ex")) {
            throw new IllegalStateException("이체쀑 μ˜ˆμ™Έ λ°œμƒ");
        }
    }
}
  • private final PlatformTransactionManager transactionManager
    • νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λ₯Ό μ£Όμž… λ°›λŠ”λ‹€. μ§€κΈˆμ€ JDBC κΈ°μˆ μ„ μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ— DataSourceTransactionManager κ΅¬ν˜„μ²΄λ₯Ό μ£Όμž… λ°›μ•„μ•Ό ν•œλ‹€.
    • λ¬Όλ‘  JPA 같은 기술둜 λ³€κ²½λ˜λ©΄ JpaTransactionManager λ₯Ό μ£Όμž… λ°›μœΌλ©΄ λœλ‹€.
  • transactionManager.getTransaction()
    • νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•œλ‹€.
    • TransactionStatus status λ₯Ό λ°˜ν™˜ν•œλ‹€. ν˜„μž¬ νŠΈλžœμž­μ…˜μ˜ μƒνƒœ 정보가 ν¬ν•¨λ˜μ–΄ μžˆλ‹€. 이후 νŠΈλžœμž­μ…˜μ„ 컀밋, λ‘€λ°±ν•  λ•Œ ν•„μš”ν•˜λ‹€.
  • new DefaultTransactionDefinition()
    • νŠΈλžœμž­μ…˜κ³Ό κ΄€λ ¨λœ μ˜΅μ…˜μ„ 지정할 수 μžˆλ‹€. μžμ„Έν•œ λ‚΄μš©μ€ λ’€μ—μ„œ μ„€λͺ…ν•œλ‹€.
  • transactionManager.commit(status)
    • νŠΈλžœμž­μ…˜μ΄ μ„±κ³΅ν•˜λ©΄ 이 λ‘œμ§μ„ ν˜ΈμΆœν•΄μ„œ μ»€λ°‹ν•˜λ©΄ λœλ‹€.
  • transactionManager.rollback(status)
    • λ¬Έμ œκ°€ λ°œμƒν•˜λ©΄ 이 λ‘œμ§μ„ ν˜ΈμΆœν•΄μ„œ νŠΈλžœμž­μ…˜μ„ λ‘€λ°±ν•˜λ©΄ λœλ‹€.

MemberServiceV3_1Test

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV2;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;

import java.sql.SQLException;

import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
 *  νŠΈλžœμž­μ…˜ - νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €
 */
@Slf4j
class MemberServiceV3_1Test {

    public static final String MEMBER_A = "memberA";
    public static final String MEMBER_B = "memberB";
    public static final String MEMBER_EX = "ex";

    private MemberRepositoryV3 memberRepository;
    private MemberServiceV3_1 memberService;

    @BeforeEach
    void before() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        memberRepository = new MemberRepositoryV3(dataSource);
        PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);   // DataSource λ₯Ό μ΄μš©ν•΄μ„œ νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ € 생성
        memberService = new MemberServiceV3_1(transactionManager, memberRepository);
    }

    @AfterEach
    void after() throws SQLException {
        memberRepository.delete(MEMBER_A);
        memberRepository.delete(MEMBER_B);
        memberRepository.delete(MEMBER_EX);
    }

    @Test
    @DisplayName("이체쀑 μ˜ˆμ™Έ λ°œμƒ")
    public void accountTransferEx() throws Exception {
        // given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberEx = new Member(MEMBER_EX, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberEx);

        // when
        assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000))
                .isInstanceOf(IllegalStateException.class);

        // then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberEx.getMemberId());
        assertThat(findMemberA.getMoney()).isEqualTo(10000);    // μ˜ˆμ™Έ λ°œμƒν–ˆκΈ° λ•Œλ¬Έμ— rollback 됨
        assertThat(findMemberB.getMoney()).isEqualTo(10000);
    }

    @Test
    @DisplayName("정상 이체")
    public void accountTransfer() throws Exception {
        // given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_B, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberB);

        // when
        log.info("START TX");
        memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
        log.info("END TX");

        // then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberB.getMemberId());
        Assertions.assertThat(findMemberA.getMoney()).isEqualTo(8000);
        Assertions.assertThat(findMemberB.getMoney()).isEqualTo(12000);
    }
}
  • new DataSourceTransactionManager(dataSource)
    • JDBC κΈ°μˆ μ„ μ‚¬μš©ν•˜λ―€λ‘œ, JDBC용 νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €( DataSourceTransactionManager )λ₯Ό μ„ νƒν•΄μ„œ μ„œλΉ„μŠ€μ— μ£Όμž…ν•œλ‹€.
    • νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λŠ” λ°μ΄ν„°μ†ŒμŠ€λ₯Ό 톡해 컀λ„₯μ…˜μ„ μƒμ„±ν•˜λ―€λ‘œ DataSource κ°€ ν•„μš”ν•˜λ‹€.

νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €: λ™μž‘ 흐름

νŠΈλžœμž­μ…˜ μ‹œμž‘


ν΄λΌμ΄μ–ΈνŠΈμ˜ μš”μ²­μœΌλ‘œ μ„œλΉ„μŠ€ λ‘œμ§μ„ μ‹€ν–‰ν•œλ‹€.
1. μ„œλΉ„μŠ€ κ³„μΈ΅μ—μ„œ transactionManager.getTransaction() 을 ν˜ΈμΆœν•΄μ„œ νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•œλ‹€.
2. νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•˜λ €λ©΄ λ¨Όμ € λ°μ΄ν„°λ² μ΄μŠ€ 컀λ„₯μ…˜μ΄ ν•„μš”ν•˜λ‹€. νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λŠ” λ‚΄λΆ€μ—μ„œ λ°μ΄ν„°μ†ŒμŠ€λ₯Ό μ‚¬μš©ν•΄μ„œ 컀λ„₯μ…˜μ„ μƒμ„±ν•œλ‹€.
3. 컀λ„₯μ…˜μ„ μˆ˜λ™ 컀밋 λͺ¨λ“œλ‘œ λ³€κ²½ν•΄μ„œ μ‹€μ œ λ°μ΄ν„°λ² μ΄μŠ€ νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•œλ‹€.
4. 컀λ„₯μ…˜μ„ νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €μ— λ³΄κ΄€ν•œλ‹€.
5. νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €λŠ” μ“°λ ˆλ“œ λ‘œμ»¬μ— 컀λ„₯μ…˜μ„ λ³΄κ΄€ν•œλ‹€. λ”°λΌμ„œ λ©€ν‹° μ“°λ ˆλ“œ ν™˜κ²½μ— μ•ˆμ „ν•˜κ²Œ 컀λ„₯μ…˜μ„ 보관할 수 μžˆλ‹€.

둜직 μ‹€ν–‰


6. μ„œλΉ„μŠ€λŠ” λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μ‹€ν–‰ν•˜λ©΄μ„œ λ¦¬ν¬μ§€ν† λ¦¬μ˜ λ©”μ„œλ“œλ“€μ„ ν˜ΈμΆœν•œλ‹€. μ΄λ•Œ 컀λ„₯μ…˜μ„ νŒŒλΌλ―Έν„°λ‘œ μ „λ‹¬ν•˜μ§€ μ•ŠλŠ”λ‹€.
7. 리포지토리 λ©”μ„œλ“œλ“€μ€ νŠΈλžœμž­μ…˜μ΄ μ‹œμž‘λœ 컀λ„₯μ…˜μ΄ ν•„μš”ν•˜λ‹€. λ¦¬ν¬μ§€ν† λ¦¬λŠ” DataSourceUtils.getConnection() 을 μ‚¬μš©ν•΄μ„œ νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €μ— λ³΄κ΄€λœ 컀λ„₯μ…˜μ„ κΊΌλ‚΄μ„œ μ‚¬μš©ν•œλ‹€. 이 과정을 ν†΅ν•΄μ„œ μžμ—°μŠ€λŸ½κ²Œ 같은 컀λ„₯μ…˜μ„ μ‚¬μš©ν•˜κ³ , νŠΈλžœμž­μ…˜λ„ μœ μ§€λœλ‹€.
8. νšλ“ν•œ 컀λ„₯μ…˜μ„ μ‚¬μš©ν•΄μ„œ SQL을 λ°μ΄ν„°λ² μ΄μŠ€μ— μ „λ‹¬ν•΄μ„œ μ‹€ν–‰ν•œλ‹€.

νŠΈλžœμž­μ…˜ μ’…λ£Œ


9. λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 λλ‚˜κ³  νŠΈλžœμž­μ…˜μ„ μ’…λ£Œν•œλ‹€. νŠΈλžœμž­μ…˜μ€ μ»€λ°‹ν•˜κ±°λ‚˜ λ‘€λ°±ν•˜λ©΄ μ’…λ£Œλœλ‹€.
10. νŠΈλžœμž­μ…˜μ„ μ’…λ£Œν•˜λ €λ©΄ λ™κΈ°ν™”λœ 컀λ„₯μ…˜μ΄ ν•„μš”ν•˜λ‹€. νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €λ₯Ό 톡해 λ™κΈ°ν™”λœ 컀λ„₯μ…˜μ„ νšλ“ν•œλ‹€.
11. νšλ“ν•œ 컀λ„₯μ…˜μ„ 톡해 λ°μ΄ν„°λ² μ΄μŠ€μ— νŠΈλžœμž­μ…˜μ„ μ»€λ°‹ν•˜κ±°λ‚˜ λ‘€λ°±ν•œλ‹€.
12. 전체 λ¦¬μ†ŒμŠ€λ₯Ό μ •λ¦¬ν•œλ‹€.

  • νŠΈλžœμž­μ…˜ 동기화 λ§€λ‹ˆμ €λ₯Ό μ •λ¦¬ν•œλ‹€. μ“°λ ˆλ“œ λ‘œμ»¬μ€ μ‚¬μš©ν›„ κΌ­ 정리해야 ν•œλ‹€.
  • con.setAutoCommit(true) 둜 λ˜λŒλ¦°λ‹€. 컀λ„₯μ…˜ 풀을 κ³ λ €ν•΄μ•Ό ν•œλ‹€.
  • con.close() λ₯Ό ν˜ΈμΆœν•΄μ…” 컀λ„₯μ…˜μ„ μ’…λ£Œν•œλ‹€. 컀λ„₯μ…˜ 풀을 μ‚¬μš©ν•˜λŠ” 경우 con.close() λ₯Ό ν˜ΈμΆœν•˜λ©΄ 컀λ„₯μ…˜ 풀에 λ°˜ν™˜λœλ‹€.

문제 ν•΄κ²° - 🧐 νŠΈλžœμž­μ…˜ ν…œν”Œλ¦Ώ 적용

νŠΈλžœμž­μ…˜ μ‚¬μš© 둜직 쀑 λ‹€μŒ 뢀뢄이 λ°˜λ³΅λœλ‹€.

//νŠΈλžœμž­μ…˜ μ‹œμž‘
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

try {
	//λΉ„μ¦ˆλ‹ˆμŠ€ 둜직
    bizLogic(fromId, toId, money);
	transactionManager.commit(status); //μ„±κ³΅μ‹œ 컀밋
} catch (Exception e) {
	transactionManager.rollback(status); //μ‹€νŒ¨μ‹œ λ‘€λ°±
    throw new IllegalStateException(e);
 }
  • 이쀑 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직만 λ‹¬λΌμ§€λŠ” 것을 확인 κ°€λŠ₯ν•˜λ‹€. 이럴 λ•Œ ν…œν”Œλ¦Ώ 콜백 νŒ¨ν„΄μ„ ν™œμš©ν•˜λ©΄ 이런 반볡 문제λ₯Ό κΉ”λ”ν•˜κ²Œ ν•΄κ²°ν•  수 μžˆλ‹€. 이λ₯Ό μ μš©ν•΄μ„œ μ„œλΉ„μŠ€ μ½”λ“œλ₯Ό μˆ˜μ •ν–ˆλ‹€.

MemberServiceV3_2

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;

import java.sql.SQLException;

/**
 *  νŠΈλžœμž­μ…˜ - νŠΈλžœμž­μ…˜ ν…œν”Œλ¦Ώ : λΉ„μ¦ˆλ‹ˆμŠ€ μ œμ™Έν•œ λ°˜λ³΅λ˜λŠ” μ½”λ“œ 제거
 */

@Slf4j
public class MemberServiceV3_2 {

//    private final PlatformTransactionManager transactionManager;
    TransactionTemplate txTemplate;

    public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepository) {
        this.txTemplate = new TransactionTemplate(transactionManager);
        this.memberRepository = memberRepository;
    }

    private final MemberRepositoryV3 memberRepository;

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {

        txTemplate.executeWithoutResult((status) -> {
            try {
                bizLogic(money, fromId, toId);
            } catch (SQLException e) {
                throw new IllegalStateException(e);
            }
        });
    }

    private void bizLogic(int money, String fromId, String toId) throws SQLException {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(toId, toMember.getMoney() + money);
    }

    // μ˜ˆμ™Έ 상황 ν…ŒμŠ€νŠΈ ν•˜κΈ° μœ„ν•΄ id ex 인 경우 검증
    private void validation(Member toMember) {
        if (toMember.getMemberId().equals("ex")) {
            throw new IllegalStateException("이체쀑 μ˜ˆμ™Έ λ°œμƒ");
        }
    }
}
  • νŠΈλžœμž­μ…˜ ν…œν”Œλ¦Ώ 덕뢄에 νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•˜κ³ , μ»€λ°‹ν•˜κ±°λ‚˜ λ‘€λ°±ν•˜λŠ” μ½”λ“œκ°€ λͺ¨λ‘ 제거된 것을 확인할 수 μžˆλ‹€.
  • MemberServiceV3_2TestλŠ” κΈ°μ‘΄κ³Ό κ°™λ‹€. ν…ŒμŠ€νŠΈλ₯Ό 싀행해보면 정상 λ™μž‘ν•˜κ³ , μ‹€νŒ¨μ‹œ 둀백도 잘 μˆ˜ν–‰λ˜λŠ” 것을 확인할 수 μžˆλ‹€.

문제 ν•΄κ²° - πŸ˜ƒ νŠΈλžœμž­μ…˜ AOP

μ„œλΉ„μŠ€ 계측에 μˆœμˆ˜ν•œ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직만 남기도둝, μŠ€ν”„λ§ AOPλ₯Ό 톡해 ν”„λ‘μ‹œλ₯Ό λ„μž…ν•˜μž.

ν”„λ‘μ‹œ λ„μž…

  • ν”„λ‘μ‹œλ₯Ό λ„μž…ν•˜κΈ° μ „ μ„œλΉ„μŠ€μ— λΉ„μ¦ˆλ‹ˆμŠ€ 둜직과 νŠΈλžœμž­μ…˜ 처리 둜직이 μ„žμ—¬ μžˆμ—ˆλ‹€.
  • ν”„λ‘μ‹œ λ„μž… ν›„μ—λŠ” μœ„μ˜ 그림처럼 νŠΈλžœμž­μ…˜ ν”„λ‘μ‹œκ°€ νŠΈλžœμž­μ…˜ 처리 λ‘œμ§μ„ λͺ¨λ‘ κ°€μ Έκ°„λ‹€. 그리고 νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•œ 후에 μ‹€μ œ μ„œλΉ„μŠ€λ₯Ό λŒ€μ‹  ν˜ΈμΆœν•œλ‹€.
  • νŠΈλžœμž­μ…˜ ν”„λ‘μ‹œ 덕뢄에 μ„œλΉ„μŠ€ κ³„μΈ΅μ—λŠ” μˆœμˆ˜ν•œ λΉ„μ¦ˆλ‹ˆμ¦ˆ 둜직만 남길 수 μžˆλ‹€.

@Transactional

  • κ°œλ°œμžλŠ” νŠΈλžœμž­μ…˜ μ²˜λ¦¬κ°€ ν•„μš”ν•œ 곳에 @Transactional μ• λ…Έν…Œμ΄μ…˜λ§Œ λΆ™μ—¬μ£Όλ©΄ λœλ‹€. μŠ€ν”„λ§μ˜ νŠΈλžœμž­μ…˜ AOPλŠ” 이 μ• λ…Έν…Œμ΄μ…˜μ„ μΈμ‹ν•΄μ„œ νŠΈλžœμž­μ…˜ ν”„λ‘μ‹œλ₯Ό μ μš©ν•΄μ€€λ‹€.

이λ₯Ό μ μš©ν•œ μƒˆλ‘œμš΄ μ„œλΉ„μŠ€ 클래슀λ₯Ό ν™•μΈν•΄λ³΄μž.

MemberServiceV3_3

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;

import java.sql.SQLException;

/**
 *  νŠΈλžœμž­μ…˜ - @Transactional AOP
 */

@Slf4j
public class MemberServiceV3_3 {

    private final MemberRepositoryV3 memberRepository;

    public MemberServiceV3_3(MemberRepositoryV3 memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Transactional  // ν•΄λ‹Ή λ©”μ„œλ“œκ°€ 호좜될 λ•Œ νŠΈλžœμž­μ…˜μ„ κ±Έκ³  μ‹œμž‘ν•œλ‹€. μ„±κ³΅ν•˜λ©΄ commit, λŸ°νƒ€μž„ μ˜ˆμ™Έκ°€ 터지면 rollback. 이게 μ–΄λ…Έν…Œμ΄μ…˜ ν•˜λ‚˜λ‘œ 끝남!
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        bizLogic(money, fromId, toId);
    }

    private void bizLogic(int money, String fromId, String toId) throws SQLException {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(toId, toMember.getMoney() + money);
    }

    // μ˜ˆμ™Έ 상황 ν…ŒμŠ€νŠΈ ν•˜κΈ° μœ„ν•΄ id ex 인 경우 검증
    private void validation(Member toMember) {
        if (toMember.getMemberId().equals("ex")) {
            throw new IllegalStateException("이체쀑 μ˜ˆμ™Έ λ°œμƒ");
        }
    }
}
  • μˆœμˆ˜ν•œ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직만 남기고, νŠΈλžœμž­μ…˜ κ΄€λ ¨ μ½”λ“œλŠ” λͺ¨λ‘ μ œκ±°ν–ˆλ‹€.
  • μŠ€ν”„λ§μ΄ μ œκ³΅ν•˜λŠ” νŠΈλžœμž­μ…˜ AOPλ₯Ό μ μš©ν•˜κΈ° μœ„ν•΄ @Transactional μ• λ…Έν…Œμ΄μ…˜μ„ μΆ”κ°€ν–ˆλ‹€.
  • @Transactional μ• λ…Έν…Œμ΄μ…˜μ€ λ©”μ„œλ“œμ— 뢙여도 되고, ν΄λž˜μŠ€μ— 뢙여도 λœλ‹€. ν΄λž˜μŠ€μ— 뢙이면 μ™ΈλΆ€μ—μ„œ 호좜 κ°€λŠ₯ν•œ public λ©”μ„œλ“œκ°€ AOP 적용 λŒ€μƒμ΄ λœλ‹€.

MemberServiceV3_3Test

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.sql.SQLException;

import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
 *  νŠΈλžœμž­μ…˜ - @Transactional AOP
 */
@Slf4j
@SpringBootTest
class MemberServiceV3_3Test {

    /**
     *  κΈ°μ‘΄ before() 방식은 μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆμ—μ„œ μ£Όμž…λ°›λŠ” 게 μ•„λ‹ˆλΌ, λ°”λ‘œλ°”λ‘œ λ°μ΄ν„°μ†ŒμŠ€μ—μ„œ κΊΌλ‚΄μ“°λŠ” 방식.
     *  μŠ€ν”„λ§ AOP λ₯Ό μ΄μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ”, (@SpringBootTest 둜)μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆλ₯Ό μƒμ„±ν•˜κ³  (@Autowired 둜)빈 등둝을 ν•˜κ³  μ£Όμž…λ°›μ•„ 써야 함.
     */

    public static final String MEMBER_A = "memberA";
    public static final String MEMBER_B = "memberB";
    public static final String MEMBER_EX = "ex";

    @Autowired
    private MemberRepositoryV3 memberRepository;
    @Autowired
    private MemberServiceV3_3 memberService;

    @TestConfiguration
    static class TestConfig {
        @Bean
        DataSource dataSource() {
            return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        }

        @Bean
        PlatformTransactionManager transactionManager() {
            return new DataSourceTransactionManager(dataSource());
        }

        @Bean
        MemberRepositoryV3 memberRepositoryV3() {
            return new MemberRepositoryV3(dataSource());
        }

        @Bean
        MemberServiceV3_3 memberServiceV3_3() {
            return new MemberServiceV3_3(memberRepositoryV3());
        }
    }

    @Test
    void AOPCheck() {
        log.info("memberService class={}", memberService.getClass());       // $$SpringCGLIB$$ -> μ„œλΉ„μŠ€μ˜ 경우 μŠ€ν”„λ§μ΄ @Transactional 보자마자 'λ„ˆλŠ” AOP 적용 λŒ€μƒμ΄κ΅¬λ‚˜!'ν•˜λ©΄μ„œ νŠΈλžœμž­μ…˜ ν”„λ‘μ‹œλ₯Ό λ§Œλ“€μ–΄μ„œ λŒ€μ‹  적용됐기 λ•Œλ¬Έμ— 클래슀 정보가 μ΄λ ‡κ²Œ λ‚˜μ˜΄? λ‹€μ‹œ 확인
        log.info("memberRepository class={}", memberRepository.getClass()); // κΈ°λ³Έ 클래슀 정보 λ‚˜μ˜΄
        assertThat(AopUtils.isAopProxy(memberService)).isTrue();            // μ„œλΉ„μŠ€λŠ” @Transactional κ±Έλ € μžˆμ–΄μ„œ AOP λ§žμœΌλ‹ˆκΉŒ true
        assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();        // λ ˆν¬λŠ” AOP μ•„λ‹ˆλ‹ˆκΉŒ false
        // TODO ν”„λ‘μ‹œ 쑰사?
    }

    @AfterEach
    void after() throws SQLException {
        memberRepository.delete(MEMBER_A);
        memberRepository.delete(MEMBER_B);
        memberRepository.delete(MEMBER_EX);
    }

    @Test
    @DisplayName("이체쀑 μ˜ˆμ™Έ λ°œμƒ")
    public void accountTransferEx() throws Exception {
        // given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberEx = new Member(MEMBER_EX, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberEx);

        // when
        assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000))
                .isInstanceOf(IllegalStateException.class);

        // then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberEx.getMemberId());
        assertThat(findMemberA.getMoney()).isEqualTo(10000);    // μ˜ˆμ™Έ λ°œμƒν–ˆκΈ° λ•Œλ¬Έμ— rollback 됨
        assertThat(findMemberB.getMoney()).isEqualTo(10000);
    }

    @Test
    @DisplayName("정상 이체")
    public void accountTransfer() throws Exception {
        // given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_B, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberB);

        // when
        log.info("START TX");
        memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
        log.info("END TX");

        // then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberB.getMemberId());
        Assertions.assertThat(findMemberA.getMoney()).isEqualTo(8000);
        Assertions.assertThat(findMemberB.getMoney()).isEqualTo(12000);
    }
}
  • @SpringBootTest : μŠ€ν”„λ§ AOPλ₯Ό μ μš©ν•˜λ €λ©΄ μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆκ°€ ν•„μš”ν•˜λ‹€. 이 μ–΄λ…Έν…Œμ΄μ…˜μ΄ 있으면 ν…ŒμŠ€νŠΈ μ‹œ μŠ€ν”„λ§ λΆ€νŠΈλ₯Ό 톡해 μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆλ₯Ό μƒμ„±ν•œλ‹€. 그리고 ν…ŒμŠ€νŠΈμ—μ„œ @Autowired 등을 톡해 μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆκ°€ κ΄€λ¦¬ν•˜λŠ” λΉˆλ“€μ„ μ‚¬μš©ν•  수 μžˆλ‹€.
  • @TestConfiguration : ν…ŒμŠ€νŠΈ μ•ˆμ—μ„œ λ‚΄λΆ€ μ„€μ • 클래슀λ₯Ό λ§Œλ“€μ–΄μ„œ μ‚¬μš©ν•˜λ©΄μ„œ 이 μ—λ…Έν…Œμ΄μ…˜μ„ 뢙이면, μŠ€ν”„λ§ λΆ€νŠΈκ°€ μžλ™μœΌλ‘œ λ§Œλ“€μ–΄μ£ΌλŠ” λΉˆλ“€μ— μΆ”κ°€λ‘œ ν•„μš”ν•œ μŠ€ν”„λ§ λΉˆλ“€μ„ λ“±λ‘ν•˜κ³  ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•  수 μžˆλ‹€.
  • TestConfig
    • DataSource μŠ€ν”„λ§μ—μ„œ 기본으둜 μ‚¬μš©ν•  λ°μ΄ν„°μ†ŒμŠ€λ₯Ό μŠ€ν”„λ§ 빈으둜 λ“±λ‘ν•œλ‹€. μΆ”κ°€λ‘œ νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €μ—μ„œλ„ μ‚¬μš©ν•œλ‹€.
    • DataSourceTransactionManager νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λ₯Ό μŠ€ν”„λ§ 빈으둜 λ“±λ‘ν•œλ‹€. μŠ€ν”„λ§μ΄ μ œκ³΅ν•˜λŠ” νŠΈλžœμž­μ…˜ AOPλŠ” μŠ€ν”„λ§ λΉˆμ— λ“±λ‘λœ νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λ₯Ό μ°Ύμ•„μ„œ μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ— νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λ₯Ό μŠ€ν”„λ§ 빈으둜 등둝해두어야 ν•œλ‹€.
  • AopCheck()
    • λ¨Όμ € AOP ν”„λ‘μ‹œκ°€ μ μš©λ˜μ—ˆλŠ”μ§€ ν™•μΈν•΄λ³΄μž. AopCheck() 의 μ‹€ν–‰ κ²°κ³Όλ₯Ό 보면 memberService 에 EnhancerBySpringCGLIB.. λΌλŠ” 뢀뢄을 톡해 ν”„λ‘μ‹œ(CGLIB)κ°€ 적용된 것을 확인할 수 μžˆλ‹€. memberRepository μ—λŠ” AOPλ₯Ό μ μš©ν•˜μ§€ μ•Šμ•˜κΈ° λ•Œλ¬Έμ— ν”„λ‘μ‹œκ°€ μ μš©λ˜μ§€ μ•ŠλŠ”λ‹€.
    • λ‚˜λ¨Έμ§€ ν…ŒμŠ€νŠΈ μ½”λ“œλ“€μ„ 싀행해보면 νŠΈλžœμž­μ…˜μ΄ 정상 μˆ˜ν–‰λ˜κ³ , μ‹€νŒ¨μ‹œ 정상 둀백된 것을 확인할 수 μžˆλ‹€.

νŠΈλžœμž­μ…˜ AOP 정리

  • 선언적 νŠΈλžœμž­μ…˜ 관리(Declarative Transaction Management)
    • @Transactional μ• λ…Έν…Œμ΄μ…˜ ν•˜λ‚˜λ§Œ μ„ μ–Έν•΄μ„œ 맀우 νŽΈλ¦¬ν•˜κ²Œ νŠΈλžœμž­μ…˜μ„ μ μš©ν•˜λŠ” 것을 선언적 트랜잭 μ…˜ 관리라 ν•œλ‹€.
  • ν”„λ‘œκ·Έλž˜λ° λ°©μ‹μ˜ νŠΈλžœμž­μ…˜ 관리(programmatic transaction management)
    • νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ € λ˜λŠ” νŠΈλžœμž­μ…˜ ν…œν”Œλ¦Ώ 등을 μ‚¬μš©ν•΄μ„œ νŠΈλžœμž­μ…˜ κ΄€λ ¨ μ½”λ“œλ₯Ό 직접 μž‘μ„±ν•˜λŠ” 것을 ν”„λ‘œκ·Έλž˜λ° λ°©μ‹μ˜ νŠΈλžœμž­μ…˜ 관리라 ν•œλ‹€.

μ‹€λ¬΄μ—μ„œλŠ” λŒ€λΆ€λΆ„ 선언적 νŠΈλžœμž­μ…˜ 관리λ₯Ό μ‚¬μš©ν•œλ‹€!


μŠ€ν”„λ§ λΆ€νŠΈμ˜ μžλ™ λ¦¬μ†ŒμŠ€ 등둝

λ°μ΄ν„°μ†ŒμŠ€ μžλ™ λ“±λ‘μ΄λž€?

  • μŠ€ν”„λ§ λΆ€νŠΈλŠ” λ°μ΄ν„°μ†ŒμŠ€( DataSource )λ₯Ό μŠ€ν”„λ§ λΉˆμ— μžλ™μœΌλ‘œ λ“±λ‘ν•œλ‹€.
  • μžλ™μœΌλ‘œ λ“±λ‘λ˜λŠ” μŠ€ν”„λ§ 빈 이름: dataSource
  • 참고둜 κ°œλ°œμžκ°€ 직접 λ°μ΄ν„°μ†ŒμŠ€λ₯Ό 빈으둜 λ“±λ‘ν•˜λ©΄ μŠ€ν”„λ§ λΆ€νŠΈλŠ” λ°μ΄ν„°μ†ŒμŠ€λ₯Ό μžλ™μœΌλ‘œ λ“±λ‘ν•˜μ§€ μ•ŠλŠ”λ‹€.

application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
  • μŠ€ν”„λ§ λΆ€νŠΈκ°€ 기본으둜 μƒμ„±ν•˜λŠ” λ°μ΄ν„°μ†ŒμŠ€λŠ” 컀λ„₯μ…˜ν’€μ„ μ œκ³΅ν•˜λŠ” HikariDataSource 이닀. 컀λ„₯μ…˜ν’€κ³Ό κ΄€λ ¨λœ 섀정도 application.properties λ₯Ό ν†΅ν•΄μ„œ 지정할 수 μžˆλ‹€.
  • spring.datasource.url 속성이 μ—†μœΌλ©΄ λ‚΄μž₯ λ°μ΄ν„°λ² μ΄μŠ€(λ©”λͺ¨λ¦¬ DB)λ₯Ό μƒμ„±ν•˜λ €κ³  μ‹œλ„ν•œλ‹€.

νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ € μžλ™ λ“±λ‘μ΄λž€?

  • μŠ€ν”„λ§ λΆ€νŠΈλŠ” μ μ ˆν•œ νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €( PlatformTransactionManager )λ₯Ό μžλ™μœΌλ‘œ μŠ€ν”„λ§ λΉˆμ— λ“±λ‘ν•œλ‹€.
  • μžλ™μœΌλ‘œ λ“±λ‘λ˜λŠ” μŠ€ν”„λ§ 빈 이름: transactionManager
  • 참고둜 κ°œλ°œμžκ°€ 직접 νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λ₯Ό 빈으둜 λ“±λ‘ν•˜λ©΄ μŠ€ν”„λ§ λΆ€νŠΈλŠ” νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λ₯Ό μžλ™μœΌλ‘œ λ“±λ‘ν•˜μ§€ μ•ŠλŠ”λ‹€.

λ°μ΄ν„°μ†ŒμŠ€μ™€ νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ € μžλ™ 등둝

  • application.properties νŒŒμΌμ— μœ„μ—μ„œ λ‚˜μ˜¨ λ‚΄μš©μ„ μž‘μ„±ν•œλ‹€.

MemberServiceV3_4Test

package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.sql.SQLException;

import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
 *  νŠΈλžœμž­μ…˜ - @Transactional AOP
 *  Datasource, Transaction Manager μžλ™ 등둝
 */
@Slf4j
@SpringBootTest
class MemberServiceV3_4Test {

    /**
     *  κΈ°μ‘΄ before() 방식은 μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆμ—μ„œ μ£Όμž…λ°›λŠ” 게 μ•„λ‹ˆλΌ, λ°”λ‘œλ°”λ‘œ λ°μ΄ν„°μ†ŒμŠ€μ—μ„œ κΊΌλ‚΄μ“°λŠ” 방식.
     *  μŠ€ν”„λ§ AOP λ₯Ό μ΄μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ”, (@SpringBootTest 둜)μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆλ₯Ό μƒμ„±ν•˜κ³  (@Autowired 둜)빈 등둝을 ν•˜κ³  μ£Όμž…λ°›μ•„ 써야 함.
     */

    public static final String MEMBER_A = "memberA";
    public static final String MEMBER_B = "memberB";
    public static final String MEMBER_EX = "ex";

    @Autowired
    private MemberRepositoryV3 memberRepository;
    @Autowired
    private MemberServiceV3_3 memberService;

    @TestConfiguration
    static class TestConfig {

        // μŠ€ν”„λ§μ΄ μžλ™μœΌλ‘œ μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆμ— 등둝해쀀 dataSorce λ₯Ό λ„£κ³ , μ•„λž˜ repo- μ—μ„œ κ°€μ Έλ‹€ μ“Έ 수 있음.
        private final DataSource dataSource;
        public TestConfig(DataSource dataSource) {
            this.dataSource = dataSource;
        }

        @Bean
        MemberRepositoryV3 memberRepositoryV3() {
            return new MemberRepositoryV3(dataSource);
        }

        @Bean
        MemberServiceV3_3 memberServiceV3_3() {
            return new MemberServiceV3_3(memberRepositoryV3());
        }
    }

    @Test
    void AOPCheck() {
        log.info("memberService class={}", memberService.getClass());       // $$SpringCGLIB$$ -> μ„œλΉ„μŠ€μ˜ 경우 μŠ€ν”„λ§μ΄ @Transactional 보자마자 'λ„ˆλŠ” AOP 적용 λŒ€μƒμ΄κ΅¬λ‚˜!'ν•˜λ©΄μ„œ νŠΈλžœμž­μ…˜ ν”„λ‘μ‹œλ₯Ό λ§Œλ“€μ–΄μ„œ λŒ€μ‹  적용됐기 λ•Œλ¬Έμ— 클래슀 정보가 μ΄λ ‡κ²Œ λ‚˜μ˜΄? λ‹€μ‹œ 확인
        log.info("memberRepository class={}", memberRepository.getClass()); // κΈ°λ³Έ 클래슀 정보 λ‚˜μ˜΄
        assertThat(AopUtils.isAopProxy(memberService)).isTrue();            // μ„œλΉ„μŠ€λŠ” @Transactional κ±Έλ € μžˆμ–΄μ„œ AOP λ§žμœΌλ‹ˆκΉŒ true
        assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();        // λ ˆν¬λŠ” AOP μ•„λ‹ˆλ‹ˆκΉŒ false
        // TODO ν”„λ‘μ‹œ 쑰사?
    }

    @AfterEach
    void after() throws SQLException {
        memberRepository.delete(MEMBER_A);
        memberRepository.delete(MEMBER_B);
        memberRepository.delete(MEMBER_EX);
    }

    @Test
    @DisplayName("이체쀑 μ˜ˆμ™Έ λ°œμƒ")
    public void accountTransferEx() throws Exception {
        // given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberEx = new Member(MEMBER_EX, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberEx);

        // when
        assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000))
                .isInstanceOf(IllegalStateException.class);

        // then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberEx.getMemberId());
        assertThat(findMemberA.getMoney()).isEqualTo(10000);    // μ˜ˆμ™Έ λ°œμƒν–ˆκΈ° λ•Œλ¬Έμ— rollback 됨
        assertThat(findMemberB.getMoney()).isEqualTo(10000);
    }

    @Test
    @DisplayName("정상 이체")
    public void accountTransfer() throws Exception {
        // given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_B, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberB);

        // when
        log.info("START TX");
        memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
        log.info("END TX");

        // then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberB.getMemberId());
        Assertions.assertThat(findMemberA.getMoney()).isEqualTo(8000);
        Assertions.assertThat(findMemberB.getMoney()).isEqualTo(12000);
    }
}
  • κΈ°μ‘΄( MemberServiceV3_3Test )κ³Ό 같은 μ½”λ“œμ΄κ³  TestConfig λΆ€λΆ„λ§Œ λ‹€λ₯΄λ‹€.
  • λ°μ΄ν„°μ†ŒμŠ€μ™€ νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λ₯Ό μŠ€ν”„λ§ 빈으둜 λ“±λ‘ν•˜λŠ” μ½”λ“œκ°€ μƒλž΅λ˜μ—ˆλ‹€. λ”°λΌμ„œ μŠ€ν”„λ§ λΆ€νŠΈκ°€
    application.properties 에 μ§€μ •λœ 속성을 μ°Έκ³ ν•΄μ„œ λ°μ΄ν„°μ†ŒμŠ€μ™€ νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €λ₯Ό μžλ™μœΌλ‘œ 생성해쀀닀.
  • μ½”λ“œμ—μ„œ λ³΄λŠ” 것 처럼 μƒμ„±μžλ₯Ό ν†΅ν•΄μ„œ μŠ€ν”„λ§ λΆ€νŠΈκ°€ λ§Œλ“€μ–΄μ€€ λ°μ΄ν„°μ†ŒμŠ€ λΉˆμ„ μ£Όμž… 받을 μˆ˜λ„ μžˆλ‹€.
  • 싀행해보면 λͺ¨λ“  ν…ŒμŠ€νŠΈκ°€ 정상 μˆ˜ν–‰λ˜λŠ” 것을 확인할 수 μžˆλ‹€.
profile
곡뢀 μ€‘μž…λ‹ˆλ‹€.

0개의 λŒ“κΈ€