본문 바로가기
Spring/개념지식 및 에러사항

Servlet vs Spring MVC

by 태옹 2021. 12. 27.

내가 분명 올해 초에 스프링 공부를 하면서 MVC부분에 대해 이론 강의를 듣고 정리를 한 기억이 있다. 

https://github.com/ty990520/springFramework/blob/main/05.md

 

GitHub - ty990520/springFramework: spring프레임워크를 공부한 내용을 정리합니다.

spring프레임워크를 공부한 내용을 정리합니다. Contribute to ty990520/springFramework development by creating an account on GitHub.

github.com

그렇지만 참 이론지식이라는 건 휘발성인 것 같다... 사실 그 당시에도 JSP? 서블릿? MVC? 아무것도 모르는 상황에서 처음보는 용어들과 개념들이 와르르 쏟아지니 당최 이해가 되지 않았다.

그래서 이번 게시물은 해당 개념들을 정리하고 실습을 통해 이해하는 시간을 가져보겠다!

 

(첨부된 코드는 스파르타코딩클럽의 [Spring 심화반] 교재를 활용하였습니다.)

 


 

 

1. HTTP request, response 처리 간편

 

먼저 컨트롤러를 사용하지 않고, 서블릿으로만 구현한다면 코드는 아래의 형식과 같다.

@WebServlet(urlPatterns = "/api/search")
public class ItemSearchServlet extends HttpServlet {
	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
		String query = request.getParameter("query");
			
			// ...

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        String itemDtoListJson = objectMapper.writeValueAsString(itemDtoList);
        out.print(itemDtoListJson);
        out.flush();
      }
}

@WebServlet 부분으로 서블릿만으로 구현된 코드라는 것을 확인할 수 있고,

doGet메소드에 HttpServletRequest 객체와 HttpServletResponse 객체가 인자로 필요한 것을 알 수 있다.

서블릿으로 구현하게 되면 request와  response처리를 위해 매번 위의 코드를 작성해주어야 하는데, 이는 다른 메소드를 작성하는 경우에도 똑같은 코드들을 중복으로 작성해서 request와 response를 처리해야하는 불편함을 감수해야 한다.

 

 

컨트롤러는 이 중복 코드를 생략할 수 있다는 큰 장점을 보여준다.

컨트롤러를 구현하는 것은 단순히 @Controller 어노테이션을 사용하는 것으로 가능하다.

@Controller
public class ItemSearchController {
	@GetMapping("/api/search")
	@ResponseBody
	public List<ItemDto> getItems(@RequestParam String query) throws IOException {
			
			// ...

			return itemDtoList;
	}
}

 

결과적으로 보면, 서블릿은 없어진 게 아니라 스프링이 서블릿의 기능을 해주고 있는 것이다.

 

HTTP request를 @RequestParam이 query라는 문자열 하나로 처리해주고, 

HTTP response를 @ResponseBody로 List<ItemDto>타입의 객체를 통해 응답 메시지를 처리해준다.

(객체 그대로 리턴해주면 스프링이 알아서 json으로 변환해주는 과정, contentType을 설정하는 과정, print, flush 등의 여러 중복되는 과정들을 해줌)

 

그래서 스프링이 대신 서블릿의 기능을 수행한다는 것이다.

 


 

2. API 이름마다 파일을 만들 필요 없음

 

만약 사용자를 관리하는 API를 만들 때, 로그인 / 로그아웃 / 회원가입 이라는 세 기능이 있다고 하자.

 

서블릿으로만 구현을 해야한다면, UserLogin.java, UserLogout.java, UserSingUp.java라는 세 개의 파일이 존재해야 한다. 결국 사용자 관리에 대한 API만 해도 여러 파일이 존재하기 때문에 그만큼 관리가 어려워질 수 있다.

 

서블릿으로 구현한 회원가입 페이지는 아래의 형식처럼 사용할 수 있다.

@WebServlet(urlPatterns = "/user/signup")
public class UserSignUpServlet extends HttpServlet {
	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) {
		// ... 
	}

	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response) {
		// ... 
	}
}

이런 코드가 쓰여진 파일이 '사용자 관리' 안에서도 여러 개로 존재한다는 의미!

 

그리고 코드를 보면 UserSignUpServlet클래스는 HttpServlet클래스를 상속받았기 때문에 doGet메소드와 doPost메소드를 오버라이딩해서 사용한다. 결국 메소드명은 변경할 수 없다는 것이다. 이 이유 때문에 url path를 다르게 해야하고, 그래서 파일명도 다르게 해야하기 때문에 분리가 필요하다.

 

 

컨트롤러는 API 마다 파일을 만들 필요가 없다는 장점이 있다. (당연히 메소드명도 변경할 수 있음)

'사용자 관리' 안에 포함된 유사한 성격의 API들을 하나의 컨트롤러로 관리할 수 있어 더욱 간편하다.

 

컨트롤러로 구현한 코드는 아래와 같다.

@Controller
public class UserController {
        @GetMapping("/user/login")
        public String login() {
        	// ...
        }

        @GetMapping("/user/logout")
        public String logout() {
        	// ...
        }

        @GetMapping("/user/signup")
        public String signup() { 
        	// ... 
        }

        @PostMapping("/user/signup")
        public String registerUser(SignupRequestDto requestDto) {
        	// ... 
        }
}

 와! 간단해!

 


 

만약 서블릿을 사용해서 상품 검색 기능을 구현한다면

더보기
@WebServlet(urlPatterns = "/api/search")
public class ItemSearchServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
	// 1. API Request 의 파라미터 값에서 검색어 추출 -> query 변수
        String query = request.getParameter("query");

	// 2. 네이버 쇼핑 API 호출에 필요한 Header, Body 정리
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Naver-Client-Id", "...");
        headers.add("X-Naver-Client-Secret", "...");
        String body = "";
        HttpEntity<String> requestEntity = new HttpEntity<>(body, headers);

	// 3. 네이버 쇼핑 API 호출 결과 -> naverApiResponseJson (JSON 형태)
        ResponseEntity<String> responseEntity = rest.exchange("https://openapi.naver.com/v1/search/shop.json?query=" + query, HttpMethod.GET, requestEntity, String.class);
        String naverApiResponseJson = responseEntity.getBody();

	// 4. naverApiResponseJson (JSON 형태) -> itemDtoList (자바 객체 형태)
	// - naverApiResponseJson 에서 우리가 사용할 데이터만 추출 -> List<ItemDto> 객체로 변환
        ObjectMapper objectMapper = new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        JsonNode itemsNode = objectMapper.readTree(naverApiResponseJson).get("items");
        List<ItemDto> itemDtoList = objectMapper
                .readerFor(new TypeReference<List<ItemDto>>() {})
                .readValue(itemsNode);

	// 5. API Response 보내기
	// 5.1) response 의 header 설정
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
	// 5.2) response 의 body 설정
        PrintWriter out = response.getWriter();
	// - itemDtoList (자바 객체 형태) -> itemDtoListJson (JSON 형태)
        String itemDtoListJson = objectMapper.writeValueAsString(itemDtoList);
        out.print(itemDtoListJson);
        out.flush();
    }
}

네이버 API를 사용한 코드이기 때문에 네이버에서 규정한 형식대로 코드를 작성해준다. 

서블릿은 request메시지와 response메시지에 대해 따로 객체를 생성했기 때문에, response메시지의 헤더와 바디를 딱딱 정해서 넣어주어야 하고, 그 때 일일이 코드로 처리해야하는 점이 불편하다.

 

위의 기능을 컨트롤러로 구현한다면

더보기
@Controller
public class ItemSearchController {

    // Controller 가 자동으로 해주는 일
    // 1. API Request 의 파라미터 값에서 검색어 추출 -> query 변수
    // 5. API Response 보내기
    // 5.1) response 의 header 설정
    // 5.2) response 의 body 설정
    @GetMapping("/api/search")
    @ResponseBody
    public List<ItemDto> getItems(@RequestParam String query) throws IOException {
	// 2. 네이버 쇼핑 API 호출에 필요한 Header, Body 정리
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Naver-Client-Id", "...");
        headers.add("X-Naver-Client-Secret", "...");
        String body = "";
        HttpEntity<String> requestEntity = new HttpEntity<>(body, headers);

	// 3. 네이버 쇼핑 API 호출 결과 -> naverApiResponseJson (JSON 형태)
        ResponseEntity<String> responseEntity = rest.exchange("https://openapi.naver.com/v1/search/shop.json?query=" + query, HttpMethod.GET, requestEntity, String.class);
        String naverApiResponseJson = responseEntity.getBody();

	// 4. naverApiResponseJson (JSON 형태) -> itemDtoList (자바 객체 형태)
	// - naverApiResponseJson 에서 우리가 사용할 데이터만 추출 -> List<ItemDto> 객체로 변환
        ObjectMapper objectMapper = new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        JsonNode itemsNode = objectMapper.readTree(naverApiResponseJson).get("items");
        List<ItemDto> itemDtoList = objectMapper
                .readerFor(new TypeReference<List<ItemDto>>() {
                })
                .readValue(itemsNode);

        return itemDtoList;
    }
}

복잡해보이는 이유는 그냥 네이버 API 사용해서 그럼... 서블릿 코드보다 훨씬 간단해진 것을 확인할 수 있다.

 

'Spring > 개념지식 및 에러사항' 카테고리의 다른 글

[MyBatis] 동적 SQL  (0) 2021.08.08
[Spring] JPA CRUD  (0) 2021.07.13
[Spring] JPA 기초  (0) 2021.07.12
RestController 생성하기  (0) 2021.07.05
Log4j import에러  (0) 2021.07.02

댓글