Skip to content

인터셉터로 관심사 분리하기

mintaek edited this page Dec 1, 2024 · 7 revisions

인터셉터로 관심사 분리하기

NestJS의 인터셉터는 요청과 응답 흐름 양쪽에서 호출되는 독특한 컴포넌트로, 공통적인 작업을 처리하거나 비즈니스 로직과 중복되는 코드를 효과적으로 분리하는 데 적합합니다.
이 특성을 활용하면 데이터 전처리, 로깅, 모니터링, 캐싱뿐만 아니라, 트랜잭션 관리나 DB 연결과 같은 반복 로직도 인터셉터로 집중화할 수 있습니다.


인터셉터의 역할

NestJS에서 인터셉터는 다음과 같은 상황에서 호출됩니다:

  1. 요청(Request): 컨트롤러에 전달되기 전에 데이터를 처리하거나 설정 작업을 수행.
  2. 응답(Response): 컨트롤러에서 반환된 결과를 가공하거나 후처리 작업을 수행.

이런 특징은 요청과 응답 모두에서 중복 로직을 최소화하고, 비즈니스 로직과 공통 작업을 분리하여 관심사 분리를 실현할 수 있게 합니다.


서비스와 컨트롤러에 DB 연결 관리 로직이 있을 때

아래는 인터셉터 없이 DB 연결 및 트랜잭션 관리 로직을 서비스와 컨트롤러에서 처리한 경우 입니다.

sequenceDiagram
    participant 사용자 as 사용자(Client)
    participant 컨트롤러 as Controller
    participant 서비스 as UserDBService
    participant 쿼리서비스 as QueryService
    participant 데이터베이스 as Database

    사용자->>컨트롤러: 요청 전송 (sessionID 포함)
    컨트롤러->>서비스: 요청 전달
    서비스->>데이터베이스: 세션 ID로 DB 연결 생성
    alt 연결 성공
        서비스->>데이터베이스: 트랜잭션 시작
        서비스->>쿼리서비스: 비즈니스 로직 전달
        쿼리서비스->>데이터베이스: 쿼리 실행
        alt 쿼리 성공
            쿼리서비스->>서비스: 실행 결과 반환
            서비스->>데이터베이스: 트랜잭션 커밋
            서비스->>컨트롤러: 처리 결과 반환
            컨트롤러->>사용자: 최종 응답 반환
        else 쿼리 실패
            쿼리서비스->>서비스: 에러 반환
            서비스->>데이터베이스: 트랜잭션 롤백
            서비스->>컨트롤러: 에러 반환
            컨트롤러->>사용자: 에러 응답 반환
        end
    else 연결 실패 (에러 1040)
        서비스->>컨트롤러: "현재 사용자가 많습니다" 에러 반환
        컨트롤러->>사용자: 에러 응답 반환
    end
    서비스->>데이터베이스: DB 연결 종료
Loading

문제점

  • 중복 코드: 컨트롤러와 서비스마다 DB 연결, 트랜잭션 관리, 에러 처리 코드가 반복됨.
  • 비즈니스 로직 혼재: 비즈니스 로직과 시스템 관리 로직(DB 연결, 에러 처리 등)이 섞여 있어 가독성과 유지보수성이 떨어짐.
  • 확장성 부족: 공통 작업(예: 로깅, 모니터링)을 추가하려면 모든 서비스와 컨트롤러를 수정해야 함.

인터셉터로 관심사 분리

인터셉터를 사용하면 위 문제를 해결할 수 있습니다. 데이터베이스 연결과 트랜잭션 관리를 인터셉터로 옮기면 서비스와 컨트롤러는 비즈니스 로직에만 집중할 수 있습니다.

sequenceDiagram
    participant 사용자 as 사용자(Client)
    participant 인터셉터 as UserDBConnectionInterceptor
    participant 데이터베이스 as Database
    participant 컨트롤러 as Controller

    사용자->>인터셉터: 요청 전송 (sessionID 포함)
    인터셉터->>데이터베이스: 세션 ID로 DB 연결 생성
    alt 연결 성공
        인터셉터->>데이터베이스: 트랜잭션 시작
        인터셉터->>컨트롤러: 요청 전달
        컨트롤러->>인터셉터: 응답 반환
        인터셉터->>데이터베이스: 트랜잭션 커밋
    else 연결 실패 (에러 1040)
        인터셉터->>사용자: "현재 사용자가 많습니다" 에러 반환
    end
    alt 컨트롤러에서 예외 발생
        인터셉터->>데이터베이스: 트랜잭션 롤백
        인터셉터->>사용자: 에러 응답 반환
    end
    인터셉터->>데이터베이스: DB 연결 종료
    인터셉터->>사용자: 최종 응답 반환
Loading
@Injectable()
export class UserDBConnectionInterceptor implements NestInterceptor {
  constructor(private readonly configService: ConfigService) {}

  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const identify = request.sessionID;

    try {
      request.dbConnection = await createConnection({
        host: this.configService.get<string>('QUERY_DB_HOST'),
        user: identify.substring(0, 10),
        password: identify,
        port: this.configService.get<number>('QUERY_DB_PORT', 3306),
        database: identify,
        infileStreamFactory: (path) => {
          return createReadStream(path);
        },
      });
    } catch (error) {
      console.error('커넥션 제한으로 인한 에러', error);
      if (error.errno == 1040) {
        throw new HttpException(
          {
            status: HttpStatus.TOO_MANY_REQUESTS,
            message: 'Too many users right now! Please try again soon.',
          },
          HttpStatus.TOO_MANY_REQUESTS,
        );
      }
    }

    await request.dbConnection.query('set profiling = 1');
    await request.dbConnection.beginTransaction();

    return next.handle().pipe(
      tap(async () => {
        await request.dbConnection.commit();
      }),
      catchError(async (err) => {
        if (err instanceof DataLimitExceedException) {
          await request.dbConnection.rollback();
          throw err;
        }
      }),
      finalize(async () => await request.dbConnection.end()),
    );
  }
}
Clone this wiki locally