week 5 junghyunlyoo n 1 - GANGNAM-JAVA/JAVA-STUDY GitHub Wiki

N+1 ๋ฌธ์ œ๋ž€?

A ์—”ํ‹ฐํ‹ฐ์™€ B ์—”ํ‹ฐํ‹ฐ๊ฐ€ 1:N ๊ด€๊ณ„์ผ ๋•Œ, 1๊ฐœ์˜ A ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•  ๋•Œ๋งˆ๋‹ค

A์— ์ฐธ์กฐ๋˜๊ณ  ์žˆ๋Š” N๊ฐœ์˜ B ์—”ํ‹ฐํ‹ฐ๊ฐ€ N๊ฐœ์˜ ๋…๋ฆฝ์ ์ธ ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ์กฐํšŒ๋˜๋Š” ํ˜„์ƒ

N+1 ๋ฌธ์ œ๊ฐ€ ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ์ด์œ ?

A ์—”ํ‹ฐํ‹ฐ์™€ A ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ฐธ์กฐํ•˜๊ณ  ์žˆ๋Š” N๊ฐœ์˜ B ์—”ํ‹ฐํ‹ฐ ๋ชจ๋‘๋ฅผ 1๊ฐœ์˜ ์ฟผ๋ฆฌ๋กœ๋„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ,

JPA ์—”ํ‹ฐํ‹ฐ์˜ fetch ์ „๋žต์„ LAZY๋กœ ์„ค์ •ํ•˜๋ฉด N+1๊ฐœ์˜ ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ์กฐํšŒ๋œ๋‹ค.

(A ์—”ํ‹ฐํ‹ฐ ์กฐํšŒ 1๋ฒˆ + N๊ฐœ์˜ B ์—”ํ‹ฐํ‹ฐ LAZY ์กฐํšŒ N๋ฒˆ)

N๋ฒˆ์˜ ์ฟผ๋ฆฌ๊ฐ€ ๋” ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ์ดํ„ฐ ์กฐํšŒ ์†๋„๊ฐ€ ๋Šฆ์–ด์ง€๊ฒŒ ๋œ๋‹ค.

์˜ˆ์‹œ ์ฝ”๋“œ

@Entity
@Getter
@NoArgsConstructor
public class Academy {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name="academy_id")
    private List<Subject> subjects = new ArrayList<>();

    @Builder
    public Academy(String name, List<Subject> subjects) {
        this.name = name;
        if(subjects != null){
            this.subjects = subjects;
        }
    }

    public void addSubject(Subject subject){
        this.subjects.add(subject);
        subject.updateAcademy(this);
    }
}
@Entity
@Getter
@NoArgsConstructor
public class Subject {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "academy_id", foreignKey = @ForeignKey(name = "FK_SUBJECT_ACADEMY"))
    private Academy academy;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "teacher_id", foreignKey = @ForeignKey(name = "FK_SUBJECT_TEACHER"))
    private Teacher teacher;

    @Builder
    public Subject(String name, Academy academy, Teacher teacher) {
        this.name = name;
        this.academy = academy;
        this.teacher = teacher;
    }

    public void updateAcademy(Academy academy){
        this.academy = academy;
    }
}
@Entity
@Getter
@NoArgsConstructor
public class Teacher {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    public Teacher(String name) {
        this.name = name;
    }
}
@Service
@Slf4j
public class AcademyService {

    private AcademyRepository academyRepository;

    public AcademyService(AcademyRepository academyRepository) {
        this.academyRepository = academyRepository;
    }

    @Transactional(readOnly = true)
    public List<String> findAllSubjectNames(){
        return extractSubjectNames(academyRepository.findAll());
    }

    /**
     * Lazy Load๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ๋ฉ”์†Œ๋“œ๋ฅผ ๋ณ„๋„๋กœ ์ƒ์„ฑ
     */
    private List<String> extractSubjectNames(List<Academy> academies){
        log.info(">>>>>>>>[๋ชจ๋“  ๊ณผ๋ชฉ์„ ์ถ”์ถœํ•œ๋‹ค]<<<<<<<<<");
        log.info("Academy Size : {}", academies.size());

        return academies.stream()
                .map(a -> a.getSubjects().get(0).getName())
                .collect(Collectors.toList());
    }
}

์—ฌ๊ธฐ์„œ ์„œ๋น„์Šค์˜ findAllSubjectNames๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ? ์•„๋ž˜ ์ฝ”๋“œ์—์„œ ํ…Œ์ŠคํŠธํ•ด๋ณด์ž

@RunWith(SpringRunner.class)
@SpringBootTest
public class AcademyServiceTest {

    @Autowired
    private AcademyRepository academyRepository;

    @Autowired
    private AcademyService academyService;

    @After
    public void cleanAll() {
        academyRepository.deleteAll();
    }

    @Before
    public void setup() {
        List<Academy> academies = new ArrayList<>();

        for(int i=0;i<10;i++){
            Academy academy = Academy.builder()
                    .name("๊ฐ•๋‚จ์Šค์ฟจ"+i)
                    .build();

            academy.addSubject(Subject.builder().name("์ž๋ฐ”์›น๊ฐœ๋ฐœ" + i).build());
            academies.add(academy);
        }

        academyRepository.save(academies);
    }

    @Test
    public void Academy์—ฌ๋Ÿฌ๊ฐœ๋ฅผ_์กฐํšŒ์‹œ_Subject๊ฐ€_N1_์ฟผ๋ฆฌ๊ฐ€๋ฐœ์ƒํ•œ๋‹ค() throws Exception {
        //given
        List<String> subjectNames = academyService.findAllSubjectNames();

        //then
        assertThat(subjectNames.size(), is(10));
    }
}

academy ์ „์ฒด๋ฅผ ์กฐํšŒํ•˜๋Š” ์ฟผ๋ฆฌ์™€

๊ฐ๊ฐ์˜ Academy๊ฐ€ ๋ณธ์ธ๋“ค์˜ subject๋ฅผ ์กฐํšŒํ•˜๋Š” ์ฟผ๋ฆฌ 10๊ฐœ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

1๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ๋„ ์กฐํšŒ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ N+1๊ฐœ์˜ ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ์กฐํšŒ๋๋‹ค. ๋‹จ 1๋ฒˆ์˜ ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ์กฐํšŒํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ?

N+1 ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• 1. Join Fetch (Inner Join)

Join Fetch๋Š” ํ•˜์œ„ ๋ฐ์ดํ„ฐ๋ฅผ Lazy ๋ฐฉ์‹์œผ๋กœ ์กฐํšŒํ•˜์ง€ ์•Š๊ณ  Eager ๋ฐฉ์‹์œผ๋กœ ์กฐํšŒํ•œ๋‹ค.

๋จผ์ € AcademyRepository์— Join Fetch๋ฅผ ์ ์šฉ์‹œํ‚จ ๋ฉ”์†Œ๋“œ๋ฅผ ๋งŒ๋“ ๋‹ค.

@Query("select a from Academy a join fetch a.subjects")
List<Academy> findAllJoinFetch();

๋งŒ์•ฝ Subject์˜ ํ•˜์œ„ Entity ๊นŒ์ง€๋„ ํ•œ๋ฒˆ์— ์กฐํšŒํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด

์•„๋ž˜์™€ ๊ฐ™์ด Join Fecth๋ฅผ ํ•œ๋ฒˆ ๋” ์ถ”๊ฐ€ํ•œ๋‹ค.

@Query("select a from Academy a join fetch a.subjects s join fetch s.teacher")
List<Academy> findAllJoinFetch();

์ด๋ ‡๊ฒŒ Join Fetch๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด N+1๊ฐœ์˜ ์ฟผ๋ฆฌ๋ฅผ 1๊ฐœ์˜ ์ฟผ๋ฆฌ๋กœ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ•œํŽธ Join Fetch๋Š” ํ•„๋“œ๋ณ„๋กœ Eager, Lazy ๋ฐฉ์‹์„ ์ฟผ๋ฆฌ์—์„œ ํ‘œํ˜„ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ ์ˆ˜์ •์— ๋Œ€ํ•œ ๋ถ€๋‹ด์ด ์žˆ๋‹ค.

N+1 ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• 2. @EntityGraph (Outer Join)

Spring JPA 2.1๋ถ€ํ„ฐ ์ง€์›ํ•˜๊ธฐ ์‹œ์ž‘ํ•œ ์–ด๋…ธํ…Œ์ด์…˜์ด๋‹ค.

@EntityGraph์˜ attributePaths์— ์ฟผ๋ฆฌ ์ˆ˜ํ–‰์‹œ ์ฆ‰์‹œ ์กฐํšŒํ•  ํ•„๋“œ๋ช…์„ ์ง€์ •ํ•˜๋ฉด Lazy๊ฐ€ ์•„๋‹Œ Eager๋กœ ์กฐํšŒ๋ฅผ ํ•˜๊ฒŒ ๋œ๋‹ค.

@EntityGraph(attributePaths = "subjects")
@Query("select a from Academy a")
List<Academy> findAllEntityGraph();

@EntityGraph์„ ์‚ฌ์šฉํ•˜๋ฉด Join Fetch ๋ฐฉ์‹์˜ ๋‹จ์ ์ด์—ˆ๋˜ ์ฝ”๋“œ ์ˆ˜์ •์— ๋Œ€ํ•œ ๋ถ€๋‹ด์„ ์–ด๋Š์ •๋„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค.

์›๋ณธ ์ฟผ๋ฆฌ์ธ select a from Academy a ๋ฅผ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ  Eager/Lazy ํ•„๋“œ๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค.


Subject์˜ ํ•˜์œ„ Entity ๊นŒ์ง€๋„ Eager ๋ฐฉ์‹์œผ๋กœ ์กฐํšŒํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด attributePaths๋ฅผ ํ•˜๋‚˜ ๋” ์ง€์ •ํ•œ๋‹ค.

@EntityGraph(attributePaths = {"subjects", "subjects.teacher"})
@Query("select a from Academy a")
List<Academy> findAllEntityGraph();

Join Fetch์™€ @EntityGraph ์‚ฌ์šฉ์‹œ ์ฃผ์˜ํ•  ์ 

Join Fetch์™€ @EntityGraph๋Š” ๋ชจ๋‘ ์นดํ…Œ์‹œ์•ˆ ๊ณฑ(Cartesian Product)์ด ๋ฐœ์ƒํ•œ๋‹ค.

๋”ฐ๋ผ์„œ Subject์˜ ์ˆ˜ ๋งŒํผ Academy๊ฐ€ ์ค‘๋ณต ์ƒ์„ฑ๋˜๊ณ  ์ค‘๋ณต ์ƒ์„ฑ๋œ Academy๋“ค ๋ชจ๋‘๊ฐ€ ๋ฆฌ์ŠคํŠธ์— ์ €์žฅ๋œ๋‹ค.


์ค‘๋ณต์„ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ• 2๊ฐ€์ง€๊ฐ€ ์žˆ๋‹ค.

  1. ์• ์ดˆ์— ์ค‘๋ณตํ•ด์„œ ์กฐํšŒํ•˜์ง€ ์•Š๋„๋ก Query select์— distinct๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

Join Fetch

@Query("select DISTINCT a from Academy a join fetch a.subjects s join fetch s.teacher")
List<Academy> findAllWithTeacher();

@EntityGraph

@EntityGraph(attributePaths = {"subjects", "subjects.teacher"})
@Query("select DISTINCT a from Academy a")
List<Academy> findAllEntityGraphWithTeacher();
  1. ์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ List๊ฐ€ ์•„๋‹Œ Set์— ์ €์žฅํ•˜์—ฌ ์ค‘๋ณต๋˜์ง€ ์•Š๊ฒŒ ์ €์žฅํ•œ๋‹ค.
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name="academy_id")
private Set<Subject> subjects = new LinkedHashSet<>();
//LinkedHashSet์„ ์‚ฌ์šฉํ•˜์—ฌ ์ˆœ์„œ๋ฅผ ๋ณด์žฅ

JPA์˜ ๊ธ€๋กœ๋ฒŒ Fetch ์ „๋žต ๊ธฐ๋ณธ ๊ฐ’

์ฆ‰์‹œ ๋กœ๋”ฉ(EAGER)

  • OneToOne
  • ManyToOne

์ง€์—ฐ ๋กœ๋”ฉ(LAZY)

  • OneToMany
  • ManyToMany
โš ๏ธ **GitHub.com Fallback** โš ๏ธ