오늘은 (회사 재정 상..의 이유로) 이어받은 외주 작업에서 북마크 기능을 구현해야했다!
북마크 기능을 구현하면서 했던 고민들과 생각을 휘발성 기억으로 잊어버릴 미래의 나를 위해 기록한다.
내가 구현해야했던 북마크 기능을 user 입장에서 이해하기 쉽도록 화면을 기준으로 설명하자면,
해당 user가 마음에 드는 product가 있을 때, product 옆에 있는 북마크 모양 아이콘을 누르면 마이페이지의 나의 북마크 탭에서 확인할 수 있는 기능이다.
나(Backend 개발자)는 해당 user가 북마크를 저장할 수 있도록 Database 테이블과 api를 만들면 된다!
이미 구축되어있는 데이터베이스에 북마크 테이블을 추가해야했다. 최대한 다른 테이블에 영향을 덜 미치면서, 북마크 기능을 구현할 때 문제가 없는 테이블 구조를 생각해보았다.
사용자마다 북마크가 존재하고, 사용자는 product 하나에 북마크 한 개만 존재하면 된다. 중복이 안 되도록 할 수 있는 방법은 여러가지가 있을 텐데, 그 중 어떤 방법을 사용할 지 고민이 되었다.
처음에는 북마크를 와다다다 눌렀을 때, 추가 취소 추가 취소 x100000 을 해주어야 한다고 생각했다. 다시 생각해보니, 와다다다 누른만큼 서버와 많은 요청을 주고받게 된다면 비용이... 어마무시하겠다는 생각이 들었다.
사용자 요청이 들어왔을 때 즉각적으로 백엔드에서 처리하는 것이 아니라, 어느 정도 텀을 두고 사용자의 마지막 요청 값이 해당 상품의 북마크 추가일 경우에 백엔드 서버에서 북마크를 추가 요청을 처리해주고, 사용자의 마지막 요청 값이 취소일 경우에는 백엔드 서버에서는 북마크가 생성되지 않았으니 취소 처리를 할 필요가 없겠구나. 라는 생각이 들었다. 물론 이 부분은 FE 개발자 분이 구현해주셔야 하는 부분이다
사용자(client)의 마지막 요청 값이 해당 상품의 북마크 추가일 경우에, 북마크 추가 로직을 실행한다.
사용자 입장에서 설명하자면, 상품의 디테일 화면 or 사용자 마이페이지의 내가 저장한 북마크 상품 리스트를 확인할 수 있는 화면에서 북마크 취소 요청이 들어온다면? 북마크 취소 로직을 실행한다.
@Entity()
@Index(['user', 'product'], {unique: true}) // user와 product의 조합에 unique 조건
export class Bookmark {
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
bookmarkId: number;
@CreateDateColumn()
createdDateTime: Date;
@UpdateDateColumn()
updatedDateTime: Date;
@ManyToOne(() => User, user => user.bookmarks)
@JoinColumn({ name: 'user_id' })
user: User;
@ManyToOne(() => Product, product => product.bookmarks)
@JoinColumn({ name: 'product_id' })
product: Product;
}
@OneToMany(() => Bookmark, bookmark => bookmark.product)
@JoinTable({
joinColumn: { name: 'product_id' },
inverseJoinColumn: { name: 'bookmark_id'}
})
bookmarks: Bookmark[];
@OneToMany(() => Bookmark, bookmark => bookmark.user)
@JoinTable({
joinColumn: { name: 'user_id' },
inverseJoinColumn: { name: 'bookmark_id'}
})
bookmarks: Bookmark[];
user와 product 테이블은 외주 작업 코드를 넘겨 받았을 때 이미 생성되어 있었다. user와 product 테이블과 얽혀있는 서비스 로직 관련 코드가 많았기 때문에 최대한 user와 product 테이블은 수정하지 않으려고 했다.
처음에는 bookmark 테이블과 user 테이블을 다대다 관계로 묶어주려고 했는데, bookmark와 product도 다대다 관계인 것이 떠올랐다.
bookmark 테이블을 user 테이블, product 테이블과 엮어주어, bookmark 테이블을 중간 테이블로 사용하기로 했다.
bookmark 생성 기준으로 필터링을 해줘야하는 기능이 있어 createdDateTime, updatedDateTime 컬럼도 추가해주었다.
인덱스로 user와 product의 조합에 unique 조건을 걸어주어 bookmark 테이블에서 userId와 productId가 같은 컬럼이 중복 생성되지 못하도록 했다.
인덱스를 걸어주면 조회속도는 빠르지만, 등록이나 수정이 느려지는데 unique 조건을 걸어 인덱스를 생성해주는게 좋은 방법일까? 고민되었다.
아직은 사용자가 많지 않은 서비스이고 북마크 등록이나 수정의 속도보다는 데이터베이스 무결성을 지키는 것이 더 우선이라고 판단했다. 또한... 벨로그 오픈소스를 보니 좋아요 기능에 unique 조건을 걸어주신 것을 찾게 되었다. 벨로그는 사용자가 매우 많은 서비스인데, unique 조건으로 데이터베이스 무결성을 우선시하신 것을 보고,.. 등록이나 수정의 속도 보다는 unique 조건을 걸어 데이터베이스 무결성을 지키는 선택에 마음이 더 기울게 되었다.
bookmark를 생성하는 서비스 로직에 user와 product 조합이 같은 북마크가 있다면, 이미 북마크된 상품이라고 에러 처리를 해주었다. entity에 이미 중복처리가 되어있지만 데이터베이스에 생성이 되려다가 롤백이 되는 것 보다는, 읽기 요청으로 미리 방지하는 것이 더 효율적인 로직이라고 생각하였다.
북마크는 물리삭제와 논리삭제중에 고민했다. 북마크는 다른 데이터와 대비했을 때 중요한 데이터가 아니라고 판단했다.상품 관련된 데이터 혹은 사용자의 정보와 대비했을 때
물리 삭제가 되었을 경우에, 사용자가 다시 북마크를 클릭해 생성하면 된다고 생각하여 물리 삭제로 구현하였다.
매일 근무 시간에 기능을 구현할 때, 한정된 시간 안에서 구현하되 최대한 효율적이고 사용자가 불편하지 않은 로직은 무엇일까? 고민한다. 고민해도... 아직 부족한 점이 많아 이게 과연 최선의 선택일까... 싶은 생각이 많이 든다....
아직은 부족한 점이 많아.... 나중에 읽었을 때 부끄러운 글이 되겠지만, 부끄러움이 쌓이다 보면! 조금 더 발전한 나를 마주할 수 있지 않을까? 😊