코드가 좋은 구조를 가지는 것은 상당히 기쁜 일이다. 그러나 좋은 구조를 생각하기 이전에 이런 구조가 왜 나온지에 대해서 생각해 볼 필요가 있다고 생각한다.
Spring
과 Nest
등의 프레임워크가 어떤 맥락에서 나온 구조인지 한 번 생각해보자.
Javascript를 기준으로 작성된 글 입니다.
백엔드 개발자라면 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) => {}
음? 클래스를 없애고 메서드를 함수로 만들었지만 딱히 문제점이 보이지 않는다.
함수를 만들었을 때의 문제점은 아래 코드처럼 간단한 함수의 경우에는 그 문제가 보이지 않는다.
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;
}
}
UserService
는 AuthService
와 SomeService
를 가져와 사용한다는 것을 바로 알 수 있다.
즉, 여러 개의 함수를 관리하기 위해서는 그 함수를 묶어주는 논리적인 그룹이 필요하다는 것이다.
더 많은 이유가 있고 더 깊은 고뇌의 결과물이지만 여기서는 생략합니다.
하지만 위 코드에서도 문제점이 발생한다.
아래 코드 처럼 내가 사용하고 싶은 함수를 묶어놓은 서비스를 가져다가 인스턴스를 만든 후 사용할 수 있다.
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
등등 다양한 키워드의 등장 맥락을 한 번 되짚어보고 싶었다.
구조화와 관련된 용어들은 어떻게 검색하더라도 충분히 많은 정보를 찾을 수 있다. 그러나 해당 키워드들이 어떤 맥락에서 탄생했는지까지도 알아보면 좋을 것 같다.