Search
🌍

3_3_8.1.1.1. title: 인터페이스 계층은 외부에서 내부에 접근하는 방법, 내부에서 외부에 접근하는 방법을 관장한다. Repository와 Gateway는 접근 방법을 분리하는 개념이고, DTO와 Mapper은 데이터 변환을 분리하는 개념이다. ORM을 활용하면 DTO와 Mapper의 복잡성을 최소화할 수 있다.

생성
prev summary
🚀 prev note
next summary
♻️ next note
관련 임시노트
9 more properties
클린 아키텍처에서 인터페이스 계층은 외부 세계와 내부 비즈니스 로직 사이의 경계를 담당한다. 이 계층은 Repository와 Gateway를 통해 외부의 다양한 형태의 데이터와 요청을 내부에서 이해할 수 있는 형태로 변환하거나 내부의 처리 결과를 외부에서 사용할 수 있는 형태로 변환하는 번역자 역할을 수행한다(ref1). 실제로 Repository는 데이터에 접근하면서 동시에 DB 레코드를 도메인 객체로 변환하는 역할을 한다(ref3:DB 레코드와 도메인 객체의 불일치는 실제로 자주 일어나는 일이다).
Controller(ref2)은 이 글에서 다루지 않는다.
인터페이스 레이어에서 Mapper와 DTO라는 개념이 등장하기도 하는데, 개인적으로(python 개발에 익숙한 사람이라면) 이 부분이 가장 와닿지 않고 헷갈릴 수 있다고 생각한다. 이때 이론적으로 가장 완성된 상태를 이해한 뒤 어떤 개념을 실제 구현 시 생략해도 괜찮을지 보는 것이 가장 이해하기 쉽다.
사용자의 id로 사용자를 찾아내는 User Repository의 메서드를 구현한다고 해 보자. 이론상 엄격함을 준수한다면 Repository는 아래 코드와 같이 DB에서 읽은 raw 데이터를 DTO로 변환한 다음, 그것을 Mapper를 이용해서 도메인 객체로 변환한다.
# 어디엔가 정의된 DTO class UserDTO(...): id = ... class SqlUserRepository(UserRepositoryInterface): def find_by_id(self, user_id: int) -> Optional[User]: # 1. 데이터베이스 접근 raw_data = self.db.execute(query, (user_id,)).fetchone() # 2. Raw 데이터를 DTO로 변환 user_dto = UserDTO( id=raw_data[0], username=raw_data[1], email=raw_data[2], # ... ) # 3. Mapper를 통해 DTO를 도메인 객체로 변환 return self.mapper.dto_to_domain(user_dto)
Python
복사
하지만 이런 방식은 굉장히 복잡하다. 만약 ORM을 사용한다면 ORM 자체가 이미 DB 레코드를 객체로 매핑해주는 역할을 하기 때문에, DTO와 역할이 크게 겹친다. 이로 인해 불필요한 보일러플레이트 코드가 생성되고, 개발 생산성이 떨어질 수 있다. ORM 모델을 DTO 역할로도 활용하면 개발 생산성을 높일 수 있다. sqlalchemy와 같은 ORM 라이브러리는 이런 상황에 대비하여 inspect와 같은 함수를 제공한다.
from sqlalchemy import inspect # SQLAlchemy ORM 모델 class UserModel(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) username = Column(String) email = Column(String) full_name = Column(String) created_at = Column(DateTime) class SqlUserRepository(UserRepositoryInterface): def find_by_id(self, user_id: int) -> Optional[User]: user_model = self.session.query(UserModel).filter_by(id=user_id).first() if not user_model: return None # SQLAlchemy inspect를 이용한 동적 변환 mapper = inspect(UserModel) user_data = {} for column in mapper.columns: user_data[column.name] = getattr(user_model, column.name) return User(**user_data)
Python
복사
하지만 inspect()를 사용한 동적 변환은 다음과 같은 제약이 있다:
1.
속성명이 정확히 일치해야 함: ORM 모델과 도메인 객체의 속성명이 다르면 사용하기 어렵다
2.
타입 변환이 필요한 경우: 예를 들어 DB의 JSON 컬럼을 파이썬 객체로 변환해야 하는 경우
3.
복합 객체 구성: 여러 테이블의 조인 결과를 하나의 도메인 객체로 만들어야 하는 경우
이런 상황 중 간단히 해결이 가능한 상황이라면 아래와 같이 직접 생성을 하는 것도 방법이다.
# SQLAlchemy ORM 모델 class UserModel(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) username = Column(String) email = Column(String) full_name = Column(String) created_at = Column(DateTime) class SqlUserRepository(UserRepositoryInterface): def find_by_id(self, user_id: int) -> Optional[User]: user_model = self.session.query(UserModel).filter_by(id=user_id).first() if not user_model: return None # 직접 변환 (간단한 경우) return User( id=user_model.id, username=user_model.username, email=user_model.email, full_name=user_model.full_name, created_at=user_model.created_at )
Python
복사
여기서 조금만 더 복잡해지더라도 명시적인 Mapper를 사용하는 것이 더 적절하다. Mapper에서 ORM 모델과 도메인 타입 사이의 변환을 담당하도록 분리한다.
# 복잡한 변환이 필요한 경우의 Mapper class UserMapper: """ORM 모델을 도메인 객체로 변환""" @staticmethod def orm_to_domain(user_model: UserModel) -> User: return User( id=user_model.id, username=user_model.username, email=user_model.email, full_name=user_model.full_name, created_at=user_model.created_at, # 복잡한 변환 로직 profile=json.loads(user_model.profile_json) if user_model.profile_json else None, status=UserStatus.from_string(user_model.status_code) ) @staticmethod def domain_to_orm(user: User) -> UserModel: return UserModel( id=user.id, username=user.username, email=user.email, full_name=user.full_name, created_at=user.created_at, profile_json=json.dumps(user.profile) if user.profile else None, status_code=user.status.to_string() ) class SqlUserRepository(UserRepositoryInterface): def find_by_id(self, user_id: int) -> Optional[User]: user_model = self.session.query(UserModel).filter_by(id=user_id).first() if not user_model: return None return UserMapper.orm_to_domain(user_model)
Python
복사
이렇게 하면 간단한 경우에는 inspect()로 생산성을 높이고, 복잡한 변환이 필요할 때 점진적으로 복잡성을 추가해 가는 개발이 가능하다.
parse me : 언젠가 이 글에 쓰이면 좋을 것 같은 재료을 보관해 두는 영역입니다.
1.
None
from : 과거의 어떤 원자적 생각이 이 생각을 만들었는지 연결하고 설명합니다.
supplementary : 어떤 새로운 생각이 이 문서에 작성된 생각을 뒷받침하는지 연결합니다.
1.
None
opposite : 어떤 새로운 생각이 이 문서에 작성된 생각과 대조되는지 연결합니다.
1.
None
to : 이 문서에 작성된 생각이 어떤 생각으로 발전되거나 이어지는지를 작성하는 영역입니다.
ref : 생각에 참고한 자료입니다.