Sequelize 튜토리얼(13)_Lazy loading vs Eager loading

차분한열정·2021년 4월 8일
3

Sequelize 튜토리얼

목록 보기
13/14

ORM을 쓸 때는 역시 특정 테이블 하나만 조회하는 것이 아니라 foreign key로 엮인 (성능상 일부러 foreign key 설정을 안 할 수도 있겠지만..) 다른 테이블의 것들도 함께 조회할 때 그 시점을 정하는 부분이 중요한 쟁점이다.

이때 메인 테이블과 엮인 다른 테이블의 row를 필요할 때,

(1) 일단 메인 테이블만 조회를 하고 필요할 때 다른 테이블의 row를 가져오는 방법 : Lazy loading(굳이 해석하자면, 게으른 로딩)

(2) 일단 메인 테이블과 함께 조회할 테이블을 미리 조인을 해서 다른 테이블의 row도 원할 때 한번에 조회하는 방법 : Eager loading(굳이 해석하자면, 탐욕적 로딩)

이렇게 두 가지 방법이 있는데요.

일단 1:1, 1:M, M:N 관계에서 이 두 가지 방법을 모두 코드로 실행해보겠습니다.

1. 1:1 관계

Student-ScholarshipAccount

(1) Lazy loading

// app.js 
const db = require('./models/index.js');

const sequelize = db.sequelize;
const Student = db.Student;
const ScholarshipAccount = db.ScholarshipAccount;
const Course = db.Course;
const Professor = db.Professor;
const Grade = db.Grade;

(async () => {
  const student = await Student.findOne({
    where: { registrationNum: 20203718 },
  });
  const scholarshipAccount = await student.getScholarshipAccount();
  console.log(scholarshipAccount.accountNum);
})();

지금 특정 학생의 장학금 계좌를 조회하고 있는데요. getScholarshipAccount라는 함수는 외래키 관계를 설정하면 자동으로 sequelize가 생성해주는 메소드입니다. 지금 끝 단어가 s가 붙지 않은 단수형이라는 점을 기억하세요. 이 코드를 실행해보면

이런 SQL이 실행됩니다. 일단은 Students 테이블에서 해당 학생 row를 조회하고, 해당 학생 row의 id 값을 StudentID 컬럼의 값으로 가진 ScholarshipAccounts 테이블에서의 row를 조회합니다. 이렇게 SQL이 총 두 번 실행된다는 걸 알 수 있습니다.

(2) Eager loading

const db = require('./models/index.js');

const sequelize = db.sequelize;
const Student = db.Student;
const ScholarshipAccount = db.ScholarshipAccount;
const Course = db.Course;
const Professor = db.Professor;
const Grade = db.Grade;

(async () => {
  const student = await Student.findOne({
    where: { registrationNum: 20203718 },
    include: ScholarshipAccount, //
  });
  const scholarshipAccount = await student.getScholarshipAccount();
  console.log(scholarshipAccount.accountNum);
})();

지금 달라진 부분은 findOne 메소드 안에 include: ScholarshipAccount 라는 옵션이 추가되었다는 점입니다. 실행해보면

아까와 달리 LEFT OUTER JOIN이 이루어진다는 것을 알 수 있습니다. 이 부분이 아주 중요합니다. 하지만 여전히 해당 학생의 장학금 계좌를 조회하는 별도의 SQL이 실행된다는 것을 알 수 있는데요.

.getScholarshipAccount() 이 메소드 부분을 .ScholarshipAccount처럼 프로퍼티를 조회하는 코드로 바꿔봅시다.

const db = require('./models/index.js');

const sequelize = db.sequelize;
const Student = db.Student;
const ScholarshipAccount = db.ScholarshipAccount;
const Course = db.Course;
const Professor = db.Professor;
const Grade = db.Grade;

(async () => {
  const student = await Student.findOne({
    where: { registrationNum: 20203718 },
    include: ScholarshipAccount,
  });
  const scholarshipAccount = student.ScholarshipAccount;
  console.log(scholarshipAccount.accountNum);
})();

실행해보면

그럼 이제 단 하나의 SQL 쿼리만 실행되는 것을 알 수 있습니다. 이렇게 Eager loading은 처음부터 나중에 참조할 테이블을 조인해서 들고있기 때문에 SQL 쿼리 횟수를 줄일 수 있습니다. 그리고 Eager loading을 할 때는

const scholarshipAccount = student.ScholarshipAccount;

이 코드처럼 마치 student 객체 안의 프로퍼티처럼 해당 row를 가져올 수 있다는 점 기억하세요.

Lazy loading의 경우 student.getScholarshipAccount(); 처럼 메소드
Eager loading의 경우 student.ScholarshipAccount 처럼 프로퍼티

로 접근한다는 사실은 다음에서도 동일하게 적용됩니다.

2. 1:M 관계

Professor - Course

(1) Lazy loaindg

이번엔 전체 Professor와 각 교수에 딸린 Course들을 전부 조회해봅시다.

const db = require('./models/index.js');

const sequelize = db.sequelize;
const Student = db.Student;
const ScholarshipAccount = db.ScholarshipAccount;
const Course = db.Course;
const Professor = db.Professor;
const Grade = db.Grade;

(async () => {
  const professors = await Professor.findAll();
  professors.forEach(async (professor) => {
    const courses = await professor.getCourses();
    console.log(`! 교수님 성함: ${professor.name}`);
    courses.forEach((course) => {
      console.log(course.title);
    });
  });
})();

지금 일단 Professors 테이블 전체 row를 조회하고 각 Professor row의 id 값을 갖고 Courses 테이블에서 ProfessorId의 값이 그 id 값과 같은 row들을 매번 조회하고 있습니다. 이것은 소위 우리가 문제시하는 N+1 문제와도 연관이 있습니다. N+1 문제는 지금처럼 첫 번째 테이블 조회 한번(1) 그리고 해당 테이블의 각 row의 id 값을 갖고 다른 테이블의 연관 row들을 조회하는 N번을 하게 되는 현상을 의미합니다. 이 N+1 문제는 Sequelize 뿐만 아니라 다른 여러 ORM에서도 중요시되는 이슈인데요. Eager loading을 하면 이 문제를 해결할 수 있습니다.

(2) Earger loading

const db = require('./models/index.js');

const sequelize = db.sequelize;
const Student = db.Student;
const ScholarshipAccount = db.ScholarshipAccount;
const Course = db.Course;
const Professor = db.Professor;
const Grade = db.Grade;

(async () => {
  const professors = await Professor.findAll({
    include: 'Courses',
  });
  professors.forEach(async (professor) => {
    const courses = professor.Courses;
    console.log(`! 교수님 성함: ${professor.name}`);
    courses.forEach((course) => {
      console.log(course.title);
    });
  });
})();

findAll 메소드에 include 옵션을 추가해주었고 그 안에 Courses라고 적어주었습니다. 이때 주의할 점은 이전에 1:1 관계에서는 include: 'Scholarship'처럼 단수형의 단어를 적어주었지만 지금처럼 1:M 관계에서는 include: 'Courses' 이렇게 복수형으로 적어주어야 합니다.

그리고 professor.getCourses() 부분도 professor.Courses로 변경하였습니다. 한번 실행 결과를 볼까요?

이렇게 Professors 테이블과 Courses 테이블을 아예 LEFT OUTER JOIN 해버리는 것을 볼 수 있습니다. 이전처럼 N+1 번의 쿼리 수행이 아닌 단 한번의 쿼리 수행으로 같은 결과를 얻을 수 있는 것이죠. 물론 조인 연산에 들어가는 DB 내의 비용이 있지만 그래도 여러번의 쿼리를 수행하는 것보다는 훨씬 효율적일 것입니다.

3. M:N 관계

Students_Courses

이제 각 학생이 각 과목에 대해 맞은 점수를 출력해봅시다.

(1) Lazy loading

// app.js
const db = require('./models/index.js');

const sequelize = db.sequelize;
const Student = db.Student;
const ScholarshipAccount = db.ScholarshipAccount;
const Course = db.Course;
const Professor = db.Professor;
const Grade = db.Grade;

(async () => {
  const students = await Student.findAll();
  students.forEach(async (student) => {
    const courses = await student.getCourses(); // (a) 
    console.log(`! 학생 이름 : ${student.name}`);
    courses.forEach((course) => {
      console.log(
        `!! 과목 이름 : ${course.title} !!! 점수 : ${course.Grade.score}` // (b)
      );
    });
  });
})();

코드가 살짝 복잡해진 것 같지만 주석 처리한 부분에만 주의하면 됩니다.

(a) student.getCourses() : 이 부분은 각 학생이 수강한 과목 목록을 가져오고 이 각 과목에 대해
(b) course.Grade.score : 해당 과목의 점수를 가져옵니다.

코드를 실행해보면

총 5번의 쿼리가 수행된 것을 알 수 있습니다. 일단
1번 : Students 테이블 전체를 조회한 후에,
N번 : Courses 테이블과 Grades 테이블을 INNER JOIN하고 그 다음 StudentID가 1인 것들, 2인 것들, 3인 것들, 4인 것들 이렇게 4번의 동작을 합니다. 뭔가 문제가 있어 보이죠?

이 코드를 Eager loading으로 바꿔 봅시다.

(2) Eager loading

const db = require('./models/index.js');

const sequelize = db.sequelize;
const Student = db.Student;
const ScholarshipAccount = db.ScholarshipAccount;
const Course = db.Course;
const Professor = db.Professor;
const Grade = db.Grade;

(async () => {
  const students = await Student.findAll({
    include: 'Courses', // 
  });
  students.forEach(async (student) => {
    const courses = await student.Courses; //
    console.log(`! 학생 이름 : ${student.name}`);
    courses.forEach((course) => {
      console.log(
        `!! 과목 이름 : ${course.title} 점수 : ${course.Grade.score}`
      );
    });
  });
})();

이전과 마찬가지로 include 옵션이 추가되고, getCourses() 부분이 Courses로 바뀐 것에 유의하세요. 실행해보면

Student 테이블과 "Courses 테이블과 Grades 테이블을 INNER JOIN 한 테이블"을 LEFT OUTER JOIN하고 있습니다. 단 한번의 쿼리만 실행되었네요. N+1 문제가 잘 해결되었습니다.

그런데 여기서 잠깐, M:N 관계에서 지금 Students 테이블을 기준으로만 조회를 해봤는데요. 이제 학생별 과목 점수가 아니라 과목별 학생 점수를 출력해보겠습니다. 코드와 그 결과만 보여드릴게요.

  • Lazy loading
const db = require('./models/index.js');

const sequelize = db.sequelize;
const Student = db.Student;
const ScholarshipAccount = db.ScholarshipAccount;
const Course = db.Course;
const Professor = db.Professor;
const Grade = db.Grade;

(async () => {
  const courses = await Course.findAll();
  courses.forEach(async (course) => {
    const students = await course.getStudents();
    console.log(`! 과목 이름 : ${course.title}`);
    students.forEach((student) => {
      console.log(
        `!! 학생 이름 : ${student.name} 점수 : ${student.Grade.score}`
      );
    });
  });
})();

이미지에 담기지 않을 정도로 많은 수의 결과들이 출력됩니다. N+1 문제가 있습니다.

  • Eager loading
const db = require('./models/index.js');

const sequelize = db.sequelize;
const Student = db.Student;
const ScholarshipAccount = db.ScholarshipAccount;
const Course = db.Course;
const Professor = db.Professor;
const Grade = db.Grade;

(async () => {
  const courses = await Course.findAll({
    include: 'Students',
  });
  courses.forEach(async (course) => {
    const students = await course.Students;
    console.log(`! 과목 이름 : ${course.title}`);
    students.forEach((student) => {
      console.log(
        `!! 학생 이름 : ${student.name} 점수 : ${student.Grade.score}`
      );
    });
  });
})();

단 하나의 쿼리만 실행됩니다. Eager loading을 통해 N+1 문제를 해결했습니다.

profile
성장의 기쁨

0개의 댓글