Home 잃어버린 SRP를 찾아서 - 퍼사드 패턴 적용기
Post
Cancel

잃어버린 SRP를 찾아서 - 퍼사드 패턴 적용기

면접 예상 질문 서비스를 구현하면서 Controller 단에 많은 서비스 인터페이스가 사용되게 되었고,

이를 개선하고자 Facade패턴을 적용하게 된 글을 공유하고자합니다.

AS-IS

우선 이번 글에서 다루게 된 클래스, ResumeController입니다.

1
2
3
4
5
6
7
8
9
@RestController
class ResumeController(
	private val promptService: PromptService,
	private val requestFormMapper: RequestFormMapper,
	private val completionService: CompletionService,
	private val objectMapper : ObjectMapper
) {
	// ...
}

면접 예상 질문을 생성하기 위해서는 다음과 같은 로직이 수행됩니다.

  1. 해당하는 프롬프트를 조회한다.
  2. 조회한 프롬프트를 사용해서 매퍼 클래스를 이용해 컴플리션 데이터를 생성한다.
  3. AI 서비스를 이용하기 위해 OpenFeign을 사용해서 요청한다.
  4. 응답 JSON 데이터를 ObjectMapper를 사용해서 바인딩한다.

즉, 사용되어야 하는 서비스 인터페이스만

  • PromptService
  • RequestFormMapper
  • CompletionService
  • ObjectMapper

4개나 되었기 때문에, 이를 어느 한 인터페이스에서 몰아서 사용하는 것 보다, Controller 클래스에서 의존하여 사용하게 되었습니다.

그러나 이러한 점은 추가적인 클래스가 필요할 때 문제가 되었는데요.

CompletionMapper라는 DTO를 변환해주는 매퍼 클래스를 하나 추가하게 되었고, 이는 마찬가지로 ResumeController의 생성자 주입을 통해 사용하는 것이 자명했습니다.

그러던 중 인프콘에서 들었던 세션에서, SOLID에 대한 내용이 떠올랐습니다.

confuse

SRP 원칙을 지키지 못하고 있는 것이 아닌가?

즉, 클래스의 변경 이유는 단 하나로 귀결되어야 한다는 이 원칙은, ‘ResumeController에서는 지켜지지 않고 있다’ 라는 생각이 들었습니다.

사용자의 요청으로 면접 예상 질문을 생성한다 라는 협력을 두고 역할을 새로 나누어 책임을 전가할 경우, 새로운 클래스가 추가되기 때문에

이는 ResumeController에 추가되고, 점점 의존하는 클래스들이 많아지게 됩니다.

이는 의존하는 클래스 중 하나라도 변경될 경우, 컨트롤러가 변경에 영향을 미치게 되고, ResumeController가 변경될 수 있는 원인이 하나가 아니게 되므로 SRP를 위배한다고 생각이 들었습니다.

그래서 저는 이러한 점을 덜어내고자 애플리케이션 계층과 프레젠테이션 계층 사이에 한가지 인터페이스를 두는 것이 좋겠다는 생각이 들었습니다.

개인적을 생각했을 때, Facade 패턴이 이 문제점을 해결해줄 수 있을 것 같았습니다.

Facade 패턴

다른 클래스들의 복잡한 집합에 대한 단순화된 인터페이스를 제공하는 구조적 디자인 패턴

introduce

퍼사드는 정면이라는 의미로 정면만 봐도 목적을 알 수 있도록 복잡한 것을 단순화 한다는 의미를 가진다고 생각하면 될 것 같습니다.

즉, 컨트롤러와 기존의 인터페이스들 사이에 퍼사드를 두어, 컨트롤러 측에서는 단순히 퍼사드만을 바라보게 되는 것입니다.

popcorn

그래서 저는 우선 장점과 단점에 대해서 찾아보았는데요.

장점: 클라이언트(컨트롤러)는 퍼사드와만 소통하기 때문에 다른 클래스와의 직접적인 의존성이 줄어든다. -> SRP를 지킬 수 있다.

단점: 퍼사드 클래스가 너무 많은 로직이나 의존성을 가지게 되면, 자체적으로 복잡해질 수 있다. 이로 인해 유지보수가 어려워질 수 있다.

로 귀결되었습니다.

저는 현재 프로젝트가 아직 개발 단계에 있고, 비즈니스 로직에 대한 개선이 필요하기 때문에 우선 컨트롤러와 서비스 레이어의 의존성을 최대한 낮추는 것이 계발 단계에서는 좋은 방법이 아닌가 라는 생각을 했습니다.

우선 한쪽에서의 의존성을 낮추어야 다른 한쪽에 대한 개발을 용이하게 가져갈 수 있기 때문입니다.

그래서 저는 ResumeFacade라는 클래스를 중간에 구현하게 되었습니다.

TO-BE

기존에 ResumeController가 사용하던 클래스들을 ResumeFacade로 옮겼습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Facade
class ResumeFacade(
    private val promptService: PromptService,
    private val requestFormMapper: RequestFormMapper,
    private val completionService: CompletionService,
    private val completionMapper: CompletionMapper,
    private val objectMapper: ObjectMapper
) {
    // 여기에서 실제 로직을 처리하고 컨트롤러와 소통할 메서드를 제공
}

@RestController
class ResumeController(private val resumeFacade: ResumeFacade) {
    // ...
}

퍼사드 클래스 자체는 SRP를 지켜지지 않고 있지 않나? 라는 생각이 들었지만,

퍼사드 클래스가 사용하는 서비스 인터페이스도 모두 Application 레이어이기 때문에

같은 레이어에 있는 클래스들보다 Presentation 레이어인 컨트롤러 클래스와의 의존성을 낮추는 것이 더 좋은 방법이라고 생각해서 적용하게 되었습니다.

그리고 Controller를 테스트 할 때 테스트용 객체를 생성하는 것이 매우 간단했습니다.

저는 주로 모킹(mocking)을 사용해서 테스트를 많이 하곤 하는데요.

기존의 로직대로면 Controller 테스트를 할 때, 다양한 서비스의 로직에 대해서도 모킹을 해야했습니다.

컨트롤러 테스트에서는 단순히 ResumeFacade의 로직에 대해서만 모킹을 적용하면 되기 때문에 한층 수월해졌으며, Facade 클래스에서는 여러 서비스를 사용하고 있기 때문에 각 서비스들의 단위 테스트를 더 세세하게 작성할 수 있었습니다.

그러나 아까 단점이라고 생각했던, 퍼사드 클래스 자체가 뚱뚱해지는 문제는 프로젝트를 개발해나가면서 고민해볼 생각입니다.

레퍼런스

This post is licensed under CC BY 4.0 by the author.

Real MySQL 8.0 - 트랜잭션과 잠금

테스트시 ControllerAdvice에 의해 예외가 잡히지 않는 경우