[속λ‹₯속λ‹₯] πŸš€ μš°λ‹Ήνƒ•νƒ• ν…ŒμŠ€νŠΈ 격리 νŠΈλŸ¬λΈ” μŠˆνŒ…κΈ°

ν—ŒμΉ˜Β·2022λ…„ 11μ›” 8일
0

μš°μ•„ν•œν…Œν¬μ½”μŠ€

λͺ©λ‘ 보기
25/30
post-thumbnail

ν•΄λ‹Ή 글은 속λ‹₯속λ‹₯ κΈ°μˆ λΈ”λ‘œκ·Έμ— μž‘μ„±λœ κΈ€κ³Ό λ™μΌν•©λ‹ˆλ‹€.

속λ‹₯속λ‹₯ 링크

0. 문제 상황

HashtagServiceTest(μ„œλΉ„μŠ€μ˜ ν…ŒμŠ€νŠΈ) μ½”λ“œλ₯Ό 짜던 λ„μ€‘μ΄μ—ˆμŠ΅λ‹ˆλ‹€.

전체 ν…ŒμŠ€νŠΈλ“€μ„ μ‹€ν–‰ν•  μ‹œ, HashtagServiceTestκ°€ 랜덀 ν™•λ₯ λ‘œ ν†΅κ³Όν•˜κ±°λ‚˜ μ‹€νŒ¨ν•˜λŠ” 상황이 λ°˜λ³΅λ˜μ—ˆμŠ΅λ‹ˆλ‹€. λŒ€λž΅ 5번 ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•  μ‹œ 1번 꼴둜 ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.

막상 HashtagServiceTest만 λŒλ Έμ„ 땐 ν…ŒμŠ€νŠΈκ°€ λ¬Έμ œμ—†μ΄ ν†΅κ³Όν–ˆμŠ΅λ‹ˆλ‹€.

랜덀 κ°€μ±  ν…ŒμŠ€νŠΈ

1. 문제 ν•΄κ²°

λžœλ€ν•œ ν™•λ₯ λ‘œ ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν•  λ•Œ κ³ λ €ν•΄μ•Ό ν•  뢀뢄은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  1. ν…ŒμŠ€νŠΈ λ©”μ†Œλ“œμ˜ 둜직이 잘λͺ»λ˜μ–΄ μˆœμ„œμ— 따라 λ‹€λ₯Έ ν…ŒμŠ€νŠΈ μ½”λ“œκ°€ 영ν–₯을 λ°›κ³  μžˆμ„ 수 μžˆλ‹€
  2. ν…ŒμŠ€νŠΈ ν™˜κ²½μ΄ 잘λͺ»λ˜μ–΄ μ˜¨μ „ν•œ μ΄ˆκΈ°ν™”κ°€ 이뀄지지 μ•Šκ³  μžˆλ‹€

λ‘˜λ‹€ κ²°κ΅­ ν…ŒμŠ€νŠΈ 격리의 λ¬Έμ œμž…λ‹ˆλ‹€.

이쀑 μ €λŠ” 2번, ν…ŒμŠ€νŠΈ ν™˜κ²½μ˜ 문제라고 νŒλ‹¨ν–ˆμŠ΅λ‹ˆλ‹€.

  1. λ¨Όμ € μ‹€ν–‰λ˜λŠ” μΈμˆ˜ν…ŒμŠ€νŠΈ(HashtagAcceptanceTest)λŠ” 항상 μ„±κ³΅ν–ˆκ³ ,
  2. HashtagServiceTest만 돌릴 μ‹œ ν…ŒμŠ€νŠΈλ“€μ΄ 항상 ν†΅κ³Όν–ˆκΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

1-1. μΈμˆ˜ν…ŒμŠ€νŠΈ μ΄ˆκΈ°ν™” 방식 λ³€κ²½ (@DirtiesContext β†’ @Sql)

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
public class AcceptanceTest {...}

μš°μ„ , HashtagAcceptanceTest μ΄ˆκΈ°ν™”λ₯Ό μœ„ν•΄ μ‚¬μš©ν•œ @DirtiesContext μ–΄λ…Έν…Œμ΄μ…˜μ— λŒ€ν•΄ μ°Ύμ•„λ΄€μŠ΅λ‹ˆλ‹€.

ν•΄λ‹Ή μ–΄λ…Έν…Œμ΄μ…˜ μ‚¬μš© μ‹œ ν…ŒμŠ€νŠΈ 격리가 이뀄지지 μ•Šμ„ λ•Œκ°€ μžˆλ‹€λŠ” 글을 보게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

Beware of @DirtiesContext - Codecleaner

1) 1μ°¨ 문제 ν•΄κ²°

μŠ€ν”„λ§ κΉƒν—ˆλΈŒμ—μ„œ ν•΄λ‹Ή λ¬Έμ œμ™€ κ΄€λ ¨λœ 이슈λ₯Ό μ°Ύμ•„λ΄€μŠ΅λ‹ˆλ‹€.

@DirtiesContext does not destroy all cached singleton beans [SPR-8857] Β· Issue #13499 Β· spring-projects/spring-framework

After a long digging, we just found, that if a method or class is annotated withΒ @DirtiesContext , it doesn't clean up all the objects. It seems, that the "old" application context (or part of it) remains in the memory and can not be cleared up by the GC.

l had a similar issue using HSQLDB with Spring 4.1 (using Boot).

Between tests (with @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) enabled) I noticed that the data was cleaned - because I used @Transactional in my tests) but that HSQLDB autoincrement numbering was not reset.

This had to do that HSQLDB was not shutdown between tests.

After adding ;shutdown=true to my jdbc url everything was working as expected.

@DirtiesContext (HashtagAcceptanceTest μ—μ„œ μ‚¬μš©)와 @Transactional (HashtagServiceTestμ—μ„œ μ‚¬μš©)을 μ‚¬μš©ν•œ ν…ŒμŠ€νŠΈλ“€μ„ λ™μ‹œμ— 돌릴 μ‹œ λ¬Έμ œκ°€ μžˆμ„ 수 μžˆλ‹€λŠ” μ½”λ©˜νŠΈκ°€ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

BEFORE application.yml

spring:
  datasource: //이 λΆ€λΆ„ μ‚­μ œ
    url: jdbc:h2:mem:db?serverTimezone=Asia/Seoul;MODE=MYSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    driver-class-name: org.h2.Driver
    username: sa
  jpa:
  	...

AFTER application.yml

spring:
	//μ‚­μ œ
  jpa:
    hibernate:
      ...

ν…ŒμŠ€νŠΈμš©Β application.ymlμ—μ„œ μ•„μ˜ˆ dataSourceΒ ν• λ‹Ήν•˜λŠ” 뢀뢄을 μ‚­μ œν–ˆλ”λ‹ˆ μ •μƒμ μœΌλ‘œ ν…ŒμŠ€νŠΈκ°€ λŒμ•„κ°”μŠ΅λ‹ˆλ‹€!

(Thanks To 였찌!)

2) 2μ°¨ 문제 ν•΄κ²° : @DirtiesContextΒ μ‚­μ œ

그런데 @DirtiesContextλŠ” κ°„νŽΈν•˜μ§€λ§Œ ν…ŒμŠ€νŠΈκ°€ λŠλ¦½λ‹ˆλ‹€. ν…ŒμŠ€νŠΈ λ©”μ†Œλ“œ λ³„λ‘œ μ»¨ν…μŠ€νŠΈλ₯Ό λ‹€μ‹œ λ„μ›Œμ•Ό ν•˜κΈ° λ•Œλ¬Έμ— λ§Žμ€ μ‹œκ°„μ΄ μ†Œμš”λ©λ‹ˆλ‹€.

μΈμˆ˜ν…ŒμŠ€νŠΈμ—μ„œ ν…ŒμŠ€νŠΈ κ²©λ¦¬ν•˜κΈ°

κ³ λ―Ό 끝에, μΈμˆ˜ν…ŒμŠ€νŠΈμ—μ„œ @Sql μ–΄λ…Έν…Œμ΄μ…˜ 및 truncate.sql문을 톡해 μ΄ˆκΈ°ν™”λ₯Ό μ§„ν–‰ν•˜λ„λ‘ λ³€κ²½ν–ˆμŠ΅λ‹ˆλ‹€.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Sql(
        scripts = {"classpath:truncate.sql"},
        executionPhase = BEFORE_TEST_METHOD)
public class AcceptanceTest {...}

public class HashtagAcceptanceTest extends AcceptanceTest {...}

ν…ŒμŠ€νŠΈμš©Β application.ymlΒ μ—­μ‹œ 기쑴의 DBλ₯Ό λͺ…μ‹œν•˜λŠ” ν˜•νƒœλ‘œ λ‘€λ°±ν–ˆμŠ΅λ‹ˆλ‹€.

spring:
  datasource:
    url: jdbc:h2:mem:db?serverTimezone=Asia/Seoul;MODE=MYSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    driver-class-name: org.h2.Driver
    username: sa
  jpa:
  	...

1-2. μ„œλΉ„μŠ€ ν…ŒμŠ€νŠΈ (ν†΅ν•©ν…ŒμŠ€νŠΈ) μ΄ˆκΈ°ν™” 방식 λ³€κ²½

BEFORE(HashtagServiceTest)

@SpringBootTest
@Transactional
class HashtagServiceTest {...}

μ„œλΉ„μŠ€ν…ŒμŠ€νŠΈ 격리λ₯Ό μœ„ν•΄ μ‚¬μš©ν•œ @Transactional μ–΄λ…Έν…Œμ΄μ…˜μ— λŒ€ν•΄ μ°Ύμ•„λ΄€μŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같은 글을 찾을 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

JPA μ‚¬μš©μ‹œ ν…ŒμŠ€νŠΈ μ½”λ“œμ—μ„œ @Transactional μ£Όμ˜ν•˜κΈ°

@Transactional μ–΄λ…Έν…Œμ΄μ…˜κ³Ό JPAλ₯Ό μ‚¬μš©ν•˜λŠ” ServiceTest 사이에 ν˜Έν™˜μ„±μ— λ¬Έμ œκ°€ 있기 λ•Œλ¬Έμ—

ν•΄λ‹Ή μ΄ˆκΈ°ν™” 방식을 ꢌμž₯ν•˜μ§€ μ•ŠλŠ”λ‹€κ³  ν•©λ‹ˆλ‹€.

이에 ServiceTestμ—μ„œλ„

@TransactionalΒ μ–΄λ…Έν…Œμ΄μ…˜ λŒ€μ‹ ,

@SqlΒ μ–΄λ…Έν…Œμ΄μ…˜ 및 truncate.sql을 μ‚¬μš©ν•΄ μ΄ˆκΈ°ν™”ν•˜λ„λ‘ λ³€κ²½ν–ˆμŠ΅λ‹ˆλ‹€.

truncate.sql

AFTER(HashtagServiceTest)

@SpringBootTest
@Sql(
        scripts = {"classpath:truncate.sql"},//sql μ„€μ •
        executionPhase = BEFORE_TEST_METHOD)
class HashtagServiceTest {...}

(+ μΆ”κ°€)

ν˜„μž¬ truncate.sql이 DB μŠ€ν‚€λ§ˆ 변경에 의쑴적인 λ¬Έμ œκ°€ μžˆμ–΄Β DatabaseCleaner둜 λ³€κ²½λœ μƒνƒœμž…λ‹ˆλ‹€.

3. 2차 문제 - LazyInitializationException

ServiceTestμ—μ„œΒ @TransactionalΒ μ–΄λ…Έν…Œμ΄μ…˜μ„ λΉΌλ‹ˆ LazyInitializationException μ—λŸ¬κ°€ ν„°μ‘ŒμŠ΅λ‹ˆλ‹€.

could not initialize proxy [com.wooteco.sokdak.hashtag.domain.Hashtag#3] - no Session
org.hibernate.LazyInitializationException: could not initialize proxy [com.wooteco.sokdak.hashtag.domain.Hashtag#3] - no Session
	...

원인은 postHashtag μ—°κ²° μ—”ν‹°ν‹°μ—μ„œ Hashtag, Postλ₯Ό μ €μž₯ν•  λ•Œ

Lazy λ‘œλ”©μœΌλ‘œ κ°€μ Έμ˜¨λ‹€λŠ” 데에 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

μ„œλΉ„μŠ€ λ©”μ†Œλ“œλ₯Ό ν˜ΈμΆœν•˜κ³ , νŠΈλžœμž­μ…˜μ΄ λ‹«νžŒ ν›„, λ‹€μ‹œ Lazy λ‘œλ”©μ„ κ±Έμ–΄ 값을 κ°€μ Έμ˜€λ €κ³  ν•˜λ‹ˆ

DB와 μ—°κ²°λœ Connection이 μ—†μ–΄μ„œ μƒκΈ°λŠ” 였λ₯˜μ˜€μŠ΅λ‹ˆλ‹€.

[Hibernate / JPA ] LazyLoading | No Session μ—λŸ¬μ— λŒ€ν•΄μ„œ

ServiceTest에 ν•œμ •ν•΄

@Transactional μ–΄λ…Έν…Œμ΄μ…˜κ³Ό @Sql μ–΄λ…Έν…Œμ΄μ…˜μ„ ν•¨κ»˜ μ‚¬μš©ν•˜λ‹ˆ

λ‹€μ‹œ ν…ŒμŠ€νŠΈκ°€ λ¬Έμ œμ—†μ΄ λŒμ•„κ°”μŠ΅λ‹ˆλ‹€...

@SpringBootTest
@Sql(
        scripts = {"classpath:truncate.sql"},
        executionPhase = BEFORE_TEST_METHOD)
@Transactional
class HashtagServiceTest {...}

4. 2μ°¨ 문제 ν•΄κ²° 및 λ…Όμ˜μ‚¬ν•­

ServiceTest 에 ν•œμ •ν–ˆμ§€λ§Œ, @Transactional ,@Sql μ–΄λ…Έν…Œμ΄μ…˜μ„ ν•¨κ»˜ μ‚¬μš©ν•œλ‹€λŠ” 것은

  1. 두 번의 μ΄ˆκΈ°ν™”κ°€ 이뀄진닀.
  2. μ„œλΉ„μŠ€ν΄λž˜μŠ€μ˜ λ©”μ†Œλ“œμ— κ±Έλ¦° @Transactional 이 λ¬΄μ‹œλœλ‹€

λŠ” λœ»μž…λ‹ˆλ‹€.


HashtagService 클래슀의 deleteAllByPostId λ©”μ†Œλ“œ. νŠΈλžœμž­μ…˜μ΄ κ±Έλ €μžˆλ‹€.

ν˜„μž¬λŠ” ν•΄λ‹Ή μ‚¬μ§„μ˜ λ©”μ†Œλ“œ 속 νŠΈλžœμž­μ…˜μ„ 검증할 수 μ—†μŠ΅λ‹ˆλ‹€.

4-1. ν•΄κ²° 방법 1 : fetch join

정석적인 방법은 Lazy loading으둜 λ“€μ–΄μ˜€λŠ” 값을 λ¦¬ν„΄ν•˜λŠ” 경우, 쿼리λ₯Ό fetch join으둜 λ°”κΏ”μ£ΌλŠ” κ²ƒμž…λ‹ˆλ‹€. 그러면 더 이상 Lazy loading이 ν•„μš”ν•˜μ§€ μ•Šκ²Œ λ˜λ©΄μ„œ μ •μƒμ μœΌλ‘œ ν…ŒμŠ€νŠΈ μ½”λ“œκ°€ ν†΅κ³Όν•˜κ²Œ λ©λ‹ˆλ‹€.

public interface PostHashtagRepository extends JpaRepository<PostHashtag, Long> {

    @Query("SELECT ph from PostHashtag ph JOIN FETCH ph.hashtag h JOIN FETCH ph.post p WHERE  p.id = :id")
    List<PostHashtag> findAllByPostId(Long id);

ν•˜μ§€λ§Œ 이λ₯Ό μœ„ν•΄μ„œλŠ” SpringDataJPA의 이점을 일뢀 포기해야 ν•©λ‹ˆλ‹€.

κ΄€λ ¨ Repository λ©”μ†Œλ“œμ— λͺ¨λ‘ fetch join을 κ±Έμ–΄μ£ΌλŠ” 쿼리λ₯Ό μ„€μ •ν•˜κ²Œ λ©λ‹ˆλ‹€.

4-2. ν•΄κ²° 방법 2 : @EntityGraph

μ΄λŸ¬ν•œ λΆˆνŽΈν•¨μ„ ν•΄κ²°ν•˜κΈ° μœ„ν•΄ @EntityGraph μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

public interface PostHashtagRepository extends JpaRepository<PostHashtag, Long> {

    @EntityGraph(attributePaths = {"post", "hashtag"})
    List<PostHashtag> findAllByPostId(Long id);

λ‹€λ§Œ EntityGraph 적용 μ‹œ Outer Join이 λ˜λŠ” λ¬Έμ œκ°€ μžˆμŠ΅λ‹ˆλ‹€.

4-3. 과제

μΌλŒ€ λ‹€ λ§€ν•‘μ—μ„œ μœ„ 두 방법을 적용 μ‹œ νŽ˜μ΄μ§€λ„€μ΄μ…˜μ„ μ‚¬μš©ν•  수 μ—†μŠ΅λ‹ˆλ‹€. 이λ₯Ό κ³ λ €ν•œ 접근이 ν•„μš”ν•©λ‹ˆλ‹€.

profile
🌱 ν•¨κ»˜ μžλΌλŠ” μ€‘μž…λ‹ˆλ‹€ πŸš€ rerub0831@gmail.com

0개의 λŒ“κΈ€