Sequelize에서 Hooks는 라이프 사이클 이벤트 전후에 실행되는 함수이다.
모델을 저장하기 전 또는 저장 후에 미리 지정해둔 작업등을 할 수 있는 것이다.
나는 프로젝트에서 이미지를 CDN서버에 저장하고 경로를 DB에 저장했는데 DB에서 해당 항목이 삭제될 때 CDN에서 같이 사진을 삭제하도록 hooks를 작성해보았다.
(1)
beforeBulkCreate(instances, options)
beforeBulkDestroy(options)
beforeBulkUpdate(options)
(2)
beforeValidate(instance, options)
[... validation happens ...]
(3)
afterValidate(instance, options)
validationFailed(instance, options, error)
(4)
beforeCreate(instance, options)
beforeDestroy(instance, options)
beforeUpdate(instance, options)
beforeSave(instance, options)
beforeUpsert(values, options)
[... creation/update/destruction happens ...]
(5)
afterCreate(instance, options)
afterDestroy(instance, options)
afterUpdate(instance, options)
afterSave(instance, options)
afterUpsert(created, options)
(6)
afterBulkCreate(instances, options)
afterBulkDestroy(options)
afterBulkUpdate(options)
생각보다 다양하다...
생성 전후, 삭제 전후, 업데이트 전후.. 등등
후크 선언은 모델파일에 같이 해주었다.
ex) 모델 예시
User.init(
{
email: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
},
profileImage: {
type: DataTypes.STRING,
defaultValue: '',
get() {
const image = this.getDataValue('profileImage');
if (image) return `${process.env.CDN_SERVER}/${image}`;
return '';
},
},
nickname: {
type: DataTypes.STRING,
allowNull: false,
},
password: {
type: DataTypes.STRING,
set(value) {
this.setDataValue('password', createHash('sha512')
.update(value + process.env.SECRET_SALT).digest('base64'));
},
},
},
{
sequelize,
modelName: 'User',
updatedAt: false,
hooks: {
afterDestroy: async (user, options) => {
const image = user.getDataValue('profileImage');
if (image) {
await s3.delete(image);
}
});
},
},
);
hooks에 대한 정의는 모델에서 컬럼을 정의하고 옵션을 넣을 때 hooks에 넣어주면 된다. 위 코드처럼 hooks에 함수를 등록해두면 삭제된 후 함수에 모델인스턴스가 인자로 들어오고 실행된다.
hooks 이벤트들을 보면 bulk일때와 아닐때를 구분하고 있는데
Model.destroy({조건})으로 삭제할 때와 ModelInstance.destory()로 삭제할 때는 발생되는 이벤트가 다르다. 전자로 삭제할 경우 여러 항목이 삭제될 수 있으므로 bulkDestory 이벤트가 발생한다.
조건으로 삭제하고 afterbulkDestory 이벤트가 아니라 각각의 항목에 afterDestory 이벤트가 발생하게 하고 싶다면 Model.destory({...조건, indivisualHooks: true}) 이것 처럼 indivisualHooks 값을 true로 주면 삭제된 각각의 항목에 대해 개별적으로 이벤트가 발생한다.
나는 모델 인스턴스를 불러온 후 삭제하므로 afterDestory 이벤트에 등록했다.
하지만 또 다른 문제가 있었다. afterDestory이벤트는 DB에서 해당 항목이 삭제된 이후 실행될 것이라고 기대했지만 DB에 삭제 요청을 보낸 이후 삭제가 올바르게 되지 않았을 때도 hooks가 실행된다는 것이었다.
때문에 트랜잭션이 성공한 이후 실행될 수 있는 방법을 찾아보았다.
공식문서에는 hooks 함수에서 options에 트랜잭션 정보를 받아올 수 있다는 이야기가 있었다. 그리고 추가 구글링을 해본 결과 생성한 트랜잭션에 대해서도 hooks를 넣을 수 있다는 사실을 알게 되었다.
그래서 나는 afterDestory 이벤트가 발생했을 때 트랜잭션이 있으면 해당 트랜잭션에 대해서 afterCommit hooks를 추가해서 커밋이 성공하고 나서 사진을 삭제할 수 있도록 수정했다.
hooks: {
afterDestroy: (user, options) => {
if (options.transaction) {
options.transaction.afterCommit(async () => {
debug('회원탈퇴 커밋 완료. 이미지 삭제시작');
const image = user.getDataValue('profileImage');
if (image) {
await s3.delete(image);
}
});
}
},
},
afterDestory이벤트가 발생하면 트랜잭션이 있는지 검사해서 트랜잭션에 afterCommit 함수를 등록해서 커밋이 성공적으로 이루어졌을 때만 사진을 삭제하도록 구현했다.