Service를 Class로 만드는 이유

민경찬·2023년 11월 23일
10

백엔드

목록 보기
10/19
post-thumbnail

서론


코드가 좋은 구조를 가지는 것은 상당히 기쁜 일이다. 그러나 좋은 구조를 생각하기 이전에 이런 구조가 왜 나온지에 대해서 생각해 볼 필요가 있다고 생각한다.

SpringNest등의 프레임워크가 어떤 맥락에서 나온 구조인지 한 번 생각해보자.

Javascript를 기준으로 작성된 글 입니다.


본론


1. Class를 사용해야 할까?

백엔드 개발자라면 Spring을 사용하든 Nest를 사용하든 Service레이어를 Class로 구현해본 경험이 있을 것이다. 그런데 왜 Class로 구현하는 것일까?

export default class UserService {
	constructor() {}
  
	public async signUp();
  
  	public async getUserByIdx();
}

class를 사용하는 이유를 이해하려면 class를 사용하지 않으면 된다.

export const signUp = async (userData) => {}

export const getUserByIdx = async (userIdx) => {}

음? 클래스를 없애고 메서드를 함수로 만들었지만 딱히 문제점이 보이지 않는다.

2. 함수로만 작성했을 때의 문제점

함수를 만들었을 때의 문제점은 아래 코드처럼 간단한 함수의 경우에는 그 문제가 보이지 않는다.

export const signUp = async (userData) => {
  	// INSERT query
	const user = await insertUserQuery(userData);
  
  	return user;
}

그러나 signUp함수에 로직이 더 추가된다면 어떻게 될까?

export const signUp = async (userData) => {
	// Check Email Authenticate State
  	await checkEmailAuthenticate(userData.email);
  
  	// Email Duplicate Check
  	await checkEmailDuplicate(userData.email);
  
  	// ...
  	await someFunc();
  
  	// INSERT query
	const user = await insertUserQuery(userData);
  
  	return user;
}

기능이 점점 추가될 수록 signUp이 필요로 하는 함수가 어떤 것인지 점점 파악하기 힘들어진다.

내가 만든 함수가 어떤 함수에까지 영향을 주는지 판단하기가 어렵다.

export default class UserService {
	constructor () {}
  	
  	public async signUp (userData) {
      
    }
}

그래서 모든 함수들을 논리적인 집합인 class로 묶어 사용하는 것이다. 만약 모든 함수가 class메서드 형태로 작성됐다면 다음과 같이 관리할 수 있을 것이다.

export default class UserService {
	constructor () {}
  	
  	private readonly authService = new AuthService();
  	private readonly someService = new SomeService();
  
  	public async signUp (userData) {
		// Check Email Authenticate State
      await this.authService.checkEmailAuthenticate(userData.email);

      // Email Duplicate Check
      await this.checkEmailDuplicate(userData.email);

      // ...
      await this.someService.someFunc();

      // INSERT query
      const user = await insertUserQuery(userData);

      return user;
    }
}

UserServiceAuthServiceSomeService를 가져와 사용한다는 것을 바로 알 수 있다.

즉, 여러 개의 함수를 관리하기 위해서는 그 함수를 묶어주는 논리적인 그룹이 필요하다는 것이다.

더 많은 이유가 있고 더 깊은 고뇌의 결과물이지만 여기서는 생략합니다.

3. 의존성 주입을 왜 할까?

하지만 위 코드에서도 문제점이 발생한다.

아래 코드 처럼 내가 사용하고 싶은 함수를 묶어놓은 서비스를 가져다가 인스턴스를 만든 후 사용할 수 있다.

export default class UserService {
	constructor () {}
  
    private readonly authService = new AuthService();
    private readonly someService = new SomeService();
}

그러나 authService의 인스턴스가 생성 된다는 문제가 있다. 서비스가 엄청나게 많아질 경우 그 모든 서비스내에서 새로운 서비스 인스턴스를 생성하게된다. 즉, 서비스 인스턴스를 외부에서 받을 수 있는 구조가 필요하다는 것이다.

export default class UserService {
	constructor (
		private readonly authService,	
        private readonly someService
  	) {}
}

이렇게 외부에서 생성자를 통해 서비스를 직접 입력받도록 할 수 있으며 하나의 서비스는 하나의 인스턴스를 만들어 관리할 수 있게된다.

의존성 주입은 다양한 효과를 가져옵니다. 나머지 부분은 생략합니다.


결론


어떤 맥락에서 생긴 구조인지를 아는 것이 그 구조를 사용함에 있어 더 깔끔한 코드를 작성하도록 도와준다.

그런 의미에서 Service, Controller, DTO, DI 등등 다양한 키워드의 등장 맥락을 한 번 되짚어보고 싶었다.

구조화와 관련된 용어들은 어떻게 검색하더라도 충분히 많은 정보를 찾을 수 있다. 그러나 해당 키워드들이 어떤 맥락에서 탄생했는지까지도 알아보면 좋을 것 같다.

0개의 댓글