코드로 우주평화

저장소 패턴(Repository Pattern) 도입기 본문

나는 이렇게 일한다/업무

저장소 패턴(Repository Pattern) 도입기

daco2020 2022. 7. 10. 23:58

*보안상 일부 명칭을 모호하게 표현하였으며 실제 소스코드가 아닌 설명을 위한 샘플 코드를 사용하였습니다.

 

 

 

저장소 패턴 도입 프로젝트 

입사 후, 첫 실무 프로젝트로 저장소 패턴(Repository Pattern) 도입 프로젝트를 진행했다.

 

 

저장소 패턴이란?

저장소 패턴은 모든 데이터가 메모리상에 존재하는 것처럼 가정하고 이를 추상화하여 데이터 접근과 관련된 구현 사항을 감춘다. 저장소를 제외한 다른 레이어는 더 이상 저장소의 구현에 대해 신경 쓸 필요 없이 인터페이스로만 소통한다.

 

출처 : 도서 [파이썬으로 살펴보는 아키텍처 패턴]

 

 

 

저장소 패턴을 도입한 이유는 기존 운영하던 ‘A’ 서버(보안상 ‘A’라 지칭)의 레이어가 Controller - Service 로만 구성되어 있었고 이와 관련하여 문제점을 가지고 있었기 때문이다.

 

 

 

 

 

 

기존 문제점

1. 비즈니스 로직과 DB 접근 로직이 강하게 결합되어있다.

'비즈니스 로직'과 'DB접근 로직'의 강한 결합은 코드의 복잡도를 높이고 가독성을 떨어뜨려 변경에 많은 비용이 들게 했다. 또한 복잡도가 높은 만큼 테스트 코드를 작성하는 데에도 어려움이 있었다.

 

2. 레이어 간 이동하는 데이터의 정보를 알기 어렵다.

보통 레이어 간 이동에 DTO(Data Transfer Object)을 사용하지만 'A'서버는 response 데이터 외에 레이어 간 이동에는 dict를 사용하고 있었다. (*DTO란, 레이어 간 데이터 교환을 하기 위해 사용하는 객체를 의미) 

 

때문에 클라이언트로 반환되기 전까지는 데이터의 정보를 알기 어려웠고 이는 서버의 안정성은 물론 개발 생산성에도 부정적인 영향을 미쳤다.

 

데이터 정보에 대한 문제는 설계와는 별개였지만 저장소 패턴을 도입하면서 함께 해결하기로 했다.

 

 

기존 문제점을 해결하기 위해 나는 다음을 적용하기로 했다.

  1. Service에서 DB 접근 로직을 분리하자
  2. Repository 레이어를 추가하고 추상화된 인터페이스에 의존함으로써 비즈니스 로직에만 집중할 수 있는 구조를 만들자
  3. DTO를 사용해 데이터의 값과 타입을 명확하게 명시하자

 

 

 

 

 

도입 결과

*먼저 도입 결과부터 언급하고 도입 과정을 설명하겠습니다.

 

1. Service와 Repository 레이어를 분리하고 인터페이스를 사용하여 결합도를 낮춘 유연한 구조를 만들었다.

Service 레이어에 모든 로직이 있어 결합도가 높았던 기존 구조
DB 접근 로직을 Repository로 분리하고 인터페이스를 만들어 레이어간 결합도를 낮추었다

 

Service 레이어는 더 이상 데이터 접근 방법에 대해 알 필요가 없어졌고 비즈니스 로직에 온전히 집중할 수 있게 되었다.

 

 

 

 

 

2. DTO를 통해 데이터가 어느 레이어에 있든 '값'과 '타입'을 확인할 수 있게 되었다.

정확히 알기 어려웠던 기존 Any타입의 데이터 뭉치에서 이제는 값과 타입을 명확히 알 수 있는 DTO객체 형태로 레이어를 이동한다.

 

 

 

 

 

 

 

도입 과정

*‘A’ 서버는 Python 과 FastAPI 프레임워크를 사용합니다.

 

 

1. 저장소 패턴 적용 과정

먼저 저장소 레이어를 추가하고 추상화된 인터페이스를 생성하였다. 나는 파이썬의 ABC를 이용해 인터페이스를 구현하였다. ABC를 상속받은 클래스는 '추상 베이스 클래스'의 역할을 수행한다.

# Repository Layer

import abc

# 인터페이스 클래스
class Repository(abc.ABC): 
    @abc.abstractmethod
    def get_name(self, user_id):
        ...

# 구현 클래스
class NameRepository(Repository):
    def get_name(self, user_id):
        # DB 접근 로직 구현

 

 

그다음 Service와 결합되어 있던 DB 접근 로직을 저장소 인터페이스의 구현체로 분리하였다. 

# 분리 전, Service Layer

def get_name(user_id):
    ...

    # service 함수에 있던 DB 접근 로직
    query = select(name).where(user.id == user_id)
    name = fetch(query)
    return name
# 분리 후, Service Layer

import NameRepository


def get_name(user_id):
    ...

    # 저장소 클래스 선언 후, 인터페이스를 통해 데이터 요청
    repo = NameRepository()
    name = repo.get_name(user_id)
    return name

 

 

마지막으로 저장소 인터페이스를 Controller 레이어에서부터 주입받아 의존관계 주입(dependency injection)을 구현하였다. 

# DI 적용 후, Service Layer

def get_name(repo, user_id):
    ...

    # 주입받은 저장소 인터페이스를 통해 데이터 요청
    name = repo.get_name(user_id)
    return name

 

'의존관계 주입'은 기존 단방향이던 의존관계를 외부 주입을 통해 상위 모듈과 하위 모듈이 모두 추상화된 인터페이스를 의존하도록 만드는 것이다. 이로써 얻는 이점은 다음과 같다.

 

1. 상위모듈과 하위 모듈의 결합도를 낮춘다.

2. 코드의 재사용성이 증가한다. (하위 모듈이 변경되어도 상위 모듈은 변경된 하위 모듈을 그대로 사용할 수 있다)

3. 테스트가 용이하다. (인터페이스를 통해 구현체를 갈아끼우기 쉽다)

4. 가독성이 좋다.

 

 

 

 

2.  DTO 적용 과정

Dataclass를 사용해 데이터의 값과 타입을 명시했다. Dataclass는 파이썬에서 클래스 방식으로 데이터를 담아두기 위해 사용하는 모듈이며 아래 코드처럼 데코레이터 형식으로 사용한다.

# # Repository Layer

from dataclasses import dataclass


@dataclass
class NameDto:
    name: str
	

class NameRepository(Repository):
   def get_name(self, user_id):
       ...
       data = fetch(query)

       # data를 NameDTO(Dataclass)에 담아 반환한다.
       name = NameDto(name=data['name'])
       return name # 이제 name은 NameDto객체로서 다음 레이어로 이동한다.

 

 

 

*Dataclass 사용법에 대해 자세히 알고 싶다면 다음 글을 참고해주세요

2022.05.18 - [Dev/Language] - Python _ @dataclass 사용법과 타입 확인

 

 

 

 

 

 

어려웠던 점

1. 작업 대상 Service 함수가 너무 많아 구조와 관계를 파악하기 어려웠다. 예를 들어 해당 함수가 어떤 controller 함수와 연결되어있고 어떤 domain model을 사용하는지 확인하기 위해 코드를 타고 타고 이동해야 했다.

 

나는 한눈에 관계를 파악하기 위해 구글 시트를 따로 만들어 아래처럼 Service의 모든 함수의 관계를 정리했다. (내용은 보안상 모자이크)

 

정리한 함수 목록의 일부

 

이렇게 한 번 정리해두니 작업 때마다 레이어 사이를 헤매지 않고도 전체적인 구조와 흐름을 파악할 수 있었다. (진행사항을 체크하고 메모를 남기는 것은 덤)

 

 

 

 

2. DTO를 사용하면서 DTO를 생성하는 코드가 반복되는 문제가 있었다. 예를 들어 DB로부터 가져온 값을 DTO로 감싸는 코드가 모든 함수에 반복 사용되었다.

 

이를 해결하기 위해 나는 DB로부터 데이터를 가져오는 '하위 함수'에 옵션 인자로 아직 선언되지 않은 Dataclass를 넣도록 했다. 그리고 내부 구현을 수정해 DTO 객체를 반환하도록 하였다.

 

요약하자면 저장소 구현체보다 더 하위 모듈에 DTO 생성 로직을 두어 불필요하게 반복되던 코드를 없앴다!

 

 

 

 

 

 

마무리 소감

취업 준비할 때 아키텍처와 패턴에 대한 내용을 계속 접해왔다. 하지만 이론으로만 접했기 때문에 그 필요성을 체감하기 어려웠고, ‘그래서 어떻게 구현하는 건데?’라는 의문이 계속 남아있었다.

 

하지만 이번 프로젝트를 통해 저장소 패턴의 필요성과 이점을 체감할 수 있었고, 이를 직접 내 손으로 구현하면서 스스로 한 단계 성장했다는 생각이 들었다. 특히 내가 작성한 코드 덕분에 ‘A’ 서버의 퀄리티가 높아졌다는 피드백을 받았을 때 굉장히 뿌듯했다!

 

마지막으로 테스트 코드의 중요성과 위대함을 느낄 수 있었다. 프로젝트 과정 중에 내게 가장 큰 도움을 준 건 단연코 ‘테스트 코드’라고 할 수 있다. 테스트 덕분에 내 실수를 단번에 찾아내고 마음 편히 개발에 집중할 수 있었다. 이제는 테스트 코드 없는 개발은 상상할 수 없다...!