TDD, DB 동시성 제어 (week2 멘토링 일지)

박재하·2023년 11월 15일
0

BE

✔️ 결론 및 To Do

앞으로 어떻게 할 지

  1. TDD는 e2e방식으로만 하자!
    • 만약 정말 복잡한 서비스 로직이 생기면 따로 그것만 TDD
      • 기본적으로 유닛테스트는 구현 후에
    • 두 가지 방식 중 선택
      1. GET /boards, POST /board, … 이렇게 모든 테스트 코드를 다 짠 다음에 구현

      2. GET /boards 테스트 코드 작성 후 구현 → 확인, POST /board 테스트 코드 작성 후 구현 → 확인, ,,,

        2번 방식 채택!

  2. Entity를 최소 컬럼으로 해서 CRUD를 구현한 후, 필요에 따라 컬럼을 추가하는 방식으로 구현하자!
    • 이렇게 해야 돌아가는 자동차를 일단 만들고 개선할 수 있을듯 - 자전거 부터 만들고 자동차로 가자
    • FE 요구사항에 맞게 지속적으로 로직과 테스트를 개선해나가기!
      • 전제가 되어야 하는 것: SWAGGER로 API 명세 자동화!

✔️ 아젠다 및 질문

  • 저희가 하는 모든 질문들에서 혹시 저희가 더 찾아보고 직접 고민하여 구현하는 것이 저희 성장에 도움이 될 것 같으신다면 무조건 알려주시기 보다는 키워드를 던져주시거나 직접 더 찾아보면 좋을 것 같다고 말씀해주시면 감사하겠습니다!!!! 🫡
  • TDD 관련 질문
    • 아래 참고사항처럼 mock 프로바이더들을 사용해서 타입만 비교하는 테스트를 작성한다고 하면 결국에는 오류가 날만한 상황이 Repository(데이터베이스)와 통신할 때라고 생각이 드는데, Service 클래스를 테스트 할 때, 저희가 일단 mock Repository로 테스트를 작성을 하고 (데이터베이스 통신이 완벽하다고 가정), 따로 또 실제 데이터베이스를 연동한 테스트를 나눠서 작성하려고 합니다. 이 과정에서 트랜잭션 롤백 등으로 테스트가 끝난 후 변경 사항(레코드 인서트 테스트 같은 경우 인서트된 레코드)들을 롤백 하려고 합니다. 실무에서는 어떤 식으로 데이터베이스 테스트를 작성하는지 궁금합니다.
    • 참고사항 1 (Controller 테스트) 아래는 예시코드입니다. 저희가 찾아본 자료들은 이런식으로 service를 mocking해서 잘 동작한다는 가정 하에 테스트 코드를 작성하는 것으로 보입니다. 지금은 Controller의 로직이 Service 기능 하나만을 리턴하는 등 너무 간단해서 테스트 코드 작성이 조금 어색한 느낌이 듭니다. 이 어색한 느낌이 Controller 로직이 너무 단순해서가 맞겠죠?
      import { Test, TestingModule } from '@nestjs/testing';
      import { AuthController } from '../../src/auth/auth.controller';
      import { AuthService } from '../../src/auth/auth.service';
      
      describe('AuthController', () => {
      	let controller: AuthController;
      	let service: AuthService;
      
      	beforeEach(async () => {
      		const module: TestingModule = await Test.createTestingModule({
      			controllers: [AuthController],
      			providers: [AuthService],
      		}).compile();
      
      		controller = module.get<AuthController>(AuthController);
      		service = module.get<AuthService>(AuthService);
      	});
      
      	it('should be defined', () => {
      		expect(controller).toBeDefined();
      	});
      
      	describe('findAll', () => {
      		it('should be defined', () => {
      			expect(controller.findAll).toBeDefined();
      		});
      
      		it('should return an array of auth', async () => {
      			const result = ['test'];
      			jest.spyOn(service, 'findAll').mockImplementation((): any => result);
      
      			expect(await controller.findAll()).toBe(result);
      		});
      	});
      
      	describe('findOne', () => {
      		it('should be defined', () => {
      			expect(controller.findOne).toBeDefined();
      		});
      
      		it('should return an auth', async () => {
      			const result = 'test';
      			jest.spyOn(service, 'findOne').mockImplementation((): any => result);
      
      			expect(await controller.findOne('1')).toBe(result);
      		});
      	});
      
      	describe('create', () => {
      		it('should be defined', () => {
      			expect(controller.create).toBeDefined();
      		});
      
      		it('should return an auth', async () => {
      			const result = 'test';
      
      			jest.spyOn(service, 'create').mockImplementationOnce((): any => result);
      
      			expect(await controller.create({})).toMatchObject(result);
      		});
      	});
      
      	describe('update', () => {
      		it('should be defined', () => {
      			expect(controller.update).toBeDefined();
      		});
      
      		it('should return an auth', async () => {
      			const result = 'test';
      			jest.spyOn(service, 'update').mockImplementation((): any => result);
      
      			expect(await controller.update('1', {})).toBe(result);
      		});
      	});
      
      	describe('remove', () => {
      		it('should be defined', () => {
      			expect(controller.remove).toBeDefined();
      		});
      	});
      });
      Q. 이런 식으로 mocking하지 않은 채 repository까지 잘 동작하는지 테스트 코드를 작성할 필요가 있지 않을까요? 일반적으로 TDD를 어떻게 진행하는지 궁금합니다.
      
      describe('create', () => {
      	it('should be defined', () => {
      		expect(controller.create).toBeDefined();
      	});
      
      	it('should return an auth (mock service)', async () => {
      		const result = 'test';
      
      		jest.spyOn(service, 'create').mockImplementationOnce((): any => result);
      
      		expect(await controller.create({})).toMatchObject(result);
      	});
      
      	it('should return an auth (real)', async () => {
      		// given
      		const user = 'test'; // TODO : 제대로된 User 인스턴스가 들어가야 함.
      
      		// when
      		const result: any = await controller.create(user);
      
      		// then
      		expect(result).toBeInstanceOf(User);
      		expect(result).toMatchObject(user);
      	});
      
      	it('should assign new id (real)', async () => {
      		// given
      		const user = 'test'; // TODO : 제대로된 User 인스턴스가 들어가야 함.
      
      		// when
      		const result: any = await controller.create(user);
      
      		expect(result).toHaveProperty('id');
      		expect(result.id).toBeInstanceOf(Number);
      	});
      });
    • 참고사항 2 (Service 테스트) Q. 예전에 스프링에서 테스트코드를 작성한 적이 있는데, 이 Nest 테스트 코드처럼 예를 들어 Service에 대한 테스트 코드를 작성한다고 하면 Repository를 Mock으로 넣어주지 않고 스프링에서는 실제 Repository를 사용하였습니다. Nest처럼 작성한 코드의 목적은 Service에 대한 테스트 코드라면 다른 요소(Repository 등)에 대한 영향을 없애고자 그렇게 한 것 같습니다.
      어떤 방식이 더 적절할까요??????????
      // board.service.spec.ts
      import { Test, TestingModule } from '@nestjs/testing';
      import { BoardService } from './board.service';
      import { Board } from './entities/board.entity';
      
      describe('BoardService', () => {
      	let service: BoardService;
      	let repository: Repository<Board>;
      
      	beforeEach(async () => {
      		const module: TestingModule = await Test.createTestingModule({
      			providers: [
      				BoardService,
      				{
      					provide: getRepositoryToken(Board),
      					useClass: Repository,
      				},
      			],
      		}).compile();
      
      		service = module.get<BoardService>(BoardService);
      		repository = module.get<Repository<Board>>(getRepositoryToken(Board));
      	});
      
      	it('should be defined', () => {
      		expect(service).toBeDefined();
      	});
      
      	describe('findAll', () => {
      		it('should be defined', () => {
      			expect(service.findAll).toBeDefined();
      		});
      		it('should return an array of boards', async () => {
      			const board = new Board();
      			board.id = 1;
      			board.title = 'test';
      			board.content = 'test';
      			board.image = null;
      			board.star_style = 'test';
      			board.star_position = 'POINT(0, 0, 0)';
      			board.author = 'test';
      			board.created_at = new Date();
      			board.modified_at = new Date();
      			const boards = [board];
      
      			jest.spyOn(repository, 'find').mockImplementation(async () => boards);
      
      			expect(await service.findAll()).toBe(boards);
      		});
      	});
      });
      Q. 여기서도 마찬가지로 mocking 없이 repository 테스트를 해야 하지 않을까요? board.repository.spec.ts를 따로 만들어서 이 부분을 테스트 한다면 어떻게 해야 할지.. 감이 잡히지 않습니다.
      
      describe('findAll', () => {
      	it('should be defined', () => {
      		expect(service.findAll).toBeDefined();
      	});
      	it('should return an array of boards (mock repository)', async () => {
      		const board = new Board();
      		board.id = 1;
      		board.title = 'test';
      		board.content = 'test';
      		board.image = null;
      		board.star_style = 'test';
      		board.star_position = 'POINT(0, 0, 0)';
      		board.author = 'test';
      		board.created_at = new Date();
      		board.modified_at = new Date();
      		const boards = [board];
      
      		jest.spyOn(repository, 'find').mockImplementation(async () => boards);
      
      		expect(await service.findAll()).toBe(boards);
      	});
      	it('should return an array of boards (real)', async () => {
      		const board = new Board();
      		board.id = 1;
      		board.title = 'test';
      		board.content = 'test';
      		board.image = null;
      		board.star_style = 'test';
      		board.star_position = 'POINT(0, 0, 0)';
      		board.author = 'test';
      		board.created_at = new Date();
      		board.modified_at = new Date();
      
      		const boards = [board];
      
      		await service.create(board); // 빈 repository에 실제로 넣어서
      
      		expect(await service.findAll()).toBe(boards);
      	});
      });
    • 또, Controller를 테스트할 때, Entity에 의존을 할텐데, 지금 TDD로 개발을 하려다보니 Entity가 없어서 (있어도 속성이 정의되어 있지 않아서) 테스트 코드 작성도 불가능합니다.
      그러면 Entity에 대한 테스트 코드를 만들고 → Entity를 실제로 구현하고 → 그 다음에 Controller 테스트 코드를 만들고 → Controller를 실제로 구현하고 → 그 다음에 Service 테스트 코드 → Service 구현 → Repository 테스 …
      이 순서로 개발해나가는 것이 맞는지가 궁금합니다.
  • 동시성 제어 관련 질문
    • 동시성 제어 기법들이 Locking, 2PL, Time stamp, Validation, MVCC 등 여러 기법이 있는 것 같은데, 이러한 기법들을 다 파고 구현해 보는 것을 추천 하시는지, 아니면 현재 프로젝트 기간이 촉박한 만큼 Nest에서 동시성 제어를 구현할 때 현업에서 자주 사용하는 방법이나, 깊이있게 학습해 볼만한 기법이 있는지 궁금합니다. (아직 위의 방법들에 대해 대충 어떤 것이구나만 살펴보고 깊이있게 파보지는 않았습니다.)
    • Nest에서 동시성 제어 구현을 조금 찾아 보았는데, 트랜잭션을 도입하거나, 엔티티 컬럼에 @VersionColumn() 어노테이션으로 버전을 관리해서 버전 불일치시 충돌이 발생하게 하는 식의 방법을 찾았습니다. 또 다른 구현 방법이 있다면 어떤 것이 있을지, 만약 저희가 직접 더 찾아 보거나 고민을 하는 것이 도움이 될 것 같으시면 말씀해주시면 감사하겠습니다!!
    • Q. 이런 걸 의도하신 게 맞는지 궁금합니다. Argon.Blog - 트랜잭션과 동시성제어에 대해
      • 요약하면 게시글의 좋아요 기능에서 발생할 수 있는 동시성 이슈와, 이를 해결할 수 있는 트랜잭션과 MySQL의 락(공유 락 lock in share mode와 배타 락 for update)을 통해 이를 해결하는 과정에 대한 포스트입니다.

      • 저희도 좋아요와 게시글 수정 기능이 있고, 이러한 update 과정에서 발생할 수 있는 동시성 이슈를, 트랜잭션과 락을 통해 해결하는 과정을 기록으로 남겨보려고 합니다.

        Q. 추가적으로, 이 과정에서 동시성 이슈를 실제로 유발하는 테스트를 하고, 해결하는 과정으로 구현을 진행하고 싶은데 방법을 잘 모르겠습니다. 이를 jest로 테스트할 수 있는 방법에 대해서 알려주실 수 있을까요?

✔️ 멘토링 내용

  • 동시성 제어 딥다이브의 의도에 관해

    • shared lock 걸거나 처리
    • 현업에서는 보통 쇼핑몰 재고.. 판매될 때 동시에 여러 명이 들어옴
      • 이럴 때 동시성 제어가 필요함. 이와 유사한 상황이 이번 프로젝트에 있을지?
      • 재고의 경우, 20개의 재고가 있는데 이걸 어떻게 재고 신청을 처리할까
        • 락을 걸 수도 있고
        • 큐를 중간에 넣어서 큐 순서대로 처리할 수도 있고
    • DB의 동시성 제어 로직 자체만 말씀하신 게 아니라, 주로 한정된 자원을 여러명이 요청했을때의 처리에 대한 고민을 해보라는 말씀을 하신 듯
  • TDD 관련 Repository를 써야 되나에 대해

    • TDD와 일반적인 Test코드는 다름!

    • TDD는 Red Green Refactor 등과 같은 방식으로 진행됨.

      • 실패하는 test를 만들고 성공하도록 구현한다.
      • POST /board 이런식으로 요청을 했을 때 처음엔 안되고 나중에는 되게! 이런식
      • e2e방식을 말씀하시는 듯
      • Controller, Service의 유닛테스트를 다 하기는 애매하다.
    • Mocking을 하거나 TestDB를 넣어서 실제로 테스트하던지 이건 사람마다 다름

      • 멘토님은 후자를 더 선호하심
      • 대부분의 CRUD 로직이면 repository에서 다 처리하는 경우가 많음
      • 근데 여기서 repository에 대한걸 다 mocking하면 큰 의미가 없을지도
    • BDD(behavior driven dev)라고 해서 상황을 만들고 거기에 대한 테스트코드를 작성하기도 함

    • TDD의 철학은 모든 요구사항을 Test로 적는다는 느낌.

    • 멘토님의 TDD 예시 (커밋로그 보세요)

      GitHub - linusdamyo/wandookong-study

      ⭐️ 글 쓰기 기능(POST /articles)에 대한 TDD 예시(테스트 코드) ⭐️

      TDD_%E1%84%8B%E1%85%A8%E1%84%89%E1%85%B5_%E1%84%87%E1%85%A1%E1%86%BC%E1%84%89%E1%85%B5%E1%86%A8

      • Synchronize: true로 하면, Memory-DB(sqlite 등)를 하면 잘 되더라.
      • env를 testdev용 설정해놓고 이걸 Database를 다르게 써서 하면됨.
      • Q. transactional rollback 방식 어떤지?
        • 아마 queryrunner를 service 테스트할때 함수 내에 집어넣어줄 수가 없어서 쉽게 되진 않을거임
  • Repository를 따로 빼는가? 아니면 그냥 Service에 때려넣는가?

    • 공식문서에서는 Service에 비즈니스 로직을 다 넣고, Repository에 지정된 메소드만 이용하는 방식을 추천하지만

    • 멘토님께서는 준섭님 방식(=보통의 Service처럼 Provider에 넣어서 따로 Custom Repository처럼 계층을 나누는 방식)과 유사하게 하심

      %E1%84%86%E1%85%A6%E1%86%AB%E1%84%90%E1%85%A9%E1%84%82%E1%85%B5%E1%86%B7%E1%84%8B%E1%85%B4_repository_%E1%84%91%E1%85%A2%E1%84%90%E1%85%A5%E1%86%AB

      (여기서 포인트는 getRepository()로 그때그때 repository 객체를 불러오는거)

      Repository는 이렇게

      inject%E1%84%92%E1%85%A1%E1%86%AF%E1%84%84%E1%85%A2%E1%84%82%E1%85%B3%E1%86%AB_%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A5%E1%87%82%E1%84%80%E1%85%A6

      Service는 이렇게

      • 준섭님 방식 %E1%84%8C%E1%85%AE%E1%86%AB%E1%84%89%E1%85%A5%E1%86%B8%E1%84%82%E1%85%B5%E1%86%B7_%E1%84%87%E1%85%A1%E1%86%BC%E1%84%89%E1%85%B5%E1%86%A8
profile
해커 출신 개발자

0개의 댓글