πŸŽ— @Transactional(readOnly = true) λΆ™μ΄λŠ” 이유

κ°œμš”

μ˜€λŠ˜μ€ @Transaction, 특히 readOnly = true μ˜΅μ…˜μ— λŒ€ν•˜μ—¬ 깊이 탐ꡬ해 보겠닀.
μš°λ¦¬λŠ” @Transaction μ–΄λ…Έν…Œμ΄μ…˜μ„ 톡해(Spring AOP둜) νŠΈλ Œμ μ…˜ λ²”μœ„λ₯Ό μ‰½κ²Œ κ΅¬ν˜„ν•  수 μžˆλ‹€.

μ—¬κΈ°μ„œ μ‘°νšŒλ§Œμ„ μœ„ν•œ λ‘œμ§μ—λŠ” @Transactional(readOnly = true)μ˜΅μ…˜μ„ μΆ”κ°€ν•˜μ—¬ μ„±λŠ₯을 λ†’μ—¬μ£ΌλŠ” 사싀은 λͺ¨λ‘ μ•Œκ³ μžˆμ„ 것이닀. μ—¬κΈ°μ„œ μ–΄λ– ν•œ λ‘œμ§μ„ 거처 μ„±λŠ₯μƒμ˜ 이점을 μ–»μ–΄λ‚΄λŠ”μ§€ 탐ꡬ해 보겠닀.




@Transactional μžμ„Ένžˆ 보기


1. νŠΈλ Œμ μ…˜μ΄λž€ 무엇인가?

뜻

λ°μ΄ν„°λ² μ΄μŠ€μ˜ μƒνƒœλ₯Ό λ³€κ²½ν•˜κΈ°μœ„ν•΄ μˆ˜ν–‰ν•˜λŠ” μž‘μ—…μ˜ λ‹¨μœ„.

λͺ©μ 

λ°μ΄ν„°μ˜ 무결성을 보μž₯ν•˜λŠ” 것.

  • 무결성 : λ°μ΄ν„°μ˜ μ •ν™•μ„±, 일관성, μœ νš¨μ„±μ΄ μœ μ§€λ˜λŠ” 것. 데이터 값이 μ •ν™•ν•œ μƒνƒœ.
    • μ •ν™•μ„± : μ€‘λ³΅μ΄λ‚˜ λˆ„λ½μ΄ μ—†λŠ” μƒνƒœ.
    • 일관성 : μž‘μ—… μ „/ν›„ 데이터 μƒνƒœκ°€ μΌκ΄€λ˜κ²Œ μœ μ§€λ˜λŠ” μƒνƒœ.
    • μœ νš¨μ„± : μ‚¬μš©μžλ‘œλΆ€ν„° 값을 μž…λ ₯받을 λ•Œ, μ •ν™•ν•œ κ°’λ§Œμ„ μž…λ ₯받도둝 ν•˜λŠ” 것.
데이터 무결성과 데이터 μ •ν•©μ„±μ˜ 차이

무결성은 μœ„μ—μ„œ μ„€λͺ…ν•œλŒ€λ‘œ 데이터가 μ •ν™•ν•˜κ³  μœ νš¨ν•œ μƒνƒœκ³ ,

정합성은 λ°μ΄ν„°μ˜ 값이 μ„œλ‘œ μΌμΉ˜ν•˜λŠ” μƒνƒœλ₯Ό λœ»ν•œλ‹€.(λ°˜μ •κ·œν™”λ₯Ό 톡해 μ΄μƒν˜„μƒμ΄ λ°œμƒν•˜λ©΄ 정합성이 κΉ¨μ§„λ‹€.)

정합성은 μ§€ν‚€μ§€λ§Œ 무결성이 κΉ¨μ§„ μƒνƒœμ˜ 예λ₯Ό λ“€μžλ©΄,
μ£Όλ¬Έν…Œμ΄λΈ”κ³Ό κ³ κ°ν…Œμ΄λΈ”μ˜ idx값이 λͺ¨λ‘ -1 이라면 정합성은 μ§€ν‚€κ³  μžˆμ§€λ§Œ, λ°˜λ“œμ‹œ 1μ΄μƒμ˜ 값을 κ°€μ Έμ•Όν•œλ‹€λŠ” idxκ°’μ˜ κ·œμΉ™μ— μ˜ν•΄ 데이터 무결성이 ν›Όμ†λ˜κ²Œ λœλ‹€.



2. @Transactional의 κΈ°λ³Έ κ°œλ…


  • Springμ—μ„œ νŠΈλ Œμ μ…˜μ„ μ„ μ–Έμ μœΌλ‘œ κ΄€λ¦¬ν•˜κΈ° μœ„ν•΄ μ‚¬μš©.
  • λ©”μ„œλ“œ λ˜λŠ” 클래슀 λ‹¨μœ„λ‘œ νŠΈλ Œμ μ…˜ λ²”μœ„ μ§€μ •.

μ‚¬μš©μ‹œ λ‹€μŒκ³Ό 같이 μ‚¬μš©λœλ‹€.

@Transactional
public void doBusinessLogic() {
    ...
}

주의점

@Transactional의 μœ„μΉ˜μ™€ ν”„λ‘μ‹œ λ©”μ»€λ‹ˆμ¦˜

  • @Transactional은 Spring AOP의 μΈν„°νŽ˜μ΄μŠ€ 기반 (JDK 동적 ν”„λ‘μ‹œ) λ˜λŠ” 클래슀 기반 (CGLIB) 으둜 ν”„λ‘μ‹œλ₯Ό μƒμ„±ν•œλ‹€.
  • λ”°λΌμ„œ λ‚΄λΆ€ 호좜(self-invocation) μ‹œ νŠΈλžœμž­μ…˜μ΄ μ μš©λ˜μ§€ μ•ŠλŠ”λ‹€.
public class A {
    @Transactional
    public void methodA() {
        methodB(); // νŠΈλžœμž­μ…˜ 적용 μ•ˆλ¨!
    }

    @Transactional
    public void methodB() { ... }
}

readOnly μ˜΅μ…˜


λͺ©μ 

@Transactional(readOnly = true)

λ‹¨μˆœ 쑰회 μ „μš© λ©”μ„œλ“œμ— μ μš©ν•˜μ—¬ λ¦¬μ†ŒμŠ€λ₯Ό μ ˆμ•½ν•œλ‹€.


μƒμ„Έν•œ μ„€λͺ…

  • μœ„μ—μ„œ μ„€λͺ…ν•œ 바와 같이, 읽기 λͺ©μ μ˜ λ‘œμ§μ— μ—¬λŸ¬ μž‘μ—…μ„ κ°„μ†Œν™” ν•˜μ—¬ μ„±λŠ₯을 λ†’μ΄λŠ” μ˜΅μ…˜μ΄λ‹€.
  • 이λ₯Ό 더 μƒμ„Ένžˆ μ„€λͺ…ν•˜κ² λ‹€.

1. μ˜μ†μ„± μ»¨ν…μŠ€νŠΈμ™€ 변경감지

변경감지(Dirty Checking)

  • μ˜μ†μ„± μ»¨ν…μŠ€νŠΈλŠ” Entity쑰회 μ‹œ 초기 μƒνƒœμ— λŒ€ν•œ SnapShot을 μ €μž₯ν•œλ‹€.
  • νŠΈλ Œμ μ…˜μ΄ Commit 될 λ•Œ, 초기 μƒνƒœ(SnapShot) 와 ν˜„μž¬ Entity의 μƒνƒœλ₯Ό λΉ„κ΅ν•œλ‹€.
  • μ΄λ•Œ, λΉ„κ΅λœ λ‚΄μš©μ— λŒ€ν•΄ update queryλ₯Ό μžλ™μœΌλ‘œ 생성해 μ“°κΈ° μ§€μ—° μ €μž₯μ†Œμ— μ €μž₯ν•œλ‹€.
  • 이후 μΌκ΄„μ μœΌλ‘œ queryλ₯Ό flush()ν•˜μ—¬ DB에 반영되게 λœλ‹€.

이와같이 μ‚¬μš©μžκ°€ 직접 update쿼리 등을 μž‘μ„±ν•˜μ§€ μ•Šμ•„λ„, μ—”ν‹°ν‹°μ˜ 변경점을 κ°μ§€ν•˜μ—¬ μžλ™μœΌλ‘œ μˆ˜μ •/반영 ν•΄μ£ΌλŠ” 것이 μ˜μ†μ„± μ»¨ν…μŠ€νŠΈμ˜ 변경감지 기법이닀.


μœ„μ„œ λ§ν–ˆλ“―, readOnly = true μ˜΅μ…˜μ΄ μΌœμ Έμžˆλ‹€λ©΄, flushλ°œμƒκ³Ό μžλ™κ°μ§€μ˜ λ™μž‘μ„ λ©ˆμΆ˜λ‹€. 즉, JPAμ„Έμ…˜μ˜ ν”ŒλŸ¬μ‹œ λͺ¨λ“œλ₯Ό MANUAL둜 λ³€κ²½ν•˜μ—¬ κ°•μ œλ‘œ flushλ₯Ό ν˜ΈμΆœν•˜μ§€ μ•ŠλŠ” ν•œ, μ—”ν‹°ν‹° μˆ˜μ •λ‚΄μ—­μ— DB에 λ°˜μ˜λ˜μ§€ μ•ŠλŠ”λ‹€.

κ²°λ‘ 

  • readOnly = trueμ˜΅μ…˜μ΄ μΌœμ Έμžˆλ‹€λ©΄, JPAλŠ” ν•΄λ‹Ή νŠΈλ Œμ μ…˜μ΄ λ‚΄μ˜ 쑰회 쿼리에 λŒ€ν•΄μ„œ, 변경감지λ₯Ό μœ„ν•œ μ΄ˆκΈ°μƒνƒœ(SnapShot)을 μ €μž₯ν•˜μ§€ μ•Šμ•„λ„ 되고, λΆˆν•„μš”ν•œ Dirty Checking을 κ±΄λ„ˆ λ›°κ²Œ ν•¨μœΌλ‘œ λ§€λͺ¨λ¦¬μ™€ CPUλ¦¬μ†ŒμŠ€λ₯Ό μ ˆκ°ν•  수 μžˆλ‹€.

2. μ½κΈ°μ „μš© μ½”λ“œμ— λŒ€ν•œ 가독성 증가

// 1
@Transactional(readOnly = true)
public Member getMember(int memberId) {
    ..
    return member;
}

// 2
@Transactional
public Loaner getLoaner(int memberId) {
    Optional<Loaner> loaner = loanerRepository.findById(loanerId);
    ..
    return loaner;
}
  • μœ„ μ½”λ“œλ₯Ό λ³΄μ•˜μ„ λ•Œ, λ¬Όλ‘  μ½”λ“œλ₯Ό λΆ„μ„ν•˜μ—¬ λ‹¨μˆœ selectλ‘œμ§μΈμ§€, μ“°κΈ° λ‘œμ§μΈμ§€ νŒλ‹¨ν•  μˆ˜μžˆκ² μ§€λ§Œ,
    λˆ„κ°€ 봐도 λ‹¨λ²ˆμ— getMember(int memberId)κ°€ μ½κΈ°μ „μš© λ©”μ„œλ“œλΌλŠ”κ²ƒμ„ νŒŒμ•…ν•  수 μžˆλ‹€.
  • μ΄λŠ” μ½”λ“œμ˜ 가독성을 ν–₯μƒμ‹œμΌœ 쀄 뿐 μ•„λ‹ˆλΌ, μ‹€μˆ˜λ‘œ 트리거 ν•  수 μžˆλŠ” μ“°κΈ° μž‘μ—…μ„ κ±°λΆ€ν•  수 μžˆλ‹€.

3. λ ˆν”Œλ¦¬μΌ€μ΄μ…˜(Replication) ν™˜κ²½μ˜ λΆ„μ‚° λΆ€ν•˜

λ ˆν”Œλ¦¬μΌ€μ΄μ…˜μ΄λž€?

β€™λ‘κ°œ μ΄μƒμ˜ DBMSλ₯Ό Master/Slave λΌλŠ” 수직적 ꡬ쑰λ₯Ό ν™œμš©ν•˜μ—¬ DB의 λΆ€ν•˜λ₯Ό λΆ„μ‚°μ‹œν‚€λŠ” κΈ°μˆ β€™

Master DBμ—λŠ” insert/update/delete 와 같은 μž‘μ—…μ„ μˆ˜ν–‰ν•˜λ„λ‘ ν•˜κ³ , select μž‘μ—…μ„ Slave DBμ—μ„œ

μž‘μ—…ν•˜λ„λ‘ κ΅¬μ„±ν•œλ‹€.


μ™œ? selectμž‘μ—…λ§Œμ„ λ”°λ‘œ λΉΌλŠ” ꡬ쑰λ₯Ό λ§Œλ“€μ—ˆμ„κΉŒ?

  • λ³΄ν†΅μ˜ 경우 selectμž‘μ—…μ˜ μ†Œμš”μ‹œκ°„μ΄ κ°€μž₯ κΈΈκΈ° λ•Œλ¬Έμ΄λ‹€.
  • Table Full Scan에 경우 데이터 κ°œμˆ˜μ— 따라 μ†Œμš”μ‹œκ°„μ΄ μ•„μ£Ό 길게 μ‚¬μš©λ  수 μžˆλ‹€.
  • 이 μ‹œκ°„λ™μ•ˆ, λ‹€λ₯Έ μž‘μ—…μ„ ν•˜μ§€ λͺ»ν•˜κ²Œ λ˜λ‹ˆ 병λͺ©μ΄ λ°œμƒν•˜λŠ” μ£Όμš”μ›μΈμ΄ λœλ‹€.

이둜 μΈν•œ μž₯점

  1. λ ˆν”Œλ¦¬μΌ€μ΄μ…˜ κ΅¬μ‘°λŠ” 볡제본 DB(Slave)λ₯Ό ν•¨κ»˜ μš΄μš©ν•¨μœΌλ‘œ, Master DBμž₯μ•  λ°œμƒμ‹œ SlaveDBλ₯Ό μŠΉκ²©μ‹œμΌœ μž₯μ• λ₯Ό λΉ λ₯΄κ²Œ 볡ꡬ할 수 μžˆλ‹€.
  2. μ‘°νšŒμž‘μ—…μ— λŒ€ν•œ νŠΈλ ˆν”½μ„ λΆ„μ‚°ν•  수 μžˆλ‹€.

do-messenger_screenshot_2025-05-23_10_48_34.png

κ²°λ‘ 

  • @Transactional(readOnly = true) μ˜΅μ…˜μ€ SlaveDBμ—μ„œ 데이터λ₯Ό κ°€μ Έμ˜€λ„λ‘ λ™μž‘μ‹œν‚€κ³ ,
    • (읽기 μ „μš© νŠΈλ Œμ μ…˜μ΄ SlaveDB둜 μžλ™ λΌμš°νŒ… λœλ‹€.)
  • 이λ₯Ό 톡해 λ ˆν”Œλ¦¬μΌ€μ΄μ…˜μ˜ λͺ©μ μ— 맞게 νŠΈλž˜ν”½ 뢄산을 μ˜¨μ „ν•˜κ²Œ μ μš©μ‹œν‚¬ 수 μžˆλ‹€.



+ @Transactional μ–΄λ…Έν…Œμ΄μ…˜μ„ μ œκ±°ν•œλ‹€λ©΄?


λ§Œμ•½, readOnly=ture μ˜΅μ…˜μ„ 톡해, μŠ€λƒ…μƒ· μ €μž₯을 막고 쑰회 속도λ₯Ό μ˜¬λ¦¬λŠ”κ²Œ λͺ©μ μ΄λΌλ©΄,

@Transactional μ–΄λ…Έν…Œμ΄μ…˜μ„ μ™„μ „νžˆ μ§€μš°λ©΄ λ˜λŠ”κ²ƒ μ•„λ‹Œκ°€?

// @Transactional(readOnly = true) -> μ£Όμ„μ²˜λ¦¬
public Member getMember(int idx) {
        Member member = memberRepository.findByIdx(idx).get();
        System.out.println(member.getName()); // Lazy Loading λ°œμƒ
        return member;
}

λ‹€μŒκ³Ό 같이 μ½”λ“œλ₯Ό κ΅¬μ„±ν–ˆλ‹€λ©΄?

  • 사싀 아무일도 μΌμ–΄λ‚˜μ§€ μ•ŠλŠ”λ‹€. (Lazy Loading이 μ •μƒμ μœΌλ‘œ λ™μž‘ν•œλ‹€.)
  • 단, OSIV μ˜΅μ…˜μ΄ true인 κ²½μš°μ—λ§Œ ν•œμ •μ΄λ‹€. β–Ά πŸŒ‹ OSIVλž€ 무엇인가

κ·Έλ ‡λ‹€λ©΄ λ‹€μŒκ³Ό 같이 osivμ˜΅μ…˜μ„ false둜 ν•˜κ³  μž¬μ‹€ν–‰ν•œλ‹€λ©΄?

// application.properties
spring.jpa.open-in-view=false

LazyInitializationException이 λ°œμƒν•˜κ²Œ λœλ‹€.

μ΄λŠ” μ˜μ†μ„± μ»¨ν…μŠ€νŠΈκ°€ μ’…λ£Œλœ 이후(μ€€μ˜μ† μƒνƒœ)에 ν•΄λ‹Ή 엔티티에 μ ‘κ·Όν•˜λ € ν•  λ•Œ λ°œμƒν•˜λŠ” μ˜ˆμ™Έμ΄λ‹€.

(DTO둜 λ³€ν™˜ν•˜μ—¬ 직접 엔티티에 μ ‘κ·Όν•  ν•„μš”κ°€ 없도둝 κ΄€λ¦¬ν•˜λŠ” μ΄μœ μ΄κΈ°λ„ 함)


λ”°λΌμ„œ, OSIVμ˜΅μ…˜μ΄ μΌœμ Έμžˆμ§€ μ•Šλ‹€λ©΄, @Transactional μ–΄λ…Έν…Œμ΄μ…˜μ€ κ°•μ œλ˜κ³ ,

쑰회용 μΏΌλ¦¬λ§Œμ„ μœ„ν•œ λ©”μ„œλ“œμ—λŠ” 항상 readOnly=ture μ˜΅μ…˜μ„ λΆ™μ—¬μ£ΌλŠ”κ²ƒμ΄ λ°”λžŒμ§ν•˜λ‹€.