[ํ ์คํธ] ํตํฉ ํ ์คํธ์์ ํ ์คํธ ๊ฐ DB ์ํ๊ฐ ๊ณต์ ๋๋ ๋ฌธ์ : @Transactional, ์ฌ์ฉํ๋ฉด ์๋๋ ์ํฉ, ๊ทธ์ธ ๋ฐฉ์
- ์๋ ํ ์คํธ ์ฝ๋๋ฅผ ์คํ ์์ผฐ๋ค
@SpringBootTest
@Import(SplearnTestConfiguration.class)
public class MemberRegisterTest {
@Autowired
private MemberRegister memberRegister;
@Test
void register(){
Member member = memberRegister.register(MemberFixture.createRegisterRequest());
assertThat(member.getId()).isNotNull();
assertThat(member.getStatus()).isEqualTo(MemberStatus.PENDING);
}
@Test
void duplicateEmailFail(){
Member member = memberRegister.register(MemberFixture.createRegisterRequest());
assertThatThrownBy(
() -> memberRegister.register(MemberFixture.createRegisterRequest())
).isInstanceOf(DuplicateEmailException.class);
}
}
duplicateEmailFail์ ์ฒซ๋ฒ์งธ register์์ ์์ธ๊ฐ ๋ฐ์ํ๋ค
ํตํฉ ํ ์คํธ์์ ํ ์คํธ ๊ฐ DB ์ํ๊ฐ ๊ณต์ ๋๋ ๊ฒ์ด ๋ฌธ์ ์๋ค.
๋ฐ๋ผ์, @Transactional ์ผ๋ก ํ ์คํธ ๋ฉ์๋๋ง๋ค ์คํ ํ ์๋ ๋กค๋ฐฑ๋๋๋ก ํ๋ค.
Spring Test์ TransactionalTestExecutionListener๊ฐ @Transactional์ ํ ์คํธ ํ ์๋ ๋กค๋ฐฑ์ ํด์ค๋ค.
@SpringBootTest(webEnvironment=RANDOM_PORT) + RestTemplate/WebTestClient๋ก ํ ์คํธํ๋ฉด
์๋ฒ๊ฐ ๋ณ๋ ์ค๋ ๋์์ ๋ณ๋ ํธ๋์ญ์ ์ ์ฌ์ฉํ๋ฏ๋ก,
ํ ์คํธ์ ๋กค๋ฐฑ์ด ์๋ฒ ์ชฝ DB ๋ณ๊ฒฝ์ ์ํฅ์ ๋ชป ์ค๋ค.
ํ ์คํธ์ ์ ํ๋ฆฌ์ผ์ด์ ์ด ์๋ก ๋ค๋ฅธ DB ์ปค๋ฅ์ /ํธ๋์ญ์ ์ ์ฌ์ฉํ๋ฉด ๋กค๋ฐฑ์ด ๋ถ๊ฐ๋ฅํ๋ค
1) Lazy Loading ๋ฌธ์
- ํ ์คํธ์ @Transactional์ด ์์ผ๋ฉด ์์์ฑ ์ปจํ ์คํธ๊ฐ ํ ์คํธ ๋๊น์ง ์ด๋ ค์์
- ์๋น์ค์์ ์ง์ฐ ๋ก๋ฉ(LAZY)์ ํธ์ถํด๋ ํ ์คํธ์์ ์ ์ ๋์
- ํ์ง๋ง ์ค์ ์ด์์์ ์๋น์ค ๋ฉ์๋๊ฐ ๋๋๋ฉด ํธ๋์ญ์ ์ด ๋ซํ๋ฏ๋ก LazyInitializationException ๋ฐ์
- ํ ์คํธ๋ ํต๊ณผํ๋๋ฐ ์ด์์์ ํฐ์ง๋ ์ํฉ
// Member ์ํฐํฐ
@Entity
public class Member {
@OneToMany(fetch = FetchType.LAZY)
private List<Order> orders; // ์ง์ฐ ๋ก๋ฉ
}
// ์๋น์ค
@Service
public class MemberService {
@Transactional
public Member findMember(Long id) {
return memberRepository.findById(id).orElseThrow();
// ์ฌ๊ธฐ์ ํธ๋์ญ์
๋ → ์์์ฑ ์ปจํ
์คํธ ๋ซํ
}
}
// ํ
์คํธ์ @Transactional์ด ์์ผ๋ฉด
@Test
@Transactional
void test() {
Member member = memberService.findMember(1L);
member.getOrders().size(); // ํ
์คํธ ํธ๋์ญ์
์ด ์์ง ์ด๋ ค์์ด์ → ์ฑ๊ณต!
}
// ์ด์์์๋
Member member = memberService.findMember(1L);
member.getOrders().size(); // ์๋น์ค ํธ๋์ญ์
์ด ์ด๋ฏธ ๋ซํ์ผ๋ฏ๋ก → LazyInitializationException!
์ด์ ํ๊ฒฝ
์ปจํธ๋กค๋ฌ ํธ์ถ
→ ์๋น์ค @Transactional ์์ → ์์์ฑ ์ปจํ
์คํธ ์ด๋ฆผ
→ memberRepository.findById(1L) -- Member ๋ก๋ฉ (orders๋ LAZY๋ผ ํ๋ก์)
→ ์๋น์ค @Transactional ๋ → ์์์ฑ ์ปจํ
์คํธ ๋ซํ
→ member.getOrders().size() -- ์์์ฑ ์ปจํ
์คํธ ์ด๋ฏธ ๋ซํ → LazyInitializationException!
ํ
์คํธ ํ๊ฒฝ (@Transactional ์์ ๋)
ํ
์คํธ @Transactional ์์ → ์์์ฑ ์ปจํ
์คํธ ์ด๋ฆผ
→ ์๋น์ค @Transactional (ํ
์คํธ ํธ๋์ญ์
์ ํฉ๋ฅ, ์๋ก ์ ๋ง๋ฆ)
→ memberRepository.findById(1L)
→ ์๋น์ค ๋ฉ์๋ ๋ (ํ์ง๋ง ํธ๋์ญ์
์ ํ
์คํธ ๊ฒ์ด๋ผ ์ ๋ซํ)
→ member.getOrders().size() -- ํ
์คํธ ํธ๋์ญ์
์ด ์์ง ์ด๋ ค์์ → ์ฑ๊ณต!
ํ
์คํธ @Transactional ๋ → ๋กค๋ฐฑ
ํต์ฌ์ ์๋น์ค์ @Transactional์ด ๋
๋ฆฝ ํธ๋์ญ์
์ ์ ๋ง๋ค๊ณ ํ
์คํธ ํธ๋์ญ์
์ ํฉ๋ฅ(๊ธฐ๋ณธ ์ ํ =
REQUIRED)ํ๊ธฐ ๋๋ฌธ์, ์๋น์ค ๋ฉ์๋๊ฐ ๋๋๋ ์์์ฑ ์ปจํ
์คํธ๊ฐ ์ด์์๋ค.
์ด์์์๋ ์๋น์ค ํธ๋์ญ์ ์ด ์ง์ง ๋๋๋๊น ํฐ์ง๋ค.
2) Flush ํ์ด๋ฐ ๋ฌธ์ (ID ์์ฑ ์ ๋ต์ ๋ฐ๋ผ ๋ค๋ฆ)
- ๋กค๋ฐฑํ ๊ฑฐ๋๊น JPA๊ฐ DB์ flush๋ฅผ ์ ํ ์ ์์
GenerationType.IDENTITY๋ save()์ ์ฆ์ INSERT ์คํ
GenerationType.SEQUENCE๋ save()์ INSERT๋ฅผ ์ปค๋ฐ ์์ ๊น์ง ๋ฏธ๋ฃฐ ์ ์์ -> ์ด๋ flush ์๋ต ๊ฐ๋ฅ
- unique ์ ์ฝ ์กฐ๊ฑด ์๋ฐ ๊ฐ์ DB ๋ ๋ฒจ ์๋ฌ๊ฐ ํ ์คํธ์์ ์ ํฐ์ง
- ํ ์คํธ๋ ํต๊ณผ, ์ด์์์ ์คํจ
์๋ฅผ ๋ค์ด, ์๋์ฒ๋ผ @Id์ @GeneratedValue(GenerationType.SEQUENCE)์ธ ์ํฐํฐ๊ฐ ์๊ณ
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE) // ํ์ธ!
private Long id;
// Member ์ํฐํฐ์ unique ์ ์ฝ
@Column(unique = true)
private String email;
//์ค๋ต..
}
์๋์ ๊ฐ์ @DataJpaTest ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ค๊ณ ํ์
@DataJpaTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
// @Transactional - @DataJpaTest์ ์ฌ์ฉํ๊ฒ ๋๋ฉด @Transactional์ด ์ด๋ฏธ ํฌํจ๋์ด ์์
@Test
void duplicateEmailFail(){
Member member = Member.register(createRegisterRequest(), createPasswordEncoder());
memberRepository.save(member);
Member member2 = Member.register(createRegisterRequest(), createPasswordEncoder());
assertThatThrownBy(() -> memberRepository.save(member2))
.isInstanceOf(DataIntegrityViolationException.class);
}
}
2๋ฒ์งธ save์์ ์์ธ๊ฐ ๋ฌ์ด์ผ ํ์ง๋ง,
๋ก๊ทธ๋ฅผ ์ดํด๋ณด๋ฉด, SEQUENCE ์ ๋ต์ ์ํ์ค์์ id๋ง ๋ฏธ๋ฆฌ ๋ฐ์์ค๊ณ ,
์ค์ INSERT๋ flush/์ปค๋ฐ ์์ ๊น์ง ๋ฏธ๋ฃจ๊ธฐ ๋๋ฌธ์,
unique ์ ์ฝ ์๋ฐ์ด ๋ฐ์ํ์ง ์์์ ์ ์ ์๋ค. (IDENTITY๋ flush๋์ ์์ธ ๋ฐ์ํจ)
Hibernate: create sequence member_seq start with 1 increment by 50
Hibernate: create table member (id bigint not null, address varchar(255), nick_name varchar(255), password_hash varchar(255), status enum ('ACTIVE','DEACTIVATED','PENDING'), primary key (id), unique (address))
2026-02-09T21:44:16.147+09:00 INFO 10145 --- [splearn] [ Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2026-02-09T21:44:16.276+09:00 INFO 10145 --- [splearn] [ Test worker] t.s.a.required.MemberRepositoryTest : Started MemberRepositoryTest in 0.878 seconds (process running for 1.221)
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build what is described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3
WARNING: A Java agent has been loaded dynamically (/Users/eundms/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy-agent/1.15.11/a38b16385e867f59a641330f0362ebe742788ed8/byte-buddy-agent-1.15.11.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
Hibernate: select next value for member_seq // ์ฒซ ๋ฒ์งธ save: ์ํ์ค ๊ฐ๋ง ๊ฐ์ ธ์ด
Hibernate: select next value for member_seq // ๋ ๋ฒ์งธ save: ์ํ์ค ๊ฐ๋ง ๊ฐ์ ธ์ด
Expecting code to raise a throwable.
java.lang.AssertionError:
Expecting code to raise a throwable.
at tobyspring.splearn.application.required.MemberRepositoryTest.duplicateEmailFail(MemberRepositoryTest.java:38)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Hibernate: insert into member (address,nick_name,password_hash,status,id) values (?,?,?,?,?) // ์ค์ INSERT๋ flush/์ปค๋ฐ ์์ ๊น์ง ๋ฏธ๋ฃธ
ํ์ง๋ง, ์ด์์์ ๋์ผํ ์ด๋ฉ์ผ์ ๋ ๋ฒ ์ ๋ ฅํ๋ ์ฝ๋๋ ํธ๋์ญ์ ์ปค๋ฐ ์์ ์ flush ๋์ด ์์ธ๊ฐ ๋ฐ์ํ๋ค
// ์ด์์์๋
memberRepository.save(new Member("a@b.com"));
memberRepository.save(new Member("a@b.com"));
// ํธ๋์ญ์
์ปค๋ฐ ์์ ์ flush → DB์์ unique constraint violation!
์ฆ, ๋ค์ ์ค๋ช ํ์๋ฉด,
์ด์ ํ๊ฒฝ (ํธ๋์ญ์
๋ถ๋ฆฌ)
์๋น์ค @Transactional ์์
→ save(member1) -- ์ํ์ค ์กฐํ
→ save(member2) -- ์ํ์ค ์กฐํ
→ ํธ๋์ญ์
์ปค๋ฐ → flush → INSERT 2๊ฑด → unique ์๋ฐ ํฐ์ง!
ํ
์คํธ ํ๊ฒฝ (ํธ๋์ญ์
๊ณต์ )
ํ
์คํธ @Transactional ์์ (= ์๋น์ค๋ ์ด ํธ๋์ญ์
์ ์ฐธ์ฌ)
→ save(member1) -- ์ํ์ค ์กฐํ
→ save(member2) -- ์ํ์ค ์กฐํ
→ ํ
์คํธ ๋ → ๋กค๋ฐฑ → flush ์ ํจ → unique ์๋ฐ ์ ํฐ์ง!
ํ
์คํธ๊ฐ ํธ๋์ญ์
์ ๊ฐ์ธ๊ณ ์์ผ๋๊น, ์๋น์ค์ @Transactional์ด ์ ํธ๋์ญ์
์ ์ ๋ง๋ค๊ณ ํ
์คํธ
ํธ๋์ญ์
์ ํฉ๋ฅํ๋ค. ๊ทธ๋ฌ๋ฉด ์ปค๋ฐ์ด ์์ํ ์ ์ผ์ด๋๊ณ (๋กค๋ฐฑ์ด๋๊น), flush๋ ์๋ต๋ ์ ์์ด์
์ด์์์๋ง ํฐ์ง๋ ๋ฒ๊ทธ๋ฅผ ์จ๊ธฐ๊ฒ ๋๋ค.
// ์ฐธ๊ณ ๋ก SEQUENCE ๋ฅผ ์์ธ ํฐ์ง๊ฒ ํ๋ ค๋ฉด ๋ช ์์ ์ผ๋ก Flush ํด์ค์ผ ํจ
@Test
void duplicateEmailFail(){
Member member = Member.register(createRegisterRequest(), createPasswordEncoder());
memberRepository.save(member);
Member member2 = Member.register(createRegisterRequest(), createPasswordEncoder());
memberRepository.save(member2);
assertThatThrownBy(() -> entityManager.flush()) // ์ฌ๊ธฐ์ ์ค์ INSERT ๋ฐ์ → ์์ธ
.isInstanceOf(DataIntegrityViolationException.class);
}
>>> ์ค์ ์ด์ ํ๊ฒฝ๊ณผ ๋์ผํ ํธ๋์ญ์ ๊ฒฝ๊ณ๋ก ํ ์คํธํ๊ณ ์ถ์ ๋๋ @Transactional์ ๋นผ๊ณ ํ ์คํธ๋ฅผ ์งํํ์
1) ๊ฐ ํ ์คํธ ๋ง๋ค ๋ค๋ฅธ ๋ฐ์ดํฐ ์ฌ์ฉํ๊ธฐ
2) ๊ฐ ํ ์คํธ ์คํ์ ์ ๋ฐ์ดํฐ ์ด๊ธฐํ
- ๊ฐ ํ ์คํธ ์์ ์ : @BeforeEach์์ ์ ๋ฆฌ
- @Sql / ๋ก์ง ํธ์ถ๋ก ๊ฐ ํ ์คํธ์ ์ ํฉํ ๋ฐ์ดํฐ ์ ๋ ฅ
- ๋ฉํ ๋ฐ์ดํฐ์์ ์ ์ฒด ํ ์ด๋ธ ๋ชฉ๋ก์ ์๋์ผ๋ก ์กฐํํด์ TRUNCATEํ๋ ํด๋์ค๋ฅผ ๋ง๋ค์ด๋๊ณ ํธ์ถ