📌 목표
3주 차는 실제로 계획한 테이블을 생성하고, 데이터를 옮기는 작업을 수행하는 것이었다.
테이블의 데이터를 새로운 테이블로 옮기기 위하여 옮길 새로운 데이터베이스를 연결하고, 데이터를 옮긴 후 테스트까지 작성해 보는 것이 목표였다.
4주 차 과제의 목표는 하나의 채팅서버에 API와 소켓서버가 통합되어 있어서 API 배포 시 소켓 서버의 세션이 끊기는 문제가 있는데, 이를 해결해 나가는 과제였다.
📌 데이터베이스 dual update
먼저 서버에 새로운 Database를 연결하여 하나의 서버에 2개의 데이터 베이스를 연결하는 것이 목표였다.
NestJS에 PostgreSQL을 연결하는 방법은 2가지를 고려하였다.
1. ts-postgres
2. typeORM
Spring 환경에서 JDBC를 사용할지 JPA를 사용할지와 같은 고민이었는데, 나는 ORM기술을 사용할 때 테이블 관리가 직관적이고 용이한 점, 복잡한 SQL문을 직접 작성하지 않아도 된다는 점, 그리고 ts-postgres방식보다 TypeORM의 공식 문서 정보가 훨씬 직관적이고 보기 좋았던 점을 고려하여 TypeORM을 통해 2개의 데이터베이스를 연결하기로 결정하였다.
dual update시 트랜잭션
여기 주의해야 할 점이 있는데, 동시에 2개의 데이터베이스에 업데이트를 하는 경우에 양쪽 모두 잘 업데이트되는지 주의해야 한다.
만약 한쪽만 업데이트를 성공하고, 다른 한쪽에 실패하는 경우가 생긴하면 트랜잭션 처리를 해줘야 한다.
async createChat(
userId: number,
roomId: number,
message: string,
): Promise<ChatEntity> {
const queryRunner = this.datasource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
const chat = new ChatEntity();
chat.senderId = userId;
chat.roomId = roomId;
chat.content = message;
await queryRunner.manager.save(chat);
try {
// 새로운 마이그레이션 chat_database에 함께 저장한다.
const chat_copy = new ChatEntity();
chat_copy.id = chat.id;
chat_copy.senderId = userId;
chat_copy.roomId = roomId;
chat_copy.content = message;
await this.newChatRepository
.createQueryBuilder()
.insert()
.into(ChatEntity, ['id', 'senderId', 'roomId', 'content'])
.values(chat_copy)
.execute();
const saved = await this.newChatRepository.findOne({
where: {
id: chat.id,
},
relations: {
likes: true,
},
});
await queryRunner.commitTransaction();
return saved;
} catch (error) {
await queryRunner.rollbackTransaction();
console.error('예외 발생 롤백');
} finally {
await queryRunner.release();
}
}
위의 코드를 보면 소프트웨어적으로 트랜잭션 처리를 해주었다.
try-catch문을 통해서 2번째 DB 업데이트가 실패하면 1번째 DB업데이트가 롤백되도록 구현하였다.
📌 배치 애플리케이션
배치 애플리케이션은 SpringBatch를 이용해서 간단하게 작성하였다.
SpringBatch를 사용하든 간단하게 for문을 통하여 데이터 조회/생성을 구현하든 크게 문제 없을것이라고 생각하였다.
dual update를 시작한 시간 t를 기록하고, t시간 이전까지의 데이터를 순차적으로 옮겨주었는데,
가장 중요한 점은 마이그레이션 진행 후 데이터가 올바르게 옮겨졌는지 확인하는 테스트였다.
정합성에 대한 테스트는 Junit5를 활용하여
1. 옮겨진 데이터의 개수는 정확한가?
2. 중복된 데이터는 없는가?
3. 기존데이터와 옮겨진 데이터의 필드값은 동일한가?
를 검증하였다.
-> 추가적으로 공부해볼 사항
배치애플리케이션을 처음을로 활용하면서 크게 간과했던 부분이 있었다. 현재는 데이터의 개수가 작기 때문에 싱글 스레드로 옮기는데 큰 문제가 없었지만 데이터가 1억 개 이상으로 넘어간다면 굉장히 오래 거린다는 문제가 있었다.
이러한 경우에는 멀티코어를 활용하는 방식으로 데이터를 옮기는 작업을 수행해보면 좋을 것 같다. SpringBatch에 멀티코어를 활용하여 효율적으로 데이터를 마이그레이션 할 수 있는 방법이 있다고 하니 추가적으로 실습해 보면 좋을 것 같다.
📌 새로운 Database 조회
마지막으로 마이그레이션이 완료 된 후에는 서버에서 새로운 DataBase를 통해 조회해 주도록 코드를 수정해 주었다.
📌 Socket I/O Adapter
우선 API를 새로 배포할 때 소켓세션이 끊기는 이유는 배포과정에서 단일 서버환경이기 때문에 수정된 서버를 배포하는 과정에서 서버가 종료되기 때문에 발생한다.
배포 시에 서버가 끊기지 않게 하기 위해서는 여러 대의 서버를 두어야 하는데, 이를 위해서 서버를 2대로 분리하는 과정을 거쳤다.
하나는 3000, 하나는 3001의 서버로 구동하였다.
이렇게 여러대의 서버를 구동하면 순차적으로 API를 배포하는 과정을 거칠 수 있기 때문에 배포 중에도 채팅이 끊기지 않게 할 수 있다.
하지만 여기서 문제가 하나 발생하였다. 정상적으로 2대의 서버를 이용하는 과정에서 사용자가 채팅을 전송했을 때 다른 port에 존재하는 socket서버에 채팅내역이 전파되지 않는 문제였다.
이를 해결하기 위하여 소켓 어댑터를 적용하였다
Socket.IO 공식 홈페이지에 따르면 Redis, MongoDB, Postgres 등의 다양한 어댑터를 제공한다.
NestJS 공식 문서에서 안내하고 있는 redis어댑터를 활용하여 빠르게 적용해 보았다.
도커를 활용해 빠르게 구성하였고, NestJS에 어댑터 설정을 추가하여 적용하였다.
redis를 활용한 어댑터는 내부적으로 pub/sub구조를 따르고 있다. pub/sub구조에 대해서는 추가적으로 학습해 볼 예정이다!
어댑터가 적용되었는지 확인하기 위하여
두 개의 서버를 실행시키고 동작을 테스트 해보았다.
아래와 같이 서로 다른 포트(3000, 3001)로 채팅방에 접속합니다.
이제 실제 3000번 포트(위 그림의 왼쪽)에서 "안녕 반가워"라는 데이터를 보낸다.
그러면 아래와 같이 왼쪽 3000번 포트의 브라우저뿐만 아니라 오른쪽 3001 포트의 브라우저 창에도 동시에 "안녕 반가워"라는 채팅이 갱신되는 것을 확인할 수 있습니다.
전파까지 확인하였고, 실제 기존의 Database와 새로운 Database모두에 잘 적재되었는지 확인하였다.
📌 마무리하며
이렇게 4주 동안의 채팅마이그레이션 업무를 수행하였다.
기존에 알고 있던 지식과 더불어서 새롭게 알게 된 내용도 정말 많았던 것 같다.
새로운 프레임워크 환경을 빠르게 학습하고, 팀원들을 위한 문서를 작성했으며, 코드를 분석하는 경험을 수행하였다.
그 과정에서 새로운 문서를 보는 습관을 많이 들인 것 같고, 코드를 분석함과 동시에 어떤 테이블을 마이그레이션해야 좋을지를 여러 가지 객관적인 근거(보안, 성능, 연관관계, 확장성 등)를 토대로 결정할 수 있었는데 이 부분이 정말 좋은 경험으로 다가왔던 것 같다.
아쉬웠던 부분은 빠르게 학습하고 적용하는 과정에서 사용한 모든 내용을 깊이 있게 다뤄보지 못했다는 점이다.
멀티쓰레드를 활용한 SpringBatch, WebSocket, redis의 pub/sub, 트랜잭션 등 계속 공부해 볼 예정이다.
'프로젝트 > 백엔드직무체험' 카테고리의 다른 글
[채팅 서비스 마이그레이션] 백엔드 직무 체험 2주차회고 (0) | 2024.01.13 |
---|---|
[채팅 서비스 마이그레이션] 백엔드 직무 체험 1주차회고 (0) | 2024.01.07 |