본 포스팅은 우아한테크코스 레벨2 웹 자동차 경주 미션을 진행하며 고민했던 내용 중 일부를 정리한 것입니다. 개인의 주관이 들어간 글인지라 틀린 내용이 있을 수 있음을 미리 말씀드립니다.
처음에는 단순하게 Repository가 Dao들을 사용해서 기능을 수행하면 된다고만 생각했다. 아래는 미션에서 구현했던 웹 기반 자동차 경주 프로그램의 Repository 클래스이다. 코드를 살펴보면 Repository는 자동차 경주 게임을 진행한 결과 정보를 담은 Dto를 넘겨받아 저장하는 save 기능만을 수행한다.
@Repository
public class WebRacingCarRepository implements RacingCarRepository {
private final RacingGameDao racingGameDao;
private final CarDao carDao;
public WebRacingCarRepository(RacingGameDao racingGameDao, CarDao carDao) {
this.racingGameDao = racingGameDao;
this.carDao = carDao;
}
@Override
public void save(RacingGameResultDto racingGameResultDto) {
int gameId = racingGameDao.save(racingGameResultDto.getRound());
carDao.save(gameId, racingGameResultDto.getCarDtos());
}
}
해당 코드를 구현하면서 Repository 자체가 왜 존재해야 하는가에 대해서는 생각해보지는 않았다. 이 부분에 대해 Repository와 Dao를 어떤 기준으로 분리를 진행했는지 리뷰어 범블비의 질문이 있었다. 다시 코드를 살펴보니 Repository는 뚜렷한 책임을 지지 않고 Dao의 기능만을 수행하는 객체가 되어버렸다.
왜 이렇게 되었는지에 대해서 생각하기 이전에 Repository와 Dao의 차이에 대해서 학습을 진행했다. 그리고 정리한 Dao와 Repository의 차이는 다음과 같았다.
- Dao는 Data Access Object라는 이름을 가지는 만큼 DB에 접근하여 데이터를 저장하고 조회하는 기능을 수행한다.
- DB에 날릴 쿼리를 작성하고 실제로 쿼리를 날려서 작업을 수행하는 것등의 역할을 수행한다.
- 일반적으로 DB 테이블 하나 당 하나의 Dao가 1:1 맵핑되어 해당 테이블에 대한 DB 작업을 Dao가 수행한다.
- Repository는 Service 레이어에서 비즈니스 로직을 수행하는데 필요한 데이터(도메인)를 찾아서 반환하는 작업을 수행한다.
- 여기에서 데이터를 찾는 작업은 반드시 DB를 통해서 이뤄져야 하는 것은 아니라는 것을 이번 학습을 통해 알게 되었다.
- Repository는 Dao를 이용해서 DB에 접근해 Service 레이어가 필요로 하는 데이터를 넘겨줄 수 있지만 그 외의 방법으로도 데이터를 저장하거나 조회할 수 있습니다.
- 또한 하나의 반환 데이터, 즉 도메인 객체를 반환하기 위해 여러 Dao를 사용할 수 있다.
- 중요한 것은 Repository가 Service에서 사용하는 데이터(도메인 객체)를 어디에서 어떻게 가져오고 저장하는지는 캡슐화가 되어야 한다는 것이다.
- 결과적으로 Repository에서 Service로 데이터를 반환할 때에는 비즈니스 로직을 바로 수행할 수 있는 도메인 객체가 생성되어 반환되어야 한다.
- 이러한 차이 이전에 Repository와 Dao 모두 계층형 아키텍처에서 Persistence Layer에 속한다는 것을 알게 되었다.
- Persistence 즉 영속성은 데이터들이 영구저장될 수 있는 특징을 갖고 있는지를 나타내는 성질이다.
여기까지 학습하고 나니 Repository의 개념 자체를 Persistence 레이어와 혼동하고 있었다는 것을 알게 되었다.
- 그래서 repository 내부에 Dao를 위치시키고 내부에서 기능을 수행하도록 구현했던 것이다. 또한 이번 미션 1단계에서 도메인 로직은 굉장히 간단했고 데이터 저장 기능만을 수행하면 되었다. 그러다 보니 Dao와 Repository가 수행하는 기능의 차이가 거의 없어지게 되었다.
- 이런 경우엔 굳이 repository를 두지 않고 바로 Dao를 통해 데이터를 저장하도록 할 수 있다.
하지만 2단계 미션 요구사항들을 살펴보니 이력조회 기능을 수행해야 할 때에는 두 개 이상의 Dao를 이용하여 데이터를 조회해야 할 일이 생길 것으로 예상되었다.
- 이런 경우에는 repository 객체를 두어 여러 Dao를 통해 Service 계층이 원하는 데이터를 완성시켜 넘겨줄 수 있는 역할을 수행하게 할 수 있을 것이라는 생각이 들었다.
이러한 관점에서 다시 코드를 리팩토링하게 되었고 정리된 부분은 다음과 같았다.
- Service 계층과 Repository 계층 간에는 Dto가 아니라 Domain 객체 혹은 데이터를 주고 받고록 구현한다.
- Repository는 Dao에게 필요한 값을 꺼내서 넣어주는 책임을 수행하도록 구현한다.
@Repository
public class WebRacingCarRepository implements RacingCarRepository {
private final RacingGameDao racingGameDao;
private final CarDao carDao;
public WebRacingCarRepository(RacingGameDao racingGameDao, CarDao carDao) {
this.racingGameDao = racingGameDao;
this.carDao = carDao;
}
@Override
public void saveRacingGame(RacingGame racingGame) {
RacingGameEntity racingGameEntity = EntityMapper.toRacingGameEntity(racingGame);
int gameId = racingGameDao.save(racingGameEntity);
List<CarEntity> carEntities = EntityMapper.toCarEntities(gameId, racingGame);
carDao.saveAll(carEntities);
}
@Override
public List<RacingGame> findAllEndedRacingGame() {
List<RacingGameEntity> endedRacingGameEntities = racingGameDao.findEndedRacingGameEntities();
List<CarEntity> endedCarEntities = carDao.findEndedCars();
List<RacingGame> racingGames = new ArrayList<>();
for (RacingGameEntity endedRacingGameEntity : endedRacingGameEntities) {
List<Car> cars = getCars(endedRacingGameEntity, endedCarEntities);
racingGames.add(new RacingGame(new Cars(cars), endedRacingGameEntity.getCount()));
}
return racingGames;
}
private List<Car> getCars(RacingGameEntity racingGameEntity, List<CarEntity> carEntities) {
return carEntities.stream()
.filter(carEntity -> carEntity.getGameId() == racingGameEntity.getId())
.map(carEntity -> new Car(carEntity.getName(), carEntity.getPosition()))
.collect(Collectors.toList());
}
}
이 때 Repository와 Dao간에 데이터를 주고 받는 책임을 지는 Entity라는 객체를 둬서 데이터를 주고 받을 수 있게 했다. Entity는 Domain 보다는 DB 테이블에 더 가까운 개념의 객체로서 각 테이블의 데이터 Row 정보를 맵핑할 수 있도록 구현했다. 그래서 Entity는 테이블에 추가되는 auto generated key 등과 같이 기존 Domain 객체는 가질 수 없는 데이터 또한 상태값으로 가질 수 있다.
결과적으로 Dao를 통해 DB 데이터를 저장하거나 조회할 때 Entity를 사용하도록 할 수 있었다. 또한 Presentation 계층과 Service 계층간 데이터 전송에 사용되는 Response / Request 객체, 즉 Dto 와도 명시적으로 구분할 수 있었다.
다만 Entity라는 개념에 대해서는 도메인 객체를 지칭하는 용어로 쓰이는 경우도 있고, 앞에서 제시했던 대로 테이블과 1:1로 맵핑되는 개념으로서 사용되는 경우도 있다. Entity의 개념을 처음 제시한 Domain Driven Design의 저자 Eric Evans는 전자처럼 도메인 객체를 지칭하는 용어로 사용했다고 한다. 하지만 테이블과 1:1 맵핑되는 개념으로도 Entity라는 용어를 이미 많이 사용하고 있기에 이를 어떤 맥락에서 사용하는지를 꼭 확인해야 한다고 한다.
[DDD를 살짝 맛만 보고 정리한 나만의 Repository
Repository의 개념
- 2004년 Eric Evans가 Domain-Driven-Design에서 처음 소개한 개념.
- 도메인에서 비즈니스 로직을 수행할 때 필요한 데이터(객체)들을 담아둔 컬렉션의 역할을 수행한다.
- Repository 인터페이스는 도메인에 속하고 그 구현체는 Persistence Layer로 구분된다.
어떤 목적을 위해 사용하는가?
- 비즈니스 로직을 수행하는 도메인 모델(서비스) 계층과 영속성 계층의 구현 기술을 분리하기 위함이다.
- 도메인 모델이 비즈니스 로직을 수행하기 위해 DAO를 직접 접근하는 방식을 취하는 것은 데이터 영속성을 위한 구현체에 대한 의존성을 크게 가지는 것이다.
- 이 경우 영속성 구현 로직에 변화가 생기면 도메인에도 그 영향력이 미치게 된다.
- DAO를 한 번 추상화하여 도메인이 이를 의존하게 한다고 해도 그 결합도를 완전히 낮추는 데는 한계가 있다.
- 그래서 Repository 패턴을 사용해서 도메인과 영속성 계층의 구현 로직 의존성을 완전히 분리하는 것이다.
결과적으로 어떤 DB와 Query를 사용하든 어떤 내부 변화가 생기든, 그 영속성 계층 기능 구현에 종속적이지 않고 도메인에 더욱 집중할 수 있게 된다.
Reference
Entity는 도메인 객체를 말하는 것인가 DB의 테이블을 말하는 것인가 - https://prolog.techcourse.co.kr/studylogs/3196
'우아한테크코스 > 학습 정리' 카테고리의 다른 글
스프링의 예외처리 과정 및 처리 방법에 대하여 (1) (0) | 2023.05.12 |
---|---|
[Level 2] Layered Architecture에 대한 개인적인 고찰 (0) | 2023.04.27 |
[Level 2] 공식문서를 통한 프레임워크 학습 방법에 대한 고찰 (1) | 2023.04.16 |
[Level 1] 인터페이스와 추상클래스에 대한 개인적인 고찰 (0) | 2023.04.08 |
[Level 1] Dto 사용에 관한 개인적인 고찰 (3) | 2023.03.19 |