본문 바로가기
개발/Java

Repository 계층, 도메인과 영속성 엔티티 사이의 간극 메꾸기

by kadokok 2023. 5. 21.

안녕하세요. 이번 글에서는 미션 동안 제가 느낀 도메인과 엔티티의 좁혀지지 않는 간극, 그리고 이를 어떻게 대처하려 했는지에 대해 다뤄보려고 합니다.

 

먼저, 제가 사용한 도메인과 엔티티의 용어 정리를 하고 갈 필요가 있을 것 같습니다.

  • 도메인: 실제 비즈니스 로직을 정의하는 영역
  • 엔티티: DB 테이블의 정보와 1:1 매핑되는 영속성 엔티티

저는 도메인과 엔티티를 위와 같이 분리하여 생각했습니다.

 

도메인 객체는 흔히 말하는 객체지향 세상 속에서 살아가는 친구들이었고, 엔티티 객체는 DB 테이블의 컬럼 값들을 필드로 가지는, 일종의 DTO 같은 친구들이었죠.

 문제점 

여기서 문제는, 객체지향 세상과 RDBMS 세상의 패러다임이 불일치한다는 점이었고, 이는 곧 도메인 객체와 엔티티 객체 사이의 엄청난 괴리감으로 이어졌습니다. 그 예로 아래의 코드를 참고해 볼까요?

public class Section {

    private final Station upStation;
    private final Station downStation;
    private final Long distance;

    ...
}
public class SectionEntity {

    private final Long sectionId;
    private final Long lineId;
    private final Long upStationId;
    private final Long downStationId;
    private final Long distance;
		
    ...
}

Section 은 도메인, SectionEntity 는 엔티티 객체입니다.

 

도메인과 엔티티 모두 Section 이라는 공통된 의미를 표현하려 하지만, 그냥 생김새부터가 많이 다른 모습입니다.

엔티티 객체는 RDBMS 세상을 대변하는 친구로서, 그 내부 필드에는 객체가 아닌 id 값을 가진다는 점, 그리고 SectionEntityLine 이라는 또 다른 의미에 대해 알고 있다는 점들이 도메인과는 상당히 다른 점이죠.

 

도메인과 엔티티 사이의 걷잡을 수 없는 간극 때문이었을까요, 실제 도메인 로직을 다루는 서비스 계층에서의 코드는 굉장히 혼란해졌는데요. 아래의 코드를 같이 보시죠.

@Transactional
public void insertSection(final SectionCreateRequest request) {
    LineEntity lineEntity = lineService.findLineByName(request.getLineName());

    StationEntity stationAEntity = stationService.findStationByName(request.getUpStation());
    StationEntity stationBEntity = stationService.findStationByName(request.getDownStation());
    Station stationA = new Station(stationAEntity.getName());
    Station stationB = new Station(stationBEntity.getName());

    Section requestSection = new Section(stationA, stationB, request.getDistance());
    SectionEntity requestSectionEntity = new SectionEntity(null, lineEntity.getLineId(), stationAEntity.getStationId(), stationBEntity.getStationId(), request.getDistance());

    List<Section> foundSections = sectionDao.findSectionsByLineId(lineEntity.getLineId())
            .stream()
            .map(sectionEntity -> {
                StationEntity stationEntityA = stationService.findStationEntityById(sectionEntity.getUpStationId());
                Station upStation = new Station(stationEntityA.getName());
                StationEntity stationEntityB = stationService.findStationEntityById(sectionEntity.getDownStationId());
                Station downStation = new Station(stationEntityB.getName());
                return new Section(upStation, downStation, sectionEntity.getDistance());
            })
            .collect(Collectors.toList());

    Sections sections = new Sections(foundSections); // 드디어 Sections 도메인 완성

    ...

    // 도메인 로직
		
    ...

    // 도메인 영속화 로직
}

위 코드의 로직 흐름을 이해하려 하지 않으셔도 됩니다.

 

제가 강조하고 싶었던 부분은, 단지 Sections 라는 핵심 도메인 객체를 얻기 위해, DB에서 필요한 값들을 조회하고, 이를 엔티티로 받아와서 상당히 많은 변환 로직을 거쳐야 한다는 점입니다.

또한, Sections 도메인 객체를 DB로부터 얻었다 하더라도, 여러 도메인 로직을 거쳐 바뀐 Sections 객체에 대해서 DB에 영속화를 하는 과정 또한 상당한 엔티티 변환 로직을 피할 수 없습니다.

 

일단 바로 위 상황을 통해, 서비스 계층의 insertSection() 메서드의 문제점을 파악해 볼 수 있을 것 같습니다.

가장 큰 문제점은, insertSection 이라는 행위를 하기 위해 필요한 부차적인 로직들이 너무 많습니다.

DB 조회를 통해 필요한 도메인 객체를 조립하고, 이를 다시 DB에 영속화하는 작업으로부터 도메인-엔티티 변환 로직이 많이 발생한다는 점이죠.

 

이런 문제점 때문에, 정작 주목받아야 할 insertSection 행위가 묻혀버리는, 일종의 주객전도 현상이 벌어지게 된 것입니다.

 어떻게 해결할까? 

제가 풀어야 할 문제점은 분명했습니다. 서비스 계층의 행위들이 도메인-엔티티 변환 로직으로부터 자유로워지는 것이죠. 그리고 서비스 계층에서 엔티티 레벨까지 내려가지 않고, 도메인 레벨 수준의 객체만을 취급하고 싶었습니다.

 

이를 해결하기 위해선, 단순 메서드 분리만으로는 불가능해 보였습니다. 애초에 서비스 계층에서 엔티티를 쫓아내고 싶었던 것이기 때문에, 계층적인 구조 분리가 필요한 상태였죠.

그렇다면, ‘도메인을 DB로부터 불러오거나, DB에 영속화하는 계층을 새롭게 두면 어떨까?’ 하는 생각이 들었습니다. 이렇게 되면, 서비스 계층에서는 더 이상 DB에 관련된 로직에 관심을 가지지 않아도 되니까요.

 

그래서 저는 Repository 라는 계층을 새롭게 두기로 결정했습니다. 위치는 서비스 계층과 DB 계층 사이입니다. 그림으로 표현하자면 아래와 같습니다.

 

이러한 기존의 계층 구조에서

이렇게 바뀌게 된 것이죠.

 

새롭게 추가된 Repository 계층의 책임은, DB 조회를 통해 도메인 객체를 조립하여 반환하거나, 도메인 객체를 DB에 영속화할 책임이 있습니다.

 

Repository 계층 도입 후의 서비스 코드는 아래와 같이 바뀌게 되었습니다.

@Transactional
public void insertSection(final SectionCreateRequest request) {
    final Long requestLineNumber = request.getLineNumber();
    final Sections sections = sectionRepository.findByLineNumber(requestLineNumber);

    final Station requestUpStation = new Station(request.getUpStation());
    final Station requestDownStation = new Station(request.getDownStation());
    final Section requestSection = new Section(requestUpStation, requestDownStation, request.getDistance());
    sections.addSection(requestSection);

    sectionRepository.updateByLineNumber(sections, requestLineNumber);
}

어떤가요? 제가 보기엔 코드가 훨씬 간결해진 모습입니다.

 

서비스 계층에서는 더 이상 DAO, 엔티티 객체를 의존하지 않습니다. 이는 모두 Repository 계층이 처리하고 있죠.

따라서, 받아온 엔티티를 어떻게 도메인으로 변환할지에 대한 관심사는 더 이상 서비스 계층의 관심사가 아닙니다. 서비스 계층은 그저 Repository 계층에게 어떤 도메인이 필요한지 요청만 보내주면 되는 것이죠.

 

또한, 이제 서비스 계층에서는 도메인 객체에만 의존하다 보니, insertSection 행위를 하기 위한 도메인 로직에 집중할 수 있습니다.

만약 필요한 도메인 로직을 모두 실행했다면, Repository 계층에 도메인을 넘겨서 DB에 영속화를 요청하기만 하면 되죠. 굉장히 간결해졌습니다.

 정리 

패러다임 불일치 문제로부터 발생한 도메인과 엔티티 사이의 간극 문제는, 코드를 필요 이상으로 비대하게 만든다는 문제점이 있었습니다.

다만 이러한 간극은, 적절한 계층 구조 분리를 통해 메꿀 수 있었습니다. 바로 Repository 계층을 통해서 말이죠.

 

기존의 서비스 계층이 가지고 있던 DB 접근 책임을 Repository 계층으로 위임한 것인데요.

이러한 덕분에 서비스 로직은 서비스 로직대로 집중할 수 있고, Repository 계층에서는 적절한 도메인-엔티티 변환을 통해 DB로부터 도메인을 조립하거나 영속화시키는 로직에만 집중할 수 있죠.

 

이런 경험을 통해, 계층 구조 분리의 중요성을 다시 한번 느꼈던 것 같습니다. 적절하게 분리된 계층들은 코드를 간결하게 만들어주는 효과를 만들어냅니다.

댓글