본문 바로가기
프로그래밍/Spring Boot

SpringBoot에서의 MVC 패턴

by 카프카뮈 2021. 2. 15.

이 글은 다음의 고민에서 시작한 글이다.

  • 내가 스프링 부트로 지금까지 만든 프로젝트들은 MVC 패턴을 잘 따르고 있나?
  • 각각의 계층은 어떤 역할을 수행하고, 어떻게 영향을 주고받아야 하나?

스프링 부트를 이용해 다양한 프로젝트를 만들어 보면서, MVC 패턴을 사용했다.

MVC 패턴이 어떤 것인지 더 자세히 알고 싶다면, 위키피디아 링크 역시 참고하길 바란다.

 

모델-뷰-컨트롤러 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 모델, 뷰, 컨트롤러의 관계를 묘사하는 간단한 다이어그램. 웹 애플리케이션에서 일반적인 MVC 구성요소 다이어그램 모델-뷰-컨트롤러(Model–View–Controller, MVC)

ko.wikipedia.org

MVC 패턴의 구조는 Model, View, Controller 세 가지로 나누어진다.

  • 모델은 어떠한 동작을 수행하는 코드를 이야기한다. 우리가 무엇을 할지를 정하고, 비즈니스 로직과 DB 등에 대한 처리를 수행한다.
  • 컨트롤러는 뷰와 모델을 이어주는 역할을 맡는다. 뷰의 요청에 따라 모델의 상태를 바꾸고, 이를 다시 뷰에 전달한다.
  • 뷰는 사용자에게 보이는 영역이다. 컨트롤러를 통해 모델에 질의를 보내고, 그 값을 사용자에게 적절하게 보여준다.

나는 Spring Web MVC가 아닌 Spring Boot를 통해 프로젝트를 진행하고 있다.

그래서 서블릿 등에 대해서는 언급하지 않고,

일단 컨트롤러/서비스/DTO/Repository/Domain 라는 5가지 요소에 대해 설명하며

MVC 구조를 내가 어떻게 사용하는지 이야기해보려고 한다.

 

다음 설명은 이동욱 님의 저서 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 를 참조하여 작성하였음을 미리 알려드린다.

스프링의 웹 계층

위의 이미지에서 보듯, 스프링에는 5가지 요소가 존재한다.

이에 대한 설명은 다음과 같다.

  1. Web Layer
    • 컨트롤러(@Controller)가 대표적이고, 이외에도 필터(@filter), 인터셉터, 컨트롤러 어드바이스 등이 포함된다.
    • 외부 요청과 응답에 대한 전반적인 영역을 의미한다.
  2. Service Layer
    • 말 그대로 서비스(@Service)이다.
    • 일반적으로 컨트롤러와 저장소(Repository, Dao)의 중간에 위치한다.
    • 트랜잭션(@Transactional)과 도메인 간의 연산 순서를 보장해 준다.
  3. Repository Layer
    • DB와 같은 데이터 저장소에 접근하는 영역이다.
    • JPA를 사용한다면 @Repository를 생각하면 된다.
  4. DTOs
    • DTO(Data Transfer Object)는 계층 간의 데이터 교환을 위한 객체를 이야기한다.
  5. Domain model
    • 개발 대상, 즉 도메인을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화한 것을 도메인 모델이라고 한다.
    • 비즈니스 로직을 처리하는 영역이다.
    • JPA를 사용한다면, @Entity가 사용되는 영역 역시 도메인 영역이라고 생각할 수 있다.

이제 위의 MVC 구조로 돌아가 보자. 나는 이렇게 MVC를 구현하고 있다.

  • 뷰 : 프론트엔드에서 담당한다. 요청을 보내고, 응답을 받아 이를 보여준다.
  • 컨트롤러 : 컨트롤러에서 담당한다. 들어온 요청에 따라 서비스의 적절한 연산을 수행하고, 결괏값을 응답으로 가공하여 보내준다. 또한 인터셉터, 예외 핸들러, 컨트롤러 어드바이스 등 통신 과정에 영향을 주는 다른 클래스들을 포함한다.
  • 모델 : 도메인 모델이 비즈니스 로직과 상태를 가지고 있는다. 서비스는 이러한 도메인 모델의 생성과 연산을 적절히 수행하며, 그 과정에서 트랜잭션을 보장한다.
  • 기타 : DB의 경우 외부와 통신을 해야 하고, Clean Architecture에서도 컨트롤러 바깥의 레이어로 둔다. 다만 Spring Data JPA를 쓰고 있으므로, Repository Layer에서의 연산 수행에 집중하고 통신 부분에 대해서는 추후 더 고민해보려고 한다.

이를 예시를 통해 한번 따라가 보자.

 

일단 임의의 뷰가 있다고 가정하자.

이 뷰는 책에 대한 웹 페이지로, 도서의 목록을 보여주고 있다.

만약 사용자가 접속하면, 컨트롤러에 /api/books라는 GET 요청을 보낸다.

(그리고 이에 대해 컨트롤러는 책의 목록을 반환해 줘야 한다)

또한 응답을 받는다면, 뷰는 이를 목록으로 만들어 사용자에게 보여준다.

 

이제 컨트롤러로 가보자.

@GetMapping("/api/books")
public ResponseEntity<List<BookResponse>> list() {
    return ResponseEntity.ok()
            .body(bookService.list());
}

컨트롤러는 뷰와 모델을 연결해준다.

먼저 들어온 요청을 받고, 이에 대해 올바른 응답을 돌려준다.

그리고 그 과정에서, 필요한 정보를 서비스에 요청한다.

 

이제 서비스를 한 번 보자.

@Transactional(readOnly = true)
public List<BookResponse> list() {
    return bookRepository.findAll()
            .stream()
            .map(BookResponse::new)
            .collect(Collectors.toList());
}

서비스는 비즈니스 로직을 처리하는 곳이 아니다.

서비스는 도메인 모델의 비즈니스 로직을 순서대로 호출해 주고,

또한 트랜잭션을 보장하면서 Repository Layer를 통해 DB와의 연산을 수행하는 곳이다.

위의 코드의 경우 조회를 수행하는 곳이기에 비즈니스 로직이 특별히 나오지는 않았다.

(여기서 비즈니스 로직은, 책의 수정이나 대출 처리 등을 예시로 생각하고 기재했다)

 

그렇다면 도메인은 어떨까? 대략 이런 형태를 띌 것이다.

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String content;
    private Boolean isLend
    
    public void loanBook() { ... }
}

도서 대출과 같은 비즈니스 로직 처리를 하고,

한편으로는 필요한 상태들을 저장한다.

이를 통해 도메인 모델을 명확히 하는 것이다.

물론 컨트롤러 등과 통신 시에는 DTO로 변환하여 보낸다.

 

Repository의 경우, JPA를 쓰고 있어서...그냥 인터페이스니까 따로 예시 코드를 넣지는 않았다.

다만 Repository Interface가 되었건 Dao가 되었건, 도메인 모델과 DB를 이어준다는 점만 주목하면 된다.

 


많이 부족한 설명이고, 중간중간 내 사견이 섞여있다.

스스로 정리하는 데에 의미를 둔 글이니 어쩔 수 없나 싶다.

다음에는 MVC 모델을 구현하며 가진 고민에 대해 포스팅하고자 한다.

 

잘못된 내용이 있다면 지적 부탁드립니다.

반응형

댓글