※ 일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.
Repository 패턴이란?
Repository패턴이란 영속화를 은폐하기 위한 디자인 패턴으로, DAO(DataAccessObject)패턴과 비슷하지만, 보다 높은 추상도로 엔티티의 조작에서 영속화 스토리지를 완전히 은폐한다.
예를 들어 DB커넥션이나 스토리지의 패스등은 Repository의 인터페이스에서 은폐되어 Repository의 유저는 영속화 스토리지가 무엇인가(예를 들어 MySQL이나 Redis등)를 의식하지 않고 데이터 저장이나 검색의 조작을 할 수 있게 된다.
이로 인해 Repository를 이용한 로직은 업무적 조작에 집중할 수 있고, 데이터 베이스의 이동 등의 영속화층의 변경이 발생했을 때에 로직에 영향을 주지 않도록 할 수 있게 된다.
// 예) 유저의 영속화, 참고를 하기 위한 리포지토리(구현은 생략)
public interface UserRepository {
User findBy(Long userId);
User store(User user);
}
// Repository를 이용하는 클래스
public class FooService {
private UserRepository userRepository;
public FooService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void registerUser(String userName) {
// Repository를 이용하여 User를 저장하지만, 영속화의 방법에 좌우되지 않는다.
User user = new User(userName);
userRepository.store(user);
}
}
매우 편리하고 사용하기 쉬운 패턴이지만, 실제로는 다양한 사용법으로 인해 이상한 버그의 온상이 되어버리는 경우가 많으므로, 이번 포스팅에서는 그러한 것들은 안티패턴으로 정리해봤다.
Repository 구현시에 있어서 흔한 안티 패턴
기능이나 롤(역할)로 Repository를 나누는 것
하나의 엔티티에 대해서, 기능이나 역할에 대해 여러 개의 Repository를 만들어버리고 마는 패턴이다. 예를 들어, 심사제로 글을 투고할 수 있는 시스템에서, 일반 유저에 의한 글의 갱신이나 참고 조작은 PostRepository, 글의 심사나 공개에 관련된 조작은 PostScreeningRepository이라는 기능 단위로 Repository를 나눠버리고 마는 것이다.
문제점
이 패턴으로는 어떤 Repository를 사용하면 좋을지 알기 어렵고, 비즈니스상의 행동에 대한 로직이 여러 개의 Repository르 흩어져버려, 데이터나 로직의 정합성이 무너질 위험이 있다.
예를 들어, 심사가 통과되지 않으면 공개되지 않는 비즈니스 요건이 있는 시스템에서 글의 심사 OK/NG시의 상태 변화 처리를 PostScreeningRepository에서 구현해놨는데, 다른 사람이 PostScreeningRepository를 경유하지 않고 무리하게 공개 상태로 갱신하여 PostRepository에 저장해버린 경우, 비지니스 요건상 올바르지 않은 데이터가 되어 버린다.
대책
상태 갱신에 관련된 행동은 Entiry나 Service에서 구현한다. 또한, 상태의 조작에 관련된 처리는 가능한 가시성을 제어하여 무리하게 변경이 되지 않도록 한다.
Repository는 단순히 스토리지에 액세스하는 수단으로 이용하고, 복잡한 비즈니스상의 로직이 되어버리지 않게 조심하자.
이번 케이스에 적용한다면, 예를 들면 PostScreeningService만을 다른 패키지에 공개하고, PostScreeningServiece의 심사 메소드를 통해서만 공개 상태로 변경되도록 하거나, Post엔티티에 공개 메소드를 준비해, 심사 NG의 경우에 공개하도록 하면 예외를 발생하도록 하는 등, 비즈니스 룰을 완전히 제어하도록 하는 것이다.
자식 테이블에대해 많은 Repository를 만드는 것
자주 볼 수 있는 패턴이다. 예를 들어, "유저"라는 엔티티가 "주소", "연락처"이라는 요소를 가지고 있고, 이것들이 유저의 자식 테이블에 표현되어 있을 때에 UserRepository, ContactRepository, AddressRepository와 같이 리포지토리를 만들어버리고 마는 경우이다.
문제점
예를 들어, 아래와 같이 비즈니스 요건의 경우를 생각해보자.
- "연락처"의 종류에는 "집", "휴대폰"이 있고, "유저"는 최소 둘 중에 하나의 연락처를 갖고 있을 필요가 있다.
ContactRepository를 만들어버리면, 위의 비즈니스 요건의 체크로직을 Repository가 가져야 하거나 체크 처리를 우회하여 연락처를 저장하고 삭제할 수 있는 오류를 만드는 원인이 된다.
대책
이러한 문제를 예방하기 위해서 DDD 집약이라는 테크닉이 매우 도움이 된다. 집약은 어떤 Entity와 관련된 오브젝트를 한개의 덩어리로 다루, 집약의 갱신은 반드시 집약 루트을 경유해야만 하도록 하여, 집약내에서의 데이터 정합성을 담보하는 테크닉이다.
예를 들어 "차" , "타이어"이라는 Entity가 있어, 차에는 반드시 타이어가 4개 있어야한다는 제약이 있는 경우, 차를 집약 루트로하고 이것을 집약로 다룬다.
타이어를 교환하고 싶을 때에는 집약 루트로 어떤 차의 change wheel 메소드를 사용하는 것으로, 차에 반드시 4개의 타이어가 있는 것을 보증한다. 집약 루트 안의 오브젝트를 직접 갱신하는 것은 안되고, 이용하는 쪽에서는 반드시 차 리포지토리를 통해서 차의 집약을 획득해, changeWheel를 사용해 타이어 교환을 하여, 다시 리포지토리를 사용하여 갱신된 차의 집약을 용속화 스토리지에 저장한다.
문제의 케이스에서는 "유저"를 집약 루트로서 "연락처", "주소"를 포함한 집약을 정의하여, 유저를 통해서 연락처의 갱신을 하는 것으로 데이터의 정합성을 보증할 수 있다.
예를 들어, 주소만 원하는 경우에도 반드시 집약을 경유해서 데이터를 획득하지만, 쿼리문이 복잡하거나 데이터의 양이 많은 경우는 다음의 패턴을 고려해보길 바란다.
복잡한 쿼리를 Repository에 힘들게 발행하는 것
이것도 자주 발생하는 패턴이다. 대략 어떤 ORM에서 해도 힘들다.
문제점
자주 있는 발생하는 경우는 다음과 같은 비즈니스 요건을 만족하도록 하는 경우이다.
- 회원 상태가 "탈퇴"가 아닌, 등록일이 과거 3개월 이내로, 최근 1개월 이내에 10건 이상의 상품을 구입한 유저
아마 유저의 ID를 키로 구입 이력 테이블을 조인하여, 구입일로 범위를 좁힌 건수를 count하는 워리를 ORM에서 어떻게든 구현하려고 할 것이다.
이러한 쿼리는 대개 프로그램적으로도 유지보수하기도 어려운 것이 되며, 더욱이 유저 목록 화면 등에서 여러 건 실행하면 퍼포먼스상으로도 문제가 일으키는 경우도 발생한다.
대책
상황이나 요구되는 퍼포먼스에 따라 몇 가지 대책이 존재한다.
1. 리포지토리에 분할하여 우직하게 여러 번 쿼리를 날리는 방법
하나의 리포지포리에서 힘들게 하려고 하지말고, 유저 리포지토리에서 "탈퇴가 아닌" + "3개월 이내에 등록"한 유저를 획득하여, 그 유저들의 ID를 키로 구입이력 리포지토리에서 "과거 1개월 이내"에 구입한 건수를 구해, 어플리케이션 레이어에서 이러한 정보를 결합한다. 액세스가 적은 서비스라면 이러한 방법에서도 문제없을 것이다.
2. CQS(Command Query Separation)을 적용한 쿼리를 잘라내는 방법
CQRS의 이야기를 하면 주제와의 연관성에서 조금 멀어지므로 여기서는 일단 Command와 Query의 분리에 대해서만 이야기하도록 하겠다.
CQS이란 Bertrean Meyer가 제안한 원리로, 모든 메소드는 부작용을 발생시키는 Command나 값을 리턴하는 Query로 분리할 수 있다는 것이다.
집약에 대한 조작은 반드시 Repository를 통해서 한다고 이야기 했지만, 이것은 변경(Command)조작을 실행할 때의 원리가 된다.
한편 복잡한 쿼리가 필요한 경우는 대체로 문의(Query)조작시에만 해당하며, 이것들은 주로 어플리케이션 고유의 요구에서 오는 경우가 많다(지금의 예에서 빗대어 표현하면 "사용자 목록 화면에서 최근 구입한 상품의 수를 표시하고 싶다"등에 해당된다).
여기서 이러한 쿼리를 Repository에 구현하는 것이아닌 직접 어플리케이션 레이터의 서비스에서 쿼리 빌드를 하여 직접 쿼리를 발행한다.
jOOQ나 ScalikeJDBC와 같은 타입 세이프 쿼리 빌더를 사용하도 좋고, 직접 jdbc를 사용하거나 MyBatis와 같은 SQL을 쓸 수 있는 ORM을 사용해도 좋다.
어플리케이션 고유의 복잡한 처리는 QueryService, 엔티티의 영속화나 획득은 Repository와 같은 느낌으로 책무를 분리함으로써 불필요하게 Repository가 복잡해지는 것을 피할 수 있다.
마무리
Repository는 용속화층의 추상으로써 매우 편리한 개념이지만, 사용법이 조금이라도 달라지면 매우 복잡한 로직이 되어버린다. DDD의 집약이나 CQS에는 이러한 문제를 회피하기 위해 매우 유익한 테크닉이 있으므로, Repository패턴을 사용할 때에 가능한 합병해서 구현하는 것을 추천한다.
참고자료
'IT > WEB' 카테고리의 다른 글
클린 아키텍처2 (0) | 2023.02.15 |
---|---|
클린 아키텍처1 (0) | 2023.02.13 |
SPA, SSR, SSG는 무엇인가? (0) | 2023.01.24 |
MVC 모델 (0) | 2023.01.22 |
Node.js를 사용하는 이유 (3) | 2022.10.06 |