DevLog ๐Ÿ˜ถ

[Spring] @SpringBootTest์—์„œ ์ง€์—ฐ ๋กœ๋”ฉ ์‚ฌ์šฉํ•˜๊ธฐ - no Session ๋ฐฉ์ง€ํ•˜๊ธฐ ๋ณธ๋ฌธ

๊ฐœ๋ฐœ์ผ์ง€

[Spring] @SpringBootTest์—์„œ ์ง€์—ฐ ๋กœ๋”ฉ ์‚ฌ์šฉํ•˜๊ธฐ - no Session ๋ฐฉ์ง€ํ•˜๊ธฐ

dolmeng2 2023. 7. 30. 14:23

๐ŸŒฑ ๋“ค์–ด๊ฐ€๊ธฐ ์ „

์šฐ๋ฆฌ ํŒ€์˜ ๊ฒฝ์šฐ E2E ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•ด์„œ @SpringBootTest๋ฅผ ํ†ตํ•ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ์šฐ๋ฆฌ ํŒ€์˜ ํŒ€์›๋ถ„์ด ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๋ฅผ ๋งŒ๋‚˜๊ฒŒ ๋˜์—ˆ๋‹ค.

failed to lazily initialize a collection of role: co.kirikiri.domain.roadmap.RoadmapContent.nodes.values: could not initialize proxy - no Session

์ƒํ™ฉ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค. (์ถ”ํ›„ ์ฝ”๋“œ๋กœ ๋” ์ž˜ ์‚ดํŽด๋ณผ ์˜ˆ์ •์ด๋‹ค.)

A๋ผ๋Š” ์ƒ์„ฑ API์™€ B๋ผ๋Š” ์กฐํšŒ API๊ฐ€ ์žˆ์„ ๋•Œ, ํŒ€ ๋‚ด์—์„œ ๊ธฐ๋Šฅ์„ ์„ธ๋ถ„ํ™”ํ•œ ๋‹ค์Œ ๊ฐ์ž ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•˜๋‹ค ๋ณด๋‹ˆ B๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ์‹œ์ ์— A๋ผ๋Š” API๊ฐ€ ์—†์–ด, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๋•Œ repository๋ฅผ ์˜์กดํ•˜์—ฌ ์ง์ ‘ save๋ฅผ ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ, save ํ›„ ๋ฐ˜ํ™˜๋œ ์—”ํ‹ฐํ‹ฐ์˜ ๊ฐ์ฒด๋ฅผ ์กฐํšŒํ•  ๋•Œ ์ง€์—ฐ ๋กœ๋”ฉ์„ ์‚ฌ์šฉํ•˜๋‹ค ๋ณด๋‹ˆ ํŠธ๋žœ์žญ์…˜์ด ํ•„์š”ํ•˜๊ฒŒ ๋˜์—ˆ๋Š”๋ฐ, ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ์—์„œ๋Š” repository๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์‹œ์ ์—์„œ๋งŒ ํŠธ๋žœ์žญ์…˜์ด ๊ฑธ๋ฆฌ๊ณ , ํ•ด๋‹น ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์‹œ์ ์—๋Š” ํŠธ๋žœ์žญ์…˜์ด ์—†์–ด ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๊ฐ€ ์—†๋Š” ์ƒํƒœ๊ฐ€ ๋œ ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฌ๋‹ค ๋ณด๋‹ˆ ์„ธ์…˜ ์ •๋ณด๊ฐ€ ์—†์–ด ์ง€์—ฐ ๋กœ๋”ฉ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

 

๋‹น์‹œ ์ด ์˜ค๋ฅ˜๋ฅผ ์ฒ˜์Œ ์ ‘ํ–ˆ์„ ๋•Œ๋Š” ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ์˜ ํŠธ๋žœ์žญ์…˜์ด ์—†์–ด์„œ ๋ฐœ์ƒํ•œ ๋ฌธ์ œ๋‹ˆ๊นŒ, A api๊ฐ€ merge ๋œ ์ดํ›„์— ์ž‘์—…ํ•˜์…”๋„ ์ถฉ๋ถ„ํ•˜์‹ค ๊ฒƒ ๊ฐ™์•„์š”! ๊ฐ™์€ ๋‹ต์„ ๋“œ๋ ธ์ง€๋งŒ, ๊ณฐ๊ณฐ์ด ์ƒ๊ฐํ•ด๋ณด๋‹ˆ ์ด๊ฒŒ ๊ณผ์—ฐ ์˜ณ์€ ํ•ด๋‹ต์ธ๊ฐ€? ๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค. (๋ฌผ๋ก  ์ง€๊ธˆ ์šฐ๋ฆฌ ํŒ€ ์ƒํ™ฉ์—์„œ๋Š” ๋น ๋ฅด๊ฒŒ API ๊ฐœ๋ฐœ์„ ๋๋‚ด์•ผ ํ•˜๋‹ˆ๊นŒ, ์ง€๊ธˆ ์ž‘์„ฑํ•ด๋ดค์ž ์–ด์ฐจํ”ผ ๋ฆฌํŒฉํ„ฐ๋ง์„ ํ•ด์•ผ ํ•˜๋‹ˆ ์ด๋ ‡๊ฒŒ ๋„˜์–ด๊ฐ„ ๊ฒƒ๋„ ์žˆ๋‹ค ๐Ÿฅฒ)

 

์•ž์œผ๋กœ๋„ ์ด๋Ÿฐ ํ…Œ์ŠคํŠธ ์ƒํ™ฉ์ด ์ƒ๊ธธ ํ…๋ฐ, ๊ทธ๋Ÿด ๋•Œ๋งˆ๋‹ค ์ด๋Ÿด ์ˆ˜๋Š” ์—†๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฒˆ์— ๊ณต๋ถ€ํ•˜๋ฉด์„œ ์ƒˆ๋กœ์šด ๋ฐฉ๋ฒ•์„ ์ ์šฉํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

 


 

๐ŸŒฑ ์—”ํ‹ฐํ‹ฐ ์†Œ๊ฐœ

ํ˜„ ๋ฌธ์ œ ์ƒํ™ฉ์—์„œ ํ•„์š”ํ•œ ๊ฐ„๋‹จํ•œ ์—”ํ‹ฐํ‹ฐ ๋‹ค์ด์–ด๊ทธ๋žจ์ด๋‹ค.

๋กœ๋“œ๋งต (Roadmap)์€ ๋กœ๋“œ๋งต ๋‚ด์šฉ๋“ค(RoadmapContents)์— ๋Œ€ํ•ด์„œ 1:N ๊ด€๊ณ„๋ฅผ ๋งบ๊ณ  ์žˆ์œผ๋ฉฐ, ๊ฐ ๋กœ๋“œ๋งต ๋‚ด์šฉ(RoadmapContent)์€ ๋กœ๋“œ๋งต ๋…ธ๋“œ๋“ค(RoadmapNodes)์— ๋Œ€ํ•ด์„œ ๋˜ ๋‹ค์‹œ 1:N ๊ด€๊ณ„๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

 

์ด๋ฅผ ์ฝ”๋“œ๋กœ ๋ดค์„ ๋•Œ๋Š” ๋Œ€๋žต์ ์œผ๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌ์„ฑ๋œ๋‹ค.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Roadmap extends BaseEntity {

    ...
    
    @Embedded
    private RoadmapContents contents = new RoadmapContents();
    
    ...
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class RoadmapContent extends BaseUpdatedTimeEntity {

    ...

    @Embedded
    private final RoadmapNodes nodes = new RoadmapNodes();

   ...

    public RoadmapNodes getNodes() {
        return nodes;
    }
}
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RoadmapNodes {

    @OneToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "roadmapContent")
    private final List<RoadmapNode> values = new ArrayList<>();

    ...
    
    public List<RoadmapNode> getValues() {
        return new ArrayList<>(values);
    }
}

์—ฌ๊ธฐ์„œ ๋ด์•ผํ•˜๋Š” ์ ์€, ๋กœ๋“œ๋งต(Roadmap) - ๋กœ๋“œ๋งต ๋ณธ๋ฌธ(RoadmapContent)๊ณผ ๋กœ๋“œ๋งต ๋ณธ๋ฌธ(RoadmapContent)๊ณผ ๋กœ๋“œ๋งต ๋…ธ๋“œ(RoadmapNode) ๋ชจ๋‘๊ฐ€ LAZY ์ „๋žต์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

 


 

๐ŸŒฑ ๋ฌธ์ œ ์ƒํ™ฉ

์•„๋ž˜์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)์„ ํ†ตํ•ด์„œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•œ ์ƒํƒœ์—์„œ ์ง„ํ–‰๋œ๋‹ค.

 

@Test
void ํ…Œ์ŠคํŠธ() throws JsonProcessingException {
    ...

    final Long ๋กœ๋“œ๋งต_์•„์ด๋”” = ๋กœ๋“œ๋งต์„_์ƒ์„ฑํ•œ๋‹ค(์•ก์„ธ์Šค_ํ† ํฐ, ์นดํ…Œ๊ณ ๋ฆฌ.getId(), "๋กœ๋“œ๋งต ์ œ๋ชฉ", "๋กœ๋“œ๋งต ์†Œ๊ฐœ๊ธ€", "๋กœ๋“œ๋งต ๋ณธ๋ฌธ",
            RoadmapDifficultyType.DIFFICULT, 30, List.of(๋…ธ๋“œ1, ๋…ธ๋“œ2));

    final RoadmapContent ๋กœ๋“œ๋งต_๋ณธ๋ฌธ = ๋กœ๋“œ๋งต์œผ๋กœ๋ถ€ํ„ฐ_๋ณธ๋ฌธ์„_๊ฐ€์ ธ์˜จ๋‹ค(๋กœ๋“œ๋งต_์•„์ด๋””);
    final List<RoadmapNode> ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค = ๋กœ๋“œ๋งต_๋ณธ๋ฌธ.getNodes().getValues(); // Here!
    ...
}

private RoadmapContent ๋กœ๋“œ๋งต์œผ๋กœ๋ถ€ํ„ฐ_๋ณธ๋ฌธ์„_๊ฐ€์ ธ์˜จ๋‹ค(final Long ๋กœ๋“œ๋งต_์•„์ด๋””) {
    final Roadmap ๋กœ๋“œ๋งต = roadmapRepository.findById(๋กœ๋“œ๋งต_์•„์ด๋””).get();
    return roadmapContentRepository.findFirstByRoadmapOrderByCreatedAtDesc(๋กœ๋“œ๋งต).get();
}

์ฝ”๋“œ์˜ ์ฒซ ๋ผ์ธ์„ ๋ณด๋ฉด ๋กœ๋“œ๋งต์„_์ƒ์„ฑํ•œ๋‹ค(); ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋กœ๋“œ๋งต ์•„์ด๋””๋ฅผ ๋ฐ˜ํ™˜๋ฐ›๊ณ  ์žˆ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ์‹ค์ œ API call์„ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์‹ค์ œ๋กœ ์ƒ์„ฑ๋œ ๋กœ๋“œ๋งต์˜ ์•„์ด๋””๋งŒ์„ ๋ฐ˜ํ™˜๋ฐ›๋Š”๋‹ค. ๋ฐ˜ํ™˜๋ฐ›์€ ์•„์ด๋””๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋กœ๋“œ๋งต ์—”ํ‹ฐํ‹ฐ๋ฅผ ์–ป์–ด์˜ค๊ธฐ ์œ„ํ•ด findById()๋ฅผ ํ†ตํ•ด ๋กœ๋“œ๋งต ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•ด์˜ค๋ฉฐ, ์—ฌ๊ธฐ์„œ ๊ฐ€์žฅ ์ตœ๊ทผ์— ์ƒ์„ฑ๋œ ๋กœ๋“œ๋งต ๋ณธ๋ฌธ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด ํ•œ ๋ฒˆ ๋” ์กฐํšŒ๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.

 

๐Ÿ’ก ๋ฌผ๋ก , ๋กœ๋“œ๋งต ์—”ํ‹ฐํ‹ฐ๋กœ๋ถ€ํ„ฐ ๊ฐ€์žฅ ์ตœ๊ทผ์˜ ๋กœ๋“œ๋งต ๋ณธ๋ฌธ์„ ๋ฐ›์•„์˜ค๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๊ฒ ์ง€๋งŒ, ํ˜„ ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์—์„œ๋Š” ํ•ด๋‹น ๋ถ€๋ถ„์ด ํ•„์š”ํ•˜์ง€ ์•Š์•˜๊ณ , ๋งŒ์•ฝ ๊ทธ๋ ‡๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค๋ฉด ๋ณธ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ณผ์ •์—์„œ๋ถ€ํ„ฐ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๊ฒƒ์ด๋‹ค.

๋˜ํ•œ, ํ˜„์žฌ ์ž‘์„ฑํ•œ ๋ถ€๋ถ„์€ ์ƒ์„ฑ API๊ฐ€ ์•„์ง ๋งŒ๋“ค์–ด์ง€์ง€ ์•Š์€ ์ƒํƒœ๋กœ ์กฐํšŒ API๋ฅผ ๋งŒ๋“ค๋‹ค ๋ณด๋‹ˆ ์ƒ์„ฑ์„ ์œ„ํ•œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋งŒ๋“œ๋ ค๊ณ  '๋กœ๋“œ๋งต ๋…ธ๋“œ๋“ค' ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•ด์˜จ ์ฝ”๋“œ์—ฌ์„œ ์ถ”ํ›„ ์ƒ์„ฑ API๊ฐ€ ๋งŒ๋“ค์–ด์ง„๋‹ค๋ฉด ์ œ๊ฑฐ๋  ๋ถ€๋ถ„์ด๊ธด ํ•˜๋‹ค!

 


 

๐Ÿ’ฌ cf. findFirst~() ๋ฉ”์„œ๋“œ์—์„œ๋Š” ํŠธ๋žœ์žญ์…˜์ด ์–ด๋”จ์„๊นŒ?

public interface RoadmapContentRepository extends JpaRepository<RoadmapContent, Long> {

    Optional<RoadmapContent> findFirstByRoadmapOrderByCreatedAtDesc(final Roadmap roadmap);
}

roadmapContentRepository์˜ ๊ฒฝ์šฐ ์œ„์™€ ๊ฐ™์ด JpaRepository๋ฅผ ์ƒ์†๋ฐ›๊ณ  ์žˆ๋‹ค.

์ด๋•Œ, JpaRepository ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌํ˜„์ฒด์ธ SimpleJpaRepository ํด๋ž˜์Šค๋ฅผ ๊ฐ€๋ฉด ์œ„์™€ ๊ฐ™์ด @Transactional์ด ๋ถ™์–ด ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœํ•˜๋Š” ์‹œ์ ์— ๋Œ€ํ•ด์„œ๋งŒ ํŠธ๋žœ์žญ์…˜์ด ๊ฑธ๋ ค์žˆ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

 

๊ธฐ๋ณธ์ ์œผ๋กœ ์Šคํ”„๋ง์—์„œ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋Š” ํŠธ๋žœ์žญ์…˜๊ณผ ์ƒ๋ช…์ฃผ๊ธฐ๊ฐ€ ๋™์ผํ•˜๊ธฐ ๋•Œ๋ฌธ์—, roadmapContentRepository๋กœ๋ถ€ํ„ฐ ์กฐํšŒํ•ด์˜จ RoadmapContent๋Š” ํŠธ๋žœ์žญ์…˜์ด ์ข…๋ฃŒ๋˜๋ฉด์„œ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์˜ ๊ด€๋ฆฌ ๋ฒ”์œ„์—์„œ๋„ ํ•จ๊ป˜ ๋ฒ—์–ด๋‚˜๊ฒŒ ๋œ๋‹ค. ํ˜„์žฌ ๋กœ๋“œ๋งต ๋ณธ๋ฌธ ์—”ํ‹ฐํ‹ฐ๋Š” ์ค€์˜์† ์ƒํƒœ๊ฐ€ ๋˜์—ˆ์Œ์„ ๊ธฐ์–ตํ•˜์ž.

 


 

final List<RoadmapNode> ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค = ๋กœ๋“œ๋งต_๋ณธ๋ฌธ.getNodes().getValues(); // Here!

๋‹ค์‹œ ๋Œ์•„์™€์„œ, here! ์ด๋ผ๊ณ  ํŠน์ •๋œ ๋ถ€๋ถ„์„ ๋ณด์ž. ํ•ด๋‹น ๋ถ€๋ถ„์ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ง€์ ์ด๋‹ค.

๋กœ๋“œ๋งต ๋ณธ๋ฌธ์œผ๋กœ๋ถ€ํ„ฐ ๋…ธ๋“œ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๊ณ  ํ•  ๋•Œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์ด๋‹ค.

public List<RoadmapNode> getValues() {
    return new ArrayList<>(values);
}

๋…ธ๋“œ์—์„œ getValues() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด values()์— ๋Œ€ํ•œ ์ ‘๊ทผ์ด ์ผ์–ด๋‚˜๊ฒŒ ๋˜๋Š”๋ฐ, ์ด๋•Œ ์•„๋ž˜์™€ ๊ฐ™์€ ์ผ๋“ค์ด ๋ฐœ์ƒํ•œ๋‹ค.

 

read() ๋ผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๋”ฐ๋ผ ๋“ค์–ด๊ฐ€๋‹ค ๋ณด๋ฉด, ์–ด๋– ํ•œ ์ดˆ๊ธฐํ™” ์ž‘์—…์ด ์ฒ˜์Œ์— ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ , ์ง€์—ฐ ๋กœ๋”ฉ ์ž‘์—…์„ ์„ธ์…˜์œผ๋กœ๋ถ€ํ„ฐ ์ปฌ๋ ‰์…˜ ์ •๋ณด๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

์ƒ์œ„์˜ ์ฃผ์„์„ ์ž˜ ์ฝ์–ด๋ณด๋ฉด, ์ดˆ๊ธฐํ™”๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•  ๋•Œ LazyInitializationException์ด ๋ฐœ์ƒํ•จ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ , ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋ฅผ ํƒ€๊ณ  ๋“ค์–ด๊ฐ€๋ฉด session ์ •๋ณด๊ฐ€ ์—†์„ ๋•Œ ์™ธ๋ถ€์˜ ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํŒ๋‹จํ•˜๋Š”๋ฐ, ์‚ฌ์šฉ ๋ถˆ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— (allowLoadOutsideTransaction=false) ํ•˜๋‹จ์˜ else ๊ตฌ๋ฌธ์œผ๋กœ ์ œ์–ด๊ฐ€ ๋‚ด๋ ค๊ฐ€๊ฒŒ ๋˜์–ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋œ๋‹ค.

์ฐธ๊ณ ๋กœ, allowLoadOutsideTransaction์˜ ๊ฒฝ์šฐ ์ œ์ผ ์ฒ˜์Œ ์„ธ์…˜ ์ •๋ณด๋ฅผ ์„ธํŒ…ํ•ด์ค„ ๋•Œ ์œ„์˜ ๋ฉ”์„œ๋“œ์—์„œ ์ ์šฉ๋œ๋‹ค.

์„ธ์…˜ ์ •๋ณด๋กœ๋ถ€ํ„ฐ ์ง€์—ฐ ๋กœ๋”ฉ์ž„์—๋„ ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

 

๊ทธ๋ ‡๋‹ค๋ฉด ํŠธ๋žœ์žญ์…˜ ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฉด ๋˜๋Š” ๊ฒŒ ์•„๋‹๊นŒ?

ํ•œ ๋ฒˆ ์ฒœ์ฒœํžˆ ํ•ด๊ฒฐํ•ด๋‚˜๊ฐ€๋ณด์ž.

 


 

๐ŸŒฑ ๊ทธ๋ƒฅ @Transactional์„ ๋ถ™์ด๋ฉด ๋˜์ง€ ์•Š๋‚˜?

@Test
@Transactional // ํŠธ๋žœ์žญ์…˜!
void ํ…Œ์ŠคํŠธ() throws JsonProcessingException {
    ...
	
    final RoadmapCategory ์นดํ…Œ๊ณ ๋ฆฌ = ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ_์ €์žฅํ•œ๋‹ค("์—ฌํ–‰");
    final Long ๋กœ๋“œ๋งต_์•„์ด๋”” = ๋กœ๋“œ๋งต์„_์ƒ์„ฑํ•œ๋‹ค(์•ก์„ธ์Šค_ํ† ํฐ, ์นดํ…Œ๊ณ ๋ฆฌ.getId(), "๋กœ๋“œ๋งต ์ œ๋ชฉ", "๋กœ๋“œ๋งต ์†Œ๊ฐœ๊ธ€", "๋กœ๋“œ๋งต ๋ณธ๋ฌธ",
            RoadmapDifficultyType.DIFFICULT, 30, List.of(๋…ธ๋“œ1, ๋…ธ๋“œ2));

    final RoadmapContent ๋กœ๋“œ๋งต_๋ณธ๋ฌธ = ๋กœ๋“œ๋งต์œผ๋กœ๋ถ€ํ„ฐ_๋ณธ๋ฌธ์„_๊ฐ€์ ธ์˜จ๋‹ค(๋กœ๋“œ๋งต_์•„์ด๋””);
    final List<RoadmapNode> ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค = ๋กœ๋“œ๋งต_๋ณธ๋ฌธ.getNodes().getValues(); 
    ...
}

 private RoadmapCategory ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ_์ €์žฅํ•œ๋‹ค(final String ์นดํ…Œ๊ณ ๋ฆฌ_์ด๋ฆ„) {
    final RoadmapCategory ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ = new RoadmapCategory(์นดํ…Œ๊ณ ๋ฆฌ_์ด๋ฆ„);
    return roadmapCategoryRepository.save(๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ);
}

ํŠธ๋žœ์žญ์…˜์ด ์—†์–ด์„œ ์ƒ๊ธด ๋ฌธ์ œ๋ผ๋ฉด, @Transactional์„ ๋ถ™์ด๋ฉด ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ๋‹ค.

์—ฌ๊ธฐ์„œ ์•„๊นŒ ์˜ˆ์ œ์™€ ๋‹ค๋ฅด๊ฒŒ ๋กœ๋“œ๋งต์— ๋Œ€ํ•œ ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ์„ฑ ๋ฉ”์„œ๋“œ๊ฐ€ ์ถ”๊ฐ€๋œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋Š”๋ฐ, ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ํฌ์ธํŠธ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋„ฃ์–ด๋‘์—ˆ๋‹ค. ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๊ฒฝ์šฐ ์ƒ์„ฑํ•˜๋Š” API๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— (๋‚˜์ค‘์— admin ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋„ฃ์„ ์˜ˆ์ •์ด์—ˆ๋‹ค.) ์œ„์™€ ๊ฐ™์ด ์ง์ ‘ save๋ฅผ ํ•ด์ค€๋‹ค.

 

ํ•˜์ง€๋งŒ, ์ •๋ง ๋œฌ๊ธˆ์—†์ด NPE๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์ด๋Š”, ์‘๋‹ต๊ฐ’์ด ์ œ๋Œ€๋กœ ๋ฐ˜ํ™˜๋˜์ง€ ์•Š์œผ๋ฉด์„œ response header ๊ฐ’์ด ์ œ๋Œ€๋กœ ๋‚ด๋ ค์˜ค์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

์‹ค์ œ๋กœ ํ•ด๋‹น ๋กœ๊ทธ์˜ ์ƒ๋‹จ์œผ๋กœ ์˜ฌ๋ผ๊ฐ€๋ณด๋ฉด, ์œ„์™€ ๊ฐ™์ด ์นดํ…Œ๊ณ ๋ฆฌ์— ๋Œ€ํ•œ Exception์ด ๋ฐœ์ƒํ•œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

์ด๋Š”, ๋กœ๋“œ๋งต ์ƒ์„ฑ API๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด์„œ create ํ•˜๋Š” ๊ณผ์ •์— request body ๊ฐ’์œผ๋กœ ๋ฐ›์€ ๋กœ๋“œ๋งต ์นดํ…Œ๊ณ ๋ฆฌ ์•„์ด๋””์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๊ฒŒ ๋˜๊ณ , ํ•ด๋‹น ๋กœ์ง์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์•„ ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜์ด๋‹ค.

 

๋ถ„๋ช… ์œ„์—์„œ ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ_์ €์žฅํ•œ๋‹ค() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ €์žฅ์„ ํ–ˆ๋Š”๋ฐ, ์ด๊ฒŒ ์–ด๋–ป๊ฒŒ ๋œ ์ผ์ผ๊นŒ?

์ด๋Š”, @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) ๋•Œ๋ฌธ์ด๋‹ค.

์ด์ „์— ํ…Œ์ŠคํŠธ ์ปจํ…์ŠคํŠธ์— ๋Œ€ํ•œ ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ•œ ์ ์ด ์žˆ๋Š”๋ฐ, ๊ทธ๋•Œ random_port ์˜ต์…˜์— ๋Œ€ํ•ด ์ด์™€ ๊ฐ™์ด ์ปค๋ฉ˜ํŠธ๋ฅผ ๋‚จ๊ฒผ๋‹ค.

random_port ์˜ต์…˜์„ ์ง€์ •ํ•˜๊ฒŒ ๋˜๋ฉด ๋ณ„๊ฐœ์˜ ์Šค๋ ˆ๋“œ์—์„œ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์‹คํ–‰๋œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

= ์ฆ‰, ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์—์„œ ์ž‘์„ฑํ•œ ๋ฉ”์„œ๋“œ์˜ ์Šค๋ ˆ๋“œ์™€ @SpringBootTest์˜ ๋ฉ”์„œ๋“œ์˜ ์Šค๋ ˆ๋“œ๊ฐ€ ๋‹ค๋ฅด๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.

 

๊ธฐ๋ณธ์ ์œผ๋กœ @Transactional์ด ๋ถ™๊ฒŒ ๋˜๋ฉด ์ž‘์—… ์Šค๋ ˆ๋“œ๋Š” ์ปค๋„ฅ์…˜ ํ’€์—์„œ Connection ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์™€์„œ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋Š”๋ฐ, ์Šค๋ ˆ๋“œ๊ฐ€ ๋‹ฌ๋ผ์ง€๊ฒŒ ๋˜๋ฉด ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋Š” Connection์ด ๋‹ฌ๋ผ์ง€๊ฒŒ ๋œ๋‹ค. ์ฆ‰, ํŠธ๋žœ์žญ์…˜์ด ์•„์˜ˆ ๋‹ฌ๋ผ์ง€๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜๋Š” ๋กœ๋“œ๋งต ์ƒ์„ฑ API์˜ ์Šค๋ ˆ๋“œ ์ž…์žฅ์œผ๋กœ์„œ๋Š” ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ์ธ ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ_์ƒ์„ฑํ•œ๋‹ค() ์Šค๋ ˆ๋“œ๊ฐ€ ํ•˜๋Š” ์ผ์„ ์ธ์‹ํ•˜์ง€ ๋ชปํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์นดํ…Œ๊ณ ๋ฆฌ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์—†๋Š” ๊ฒƒ์ด๋‹ค.

 

์—ฌ๊ธฐ์„œ ํ•œ ๊ฐ€์ง€ ์‚ฝ์งˆ์„ ํ–ˆ๋Š”๋ฐ, ์ฝ˜์†”์ƒ์œผ๋กœ๋Š” ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธธ๋ž˜ ๊ณ„์† insert๊ฐ€ ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์—ˆ๋‹ค. (IDENTITY ์ „๋žต์œผ๋กœ ์ธํ•ด)

ํ•˜์ง€๋งŒ, ์‹ค์ œ๋กœ DB์— ๊ฐ€์„œ ํ™•์ธํ•ด๋ณด๋‹ˆ ๊ฒฐ๊ณผ๊ฐ€ ์ €์žฅ๋˜์ง€ ์•Š์•˜์—ˆ๊ณ , ์•„๋งˆ ์ฟผ๋ฆฌ๋งŒ ๋ฐœ์ƒํ•œ ๊ฒƒ ๊ฐ™๋‹ค๊ณ  ์ถ”์ธก๋œ๋‹ค.

(ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ ํ›„ ์ปค๋ฐ‹์ด ๋˜์ง€ ์•Š์€ ์ƒํƒœ๋ผ๊ณ  ๋ณด๋Š” ๊ฒŒ ๋” ์ •ํ™•ํ•  ๊ฒƒ ๊ฐ™๋‹ค.)

@Transactional์„ ์ œ๊ฑฐํ•˜๋ฉด ์œ„์™€ ๊ฐ™์ด ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ €์žฅ๋œ๋‹ค. (ํŠธ๋žœ์žญ์…˜์ด ์—†์œผ๋‹ˆ ๋ฐ”๋กœ DB์— ์ €์žฅ)

 

 

๊ทธ๋Ÿฌ๋ฉด ์ด ๋ฌธ์ œ๋ฅผ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•ด์•ผ ํ• ๊นŒ?

RANDOM_PORT๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฒŒ ๋˜๋ฉด E2E ํ…Œ์ŠคํŠธ์˜ ์˜๋ฏธ๊ฐ€ ์—†๊ณ , API call๋กœ ๋Œ€์ฒดํ•  ์ˆ˜ ์—†๋Š” ์ƒํ™ฉ์ด๋‹ค.

 


 

๐ŸŒฑ ์ง€์—ฐ ๋กœ๋”ฉ์ด ํ•„์š”ํ•œ ์‹œ์ ์—์„œ๋งŒ ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•˜๊ธฐ - ๋ฉ”์„œ๋“œ ๋ถ„๋ฆฌํ•˜๊ธฐ

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋‚ฌ๋˜ ๊ทผ๋ณธ์ ์ธ ๋ฌธ์ œ๋Š”, ์ง€์—ฐ ๋กœ๋”ฉ์„ ํ•˜๋Š” ์‹œ์ ์— ํŠธ๋žœ์žญ์…˜์ด ์กด์žฌํ•˜์ง€ ์•Š์•„ ์กฐํšŒ๋ฅผ ํ•ด์˜ฌ ์ˆ˜ ์—†์—ˆ๋˜ ๊ฒƒ์ด๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์กฐํšŒํ•˜๋Š” ์‹œ์ ์— ์ƒˆ๋กœ์šด ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•ด์ฃผ๋Š” ๊ฑด์€ ์–ด๋–จ๊นŒ?

@Test
void ํ…Œ์ŠคํŠธ() throws JsonProcessingException {
    ...
	
    final RoadmapCategory ์นดํ…Œ๊ณ ๋ฆฌ = ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ_์ €์žฅํ•œ๋‹ค("์—ฌํ–‰");
    final List<RoadmapNode> ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค = ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค์„_๋ฐ˜ํ™˜ํ•œ๋‹ค(์•ก์„ธ์Šค_ํ† ํฐ, ์นดํ…Œ๊ณ ๋ฆฌ, ๋…ธ๋“œ1, ๋…ธ๋“œ2);
    
    ...
}


@Transactional // ํŠธ๋žœ์žญ์…˜!
public List<RoadmapNode> ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค์„_๋ฐ˜ํ™˜ํ•œ๋‹ค(final String ์•ก์„ธ์Šค_ํ† ํฐ, final RoadmapCategory ์นดํ…Œ๊ณ ๋ฆฌ, final RoadmapNodeSaveRequest ๋…ธ๋“œ1,
                                        final RoadmapNodeSaveRequest ๋…ธ๋“œ2) {
    final Long ๋กœ๋“œ๋งต_์•„์ด๋”” = ๋กœ๋“œ๋งต์„_์ƒ์„ฑํ•œ๋‹ค(์•ก์„ธ์Šค_ํ† ํฐ, ์นดํ…Œ๊ณ ๋ฆฌ.getId(), "๋กœ๋“œ๋งต ์ œ๋ชฉ", "๋กœ๋“œ๋งต ์†Œ๊ฐœ๊ธ€", "๋กœ๋“œ๋งต ๋ณธ๋ฌธ",
            RoadmapDifficultyType.DIFFICULT, 30, List.of(๋…ธ๋“œ1, ๋…ธ๋“œ2));

    final RoadmapContent ๋กœ๋“œ๋งต_๋ณธ๋ฌธ = ๋กœ๋“œ๋งต์œผ๋กœ๋ถ€ํ„ฐ_๋ณธ๋ฌธ์„_๊ฐ€์ ธ์˜จ๋‹ค(๋กœ๋“œ๋งต_์•„์ด๋””);
    return ๋กœ๋“œ๋งต_๋ณธ๋ฌธ.getNodes().getValues();
}

ํ•˜์ง€๋งŒ, ์œ„ ์ฝ”๋“œ๋Š” ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ด๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ @Transactional์€ Spring AOP๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

์ด๋•Œ ํฌ๊ฒŒ 2๊ฐ€์ง€์˜ ํŠน์ง•์ด ์กด์žฌํ•œ๋‹ค.

1. ํƒ€๊ฒŸ ํด๋ž˜์Šค๋ฅผ ์ƒ์†ํ•˜์—ฌ ํ”„๋ก์‹œ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ƒ์† ์ž์ฒด๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•œ private ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•ด์„œ๋Š” ์ ์šฉ ๋ถˆ๊ฐ€

2. ๋™์ผํ•œ ํด๋ž˜์Šค์˜ ๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๋ฉด ํ”„๋ก์‹œ๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š๊ณ  ๋Œ€์ƒ ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜์–ด ์ ์šฉ ๋ถˆ๊ฐ€

 

์œ„ ์ฝ”๋“œ์—์„œ๋Š” 2๋ฒˆ์˜ ๊ฒฝ์šฐ, ๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ํŠธ๋žœ์žญ์…˜์„ ์ ์šฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒƒ์ด๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด, ๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ ๋Œ€์‹ ์— ์™ธ๋ถ€ ํด๋ž˜์Šค๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋˜์ง€ ์•Š์„๊นŒ?

 


 

๐ŸŒฑ ์ง€์—ฐ ๋กœ๋”ฉ์ด ํ•„์š”ํ•œ ์‹œ์ ์—์„œ๋งŒ ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•˜๊ธฐ - ํด๋ž˜์Šค ๋ถ„๋ฆฌํ•˜๊ธฐ

@Component
@TestConstructor(autowireMode = AutowireMode.ALL)
public class RoadmapTestHelper {

    private final RoadmapRepository roadmapRepository;
    private final RoadmapContentRepository roadmapContentRepository;

    public RoadmapTestHelper(final RoadmapRepository roadmapRepository,
                             final RoadmapContentRepository roadmapContentRepository) {
        this.roadmapRepository = roadmapRepository;
        this.roadmapContentRepository = roadmapContentRepository;
    }

    @Transactional(readOnly = true)
    public List<RoadmapNode> ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค์„_์กฐํšŒํ•œ๋‹ค(final Long ๋กœ๋“œ๋งต_์•„์ด๋””) {
        final RoadmapContent ๋กœ๋“œ๋งต_๋ณธ๋ฌธ = ๋กœ๋“œ๋งต์œผ๋กœ๋ถ€ํ„ฐ_๋ณธ๋ฌธ์„_๊ฐ€์ ธ์˜จ๋‹ค(๋กœ๋“œ๋งต_์•„์ด๋””);
        return ๋กœ๋“œ๋งต_๋ณธ๋ฌธ.getNodes().getValues();
    }

    private RoadmapContent ๋กœ๋“œ๋งต์œผ๋กœ๋ถ€ํ„ฐ_๋ณธ๋ฌธ์„_๊ฐ€์ ธ์˜จ๋‹ค(final Long ๋กœ๋“œ๋งต_์•„์ด๋””) {
        final Roadmap ๋กœ๋“œ๋งต = roadmapRepository.findById(๋กœ๋“œ๋งต_์•„์ด๋””).get();
        return roadmapContentRepository.findFirstByRoadmapOrderByCreatedAtDesc(๋กœ๋“œ๋งต).get();
    }
}

๋กœ๋“œ๋งต ๋…ธ๋“œ๋ฅผ ์กฐํšŒํ•˜๊ธฐ ์œ„ํ•œ ๋ณ„๋„์˜ Helper ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ด์ค€๋‹ค. 

๋กœ๋“œ๋งต_๋ณธ๋ฌธ ์—”ํ‹ฐํ‹ฐ๊ฐ€ detached ์ƒํƒœ๊ฐ€ ๋˜์ง€ ์•Š๋„๋ก ์กฐํšŒ๋ฅผ ํ•ด์˜ค๋Š” ์‹œ์ ์˜ ๋ฉ”์„œ๋“œ๋ถ€ํ„ฐ helper ํด๋ž˜์Šค์— ๋‘์—ˆ์œผ๋ฉฐ, ๋ณธ๋ฌธ์œผ๋กœ๋ถ€ํ„ฐ ๋…ธ๋“œ ์ •๋ณด๋ฅผ ์ง€์—ฐ๋กœ๋”ฉ์„ ํ†ตํ•ด ๊ฐ€์ ธ์™€์„œ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค.

 

@Test
void ํ…Œ์ŠคํŠธ() throws JsonProcessingException {
    ...
	
    final RoadmapCategory ์นดํ…Œ๊ณ ๋ฆฌ = ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ_์ €์žฅํ•œ๋‹ค("์—ฌํ–‰");
    final Long ๋กœ๋“œ๋งต_์•„์ด๋”” = ๋กœ๋“œ๋งต์„_์ƒ์„ฑํ•œ๋‹ค(์•ก์„ธ์Šค_ํ† ํฐ, ์นดํ…Œ๊ณ ๋ฆฌ.getId(), "๋กœ๋“œ๋งต ์ œ๋ชฉ", "๋กœ๋“œ๋งต ์†Œ๊ฐœ๊ธ€", "๋กœ๋“œ๋งต ๋ณธ๋ฌธ",
            RoadmapDifficultyType.DIFFICULT, 30, List.of(๋…ธ๋“œ1, ๋…ธ๋“œ2));
    
    // Here!
    final List<RoadmapNode> ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค = roadmapTestHelper.๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค์„_์กฐํšŒํ•œ๋‹ค(๋กœ๋“œ๋งต_์•„์ด๋””);
    ...
}

๊ทธ๋ฆฌ๊ณ  ๊ธฐ์กด์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ helper ํด๋ž˜์Šค๋ฅผ ํ†ตํ•ด์„œ ๋…ธ๋“œ๋ฅผ ์กฐํšŒํ•ด์™”๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ์œ„์™€ ๊ฐ™์ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ์„ฑ๊ณตํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!

 

์™€ ์„ฑ๊ณต~!

 

ํ•˜์ง€๋งŒ, ์œ„ ๋ฐฉ๋ฒ•์€ ๋งค์šฐ ์ฐ์ฐํ•˜๋‹ค. ์ง€์—ฐ๋กœ๋”ฉ์ด ํ•„์š”ํ•œ ์ฝ”๋“œ๋งˆ๋‹ค ์ด๋ ‡๊ฒŒ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค๋กœ ๋ถ„๋ฆฌํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. 

๋งŒ์•ฝ ํ…Œ์ŠคํŠธํ•˜๋Š” ์ผ€์ด์Šค๊ฐ€ ์ •๋ง ๋งŽ์•„์ง„๋‹ค๋ฉด ๊ทธ๋Ÿด ๋•Œ๋งˆ๋‹ค helper ํด๋ž˜์Šค์— ๋ฉ”์Šค๋“œ๊ฐ€ ์—„์ฒญ๋‚˜๊ฒŒ ๋Š˜์–ด๋‚˜๊ฒŒ ๋  ๊ฒƒ์ด๋‹ค.

์ด๋Š” ์ „ํ˜€ ๊ฐœ๋ฐœ์ž๋‹ต์ง€ ๋ชปํ•œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์ด๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์„ ๋ชจ์ƒ‰ํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค.

 


 

๐ŸŒฑ ์ง€์—ฐ ๋กœ๋”ฉ์ด ํ•„์š”ํ•œ ์‹œ์ ์—์„œ๋งŒ ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•˜๊ธฐ - ํ•จ์ˆ˜ํ˜• ์ธํ„ฐํŽ˜์ด์Šค ํ™œ์šฉํ•˜๊ธฐ

ํ•„์š”ํ•  ๋•Œ๋งˆ๋‹ค helper ํด๋ž˜์Šค์˜ ๋ฉ”์„œ๋“œ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ํŠธ๋žœ์žญ์…˜์ด ํ•„์š”ํ•œ ๋กœ์ง์— ๋Œ€ํ•ด์„œ๋งŒ ์™ธ๋ถ€ ํด๋ž˜์Šค์—์„œ ์‹คํ–‰๋˜๋„๋ก ๋งŒ๋“ค ์ˆ˜๋Š” ์—†์„๊นŒ? ์ฆ‰, ๋ฉ”์„œ๋“œ๋ฅผ ์ธ์ž๋กœ ๋„˜๊ฒจ์„œ ์–ด๋– ํ•œ ๊ณณ์—์„œ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ถ์€ ๊ฒƒ์ด๋‹ค.

 

์ž๋ฐ”์—์„œ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋žŒ๋‹ค์‹์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ์ธ์ž๋กœ ๋„˜๊ธด ๊ฐ’์œผ๋กœ List<RoadmapNode>๋ฅผ ๋ฐ›์•„์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— T๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ๊ฐ€์ง„ ํ•จ์ˆ˜ํ˜• ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ•˜๋‚˜ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

@FunctionalInterface
public interface TransactionalTask<T> {
    T execute();
}
๐Ÿ’ก ํ•จ์ˆ˜ํ˜• ์ธํ„ฐํŽ˜์ด์Šค๋ž€?
์˜ค์ง 1๊ฐœ์˜ ์ถ”์ƒ ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง€๋Š” ์ธํ„ฐํŽ˜์ด์Šค. 
์—ฌ๊ธฐ์„œ ์ถ”์ƒ ๋ฉ”์„œ๋“œ๋ž€, ์ž์‹ ํด๋ž˜์Šค์—์„œ ๋ฐ˜๋“œ์‹œ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ํ•ด์•ผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ์ด๋‹ค.

 

๊ทธ๋ฆฌ๊ณ , ํŠธ๋žœ์žญ์…˜์ด ํ•„์š”ํ•œ ์ž‘์—…์„ ๋„์™€์ค€๋‹ค๋Š” ์˜๋ฏธ๋กœ ํ•˜๋‚˜์˜ ํ—ฌํผ ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜๋„๋ก ํ•˜์ž.

@Component
public class TransactionHelper {

    @Transactional(readOnly = true)
    public <T> T getResult(final TransactionalTask<T> task) {
        return task.execute();
    }
}

ํ—ฌํผ ํด๋ž˜์Šค์—์„œ๋Š” TransactionTask ํƒ€์ž…์˜ ์–ด๋– ํ•œ ๋žŒ๋‹ค์‹์„ ๋ฐ›์•„์„œ, ํ•ด๋‹น ๋žŒ๋‹ค์‹์„ @Transactional์ด ๊ฑธ๋ฆฐ ์ƒํƒœ๋กœ ์‹คํ–‰ํ•ด์ฃผ๋Š” ์—ญํ• ์„ ์ง„ํ–‰ํ•œ๋‹ค. ์ด์ œ ์ด ํ—ฌํผ ํด๋ž˜์Šค๋ฅผ ํ™œ์šฉํ•˜๊ฒŒ ๋˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

 

@Test
void ํ…Œ์ŠคํŠธ() throws JsonProcessingException {
    ...
	
    final RoadmapCategory ์นดํ…Œ๊ณ ๋ฆฌ = ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ_์ €์žฅํ•œ๋‹ค("์—ฌํ–‰");
    final Long ๋กœ๋“œ๋งต_์•„์ด๋”” = ๋กœ๋“œ๋งต์„_์ƒ์„ฑํ•œ๋‹ค(์•ก์„ธ์Šค_ํ† ํฐ, ์นดํ…Œ๊ณ ๋ฆฌ.getId(), "๋กœ๋“œ๋งต ์ œ๋ชฉ", "๋กœ๋“œ๋งต ์†Œ๊ฐœ๊ธ€", "๋กœ๋“œ๋งต ๋ณธ๋ฌธ",
            RoadmapDifficultyType.DIFFICULT, 30, List.of(๋…ธ๋“œ1, ๋…ธ๋“œ2));
            
    final List<RoadmapNode> ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค = transactionHelper.getResult(new TransactionalTask<List<RoadmapNode>>() {
            @Override
            public List<RoadmapNode> execute() {
                final RoadmapContent ๋กœ๋“œ๋งต_๋ณธ๋ฌธ = ๋กœ๋“œ๋งต์œผ๋กœ๋ถ€ํ„ฐ_๋ณธ๋ฌธ์„_๊ฐ€์ ธ์˜จ๋‹ค(๋กœ๋“œ๋งต_์•„์ด๋””);
                return ๋กœ๋“œ๋งต_๋ณธ๋ฌธ.getNodes().getValues();
            }
        });
    ...
}

transactionHelper ํด๋ž˜์Šค์˜ getResult() ๋ฉ”์„œ๋“œ์˜ ์ธ์ž๋กœ ํ•จ์ˆ˜ํ˜• ์ธํ„ฐํŽ˜์ด์Šค์˜ ์ถ”์ƒ ๋ฉ”์„œ๋“œ์ธ execute()์˜ ์ต๋ช… ํด๋ž˜์Šค๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค.

๊ทธ๋ฆฌ๊ณ , ํ•ด๋‹น ๊ตฌํ˜„์ฒด์— ๋กœ๋“œ๋งต ๋ณธ๋ฌธ์„ ์กฐํšŒํ•˜์—ฌ ๋กœ๋“œ๋งต ๋…ธ๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋กœ์ง์„ ๋„ฃ์—ˆ๋‹ค.

๐Ÿ’ก ์ต๋ช… ํด๋ž˜์Šค
ํด๋ž˜์Šค์˜ ์„ ์–ธ๊ณผ ์ธ์Šคํ„ด์Šคํ™”๋ฅผ ๋™์‹œ์— ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ํด๋ž˜์Šค๋กœ, ์ด๋ฆ„์ด ์—†๋Š” ํด๋ž˜์Šค๋ผ ์ต๋ช… ํด๋ž˜์Šค๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

 

์ด๋•Œ, ์ต๋ช… ํด๋ž˜์Šค์˜ ๊ฒฝ์šฐ ๋žŒ๋‹ค๋กœ ์ถ•์•ฝํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์ด ๋” ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋ฆฌํŒฉํ„ฐ๋ง์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

@Test
void ํ…Œ์ŠคํŠธ() throws JsonProcessingException {
    ...
	
    final RoadmapCategory ์นดํ…Œ๊ณ ๋ฆฌ = ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ_์ €์žฅํ•œ๋‹ค("์—ฌํ–‰");
    final Long ๋กœ๋“œ๋งต_์•„์ด๋”” = ๋กœ๋“œ๋งต์„_์ƒ์„ฑํ•œ๋‹ค(์•ก์„ธ์Šค_ํ† ํฐ, ์นดํ…Œ๊ณ ๋ฆฌ.getId(), "๋กœ๋“œ๋งต ์ œ๋ชฉ", "๋กœ๋“œ๋งต ์†Œ๊ฐœ๊ธ€", "๋กœ๋“œ๋งต ๋ณธ๋ฌธ",
            RoadmapDifficultyType.DIFFICULT, 30, List.of(๋…ธ๋“œ1, ๋…ธ๋“œ2));
            
    final List<RoadmapNode> ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค = transactionHelper.getResult(() -> {
    	final RoadmapContent ๋กœ๋“œ๋งต_๋ณธ๋ฌธ = ๋กœ๋“œ๋งต์œผ๋กœ๋ถ€ํ„ฐ_๋ณธ๋ฌธ์„_๊ฐ€์ ธ์˜จ๋‹ค(๋กœ๋“œ๋งต_์•„์ด๋””);
    	return ๋กœ๋“œ๋งต_๋ณธ๋ฌธ.getNodes().getValues();
    });
    
    ...
}

์ตœ์ข…์ ์œผ๋กœ ์‹คํ–‰ํ•ด๋ณด๋ฉด ์œ„์™€ ๊ฐ™์ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋„ ์ž˜ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค! ๐Ÿ‘

 

์ด์ œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ์ง€์—ฐ๋กœ๋”ฉ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์— ๋Œ€ํ•ด (then์ ˆ ๊ฐ™์ด ์‘๋‹ต๊ฐ’์„ ๊บผ๋‚ด์–ด ๊ฒ€์ฆํ•  ๋•Œ) ์ด๋ฅผ ์ž˜ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

์˜ค๋žœ๋งŒ์— ํ•จ์ˆ˜ํ˜• ์ธํ„ฐํŽ˜์ด์Šค ๊ฐ™์€ ๊ฐœ๋…์„ ๋ณด๋‹ค ๋ณด๋‹ˆ๊นŒ ํ—ท๊ฐˆ๋ ธ๋Š”๋ฐ, ์•„๋ฌด์ชผ๋ก ์ž˜ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋‹คํ–‰์ด๋‹ค.

๊ฝค ์˜๋ฏธ์žˆ๋Š” ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…์„ ํ•œ ๊ฒƒ ๊ฐ™์•„์„œ ์žฌ๋ฐŒ์—ˆ๋‹ค ๐Ÿ˜Š

 


+ ์ถ”๊ฐ€

๊ฐ“๋ง๋ž‘ ์„ ์ƒ๋‹˜๊ป˜์„œ ๋” ์ข‹์€ ๋ฐฉ๋ฒ•์„ ์ œ์‹œํ•ด์ฃผ์…”์„œ ๊ณต์œ !

@Autowird
private TransactionTemplate transactionTemplate;

@Test
void ํ…Œ์ŠคํŠธ() throws JsonProcessingException {
    ...
	
    final RoadmapCategory ์นดํ…Œ๊ณ ๋ฆฌ = ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ_์ €์žฅํ•œ๋‹ค("์—ฌํ–‰");
    final Long ๋กœ๋“œ๋งต_์•„์ด๋”” = ๋กœ๋“œ๋งต์„_์ƒ์„ฑํ•œ๋‹ค(์•ก์„ธ์Šค_ํ† ํฐ, ์นดํ…Œ๊ณ ๋ฆฌ.getId(), "๋กœ๋“œ๋งต ์ œ๋ชฉ", "๋กœ๋“œ๋งต ์†Œ๊ฐœ๊ธ€", "๋กœ๋“œ๋งต ๋ณธ๋ฌธ",
            RoadmapDifficultyType.DIFFICULT, 30, List.of(๋…ธ๋“œ1, ๋…ธ๋“œ2));
    
    final List<RoadmapNode> ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค = transactionTemplate.execute(new TransactionCallback<List<RoadmapNode>>() {
            @Override
            public List<RoadmapNode> doInTransaction(final TransactionStatus status) {
                final RoadmapContent ๋กœ๋“œ๋งต_๋ณธ๋ฌธ = ๋กœ๋“œ๋งต์œผ๋กœ๋ถ€ํ„ฐ_๋ณธ๋ฌธ์„_๊ฐ€์ ธ์˜จ๋‹ค(๋กœ๋“œ๋งต_์•„์ด๋””);
                return ๋กœ๋“œ๋งต_๋ณธ๋ฌธ.getNodes().getValues();
            }
        });
    
    ...
}

์Šคํ”„๋ง์—์„œ๋Š” ์ด๋ฏธ TransactionTemplate ์ด๋ผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด์„œ ์ œ๊ณต์„ ํ•ด์ฃผ๊ณ  ์žˆ์—ˆ๋‹ค... ใ…Žใ…Ž (๊ทธ๋ƒฅ ํ˜ผ์ž ๊ตฌํ˜„ํ•œ ์‚ฌ๋žŒ ๋จ)

์œ„์™€ ๊ฐ™์ด ๋ฐ˜ํ™˜๊ฐ’์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋ผ๋ฉด TransactionCallback์„ ์ธ์ž๋กœ ๋ฐ›์œผ๋ฉด ๋œ๋‹ค.

๋žŒ๋‹ค๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋” ์ถ•์•ฝ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

@Autowird
private TransactionTemplate transactionTemplate;

@Test
void ํ…Œ์ŠคํŠธ() throws JsonProcessingException {
    ...
	
    final RoadmapCategory ์นดํ…Œ๊ณ ๋ฆฌ = ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ_์ €์žฅํ•œ๋‹ค("์—ฌํ–‰");
    final Long ๋กœ๋“œ๋งต_์•„์ด๋”” = ๋กœ๋“œ๋งต์„_์ƒ์„ฑํ•œ๋‹ค(์•ก์„ธ์Šค_ํ† ํฐ, ์นดํ…Œ๊ณ ๋ฆฌ.getId(), "๋กœ๋“œ๋งต ์ œ๋ชฉ", "๋กœ๋“œ๋งต ์†Œ๊ฐœ๊ธ€", "๋กœ๋“œ๋งต ๋ณธ๋ฌธ",
            RoadmapDifficultyType.DIFFICULT, 30, List.of(๋…ธ๋“œ1, ๋…ธ๋“œ2));
    
    final List<RoadmapNode> ๋กœ๋“œ๋งต_๋…ธ๋“œ๋“ค = transactionTemplate.execute(status -> {
            final RoadmapContent ๋กœ๋“œ๋งต_๋ณธ๋ฌธ = ๋กœ๋“œ๋งต์œผ๋กœ๋ถ€ํ„ฐ_๋ณธ๋ฌธ์„_๊ฐ€์ ธ์˜จ๋‹ค(๋กœ๋“œ๋งต_์•„์ด๋””);
            return ๋กœ๋“œ๋งต_๋ณธ๋ฌธ.getNodes().getValues();
    });
    
    ...
}

์ง€๊ธˆ ๋ณด๋ฉด ์ง์ ‘ ๋งŒ๋“  transactionHelper์™€ ์™„์ „ ๋™์ผํ•˜๋‹ค๊ณ  ๋ด๋„ ๋ฌด๋ฐฉํ•˜๋‹ค. ๊ทธ๋ƒฅ ์š”๋ ‡๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ๋” ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

 

๋งŒ์•ฝ, ๋ฐ˜ํ™˜๊ฐ’์ด ํ•„์š”์—†๋‹ค๋ฉด TransactionCallbackWithoutResult๋ฅผ ์ธ์ž๋กœ ๋ฐ›์œผ๋ฉด ๋œ๋‹ค.

@Autowird
private TransactionTemplate transactionTemplate;

@Test
void ํ…Œ์ŠคํŠธ() throws JsonProcessingException {
    ...
	
    final RoadmapCategory ์นดํ…Œ๊ณ ๋ฆฌ = ๋กœ๋“œ๋งต_์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ_์ €์žฅํ•œ๋‹ค("์—ฌํ–‰");
    final Long ๋กœ๋“œ๋งต_์•„์ด๋”” = ๋กœ๋“œ๋งต์„_์ƒ์„ฑํ•œ๋‹ค(์•ก์„ธ์Šค_ํ† ํฐ, ์นดํ…Œ๊ณ ๋ฆฌ.getId(), "๋กœ๋“œ๋งต ์ œ๋ชฉ", "๋กœ๋“œ๋งต ์†Œ๊ฐœ๊ธ€", "๋กœ๋“œ๋งต ๋ณธ๋ฌธ",
            RoadmapDifficultyType.DIFFICULT, 30, List.of(๋…ธ๋“œ1, ๋…ธ๋“œ2));
    
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(final TransactionStatus status) {
                final RoadmapContent ๋กœ๋“œ๋งต_๋ณธ๋ฌธ = ๋กœ๋“œ๋งต์œผ๋กœ๋ถ€ํ„ฐ_๋ณธ๋ฌธ์„_๊ฐ€์ ธ์˜จ๋‹ค(๋กœ๋“œ๋งต_์•„์ด๋””);
                ๋กœ๋“œ๋งต_๋ณธ๋ฌธ.getNodes().getValues();
            }
        });
        
    ...
}

์ข‹์€ ๋ฐฉ๋ฒ• ๊ณต์œ ํ•ด์ค€ ๋ง๋ž‘ ์„ ์ƒ๋‹˜ ๊ณ ๋งˆ์›Œ์š” ๐Ÿ™‡‍โ™€๏ธ

Comments