개발자로 일을 하다 보면 개발에 관련된 모든 것들은 추상화로 이루어져 있다는 생각이 들 때가 있다. 결국 개발 언어나 라이브러리, 프레임워크 전부 다 내부적으로 어떻게 구동되는지는 몰라도 결과값을 유추할 수 있고 해당 결과값들의 집합으로 완성된 서비스를 만들 수 있다.
서비스를 구현할 때 좋은 구조의 설계를 하기 위해 역할과 책임에 따라서 계층을 나누게 된다. 보통 3계층 아키텍처를 많이 사용하는데 구조는 아래와 같다.
1. 비즈니스 로직
비즈니스 로직은 서비스의 핵심 로직이라고 생각하기 때문에 Service 계층에서 구현하고 다른 계층의 문제가 전파되지 않게 아키텍처 설계를 하고 있다.
1-1. Service 계층에서의 의존성
회사에서 개발할 때 Service 계층에선 Repository 계층을 의존하게 구현한다. 의존성 주입을 통해 결합도를 낮추는 방식을 사용하고 있고, Repository 계층에 있는 로직들이 어떻게 구동되는지 전혀 알지 못하고 추상화된 메서드 이름으로만 사용한다. 이처럼 계층 간의 관계도 의존성 주입을 통해 추상화되어 관리하고 있다.
2. Service 계층의 역할
현재 백엔드 팀에서 구현하고 있는 Service 계층 예제 코드를 보면 역할이 너무 포괄적이라고 느껴진다. 아래는 현재 사용하고 있는 Service 계층의 예시이다.
@Injectable()
export class ProgressService {
constructor(
@Inject(IUsersRepository) private usersRepository: IUsersRepository,
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
) {}
async confirmProjectProcess(projectId: number, userId: number): Promise<any> {
// 유저 데이터 가져오고
const isUser = await this.usersRepository.findUserDataByUserId(userId);
// 유저 데이터 체크하고
this.checkUser(isUser);
// 유저 이메일도 체크하고
if (!isUser._email) {
throw new BadRequestException('ERR');
}
// 유저 포인트 증가시키고
const userPoint = isUser._point + CONSTATNS.UPGRADEGRADE_POINT;
const userAccount = isUser._account + userPoint;
//... 그 외 기타 비즈니스 로직이 있다고 가정
// 유저에게 알림보내고
await this.notificationRepository.createNotificationByProjectIdAndUserId(projectId, userId);
// 계산된 유저 포인트 리턴
return { userPoint };
}
private checkUser(userData: GetProgressDataOutput) {
if (!userData) {
throw new BadRequestException('ERR2');
}
}
}
해당 코드를 보면 유저 데이터를 가져와서 검증하고, 포인트를 증가시키기도 하고 계좌에 관련된 로직도 여기서 구현을 한다. 위 코드는 예시로 만든 예제 코드라서 코드를 이해하는데 문제가 없지만, 실제 구현된 Service 계층의 비즈니스 로직을 보면 수많은 검증과 계산들이 들어가 있어 하나의 매서드에 수백 줄의 구현 로직이 들어가기도 한다.
2-1. 해당 코드의 문제점
지금까지는 이렇게 복잡하게 구현 로직이 들어간 비즈니스 로직이 당연하다고 생각하면서 개발을 했는데 서비스를 구현하다 보니 코드의 가독성이 너무 저하되는 문제가 있었다. 결국 내가 짠 코드를 다른 팀원뿐만 아니라 내가 봐도 이해하는데 불편함을 느끼는 문제였다.
2-2. 해결책
이에 대한 해결책으로는 결국 구현 로직과 비즈니스 로직의 계층을 분리하고 구현 로직을 추상화해 비즈니스 로직에서 사용하는 것이라고 생각했다. 계층이 하나 늘어나서 구현할 코드의 양이 늘었다라고 생각할 수도 있지만 구현 로직의 재사용 가능성도 생기고 코드의 양이 늘어서 생기는 단점보다 역할을 잘 나눠서 생기는 장점이 더 크다고 느꼈다.
2-3. 수정된 예제 코드
@Injectable()
export class ProgressService {
constructor(
private userManager: UserManager,
private pointManager: PointManager,
private notificaitonManager: NotificaitonManager,
) {}
async confirmProjectProcess(projectId: number, userId: number): Promise<any> {
const user = await this.userManager.check(userId);
const point = await this.pointManager.calculation(userId);
await this.notificationManager.append(projectId, userId);
return { point };
}
}
사실 해당 코드가 이전과 많이 달라진 건 없지만 구현 로직 계층을 새로 만들어서 Service 계층에서 구현 로직 계층을 주입받아 사용하고 있다. 구현 로직 계층에 있는 메서드들이 어떻게 동작되는지는 모르지만 직관적으로 어떤 결과를 갖는지에 대한 내용은 메서드 이름을 통해서 알 수 있기 때문에 다른 사람이 내가 작성한 코드를 보고 어떤 흐름으로 진행되고 어떤 결과를 리턴하는지를 알 수 있게 된다.
3. 개인적인 생각
사실 아무리 재사용 가능성이 있다고 하더라도 계층을 하나 추가하는 것만으로 구현해야 할 코드의 양은 늘어날 수밖에 없다. 그럼에도 계층을 나누고 추상화해 의존성을 관리하는 게 더 큰 이점을 가져오기 때문에 추가했다. 먼저 어느 누가 봐도 비즈니스 로직의 역할과 책임을 이해할 수 있다. 또한 비즈니스 로직에 구현 로직이 붙어 제한 없이 길어지는 것을 방지할 수 있기 때문에 작성자가 코드를 타이트하게 관리할 수 있다는 장점이 있다.
한 가지 해결하지 못한 부분은 테스트 코드에 대한 부분이다. 구현 로직 계층을 추상화해 사용하는데 만약 비즈니스 로직에 대한 테스트를 구현해야 한다면 구현 로직 계층 리턴 값의 경우의 수를 전부 지정해줘야 해야 하는 어려움이 있을 것 같다. 이 부분에 대해선 좀 더 좋은 방법이 있는지 찾아보고 포스팅해야겠다.
'Architecture' 카테고리의 다른 글
AWS Lambda - SQS를 활용한 인프라 아키텍처 재설계 ( + S3, 맥미니?) (0) | 2023.02.27 |
---|---|
헥사고날 아키텍처(포트 앤 어뎁터 아키텍처)_Express example (0) | 2022.09.29 |