[JavaScript] 왜 불러온 객체는 메서드 사용이 불가능할까? feat. 직렬화 & 역직렬화

Profile-exe·2023년 7월 2일
0

NodeJS

목록 보기
3/3
post-thumbnail

"어?~" 왜 메서드 호출 안됨?
...

Udemy에서 node.js 강의를 듣고있는데, 파일시스템이나 데이터베이스에서 저장한 데이터를 불러온 뒤 객체로 파싱하면 메서드 사용이 불가능했다.

이 포스트에서 불가능했던 이유와 해결 방법을 기록했다.


🚨 에러 발생 예시

간단한 클래스를 정의하고 파일/DB에서 저장 및 불러오고 메서드를 사용하는 예시를 들어보겠다.

🧾 User 클래스 정의

User클래스는 nameage 속성, getName()getAge() 메서드를 제공하는 클래스다.

class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  getName() {
    return this.name;
  }
  
  getAge() {
    return this.age;
  }
}

📌 파일에서 객체 저장 및 불러오기

fs모듈을 이용한다.
JSON.stringify()JSON.parse()로 문자열, 객체로 파싱 후 저장 및 불러온다.

const fs = require('fs');

// 객체 생성 및 파일 저장
const user = new User('John Doe', 25);
fs.writeFileSync('user.json', JSON.stringify(user));

// 파일에서 객체 불러오기
const data = fs.readFileSync('user.json', 'utf8');
const loadedUser = JSON.parse(data);

// 메서드 호출 - 에러발생!
console.log(loadedUser.getName()); // Error: loadedUser.getName is not a function
console.log(loadedUser.getAge());  // Error: loadedUser.getAge is not a function

📌 데이터베이스에서 객체 저장 및 불러오기

mongodb 사용 예시로 DB 연결 관련 코드는 축약하고 논리 위주로 작성하였다.

const db = getDb(); // 데이터베이스 connection 얻기

// 객체 생성 및 데이터베이스에 저장
async function insertUser(name, age) {
	const user = new User(name, age);
	await db.collection('users').insertOne(user);
}

// 데이터베이스에서 객체 불러오기
async function findUser(userId) {
	const user = await db
    	.collection('users')
        .findOne({ _id: userId }); 
	return user;
}

async function main() {
	insertUser('John', 25);
  
  	const userId = '임의의 유저 아이디';
	const loadedUser = await findUser(userId);

  	// 에러 발생!
	console.log(loadedUser.getName()); // Error: loadedUser.getName is not a function
	console.log(loadedUser.getAge());  // Error: loadedUser.getAge is not a function
}

main();

🔍 직렬화 & 역직렬화

이와 같은 에러는 직렬화역직렬화에 대한 이해가 필요하다.
아래 코틀린 공식문서에 직렬화에 및 역직렬화에 대한 설명이 있다.
Serialization | Kotlin Documents

💬 직렬화 (Serialization)

응용 프로그램에서 쓰는 데이터를 네트워크를 통해 전송하거나 DB 또는 파일에 저장 가능한 형식으로 바꾸는 프로세스

💬 역직렬화 (Deserialization)

외부 소스에서 데이터를 읽고 이를 런타임 객체로 바꾸는 반대 프로세스

파일시스템이나 데이터베이스에 데이터를 전송 및 불러오는 행위를 각각 직렬화와 역직렬화로 생각하면 된다.


🤔 에러 발생 원인

이번 문제를 해결하려면 JavaScript에서의 직렬화 / 역직렬화가 어떻게 이루어지는지 알아야한다.

💬 JavaScript 객체의 직렬화 & 역직렬화

우선 파일 및 데이터베이스에 저장하는 경우 객체의 속성만 저장된다.

  • 파일에 저장하는 경우
{"name":"John Doe","age":25}
  • 데이터베이스에 저장하는 경우
{"_id":{"$oid":"64a132951d6cb2e438455023"},"name":"John","age":{"$numberInt":"25"}}

instanceof don't work for JSON

파일이나 데이터베이스에서 데이터를 불러와 객체로 파싱(역직렬화)하면 해당 객체는 no prototype 객체가 된다. 즉, 순수하게 객체의 속성만 담고있는 객체가 되어버린다.

  • no prototype 객체
    • spread(...) 문법 사용 불가능 - no iterator 에러
    • 객체 instanceof 클래스 === false - 해당 클래스의 인스턴스가 아닌 것으로 간주

📝 원인 정리

객체의 상태만을 포함한 JSON 형태의 데이터나 데이터베이스 레코드는 해당 클래스의 메서드 코드를 포함하고 있지 않기 때문에 메서드 호출이 불가능한 것이다

또한, no prototype 객체라서 spread문법이나 instanceof도 불가능하다.


✔️ 해결 방안

객체의 속성을 추출해 new 연산자로 클래스의 인스턴스를 생성해서 이용하면 된다.
객체 구조분해할당을 통해 간결하고 이해하기 쉬운 코드를 작성할 수 있다.

const { name, age } = loadedUser; // 구조분해할당
const newUser = new User(name, age);

console.log(newUser.getName()); // "John"
console.log(newUser.getAge());  // 25

✨ 응용하기 - 코드에 해결 방안 적용

내가 듣는 강의에서는 프로젝트를 mvc패턴으로 구성했다.

미들웨어를 등록해 사용자를 얻어 req.user에 저장
이후 controller에서 req.user에 접근해 user를 사용할 수 있도록 하는 코드이다.

model에서 클래스를 정의하는데, 주어진 iduser를 반환하는 함수 findById 메서드에서 이 해결방안을 적용할 수 있었다.

findById에서 User클래스의 인스턴스를 넘겨주므로 req.user로 얻은 사용자 객체는 메서드를 사용할 수 있다.

📃 해결 코드

  • models/user.js : findById 메서드에 해결 방안 적용
class User {
  constructor(name, email, cart, id) {
    this.name = name;
    this.email = email;
    this.cart = cart; // {items: []}
    this._id = id ? new ObjectId(id) : null;
  }

  ... (다른 메서드들)

  getCart() { ... } // 사용자의 장바구니를 얻는 메서드

  static async findById(userId) {
    try {
      const db = getDb();
      const user = await db
        .collection('users')
        .findOne({ _id: new ObjectId(userId) });

      // 데이터베이스나 파일에서 불러온 객체는 단순히 데이터의 상태만을 가지고 있음
      // 따라서 User의 메서드를 사용할 수 있도록
      // 객체 생성에 필요한 속성들을 가져온 후 new User로 인스턴스 반환
      const { name, email, cart, _id } = user;
      return new User(name, email, cart, _id);
    } catch (err) {
      console.log(err);
    }
  }
}

  • app.js : req.user에 저장해 controller에서 접근할 수 있도록 하는 미들웨어 등록
app.use((req, res, next) => {
  User.findById('사용자 아이디')
    .then((user) => {
      req.user = user; // req 객체에 담으면 다음 미들웨어에서 접근 가능
      next(); 		   // next()를 호출해 다음 미들웨어로 요청이 이동하도록함
    })
    .catch((err) => console.log(err));
});

  • controllers/shop.js : app.js에서 등록한 user를 요청 객체(req)에서 불러와 메서드 사용 가능
module.exports = {
  
  ... (다른 컨트롤러들)
  
  postCartDeleteProduct: async (req, res, next) => {
    const prodId = req.body.productId;
    try {
      const cart = await req.user.getCart(); // User 클래스의 인스턴스라 메서드 사용 가능
      
      ... (나머지 로직)

      res.redirect('/cart');
    } catch (err) {
      console.log(err);
    }
  }, 
}

📝 정리

  • JavaScript에서 파일 및 데이터베이스에 데이터를 저장(직렬화) / 불러오기(역직렬화)를 하면 객체의 속성만 저장되고 불러옴

  • 불러온 객체는 메서드를 지니지 않으며 no prototype - spread문법과 instanceof 불가

  • 객체의 속성을 받아 new 연산자로 새로운 인스턴스를 생성해야함

profile
컴퓨터공학과 학부생

0개의 댓글