G6의 기본적인 추상클래스 사용 방법/패턴 정리
- 가장 많이 사용되는 클래스를 기준으로 G6는 어떤 방식으로 클래스를 관리하는지를 정리해 보고자 한다.
1. 가장 많이 사용되는 api의 정의
- api/v1/routers/ 에는 엔드포인트가 정의되어 있다.
- 해당 view들의 대표 혹은 include_router()로 참조되는 하위 라우터들은 __init__.py에 정의되어 있다.
- 이 중 대표 api는 다음과 같다.
- 추적하고자 하는 url 및 의존성은 /api/v1이다.
router = APIRouter(prefix="/api/v1",
dependencies=[Depends(check_use_api),
Depends(set_current_connect)])
2. 추적하려는 의존성 정리
- 의존성은 두개로 정의되어 있다.
- check_use_api, set_current_connect이다.
- check_use_api은 단순한 구조이기 때문에 set_current_connect를 추적한다.
3. set_current_connect 정리
- gpt를 이용해 함수의 내용을 추가 정리한다.
async def set_current_connect(
request: Request,
db: db_session,
service: Annotated[CurrentConnectServiceAPI, Depends()],
member_service: Annotated[MemberServiceAPI, Depends()],
):
"""
현재 접속자 정보 설정
Parameters:
- request (Request): FastAPI의 Request 객체로부터 클라이언트 IP와 URL 경로를 추출합니다.
- db (db_session): SQLAlchemy 세션을 통해 데이터베이스와의 상호작용을 담당합니다.
- service (CurrentConnectServiceAPI): 현재 접속자 관련 비즈니스 로직을 처리하는 서비스 클래스의 인스턴스입니다.
- member_service (MemberServiceAPI): 회원 관련 비즈니스 로직을 처리하는 서비스 클래스의 인스턴스입니다.
Raises:
- ProgrammingError: 데이터베이스 처리 중 오류가 발생할 수 있습니다.
Returns:
- None
Description:
이 함수는 요청된 클라이언트의 IP 주소와 URL 경로를 기반으로 현재 접속자 정보를 관리합니다.
1. 클라이언트의 IP 주소를 추출하고, 요청의 URL 경로를 얻습니다.
2. 옵셔널한 OAuth2 토큰을 통해 현재 로그인한 회원 정보를 조회합니다.
3. 관리자 확인 후, 현재 접속자 정보를 업데이트 또는 생성합니다.
4. 마지막으로, 현재 로그인 이력을 삭제하고, 회원 데이터를 데이터베이스와 동기화합니다.
Note:
- cf_admin이 mb_id와 일치하지 않을 경우에만 현재 접속자 정보를 업데이트 또는 생성합니다.
"""
try:
current_ip = get_client_ip(request) # 클라이언트의 IP 주소를 가져옵니다.
path = request.url.path # 요청의 URL 경로를 가져옵니다.
token = await oauth2_optional(request) # OAuth2 토큰을 옵셔널하게 조회합니다.
member = await get_current_member_optional(token, member_service) # 현재 회원 정보를 조회합니다.
mb_id = getattr(member, "mb_id", "") # 회원의 mb_id를 가져옵니다.
cf_admin = getattr(request.state.config, "cf_admin", "admin") # 설정에서 관리자 ID를 가져옵니다.
if cf_admin != mb_id:
current_login = service.fetch_current_connect(current_ip)
if current_login:
service.update_current_connect(current_login, path, mb_id)
else:
service.create_current_connect(current_ip, path, mb_id)
service.delete_current_connect() # 현재 로그인한 이력을 삭제합니다.
if member:
db.refresh(member) # 데이터베이스와의 세션을 통해 회원 데이터를 동기화합니다.
except ProgrammingError as e:
print(e) # 데이터베이스 처리 중 발생한 오류를 출력합니다.
- 클라이언트의 IP 주소와 요청의 URL 경로를 가져와서 접속자 정보를 관리한다.
- OAuth2 토큰을 통해 현재 로그인한 회원 정보를 가져와서 관리자 확인을 수행한다.
- 관리자인 경우에만 현재 접속자 정보를 업데이트 또는 생성하고, 마지막으로 현재 로그인 이력을 삭제한다.
- 회원 정보가 있는 경우 데이터베이스와의 세션을 통해 데이터를 동기화한다.
- 이 함수는 데이터베이스 처리 중 발생할 수 있는 예외를 처리하기 위해 ProgrammingError를 catch하고, 발생한 오류를 출력한다.
- 해당 함수는 CurrentConnectServiceAPI와 MemberServiceAPI를 의존성 주입으로 사용한다.
4. CurrentConnectServiceAPI 정리
class CurrentConnectServiceAPI(CurrentConnectService):
"""
API 요청에 사용되는 CurrentConnectService 구현 클래스.
- 이 클래스는 API와 관련된 특정 예외 처리를 오버라이드하여 구현합니다.
"""
def raise_exception(self, status_code: int, detail: str = None):
raise HTTPException(status_code=status_code, detail=detail)
- CurrentConnectService를 상속받는다.
- CurrentConnectService는 BaseService 추상화 클래스를 상속받는다.
- raise_exception 함수는 추상화 메서드이며 CurrentConnectService에 정의된다.
- 하지만 CurrentConnectServiceAPI가 CurrentConnectService를 상속하면서 raise_exception는 다시 오버라이딩 된다.
- 차이는 AlertException() 예외처리가 HTTPException()로 변경된다는 점이다.
5. class CurrentConnectService(BaseService):
- class CurrentConnectService() 클래스는 현재 접속자 관련 서비스를 제공하는 종속성 주입 클래스로 실질적인 기능 함수들을 구현하고 있다.
"""현재 접속자 관련 기능을 제공하는 서비스 모듈입니다."""
from datetime import datetime, timedelta
from typing import Any
from cachetools import LRUCache, cached
from cachetools.keys import hashkey
from fastapi import Request
from sqlalchemy import Row, Select, Sequence, delete, func, insert, select
from core.database import db_session
from core.exception import AlertException
from core.models import Login, Member
from service import BaseService
class CurrentConnectService(BaseService):
"""
현재 접속자 관련 서비스를 제공하는 종속성 주입 클래스입니다.
"""
def __init__(self, request: Request, db: db_session) -> None:
self.request = request
self.db = db
self.admin = getattr(request.state.config, "cf_admin", "admin")
login_minute = getattr(request.state.config, "cf_login_minutes", 10)
self.base_date = datetime.now() - timedelta(minutes=login_minute)
def raise_exception(self, status_code: int, detail: str = None, url: str = None):
return AlertException(status_code=status_code, detail=detail, url=url)
@cached(LRUCache(maxsize=1),
key=lambda self, only_member=False: hashkey("connects_count", only_member))
def fetch_total_records(self, only_member: bool = False) -> int:
"""현재 접속중인 회원의 총 수를 반환합니다."""
query = self._base_query(only_member)
return self.db.scalar(query.add_columns(func.count(Login.mb_id)))
def fetch_corrent_connects(self, only_member: bool = False,
offset: int = 0, per_page: int = 10) -> Sequence[Row[Any]]:
"""현재 접속중인 회원 목록을 반환합니다."""
query = self._base_query(only_member)
return self.db.execute(
query.add_columns(Login, Member)
.outerjoin(Member, Login.mb_id == Member.mb_id)
.order_by(Login.lo_datetime.desc())
.offset(offset).limit(per_page)
).all()
def fetch_current_connect(self, ip: str) -> Login:
"""특정 IP의 현재 접속자 정보를 반환합니다."""
return self.db.scalar(select(Login).where(Login.lo_ip == ip))
def create_current_connect(self, ip: str,
path: str, mb_id: str = "") -> None:
"""현재 접속자 정보를 생성합니다."""
self.db.execute(
insert(Login).values(
lo_ip=ip,
mb_id=mb_id,
lo_location=path,
lo_url=path
)
)
self.db.commit()
# 캐시 초기화
self.fetch_total_records.cache_clear()
def update_current_connect(self, login: Login,
path: str, mb_id: str = "") -> None:
"""현재 접속자 정보를 갱신합니다."""
login.mb_id = mb_id
login.lo_datetime = datetime.now()
login.lo_location = path
login.lo_url = path
self.db.commit()
def delete_current_connect(self) -> None:
"""설정 시간 이전의 현재 접속자 정보를 삭제합니다."""
result = self.db.execute(
delete(Login).where(Login.lo_datetime < self.base_date)
)
self.db.commit()
# 캐시 초기화
if result.rowcount:
self.fetch_total_records.cache_clear()
def _base_query(self, only_member: bool = False) -> Select:
"""기본 쿼리를 반환합니다."""
query = select().where(
Login.mb_id != self.admin,
Login.lo_ip != "",
Login.lo_datetime > self.base_date
)
if only_member:
query = query.where(Login.mb_id != "")
return query
- BaseService는 추상화 클래스이다.
- set_current_connect의 다른 의존성 주입인 MemberServiceAPI() 클래스도 최상위 클래스는 동일한 BaseService를 상속받아 클래스가 구현되어 있는걸 확인할 수 있었다.
"""서비스 클래스에서 필요한 기능을 제공하는 모듈입니다."""
import abc
class BaseService(metaclass=abc.ABCMeta):
"""
모든 서비스 클래스의 기본이 되는 추상 기반 클래스입니다.
"""
@abc.abstractmethod
def raise_exception(self, status_code: int, detail: str = None):
"""
서비스 클래스에서 발생할 수 있는 예외를 처리하기 위한 추상 메서드입니다.
Args:
status_code (int): HTTP 상태 코드를 나타내는 정수입니다.
detail (str, optional): 예외 상황에 대한 추가적인 설명을 제공하는 문자열입니다. Defaults to None.
"""
6. MemberServiceAPI 클래스
- CurrentConnectServiceAPI와 마찬가지로 raise_exception()를 오버라이딩 하고 있다.
class MemberServiceAPI(MemberService):
"""
API 요청에 사용되는 MemberService 구현 클래스.
- 이 클래스는 API와 관련된 특정 예외 처리를 오버라이드하여 구현합니다.
"""
def raise_exception(self, status_code: int = 400, detail: str = None, url: str = None):
raise HTTPException(status_code=status_code, detail=detail)
7. MemberService 클래스
- 회원 관련 서비스를 제공하는 종속성 주입 클래스이다.
- 회원 정보 조회, 인증, 상태 검증 등의 기능을 포함한다.
- 클래스 코드가 길기에 첨부는 생략한다.
- 실재 기능함수가 모두 구현된 MemberService는 추상클래스 BaseService를 상속받아 raise_exception를 구현한다.
class MemberService(BaseService):
"""
회원 관련 서비스를 제공하는 종속성 주입 클래스입니다.
- 회원 정보 조회, 인증, 상태 검증 등의 기능을 포함합니다.
### Example
```python
@router.get("/members/{mb_id}")
async def read_member(
member_service: Annotated[MemberService, Depends()],
current_member: Annotated[Member, Depends(get_current_member)]
):
return member_service.get_member_profile(current_member)
```
"""
...
8. 개발 패턴을 질문해보기
- gpt와 클로드에게 질문해 보았을때, 클로드가 더 체계적이고 형식에 준하는 답변을 반환하였다.
- 질문 내용
fastapi의 전역 의존성 주입으로 특정 기능이 무조건 실행이 된다.
router = APIRouter(prefix="/api/v1",
dependencies=[Depends(check_use_api),
Depends(set_current_connect)])
set_current_connect은 다시 의존성 주입받은 api 클래스를 이용해 원하는 기능을
async def set_current_connect(
request: Request,
db: db_session,
service: Annotated[CurrentConnectServiceAPI, Depends()],
member_service: Annotated[MemberServiceAPI, Depends()],
):
MemberServiceAPI 클래스는 다음과 같다.
class MemberServiceAPI(MemberService):
"""
API 요청에 사용되는 MemberService 구현 클래스.
- 이 클래스는 API와 관련된 특정 예외 처리를 오버라이드하여 구현합니다.
"""
def raise_exception(self, status_code: int = 400, detail: str = None, url: str = None):
raise HTTPException(status_code=status_code, detail=detail)
실재 모든 기능 함수는 MemberService에 구현되어 있다.
결국 의존성에 의해 반환되는 클래스의 기능은 MemberService에 정의되어 있다.
MemberService는 추상화 클래스인 BaseService를 상속받는다.
추상화 클래스는 다음과 같다.
class BaseService(metaclass=abc.ABCMeta):
"""
모든 서비스 클래스의 기본이 되는 추상 기반 클래스입니다.
"""
@abc.abstractmethod
def raise_exception(self, status_code: int, detail: str = None):
"""
서비스 클래스에서 발생할 수 있는 예외를 처리하기 위한 추상 메서드입니다.
Args:
status_code (int): HTTP 상태 코드를 나타내는 정수입니다.
detail (str, optional): 예외 상황에 대한 추가적인 설명을 제공하는 문자열입니다. Defaults to None.
"""
이와 같은 디자인과 동일한 방식의 개발 패턴이 있을까? 어떤 설계 패턴을 지향하는지 알고 싶다.
9. 개발 패턴 정리
9.1. 의존성 주입 패턴 (Dependency Injection)
- 먼저 가장 두드러진 패턴은 의존성 주입(Dependency Injection) 패턴이다. FastAPI의 Depends를 사용하여 서비스 클래스들을 주입받고 있다. 이 패턴은 다음과 같은 이점을 제공한다.
9.1.1. 코드의 결합도를 낮추고 유연성을 높인다.
9.1.2. 단위 테스트가 용이해진다.
9.1.3. 코드 재사용성이 증가한다.
9.2. 템플릿 메서드 패턴 (Template Method)
- BaseService 추상 클래스와 그것을 상속받는 MemberService, 그리고 다시 MemberService를 상속받는 MemberServiceAPI 구조는 템플릿 메서드 패턴을 활용하고 있다.
9.2.1. 추상 기본 클래스에서 알고리즘의 뼈대를 정의한다.
9.2.2. 하위 클래스들은 이 뼈대를 유지하면서 특정 단계를 재정의한다.
- 여기서 raise_exception는 템플릿 메서드로, 구현 방식은 하위 클래스에 따라 달라진다.
9.3. 전략 패턴 (Strategy Pattern)
- MemberService와 MemberServiceAPI의 관계는 전략 패턴의 측면도 있다. MemberServiceAPI는 API 컨텍스트에 특화된 예외 처리 전략을 제공한다.
9.3.1. 알고리즘군을 정의하고 각각 캡슐화하여 교체 가능하게 만든다.
9.3.1. 클라이언트로부터 알고리즘을 분리하여 독립적으로 변형이 가능하게 한다.
9.4. 미들웨어 패턴 (Middleware Pattern)
- FastAPI의 라우터에 dependencies 매개변수를 통해 전역 의존성을 설정하는 방식은 미들웨어 패턴과 유사히다. 모든 요청이 특정 의존성 함수(check_use_api, set_current_connect)를 통과하도록 강제한다.
9.5. 서비스 계층 패턴 (Service Layer Pattern)
- 코드는 비즈니스 로직을 서비스 클래스(MemberService, CurrentConnectServiceAPI 등)에 캡슐화하고 있다. 이는 서비스 계층 패턴의 전형적인 구현으로, 다음과 같은 특징이 있다.
9.5.1. 비즈니스 로직을 응용 프로그램의 다른 계층으로부터 분리한다.
9.5.2. 로직의 재사용성을 높이고 코드 중복을 줄인다.
9.5.3. 관심사의 분리(Separation of Concerns)를 촉진한다.
9.6. 레포지토리 패턴 (Repository Pattern)
- 코드 예시에는 명시적으로 나와있지 않지만, db_session을 주입받는 것으로 보아 데이터 접근을 위한 레포지토리 패턴도 사용하고 있을 가능성이 높다.
9.7. 어댑터 패턴 (Adapter Pattern)
- MemberServiceAPI가 MemberService를 확장하면서 API 특화 기능을 추가하는 방식은 어댑터 패턴의 일종으로 볼 수 있다. 기존 서비스를 새로운 인터페이스(여기서는 API 환경)에 적응시키고 있다.
9.8. 이 설계가 지향하는 것
- 이 코드 설계는 크게 다음을 지향한다.
9.8.1. 관심사의 분리: 각 클래스와 계층이 특정 책임만을 가진다.
9.8.2. 확장성: 추상 클래스와 상속을 통해 새로운 기능을 쉽게 추가할 수 있다.
9.8.3. 유지보수성: 의존성 주입과 느슨한 결합을 통해 코드 수정이 용이하다.
9.8.4. 재사용성: 공통 기능을 추상화하고 상속을 통해 재사용한다.
10.8.6. 테스트 용이성: 의존성 주입을 통해 단위 테스트가 용이해진다.
- 이러한 접근 방식은 크게 클린 아키텍처(Clean Architecture) 또는 계층형 아키텍처(Layered Architecture)의 원칙을 따르고 있다고 볼 수 있다.
'Study > fastapi' 카테고리의 다른 글
fastapi와 sqlalchemy admin인 sqladmin을 이용해 celery beat을 admin으로 관리하기 (0) | 2025.03.22 |
---|---|
sqlalchey admin을 사용하여 관리자 페이지 만들기 (0) | 2025.03.18 |
fastapi - G6 간단하게 훓어보기 - 2) API 호출 (0) | 2025.02.21 |
depends() 함수를 정의하는 방법들 (0) | 2025.02.19 |
fastapi - G6 간단하게 훓어보기 - 1) 설치 (0) | 2025.02.19 |
댓글