본문 바로가기
프로그래밍/기타

[기타] DDD(domain driven development)의 계층 구조(layered architecture)에 대해서 알아보자

by 사바라다 2021. 4. 24.

 

안녕하세요. 이전 우리는 [spring + 객체 지향 원칙] Spring에서의 의존성 역전의 원칙(Dependency Inversion Principle) 포스팅에서 Layered Architecture에 대해서 간단히 확인해보았습니다.

Layered Architecture는 코드의 아키텍처를 구성할 때 주로 사용되며 일반적으로 3 계층 또는 4 계층으로 나누어 사용합니다. 3 계층으로 나눌때는 표현계층(Presentation Layer) - 서비스 계층 (Business Layer) - 영속성 계층(Persistence Layer) 으로 나누어 사용하곤 합니다. DDD를 이용할 때도 이에 대응되는 계층적인 구조가 있는데요. 오늘 배워볼 내용은 DDD에서의 계층의 분화에 대한 부분입니다.

각 계층별 설명

개략적으로 보자면 DDD를 구성할 때 계층 구조는 아래의 이미지 처럼 잡습니다. 위에 있는 계층에서는 아래 있는 계층에 접근이 가능하지만 아래에서 위로는 불가능 한것을 기본으로 하고 있습니다.

아래에서 각 계층에 대한 설명과 예제를 함께 보도록 하겠습니다. 예제는 Spring을 기준으로 하며 게임을 얻어오는 로직입니다.

사용자 인터페이스 (표현 계층)

사용자의 요청에 대해 해석하고 응답하는 일을 책임지는 계층입니다. 만약 시스템이 B2B 시스템이라고 한다면 사용자는 유저가 아닌 다른 시스템일 수 있습니다. 아래 코드는 일반적으로 개발할 때 많이 사용하는 API 호출에 대한 응답을 나타내고 있는 코드입니다. 코드를 보시면 해당 클래스는 요청을 받고 응답을 리턴할 뿐이며 실제 비즈니스 로직은 GameService로 위임하고 있는것을 확인할 수 있습니다.

@RestController
@RequestMapping("/api/v1/games")
public class GameController {

    private final GameService gameService;

    public GameControllerV1(GameService gameService) {
        this.gameService = gameService;
    }

    @GetMapping("/main")
    public ResponseEntity<ApiResponse<GameDetailResponse>> getGameDetail() {
        return ApiResponse.success(gameService.getGameDetailResponse(gameId));
    }
}

응용 계층

소프트웨어가 비즈니스 로직을 정의하고 정상적으로 수행될 수 있도록 도메인 계층과 인프라스트럭쳐 계층을 연결해주는 역할을 하는 계층입니다. 이 계층은 많은 정보를 가지고 있지 않게 유지하는 것이 중요합니다. 실질적인 데이터의 상태 변화 등의 처리는 도메인 계층에서 진행할 수 있도록 위임하는것이 중요합니다. 응용 계층에서 진행하는 일은 일반적으로 transaction 관리, DTO 변환, 그리고 모듈간의 연계 이렇게 3가지를 들 수 있습니다.

아래의 코드를 보도록 하겠습니다. 아래의 getGameDetailReseponse 메서드를 보시면 일단 @Transactional로 하나의 비지니스 로직으로 트랜잭션을 묶은 것을 알 수 있습니다. 또한로직을 보면 gameRepository라는 인트라스트럭쳐 계층을 통해 도메인 정보를 얻어옵니다. 그리고 찾지 못한다면 Exception 처리를하며 마지막으로 표현계층으로 전달 하기 위해 DTO 변환을 진행하고 있습니다.

@Service
public class GameService {

    private final GameRepository gameRepository;

    public GameService(GameRepository gameRepository) {
        this.gameRepository = gameRepository;
    }

    @Transactional(readonly = true)
    public GameDetailResponse getGameDetailResponse(Long gameId) {

        GameDetailDTO gameDetailDTO = gameRepository.getGameDetailDTO(gameId)
                .orElseThrow(() -> new PikaRuntimeException(ErrorStatus.NOT_FOUND_GAME_ERROR));

        return new GameDetailResponse(gameDetailDTO);
    }
}

도메인 계층 (모델 계층)

비즈니스 규칙, 정보에 대한 실질적인 도메인에 대한 정보를 가지고 있으며 이 모든것을 책임지는 계층입니다. 이 계층에서는 업무 상황을 반영하여 상태를 제어하는 역할에 집중하는 계층입니다. 이 계층이 업무용 소프트웨어의 핵심입니다.

아래 코드를 보도록 하겠습니다. 클래스를 보시면 엔티티 클래스 하나입니다. 도메인이라는 것은 비즈니스를 구성하는 영역이라고 볼 수 있습니다. 따라서 그 안에는 이처럼 Entity와 이를 다른 Entity와 연결해주기 위한 Service 등이 존재 할 수 있습니다. 아래 클래스는 하나의 Entity이지만 게임 Aggregation의 Root라고 봐주시면 됩니다. 이런 용어들은 추후에 한번더 이론적인 내용에 대해서 추가설명을 할 때 다시 제대로 알려드리도록 하겠습니다.

도메인 계층의 핵심은 업무 상황을 반영하여 상태를 제어하는 역할에 집중한다고 말씀드렸습니다. 아래의 코드에서는update 메서드가 바로 그 역할을 하고 있습니다. Game의 데이터를 변경하는 것은 응용 계층에서 진행하는 것이 아닌 도메인 계층에서 진행함으로써 계층간의 책임의 분리 및 코드의 간결성을 유지할 수 있습니다.

@Entity
@Getter
@NoArgsConstructor
public class Game {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "name")
    private String name;

    @OneToMany(mappedBy = "game", cascade = CascadeType.ALL)
    private List<Screenshot> screenshots = new ArrayList<>();

    @Builder
    public Game(Long id, String name, List<Screenshot> screenshots) {

        this.id = id;
        this.name = name;

        if (!CollectionUtils.isEmpty(screenshots)) {
            this.screenshots = screenshots;
            this.screenshots.forEach(screenshot -> screenshot.setGame(this));
        }
    }

    public void update(String name) {

        if (StringUtils.isEmpty(name)) {
         throw new IllegalArgumentException("게임 이름이 비어있습니다.");
        }

        this.name = name;
    }
}

인프라스트럭처 계층

상위 계층을 지원하는 일반화된 기술적 기능을 제공하는 계층입니다. 일반적으로 외부 시스템을 호출한다거나 하는 역할을 담당합니다. 해당 계층에서 얻어온 정보를 응용계층 또는 도메인 계층에 전달하는 것을 주 역할로 담당합니다.

아래의 코드를 보도록 하겠습니다. 아래의 코드는 spring data JPA를 사용하여 데이터를 가져오는 interface 선언을 한 부분입니다. 이 코드를 통해서 DB를 접근하여 조건에 맞는 데이터를 객체로 가져올 수 있습니다.

public interface GameRepositoryV1 extends JpaRepository<Game, Long>, GameRepositoryV1Custom {
}

public interface GameRepositoryV1Custom {
    Optional<GameDetailDTO> getGameDetailDTO(Long gameId);
}

마무리

오늘은 이렇게 layered architecture에 대해 알아보는 시간을 가져보았습니다.

다음시간에도 여러분들과 저에게 모두 도움이 되는 내용으로 찾아뵙도록 하겠습니다.

감사합니다.

참조

도메인 주도 설계: 소프트웨어의 복잡성을 다루는 지혜 (에릭 에반스)

dzone_layered-architecture-is-good

댓글