본문 바로가기

웹 개발/스프링

[스프링 부트 심화] 스프링 동작 원리

0. 스프링 학습 전략

스프링은 2004년 1.0 버전 출시 이후 20년간 진화한 프레임워크로서 '개발 편의성 증대'에 초점을 맞춰 발전해 왔습니다. 따라서 사용법은 쉬워져 왔으나 원리 파악이 어려운 단저을 가지고 있습니다. 방대한 양의 관련 기능과 옵션을 학습하기 위해서는 학습 전략을 설정하는 것이 중요합니다. 

 

- 작은 프로젝트에서 시작하기 : 사용법 위주의 경험, 프로젝트에 기능을 추가

- 사용법 위주로 학습하기 : 스프링 사용법을 먼저 학습, 적용한 후 원리를 학습하기. 단, 이해가 가는 부분까지만 학습하기

- 원리를 파악해야 하는 이유 : 현업 개발의 정확도와 신뢰도를 향상, 일반적이지 않은 어려운 요구사항을 해결할 때 필요

- 주제 별 학습 : 스프링은 모듈화가 잘 제공되어 있어 필요한 부분만 추가해서 사용가능. 예) 스프링 3계층, 시큐리티, 테스트, JPA, AOP...

 

스프링 부트 심화1 : 스프링 동작 원리 파악

스프링 부트 심화2 : 스프링 시큐리티를 이용해 회원가입, 로그인, 로그아웃 기능 구현

스프링 부트 심화3 : 테스트 코드 구현

스프링 부트 심화4 : JPA를 이용한 데이터베이스 관리

스프링 부트 심화5 : AOP

 

소프트웨어 개발 프로세스에 맞춰 구현 및 동작 원리 파악 : https://ko.wikipedia.org/wiki/%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4_%EA%B0%9C%EB%B0%9C_%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4

 

소프트웨어 개발 프로세스 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전.

ko.wikipedia.org

요구사항 분석 -> 설계 -> 구현 -> 테스트 -> 유지보수

 

- 요구사항 분석 : 기획자로부터 요구사항 전달 받기 > 기획자, 디자이너, 관계자와 협의

- 설계 : 프론트엔드 개발자와 협업을 위한 API 설계, DB설계

- 구현 : 스프링으로 서버 구현

- 테스트 : UI 연동 없이 테스트 (Advanced REST Client-ARC, POSTMAN), UI 연동 후 통합 테스트, 테스트 코드 작성

- 유지보수 : 운영 중 발생한 문제의 코드 수정, 새로운 기능 추가

 

 

1. 요구사항분석

- 상품 검색 기능 : 키워드 입력 시 상품이름, 최저가, 링크, 이미지 정보를 네이버 API로부터 가져옴

- 관심 상품 등록 기능 : DB에 상품 정보를 입력 title, link, image, lprice, myprice(->0원)

- 희망 최저가 설정 : 등록된 관심 상품의 myprice업데이트

- 관심 상품 조회 : DB를 조회, 희망하는 최저가보다 저렴한 경우 '최저가'표시, 매일 1시마다 최저가 업데이트

- API : 

 

프로젝트 셋팅 :

셋팅 > 에디터 > 일반 > 자동 가져오기 > 항상 

플러그인 : monokai pro theme 테마 추가 -> 설정 > 모양 및 동작 > 모양 > 테마 설정

H2 설정 : src/main/resources/application.properties : 

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:springcoredb

H2 접속 : http://localhost:8080/h2-console > jdbc:h2:mem:springcoredb

 

 

 

2. 스프링의 Controller와 Servlet을 비교해보기

Servlet(서블릿)은 자바를 사용하여 웹 페이지를 동적으로 생성하는 프로그램을 말합니다. 

 

웹 서블릿 구현 : @ServletComponentScan 어노테이션 추가해 주어야 합니다. 

 

 

@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", "zdqMoIkFaK8uKvC2oNY2");
        headers.add("X-Naver-Client-Secret", "LiZfsgtuD5");
        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();
    }
}

 

 

스프링 controller로 구현 : HTTP request, response 처리를 위해 매 번 작성해야 할 중복된 코드를 생략이 가능합니다.

위의 예시에서 처럼 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();
    }
}

controller에서는 서블릿의 중복된 코드를 Spring에서 대신 처리해 줍니다. 

@RequestParam을 통해 query를 가져 왔습니다. 

List<ItemDto> 객체를 반환해 주기만 하면 json 변환, ContentType 설정, out.print(json객체), out.flush()와 같은 작업을 대신 처리해 줍니다. 

@Controller
public class ItemSearchController {

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

 

또한 스프링 컨트롤러는 API 이름마다 파일을 만들어주지 않아도 됩니다. 만약 다음과 같은 API가 있을 때 

 

서블릿으로 구현해 보도록 하겠습니다.  HttpServlet을 상속받아 override를 해주기 때문에 doGet 함수를 매 번 재정의 해주어야 합니다. 같은 URL을 가지고 있다면 하나의 서블릿에서 정의가 가능하기 때문에 서블릿 파일은 총 3개가 됩니다.

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

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

}

 

반면 Controller 코드는 API 마다 파일을 만들 필요가 없습니다. 보통 유사한 성격의 API를 하나의 Controller에서 관리하게 됩니다. 아래 Controller에서 확인할 수 있듯이 회원 기능 관련 API를 한 곳에서 관리가 가능하며 함수 이름을 doGET이나 doPOST와 같은 메소드를 오버라이딩 해서 사용하는 것이 아닌 개별적인 함수 이름을 사용할 수 있습니다. 

@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) {
        // ... 
    }
}

 

 

3. 스프링 MVC

스프링에는 MVC 디자인 패턴이 있습니다. (Model -View - Controller)

 

MVC 패턴을 학습 하기에 앞서 서버가 페이지를 전달하는 과정에대해 먼저 살펴 보도록 하겠습니다.

웹 페이지에는 정적 웹 페이지와 동적 웹 페이지가 있습니다. 정적 웹 페이지의 경우 클라이언트가 서버에 요청을 보낼 때 서버가 가지고 있는 리소스를 그대로 반환하게 됩니다. 이 때 Controller는 클라이언트의 요청을 Model로 받아 리소스를 View에 전달하게 됩니다. 

 

동적 웹 페이지는 Template engine에 View, Model을 전달합니다. View에는 동적 HTML 파일을, Model에는 View에 적용할 정보를 포하하고 있습니다. Template engine은 View에 Model을 적용합니다. 이를 통해 동적 웹 페이지가 생성됩니다. 예를들어 사용자가 로그인 성공 시 로그인 된 사용자의 id를 추가한 페이지를 생성할 수 있습니다. 이러한 동적 웹 페이지를 생성하는 Template engine에는 타임리프(Thymeleaf), Groovy, FreeMarker, Jade, JSP 등이 있습니다. JSP 역시 서블릿과 함께 많이 사용되지만 스프링에서는 사용하지 않는 것이 좋습니다. 결과적으로 사용자에게 View를 전달하게 됩니다. 

 

타임리프를 이용해 스프링 MVC를 구현해 보도록 하겠습니다. 

 

 

 

4. HTTP 메시지

 

구현에 앞서 HTTP 메시지에대해 살펴 보도록 하겠습니다. 클라이언트와 서버 간 Request와 Reponse 사이에는 HTTP 메시지 규약이 있습니다.

https://developer.mozilla.org/ko/docs/Web/HTTP/Messages

 

HTTP 메시지 - HTTP | MDN

HTTP 메시지는 서버와 클라이언트 간에 데이터가 교환되는 방식입니다. 메시지 타입은 두 가지가 있습니다. 요청(request)은 클라이언트가 서버로 전달해서 서버의 액션이 일어나게끔 하는 메시지

developer.mozilla.org

이 규약에 따라 HTTP 메시지의 구조는 다음과 같습니다. 상태줄, 헤더, 바디입니다.

Request 시작줄 Start line은 상태줄이라 합니다. 시작줄에는 API 요청 내용이 들어갑니다. 예를들어 우리가 naver.com 을 브라우저에 치면 method, url, HTTP 버전이 시작줄에 들어갑니다. 

GET naver.com HTTP/1.1

Request 헤더는 요청과 관련된 메타 정보가 담겨 있습니다. 헤더에는 여러 가지가 있지만 Content-Type에 대해서만 살펴 보겠습니다. 헤더에 Content-type은 필수적으로 기입되어야 하는 것은 아니지만 다음과 같은 경우를 짚고 넘어가면 됩니다.

- HTML <form> 태그로 요청을 할 시 :

Content type: application/x-www-form-urlencoded

- AJAX 요청을 할 시 :

Content type: application/json

Request 바디에는 실제 서버에 전송할 데이터가 담깁니다. 메서드에 따라 다음과 같은 정보가 담기게 됩니다.

- GET 요청을 할 시 : (보통) 없음

- POST 요청을 할 시 : 사용자가 입력한 폼 데이터 또는 JSON 데이터

 

 

 

Response에 대해 살펴 보겠습니다. Response의 시작줄은 API 요청 결과 정보를 담고 있습니다. 상태 코드와 상태 텍스트를 볼 수 있습니다. 상태코드란 서버에서 클라이언트 요청을 어떻게 처리했는지에대한 결과를 나타냅니다. HTTP 404는 Not Found 에러로서 클라이언트가 요청한 url이 없음을 의미합니다.

HTTP/1.1 404 Not Found

Response 헤더 역시 여러가지가 있지만 Content-Type과 Locatoin에 대해서만 살펴 보도록 하겠습니다. 

- 본문 내용이 없는 경우 -> (없음)

- Response 본문 내용이 HTML인 경우

Content type: text/html

- Response 본문 내용이 JSON인 경우

Content type: application/json

- Location : Redirect 할 페이지 URL -> 클라이언트가 해당 주소를 받아 Redirect 합니다.

Location: http://localhost:8080/hello.html

마지막으로 Response Body를 살펴 보겠습니다. 이 역시도 Content-type과 맞물려 여러 데이터를 클라이언트에 전달할 수 있습니다.

- HTML 데이터를 응답할 경우

<!DOCTYPE html>
<html>
  <head><title>By @ResponseBody</title></head>
   <body>Hello, Spring 정적 웹 페이지!!</body>
</html>

- JSON 데이터를 응답할 경우

{ 
  "name":"홍길동",
  "age": 20
}

 

 

 

5. Response Body와 스프링 관계

이러한 HTTP 메시지의 구조와 타입을 바탕으로 스프링의 Controller는 HTTP Response에 따라 다음과 같은 관계를 갖고 있습니다. 

- 정적 웹 페이지를 불러오는 경우 : "http://localhost:8080/hello.html"과 같이 주소를 입력하면 resources/static에 위치한 hello.html 파일 을 불러옵니다. 

 

- 정적 웹 페이지를 Redirect 로 불러오는 경우 : controller로 설정된 url을 따라 hello.html 페이지를 redirect로 불러옵니다. 

브라우저에 "http://localhost:8080/hello/response/html/redirect"로 요청을 보냈지만 브라우저에서는 redirection이 되어 "http://localhost:8080/hello.html"와 같은 페이지를 보여줍니다. controller를 살펴보면 return에서 "redirect:/hello.html" 을 반환합니다. 이는 클라이언트에게 redirection을 요구하며 이때의 Location을 "/hello.html" 로 지정합니다. 

@Controller
@RequestMapping("/hello/response")
public class HelloResponseController {

    @GetMapping("/html/redirect")
    public String htmlFile() {
        return "redirect:/hello.html";
    }
}

 

redirection 과 관련된 정보는 개발자 도구의 네트워크에서 redirect 의 헤더를 살펴 보며 확인할 수 있습니다. 클라이언트는 host 주소 뒤에 /hello.html 을 붙여 요청을 하게 됩니다. 그래서 redirect 이후 hello.html을 받아오는 것을 확인할 수 있습니다.

이렇듯 서버 개발자는 redirect를 구현할 때 반환 시 "redirect:/[static 폴더 내 파일]"와 같이 작성해 주면 됩니다. 

 

- 정적 웹 페이지를 Template engine으로 View 전달하는 경우 : 

다음과 같은 컨트롤러에 "http://localhost:8080/hello/response/html/templates" 요청을 보내보도록 하겠습니다. 이 경우 템플릿 엔진에 View를 전달해주게 됩니다. 여기서 반환해 주고 있는 문자열 "hello"는 View name 이 됩니다. 타임리프의 기본 설정에는

prefix : "classpath:/templates/"

suffix : ".html"

가 있습니다. 따라서 "hello" 문자열을 반환하게 되면 타임리프가 "resources/templates/hello.html" 경로로 변환시켜 줍니다.  

@Controller
@RequestMapping("/hello/response")
public class HelloResponseController {
    @GetMapping("/html/templates")
    public String htmlTemplates() {
        return "hello";
    }
}

템플릿 엔진은 또한 @ResponseBody를 통해 HTML 데이터를 반환할 수 있습니다. 이는 View를 제공하는 것이 아닌 ResponseBody에 전달받은 내용을 클라이언트가 보여주게 됩니다. 사실 앞서 hello.html 파일을 넘겨주는 경우에도 파일에 들어 있는 텍스트 내용을 복사해 Response Body에 붙여서 사용자에게 보여지게 됩니다.

@Controller
@RequestMapping("/hello/response")
public class HelloResponseController {
    @GetMapping("/body/html")
    @ResponseBody
    public String helloStringHTML() {
        return "<!DOCTYPE html>" +
                "<html>" +
                "<head><meta charset=\"UTF-8\"><title>By @ResponseBody</title></head>" +
                "<body> Hello, 정적 웹 페이지!!</body>" +
                "</html>";
    }
}

위의 예시 처럼 구현을 할 시 불편함이 큰 탓에 위 내용들을 파일로 관리하게 됩니다. 중요한 점은 @ResponseBody 어노테이션을 작성할 시 View를 통과하지 않습니다. 즉, 템플릿 엔진으로 넘기지 않고 반환하는 데이터를 Response Body에 넣어줍니다. 

 

- 동적 웹 페이지 전달하기 :

이제 동적 웹 페이지를 전달하는 경우를 살펴 보도록 하겠습니다. 동적 웹 페이지에서는 Model이 등장합니다. model에는 "visits" 라는 이름으로 visitCount 값을 설정합니다. 이러한 model 정보와 함께 View 정보(View name)을 타임리프에서 넘겨 주게 되면 "resources/templates/hello-visit.html" 파일에 Model 정보에 따라 페이지가 구성됩니다.

@Controller
@RequestMapping("/hello/response")
public class HelloResponseController {

    private static long visitCount = 0;

    @GetMapping("/html/dynamic")
    public String helloHtmlFile(Model model) {
        visitCount++;
        model.addAttribute("visits", visitCount);
    // resources/templates/hello-visit.html
        return "hello-visit";
    }
}

hello-visit.html 파일의 내용을 보면 타임리프의 문법에 따라 동적으로 데이터가 적용됩니다. 앞서 모델의 "visits"에는 visitCount 정보가 담겨 있었습니다. 즉, visitCount 정보가 model에 담겨 타임리프 템플릿에의해 적용이 됩는 것입니다. 

<div>
  (방문자 수: <span th:text="${visits}"></span>)
</div>

th:text="${변수명}"은 타임리프의 문법입니다. model 정보를 생성할 때 변수명에 따른 데이터를 지정해 주어야 합니다. 클라이언트의 입장에서는 동적으로 입력된 과정과 문법을 알 수 없습니다.

 

- JSON 데이터를 내려주는 경우 : 

만약 서버에서 JSON 데이터와 같이 "{ \"name\" : \"BTS\", \"age\" : \"28\" }" 과 같은 문자열을 반환할 경우 클라이언트는 Content Type을 text/html 로 인식합니다. 따라서 Response Header에 추가적으로 데이터 처리를 해주어야 합니다. 

 

만약 위에서와 같이 JSON 데이터를 보내고자 한다면 String 외의 자바 객체를 활용하는 것이 좋습니다. 자바 객체인 Class로 생성된 데이터는 스프링 서버에서 반환 시 곧바로 application/json 타입으로 보내지게 됩니다. 

@Controller
@RequestMapping("/hello/response")
public class HelloResponseController {
 
    @GetMapping("/json/string")
    @ResponseBody
    public ResponseEntity<String> helloStringJson() {
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_TYPE, "application/json");
        return ResponseEntity.ok()
                .headers(headers)
                .body("{\"name\":\"BTS\",\"age\":28}");
    }
    
    @GetMapping("/json/class")
    @ResponseBody
    public Star helloJson() {
        return new Star("BTS", 28);
    }
}

 

즉, 정리하자면 컨트롤러에 ResponseBody가 없는 경우 보통 반환 타입은 String이며 반환값이 View Name인 경우 템플릿 엔진에 view를 전달해 줍니다. 타임리프를 기준으로 보면 "/templates/{View name}.html" 을 찾아 View를 가져 옵니다. 따라서 Response Header의 Content type은 text/html 이게 됩니다. 

 

그런데 위와 같이 Respponse Body가 없고 반환값이 String 임 경우에도 "redirect:/"가 있다면 View를 통하지 않고 redirect 요청을 하게 됩니다. redirect 되는 URL은 Http Response Location에 전달되어 html을 주는 것이 아닌  클라이언트가 재방문할 주소를 돌려주게 됩니다. 

 

Response Body가 있는 경우에 반환값이 String인 경우 무조건 Content Type이 text/html로 지정되어 내려줍니다. 만약 템플릿 엔진이 있을 경우 View가 있는지 확인한 후 내려주게 됩니다. 

 

Response Body에 String 외의 자바 객체가 반환 되는 경우 스프링이 json 객체로 변환을 해주며 Content-Type이 application/json으로 지정됩니다. 

 

 

 

6. 웹의 발전과 RestController의 등장

 

이렇게 복잡하다 느껴질 정도로 다양한 방식으로 데이터를 내려 줄 수 있게 된 것은 웹의 발전 역사와 관련되어 있습니다. 웹의 초창기에 대부분의 웹 페이지는 정적인 웹 페이지였습니다. 서버에서 가지고 있는 정보를 내려주기만 하면 되었습니다. 그러다가 웹 서비스의 요구사항이 늘어나고 동적으로 처리 해줄 수 있는 기술들이 발달하면서 주로 동적 웹 페이지를 생성하게 되었습니다. 최근에는 템플릿 엔진들이 많이 사용되고 있기도 하지만 여전히 현업에서는 jsp가 많이 사용되고 있습니다. 

마지막으로 웹 페이지의 수정을 위해 JSON 데이터만 주고 받는 것으로 많이 변경되었습니다. HTML을 내려주는 과정은 최소화 하는 것이 좋기 때문에 처음에만 클라이언트에서 HTML을 내려 받고 그 이후로는 AJAX요청을 통해 데이터만 주고 받는 것으로 발전했습니다. 해당 내용은 싱글 페이지 어플리케이션이라는 최근 트렌드를 설명하고 있습니다.

싱글 페이지 어플리케이션(SPA, Single Page Application) 

 

싱글 페이지 애플리케이션 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 싱글 페이지 애플리케이션(single-page application, SPA, 스파)은 서버로부터 완전한 새로운 페이지를 불러오지 않고 현재의 페이지를 동적으로 다시 작성함으로써 사

ko.wikipedia.org

 

이러한 웹 개발 시 SPA 구현을 목표로 하며 등장한 기술이 RestController 입니다. 스프링의 @RestController는 @Controller + @ResponseBody로서 다음과 같이 구현 할 수 있습니다. 

@RestController
@RequestMapping("/hello/rest")
public class HelloRestController {

    @GetMapping("/json/string")
    public String helloHtmlString() {
        return "<html><body>Hello @ResponseBody</body></html>";
    }
    @GetMapping("/json/list")
    public List<String> helloJson() {
        List<String> words = Arrays.asList("Hello", "Controller", "And", "JSON");

        return words;
    }
}

helloHtmlString() 함수의 경우 View name을 전달하지 않고 Response Body에 문자열 데이터를 반환 합니다. 이 때의 Content-Type은 text/html입니다.

helloJson() 함수의 경우 자바 객체를 반환합니다. 따라서 Content type이 application/json인 JSON 객체를 ResponseBody에 전달하게 됩니다. 

 

 

7. 스프링 MVC 동작원리

1. Client -> DispatcherServlet : 

- 가장 앞 단에서 클라이언트에서 요청을 받습니다. FrontController 라고도 불립니다.

- DispatcherServlet은 클라이언트로부터 들어온 요청을 처리해 줄 Controller를 찾고 해당 요청을 전달합니다. 

 

2. DispatcherServlet -> Handler mapping : 

- DispatcherServlet이 클라이언트로부터 들어온 요청을 처리해줄 Controller를 찾도록 HandlerMapping에 요청합니다. HandlerMapping에는 API path와 Controller 함수가 매칭이 되어 있습니다.

GET /hello/html/dynamic -> HomeController 의 helloHtmlFile() 함수
GET /user/login -> UserController 의 login() 함수
GET /user/signup -> UserController 의 signup() 함수
POST /user/signup -> UserController 의 registerUser() 함수

이렇게 DispatcherServlet이 Handler mapping을 통해 어떤 Controller 함수에 요청을 전달할지 찾은 다음에 해당 요청을 전달하게 해줍니다. 

- DispatcherServlet은 또한 Controller 함수가 요구하는 Request 정보를 전달해 줍니다. 예를들어 Controller 함수의 @RequestPram String query와 같은 식의 파라미터에 클라이언트 요청으로부터 필요한 정보를 전달해주게 됩니다. 

 

3. Controller -> DispatcherServlet : 

- Controller 함수가 DispatcherServlet으로부터 받은 클라이언트의 요청을 처리하고 반환합니다.

4. @ResponseBody 어노테이션이 없는 경우 Model 정보와 View 정보를 DispatcherServlet으로 전달합니다. 

 

5. ViewResolver :  Controller의 처리가 끝나 DispatcherServlet으로 model, view name 이 전달되었습니다. 타임리프와 같은 템플릿 엔진을 통해 view에 model을 적용합니다. 

6. View -> 7. Client : 

- ViewResolver를 통해 model과 합쳐진 View는 Client에 전달됩니다. 

 

 

8. 스프링 MVC의 이해 - Request

Controller와 HTTP Request 메시지

- @PathVariable : 요청하는 URL 경로에 담겨 있는 데이터를 가져옵니다. 생략이 불가능하기 때문에 반드시 명시해 주어야 합니다.

- @RequestParam : 요청이 GET인 경우 URL query의 데이터를 가져옵니다. 만약 요청이 POST인 경우 Request Body의 데이터를 가져오게 됩니다. 즉, 스프링에서는 요청 메소드가 달라지더라도 통일된 방식으로 구현할 수 있습니다.

- @ModelAttributue: 요청이 form 데이터를 전달하는 상황에서 만댝 많은 양의 데이터를 보내는 경우 @RequestParam을 여러개 쓸 필요 없이 각 데이터들이 대응하는 자바 클래스 객체에 데이터가 전달됩니다. 단, 전달받는 DTO가 반드시 Setter를 설정이 되어 있어야 합니다. 

- @RequestBody : 요청이 JSON 데이터이며 페이지 이동이 없는 경우 DTO에 데이터가 전달받습니다. DTO에는 Setter 설정이 필요가 없습니다. @RequestBody를 통해 json 데이터를 주고 받으며 웹 페이지의 일부분만을 수정하는 SPA를 구현할 수 있습니다. 

 

- 퀴즈 : 로그인 처리를 위한 컨트롤러 구현하기

로그인 처리 조건 : 

아이디와 패스워드가 같은 경우에만 아이디를 보이게 하기

 

 

 

 

- 퀴즈 정답 : 

@Controller
public class LoginController {

    @GetMapping("/login")
    public String getLoginPage() {
        return "redirect:/login-form.html";
    }

    @PostMapping("/login")
    public String login(
            @RequestParam String id,
            @RequestParam String password,
            Model model
    ) {
        if (id.equals(password)) {
            model.addAttribute("loginId", id);
        } 
        
        return "login-result";
    }
}

 

스프링 mvc에 대해 학습 했습니다. 추가적인 학습을 원한다면 공식문서를 참고합니다.( 스프링 웹 MVC 공식문서 )

 

Web on Servlet Stack

Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, "Spring Web MVC," comes from the name of its source module (spring-webmvc), but it is more commonl

docs.spring.io

 

 

9. Controller, Service, Repository 분리

만약 Controller에 관심 상품을 등록하는 기능을 분리하지 않을 경우 다음과 같은 알고리즘으로 요청이 처리가 될 것입니다. POST /api/products로 요청이 들어오면 Request Body의 내용을 createProduct가 dto의 형태로 받게 됩니다. 요청 받은 dto를 저장하기위해 객체를 만들어 줍니다. 그리고 해당 객체를 DB에 저장하기위해 DB를 연결하고 SQL query를 작성하고 DB에 해당 쿼리문을 실행합니다. 사용이 끝난 DB는 연결을 해제하고 요청을 마치며 응답을 보냅니다. 

 

@RequiredArgsConstructor // final로 선언된 멤버 변수를 자동으로 생성합니다.
@RestController // JSON으로 데이터를 주고받음을 선언합니다.
public class AllInOneController {

    // 신규 상품 등록
    @PostMapping("/api/products")
    public Product createProduct(@RequestBody ProductRequestDto requestDto) throws SQLException {
    // 요청받은 DTO 로 DB에 저장할 객체 만들기
        Product product = new Product(requestDto);

    //   DB 연결
        Connection connection = DriverManager.getConnection("jdbc:h2:mem:springcoredb", "sa", "");

    //   DB Query 작성
        PreparedStatement ps = connection.prepareStatement("select max(id) as id from product");
        ResultSet rs = ps.executeQuery();
        if (rs.next()) {
    // product id 설정 = product 테이블의 마지막 id + 1
            product.setId(rs.getLong("id") + 1);
        } else {
            throw new SQLException("product 테이블의 마지막 id 값을 찾아오지 못했습니다.");
        }
        ps = connection.prepareStatement("insert into product(id, title, image, link, lprice, myprice) values(?, ?, ?, ?, ?, ?)");
        ps.setLong(1, product.getId());
        ps.setString(2, product.getTitle());
        ps.setString(3, product.getImage());
        ps.setString(4, product.getLink());
        ps.setInt(5, product.getLprice());
        ps.setInt(6, product.getMyprice());

    // DB Query 실행
        ps.executeUpdate();

    // DB 연결 해제
        ps.close();
        connection.close();

    // 응답 보내기
        return product;
    }

    // 설정 가격 변경
    @PutMapping("/api/products/{id}")
    public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto) throws SQLException {
        Product product = new Product();

    // DB 연결
        Connection connection = DriverManager.getConnection("jdbc:h2:mem:springcoredb", "sa", "");

    // DB Query 작성
        PreparedStatement ps = connection.prepareStatement("select * from product where id = ?");
        ps.setLong(1, id);

    // DB Query 실행
        ResultSet rs = ps.executeQuery();
        if (rs.next()) {
            product.setId(rs.getLong("id"));
            product.setImage(rs.getString("image"));
            product.setLink(rs.getString("link"));
            product.setLprice(rs.getInt("lprice"));
            product.setMyprice(rs.getInt("myprice"));
            product.setTitle(rs.getString("title"));
        } else {
            throw new NullPointerException("해당 아이디가 존재하지 않습니다.");
        }

    // DB Query 작성
        ps = connection.prepareStatement("update product set myprice = ? where id = ?");
        ps.setInt(1, requestDto.getMyprice());
        ps.setLong(2, product.getId());

    // DB Query 실행
        ps.executeUpdate();

    // DB 연결 해제
        rs.close();
        ps.close();
        connection.close();

    // 응답 보내기 (업데이트된 상품 id)
        return product.getId();
    }

    // 등록된 전체 상품 목록 조회
    @GetMapping("/api/products")
    public List<Product> getProducts() throws SQLException {
        List<Product> products = new ArrayList<>();

    // DB 연결
        Connection connection = DriverManager.getConnection("jdbc:h2:mem:springcoredb", "sa", "");

    // DB Query 작성 및 실행
        Statement stmt = connection.createStatement();
        ResultSet rs = stmt.executeQuery("select * from product");

    // DB Query 결과를 상품 객체 리스트로 변환
        while (rs.next()) {
            Product product = new Product();
            product.setId(rs.getLong("id"));
            product.setImage(rs.getString("image"));
            product.setLink(rs.getString("link"));
            product.setLprice(rs.getInt("lprice"));
            product.setMyprice(rs.getInt("myprice"));
            product.setTitle(rs.getString("title"));
            products.add(product);
        }

    // DB 연결 해제
        rs.close();
        connection.close();

    // 응답 보내기
        return products;
    }
}

AllInOneController의 문제점 : 

- 한 개의 클래스에 너무 많은 임의의 코드가 존재해 코드의 이해가 어려우며 처음부터 끝까지 다 읽어야 코드 내용을 이해할 수 있습니다.

- 현업에서는 코드 추가 변경 오청이 빈번히 발생합니다. 이럴 때 변경 요청에 따라서 하나의 코드로 되어 있다면 변경이 어렵습니다. 

 

위의 AllInOneController의 API 처리는 절차적 프로그래밍 방식으로 적용되었씁니다. 컴퓨터가 해야할 일을 순차적으로 나열되어 있습니다.

 

절차적 프로그래밍은 작성 시에 직관적이어서 편리하지만 양이 많아질수록 정리가 어렵고 내용을 찾기가 어렵습니다.

 

절차지향 프로그래밍의 이 러한 단점으로 인해 객체지향 프로그래밍을 해주어야 합니다. 소프트웨어의 규모가 점점 커지면서 여러 사람들이 작업을 할 때 하나의 객체에 하나의 의미를 부여하는 것이 관리하기에 더 좋습니다. 

 

하지만 객체지향 프로그래밍은 처음 설계를 잘못할 시 수정해 주어야 할 작업이 늘어나는 문제점을 가지고 있습니다. 

 

따라서  처음엔 절차적 프로그래밍으로 구현을 한 이후 객체지향 프로그래밍으로 "리팩토링"을 해주는 것이 좋습니다. 

리팩토링이란?
: 기능 상의 변경 없이 프로그래밍의 구조를 개선하는 것

1) 하나의 파일에 너무 많은 코드가 들어가지 않도록 합니다.
2) 역할별로 코드를 분리합니다.
3) 코드를 읽기 편하게 적습니다. 

 

이제 AllInOneController를 분리하겠습니다.

서버 처리 패턴은 크게 3 가지로 분리할 수 있습니다. Controller, Service, Repository 입니다.

- Controller : 클라이언트로 요청을 받고 요청을 처리하는 서비스에게 전달하는 역할을 합니다. 또, 클라이언트에게 응답을 합니다.

- Service : 서비스는 사용자의 요구사항을 처리하는 '비즈니스 로직'을 처리합니다. 서비스 코드는 계속 비대해지게 됩니다. 또한 DB 정보가 필요한 경우 Repository에 요청을 합니다.

- Repository : DB를 관리합니다. (연결, 해제, 자원관리), DB의 CRUD 작업을 처리합니다. 

 

 

 

 

10. 객체의 중복된 코드를 삭제하기

- this 키워드 사용하기 : 생성자에 중복된 객체를 작성하기, 중복된 객체를 멤버 변수로 선언하고 생성자에서 해당 멤버 변수를 선언합니다. 

    private final ProductService productService;

    public ProductController(){
        ProductService productService = new ProductService();
        this.productService = productService;
    }

- DI 란? 강한 결합을 주의해야 합니다. 만약 가장 아랫 단에서 멤버 변수가 추가된다면 상위 계층에서 새로운 생성자를 선언해야 합니다. 또한 해당 계층을 사용하는 상위 계층 에서도 모든 수정이 필요합니다. 

따라서 이러한 강한 결합을 해결하기 위해 다은과 같은 원칙을 따라야 합니다.

"강한 결합" 해결하기
1. 각 객체의 객체 생성은 오직 한 번으로 제한한다.
2. 생성된 객체를 모든 곳에서 재사용한다. 

이전의 this 키워드를 사용했을 때 발생한 "강한 결합"의 문제를 해결하기위해 생성된 객체를 재사용하는 코드 입니다. 

Class Service1 {
	private final Repository1 repitory1;

	// repository1 객체 사용
	public Service1(Repository1 repository1) {
		//this.repository1 = new Repository1();
		this.repository1 = repository1;
	}
}

// 객체 생성
Service1 service1 = new Service1(repository1);
public class Repository1 { ... }

// 객체 생성
Repository1 repository1 = new Repository1();

new 키워드로 repository1 를 재생산 하는 대신 기존에 있는 repository1 객체를 사용하게 됩니다.

Service1 클래스에서 new 키워드를 통해 Repository1을 생성하는 것이 아니라!!

Repository1 클래스에서 new 키워드로 만든 repository1을 Service1 클래스에서 사용하는 것입니다. 

public class Repository1 {

	public Repository1(String id, String pw) {
    // DB 연결
    Connection connection = DriverManager.getConnection("jdbc:h2:mem:springcoredb", id, pw);
  }
}

// 객체 생성
String id = "sa";
String pw = "";
Repository1 repository1 = new Repository1(id, pw);

이제 느슨한 결합을 통해 Repository1의 생성자 변경이 다른 계층에 피해를 주지 않습니다. new Repository(id, pw)로 생성된 repository1을 service1에서 사용하기 때문입니다. 

DII는 의존성 주입의 약자로서 제어의 역전(IoC, Inversion of Controll)을 의미합니다. 

기존의 프로그램의 제어 흐름은 Controller에서 Service1이 필요하면 생성해서 만들어 사용하고 마찬가지로 service1이 필요한 repository1을 생성해 만들어 사용했습니다. 반면 제어의 역전은 service1이 만들어질 때 이미 만들어진 repository1을 가져다가 씁니다. 마찬가지로 controller에서도 service1을 쓸 때 이미 만들어진 것을 갖다 재사용합니다. 

 

이러한 IoC는 용도에 맞게 필요한 객체를 가져다 사용하는 것을 의미합니다. 이를 DI(Dependency Injection)이라 합니다. 원래는 Service가 Repsotory에 의존성이 있었으나 거꾸로 Repository에서 Service로 주입해준다는 의미입니다. 이는 사용할 객체가 어떻게 만들었 졌는지 알 필요가 없으며 실생활에서 도구를 사용하는 것과 같기 때문에 직관적입니다. 실생활에서 만약 가위를 필요로 한다면 가져다가 용도에 맞게 가져다가 사용합니다. 이미 만들어진 가위를 가져다가 사용하는 것이지 가위를 새로 만들지 않는 것과 같습니다. 

 

앞서 DI를 사용할 때의 장점에대해 살펴 보았습니다. 그런데 DI를 사용하기위해선 객체 생성이 우선 되어야 합니다. 객체를 어디에 생성할지는 개발자가 아닌 스프링 프레임 워크가 필요한 관리하게 됩니다. 

빈(Bean) : 스프링이 생성하고 관리하는 객체
스프링 IoC 컨테이너 : 빈을 모아둔 통

스프링에 빈과 IoC 컨테이너를 등록하고 사용하는 방법을 살펴 보겠습니다. 

 

- 컴포넌트 등록 방법 : @Component 어노테이션을 클래스 위에 등록합니다.

이 때, @Component 어노테이션은 두 가지 일을 합니다. 1. 해당 객체를 생성하기, 2. 스프링 IoC 컨테이너에 빈 저장하기

빈 이름은 클래스의 앞글자만 소문자로 변경됩니다. 즉, ProductService 클래스의 빈은 productService가 됩니다. 

 

- 컴포넌트는 @ComponentScan에서 설정해준 basePackages의 하위 패키지 안에서 등록이 됩니다. @SpringBootApplication에 의해 default 설정이 되어 있습니다. 

 

- 컴포넌트로 객체를 생성할 수 있지만 만약 파라미터가 있을 경우 값을 지정해 주어야 합니다. 이러한 경우는 컴포넌트로 등록을 할 수 없기 때문에 빈으로 등록합니다. 

 

- 빈 등록 방법 : 

스프링이 제일 처음 동작할 때 @Configuration 을 살펴봅니다. 이 때, @Bean을 등록함으로서 값을 전달해야 할 객체를 반환해 줍니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfiguration {

    @Bean
    public ProductRepository productRepository() {
    String dbUrl = "jdbc:h2:mem:springcoredb";
    String dbId = "sa";
    String dbPassword = "";

    return new ProductRepository(dbUrl, dbId, dbPassword);
    }
}