본문 바로가기

더 나은 엔지니어가 되기 위해/지금은 안쓰는 자바

[부스트코스 웹 프로그래밍] 스프링 MVC

부스트코스 웹 프로그래밍 BE 영상을 보며 공부한 것을 간단히 정리한다.

1. Spring MVC

Spring MVC 란

  • Spring 프레임워크에서 제공하는 웹 모듈이다.
  • MVC 는 Model-View-Controller 의 약자로, 기본 시스템 모듈을 MVC 로 나누어 구현되어있다.
    • Model 은 '데이터' 디자인을 담당한다.
      • ex. 상품 목록, 주문 내역 등
    • View 는 '실제로 렌더링되어 보이는 페이지' 를 담당한다.
      • ex. .JSP 파일들이 여기에 해당된다.
    • Controller 는 사용자의 요청을 받고, 응답을 주는 로직을 담당한다.
      • ex. GET 등의 uri 매핑이 여기에 해당된다.
  • Spring MVC 모듈을 사용하여, 백엔드 프로그래밍의 기본 프레임워크를 잡는다.
    • Web 서버에 특화되어 만들어진 모듈이라, 개발자가 해야할 영역을 더 적게 만들어준다.
    • 즉 기존에 Spring 보다 더 깔끔하고 간편하게 개발 가능.
  • 별다른 말이 없으면 MVC Model 2 아키텍처를 사용하는 것으로 생각한다.

출처 : 부스트코스( https://www.edwith.org/boostcourse-web/lecture/16762/ )

기본 동작 흐름

출처 : 부스트코스(https://www.edwith.org/boostcourse-web/lecture/16762/)

전체 흐름은 다음과 같다.

요청 -> 프론트 컨트롤러 -> 핸들러 매핑 -> 핸들러 어댑터 -> 컨트롤러 -> 로직 수행(서비스) -> 컨트롤러 -> 뷰 리졸버 -> 응답(jsp, html)
  • 컨트롤러 중에서도, 맨 앞단에서 유저의 유청을 받는 컨트롤러를 프론트 컨트롤러라고 한다.
    • DispatcherServlet 객체가 이 역할을 한다.
    • 본격적으로 로직에 들어오기 전에, 요청에 대한 선처리 작업을 수행한다.
    • ex. 지역 정보 결정, 멀티파트 파일 업로드 처리 등
  • 프론트 컨트롤러는 요청을 핸들러 매핑을 통해 해당 요청을 어떤 핸들러가 처리해야하는지를 매핑한다.
    • HandlerMapping 객체가 핸들러 매핑에 대한 정보를 담고있다.
  • 이렇게 매핑된 핸들러를 실제로 실행하는 역할은 핸들러 어댑터가 담당한다.
    • HandlerAdapter 객체가 이 역할을 한다.
  • 컨트롤러는 해당 요청을 처리하는 로직을 담고있다.
    • 보통 요청의 종류 혹은 로직의 분류에 따라 내부적으로 Service 단위로 나누어 모듈화 한다.
    • 각 서비스에서는 DB 접근할 수 있는 Repository 객체를 이용하여 데이터에 접근할 수 있다.
  • 컨트롤러는 서비스에서의 로직 처리 후, 결과를 뷰 리졸버를 거쳐 뷰 파일을 렌더링하여 내보낸다.
    • ViewResolver 객체가 이 역할을 한다.

2. Spring MVC 구현

1) DispatcherServlet 을 프론트 컨트롤러로 세팅

<!-- web.xml -->

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
  <display-name>Spring JavaConfig Sample</display-name>

  <servlet>
    <!-- 2. 해당 서블릿의 구현체는 DispatcherServlet 로 정의-->
    <servlet-name>mvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 3. contextClass는 AnnotationConfigWebApplicationContext 를 사용-->
    <init-param>
      <param-name>contextClass</param-name>
      <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </init-param>
    <!-- 4. context 에 대해 따로 설정해둔 클래스의 위치를 파라미터로 줌.
    여기서는 사용자가 정의한 WebMvcContextConfiguration 을 사용-->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>kr.or.connect.mvcexam.config.WebMvcContextConfiguration</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <!-- 1. / 로 들어오는 요청은 mvc 라는 이름의 servlet 이 처리-->
    <servlet-name>mvc</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>
// WebMvcContextConfiguration.java

package org.example.guestbook.config;

...

// 설정 파일임을 Spring이 알게함.
@Configuration 
// Web에 필요한 빈들을 대부분 자동으로 설정. 주로 아래처럼 커스텀으로 설정해야할 때, WebMvcConfigurerAdapter 를 상속받아 클래스로 구현
@EnableWebMvc
// 해당 패키지에 정의된 클래스중 컴포넌트들을 빈으로 등록해놓음.
// @Controller, @Service, @Repository, @Component 가 달린 객체를 찾음. 
@ComponentScan(basePackages = { "kr.or.connect.mvcexam.controller" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {

    // 자바 파일이 아닌, Resource 파일들에 대한 url 요청이 왔을 경우, 해당 경로에서 찾을 수 있게 설정
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

        registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/META-INF/resources/webjars/").setCachePeriod(31556926);
        registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926);
        registry.addResourceHandler("/img/**").addResourceLocations("/img/").setCachePeriod(31556926);
        registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);
      }

    // default servlet handler를 사용하게 함
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    // / 로 접근시 main 템플릿(jsp) 로 가게함.
    @Override
    public void addViewControllers(final ViewControllerRegistry registry) {
        System.out.println("addViewControllers가 호출됩니다. ");
        registry.addViewController("/").setViewName("main");
    }

    // 렌더링되는 view 파일들의 경로와 확장자명 설정
    @Bean
    public InternalResourceViewResolver getInternalResourceViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
}

2) 이 외 전반적인 모듈 구성

config/*

  • 각종 설정 클래스 파일들을 담고 있음.
  • 클래스 앞에 @Configuration 이 붙음.
  • WebMvc 설정 관련 Config 를 제외하고 나머지는 모듈화한 뒤 Application 에서 모두 import
  • @Controller 관련 빈들은 WebMvcContextConfiguration 에서 @ComponentScan 으로 찾아줘야하고,
    @Service, @Service, @Component 관련 빈들은 ApplicationConfig 에서 찾아줘야 함.
  • 예를 들어 ApplicationConfig 는 다음과 같음.
@Configuration
@ComponentScan(basePackages = {"org.example.guestbook.dao", "org.example.guestbook.service"})
@Import({DBConfig.class})
public class ApplicationConfig {
}

controller/*

  • 각종 컨트롤러 클래스 파일들을 담고 있음.
  • 클래스 앞에 @Controller 가 붙음.
  • 각 컨트롤러 코드는 URI 매핑을 담당.
  • Serivce 인스턴스를 가져와 로직을 실행하고, View 단에 나가기 전후 작업을 담당
  • 예를 들어 GuestbookController 는 다음과 같음.
@Controller
public class GuestbookController {
    // @Autowired 를 통해 스프링이 관리하는 빈을 가져올 수 있음.
    @Autowired
    GuestbookService guestbookService;

    @GetMapping(path="/list")
    public String list(@RequestParam(name="start", required = false, defaultValue = "0") int start, ModelMap model) {

        // start로 시작하는 방명록 목록 구하기
        List<Guestbook> list = guestbookService.getGuestbooks(start);

        ...

        model.addAttribute("list", list);

        // list.jsp 로 렌더링
       return "list";
    }

    @PostMapping(path="/write")
    public String write(@ModelAttribute Guestbook guestbook,
                        HttpServletRequest request) {
        String clientIp = request.getRemoteAddr();
        guestbookService.addGuestbook(guestbook, clientIp);
        return "redirect:list";
    }
}

dao/*

  • DB 에 대해 접근할 때 사용하는 클래스 파일들을 담고 있음.
  • 클래스 앞에 @Repository 가 붙음.
  • 실제 dao 클래스와, 사용할 SQL 만을 담고있는 클래스가 따로 모듈화해서 사용함.
  • 예를 들어 GuestbookDaoGuestbookDaoSqls 는 다음과 같이 생음.
// GuestbookDao.java

import static org.example.guestbook.dao.GuestbookDaoSqls.*;

@Repository
public class GuestbookDao {
    private NamedParameterJdbcTemplate jdbc;
    private SimpleJdbcInsert insertAction;
    private RowMapper<Guestbook> rowMapper = BeanPropertyRowMapper.newInstance(Guestbook.class);

    public GuestbookDao(DataSource dataSource) {
        this.jdbc = new NamedParameterJdbcTemplate(dataSource);
        this.insertAction = new SimpleJdbcInsert(dataSource)
                .withTableName("guestbook")
                .usingGeneratedKeyColumns("id");
    }

    public List<Guestbook> selectAll(Integer start, Integer limit) {
        ...
    }

    public Long insert(Guestbook guestbook) {
        ...
    }

    public int deleteById(Long id) {
        ...
    }

    ...
}
// GuestbookDaoSqls.java

public class GuestbookDaoSqls {
    public static final String SELECT_PAGING = "SELECT id, name, content, regdate FROM guestbook ORDER BY id DESC limit :start, :limit";
    public static final String DELETE_BY_ID = "DELETE FROM guestbook WHERE id = :id";
    public static final String SELECT_COUNT = "SELECT count(*) FROM guestbook";
}

dto/*

  • 데이터를 모델링한 클래스 파일들을 담고있음.
  • 필드(프로퍼티)와 Getter, Setter 를 가짐.
  • 예를 들어 Guestbook 는 다음과 같이 같음.
public class Guestbook {
    private Long id;
    private String name;
    private String content;
    private Date regdate;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }
    ...
}

service/*

  • 서비스 로직을 담는 클래스 파일들을 담고있음.
  • 클래스 앞에 @Service 가 붙음.
  • Interface로 핵심 로직 먼저 정의한 후, 클래스로 구현.
  • 필요한 경우, Dao 를 직접 사용하는 클래스임.
  • 예를 들어 GuestbookServiceImpl 는 다음과 같음
@Service
public class GuestbookServiceImpl implements GuestbookService {
    @Autowired
    GuestbookDao guestbookDao;

    @Autowired
    LogDao logDao;

    @Override
    @Transactional // 기본 값 read only transaction 적용
    public List<Guestbook> getGuestbooks(Integer start) {
        return guestbookDao.selectAll(start, GuestbookService.LIMIT);
    }

    @Override
    @Transactional(readOnly = false)
    public int deleteGuestbook(Long id, String ip) {
        int deleteCount = guestbookDao.deleteById(id);
        Log log = new Log();
        log.setIp(ip);
        log.setMethod("delete");
        log.setRegdate(new Date());
        logDao.insert(log);
        return deleteCount;
    }

    ...
}

webapp/WEB-INF/views/*

  • 렌더링 되는 뷰 관련 파일(.jsp)들을 담고있음.
  • 이 파일들은 뷰 리졸버를 거쳐, 최종적으로 렌더링 되기 직전의 파일임.

3. 레이어드(Layered) 아키텍처

여러 모듈들을 일종의 레이어 단위로 나누는 설계 방식을 말한다.

크게 다음 레이어들로 구성한다.

  • Presentation Layer
    • 화면 조작 또는 사용자 입력을 처리하는 레이어
  • Service Layer (Domain)
    • 비즈니스와 관련된 도메인 로직을 처리하는 레이어
    • 하나의 비즈니스 로직은 하나의 트랜잭션 단위로 동작.
  • Repository Layer (Data source)
    • 도메인에서 필요로 하는 데이터를 조작하기 위한 레이어

출처 :  https://anchormen.nl/blog/big-data-services/spring-boot-tutorial/