Spring Framework: Controller와 View의 상호작용

Beautify.log·2021년 12월 28일
0

Spring Framework

목록 보기
2/2
post-thumbnail

안녕하세요. 이번 포스팅에서는 스프링 프레임워크에서 컨트롤러와 뷰의 통신에 대해 알아보려고 합니다.

여기에서 컨트롤러는 우리가 생각하는 백엔드의 개념이고 뷰는 프론트엔드 개념입니다.

이전포스팅에서 WAS와 Client가 소통하는 것에 대해 살펴보았습니다.

클라이언트가 유튜브라는 서버에서 "방탄소년단 Permission to Dance 뮤직비디오 보여줘!"라고 요청하면 서버는 그것을 찾아서 클라이언트에게 보여(응답해)줍니다.

다시 말해서 가게에서 쓰레기봉투를 사려는 사람은 클라이언트이고 쓰레기봉투를 건네주는 사람은 서버가 되는 것입니다.

각각의 행위를 클라이언트 → 서버인 경우 요청(Request), 서버 → 클라이언트인 경우 응답(Response)이라고 할 수 있지요.

그렇다면 이렇게 서버와 클라이언트가 주거니 받거니 하는 행동을 내부적으로 살펴보겠습니다.

이 때 클라이언트의 반응에 따라 보여줄 정보를 처리하는 클래스를 컨트롤러(Controller) 라고 할 것이며, 이에 따라 사용자에게 보이는 화면을 뷰(View) 라고 하겠습니다.

그렇다면 이들이 어떻게 작동하는지 살펴보겠습니다.


컨트롤러와 뷰


기본 개념

클라이언트의 반응에 따라 어떤 것을 보여줄 지를 결정해주는 클래스를 컨트롤러라고 합니다. 예를 들어 웹 페이지를 호출했을 때 가장 메인에 호출되는 index.html이 있다고 가정해봅시다.

검색창에 어떤 키워드를 입력하고 검색버튼을 누르면 입력값이 submit 되면서 서버로 전송되고 그에 해당하는 결과를 보여줍니다.

이 때 컨트롤러의 역할은 검색어로 받은 값에 대해 어떤 페이지를 보여주라고 명령하는 역할을 하고, 그 결과로 뷰에 페이지를 보여주게 되는 것입니다.

컨트롤러 클래스 작성하기

컨트롤러도 결국은 클래스입니다. 예를 들어 TestController라고 가정하고 컨트롤러를 만들어주겠습니다.

import org.springframework.stereotype.Controller;

@Controller
public class TestController {
	// 내용
}

이렇게 클래스를 만들어주고 이 클래스가 컨트롤러임을 알려주기 위해 어노테이션(@Controller)를 작성해주었습니다.

그리고 이 클래스의 내부에는 매핑이라는 함수가 들어가는데 마찬가지로 각각의 함수 위에는 매핑함수임을 알려주기 위한 어노테이션(@GetMapping 또는 @PostMapping)이 선언되어야 합니다.

그래서 메인페이지를 보여주는 매핑함수를 작성하면

import org.springframework.stereotype.Controller;

@Controller
public class TestController {

	@GetMapping("/")
	public String helloWorld() {
		System.out.println("Hello World!");
		return "index";
	}

}

의 형태로 작성할 수 있습니다.

이 때 @GetMapping@PostMapping의 차이는 전송방식의 차이인데, html의 form태그에 넣어주는 어트리뷰트 중 method를 기억하시면 됩니다.

method에는 get이나 post를 지정해주게 되는데 get은 주소창에 전송하는 값을 파라미터(또는 쿼리)로 직접 보여주며 전송하게 되고, post는 이를 노출하지 않는 차이가 있습니다.

위에서 든 예시는 주소창 맨 뒤에(/)인 경우를 매핑하여 return으로 index.jsp를 보여주게 됩니다.

index.jsp를 작성해보겠습니다. JSP는 Java Server Page로 생긴 것은 html문서와 비슷하게 생겼지만 자바 코드를 쓸 수 있습니다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8" %>
<%@ page session="false" %>

<html>
<head>
    <title>Home</title>
</head>

<body>
<a href="hello">hello로 가기</a>

<%
    System.out.println("JSP");
%>

<%-- 주석처리하는 법 --%>
</body>
</html>

이렇게 jsp는 html문서와 비슷하게 생겼지만 자바 코드를 실행할 수 있습니다. 이 때 자바코드는 <% %>안에 작성해주어야 합니다.

톰캣 서버로 실행했을 때 기본적으로 보이는 주소는 http://localhost:9000/이므로 뒤에 슬래시가 붙기 때문에 이를 매핑하여 index.jsp를 뷰로 보여줄 것입니다.

또한 form 태그의 action이나 어떤 DOM의 name 어트리뷰트도 매핑할 값으로 받기도 합니다.

DTO로 두개 이상의 정보 넘겨주기

컨트롤러에서 매핑하여 처리할 함수의 파라미터가 두개 이상이라면 가급적 DTO를 별도로 작성해서 관리해 주는 것이 권고됩니다.

예를 들어 다음과 같은 JSP 파일이 있다고 가정했을 때,

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h3>여기는 World</h3>
    <form action="/account" method="post" >
        name: <input type="text" name="name" /> <br>
        age : <input type="text" name="age" /> <br>
        <input type="submit" value="Send" />
    </form>
</body>
</html>

@GetMapping을 쓴다면

@GetMapping("account")
public String account(String name, int age) {

	System.out.println(name + ", " + age);
	return "ex";
    
}

보통은 이렇게 쓸 수 있지만 매핑 함수의 파라미터가 두개이므로 나중에 변수가 더 늘어나거나 할 때 유지보수를 용이하게 하고자 DTO를 따로 만들어주겠습니다.

DTO에는 생성자와 getter, setter가 들어갑니다. setter는 쓰지 않을 것이므로 지웠습니다.

// HumanDto

public class HumanDto {

	private String name;
	private int age;

	public humanDto(String name, int age) {
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public int getAge() {
		return age;
	}

}

이 때는 컨트롤러에서 매핑함수의 파라미터를 humanDto로 받아서 이름과 나이를 얻어오면 됩니다.

@GetMapping("account")		// form 태그의 action
public String account(humanDto human) {

	System.out.println(human.getName());
	System.out.println(human.getAge());

	return "account";	// 이 때 account를 action으로 받는 페이지를 보여주게 됩니다..

}

입력받은 값 가공하여 전달하기


Model 객체

입력받은 데이터를 가공하여 다시 프론트엔드단(.jsp)로 보내줄 수 있는 방법이 있는데, 이 때는 Model이라는 객체를 사용해야 합니다.

Model은 스프링 프레임워크에서 제공하는 객체로 매핑함수의 파라미터로 사용하면 action으로 받은 값을 검사하여 다시 프론트엔드단으로 넘겨줄 수 있습니다.

@GetMapping("hello")
public String hello (Model model) {

	System.out.println("HelloController hello() " + new Date());

	String name = "성춘향";
	model.addAttribute("_name", name);	// name이라는 Object로 name을 보냄

	return "hello";	// 짐쌌으니 이동해
    
}

이 때 매핑함수의 파라미터로 Model 객체를 사용하였고, 값을 내보내기 위해 mode.addAttribute()를 사용합니다. Model은 객체이기 때문에 값을 내보낼 때 객체형태로 내보내주며, mode.addAttribute의 인자로는 ("객체명", 객체)의 꼴로 들어갑니다.

<%-- test.jsp --%>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>Hello JSP 입니다!</h1>
<%
    // 짐풀기
    String name = (String)request.getAttribute("_name");
%>
<p><%= name%></p>
name: <input type="text" value="<%= name%>" />
</body>
</html>

앞에서 언급한 것처럼 Model은 객체 형태로 값을 반환해주기 때문에 jsp에서 requset.getAttribute("객체명")으로 값을 불러와준 후 반드시 캐스팅 변환을 해주어야 합니다.

마찬가지로 addAttribute()를 사용해야 할 값이 많아진다면 DTO를 별도로 만들어주는 것이 좋습니다.

Model 객체 활용하여 리스트에 값을 담아 한번에 전달하기

뿐만 아니라 리스트에 담아서 전달할 수도 있습니다.

전달해 줄 값이 많다는 것은 DTO를 만들어야 한다는 것이겠지요?

// HumanDto.java

public class HumanDto {

    private String job;
    private int age;
    
    public HumanDto(String job, int age) {
        this.job = job;
        this.age = age;
    }

    public String getJob() {
        return job;
    }

    public int getAge() {
        return age;
    }
    
}

이제 HumanDto를 제네릭으로 갖는 Array List를 만들어주고 값을 넣어줍니다.

@GetMapping("world")
public String world (Model model) {

	System.out.println("HelloController world()" + new Date());
    
	List<HumanDto> list = new ArrayList<>();
	list.add(new HumanDto("프로그래머", 15));
	list.add(new HumanDto("게이머", 18));
	list.add(new HumanDto("의사", 45));

	model.addAttribute("list", list);

	return "world";

}

Model 객체를 이용하여 addAttribute()로 짐을 싸줍니다.

그리고 World.jsp로 돌아와 짐을 풀어줍니다.

<table border="1">
    <tr>
        <th>JOB</th>
        <th>AGE</th>
    </tr>


    <%
        for (HumanDto dto : list) {
    %>
    <tr>
        <td><%= dto.getJob()%>
        </td>
        <td><%= dto.getAge()%>
        </td>
    </tr>
    <%
        }
    %>
</table>

이렇게 리스트에 값을 담으면 한번에 전달해줄 수 있습니다.


axios로 비동기 처리 구현하기


axios와 비동기 처리

axios는 비동기 처리를 보다 쉽게 해주기 위한 자바스크립트 라이브러리입니다.
axios를 살펴보기 전에 Promise객체에 대해 우선 알아야합니다.

MDN에서는 Promise를 이렇게 정의하고 있습니다.

비동기 연산이 종료된 이후에 결과 값과 실패 사유를 처리하기 위한 처리기
https://url.kr/8kn1vp

자 그러면 비동기 연산이 무엇이며 Promise는 상황별로 어떻게 써야 할까요?

비동기 처리 방식, 즉 비동기 연산은 API나 DB로부터 데이터를 불러오거나 전달해주는 일처럼 일정 시간이 소요되지만 해당 처리가 다 끝날 때까지 나머지 로직을 처리하지 않을 때 비효율적인 상황이 발생하는데, 이 상황을 보다 효율적으로 해결할 수 있는 연산 방식입니다.

따라서 이 상황에 대해 성공이나 실패시에 어떻게 행동하라는 것을 명시해주어야 하며, Promise 객체는 이를 위해 존재하는 것입니다.

그러면 각 상황별로 어떻게 명시해주어야 하는지 살펴보겠습니다.

상황전달의미
대기-get이나 post 처리를 요청하고 난 후 이행되었거나 거부되지 않은 상태
성공.then()연산이 성공적으로 수행되었을 때 처리할 작업
실패.catch()연산이 실패하였을 때 처리할 작업

다음과 같이 자바스크립트 코드를 작성했다고 가정해봅시다.

console.log("Hello")
setTimeout(function() {
	console.log("World");
}, 3000);
console.log("Hi");

콘솔에 어떤 결과가 출력될까요?

결과는 이렇습니다. 순서상으로 보면 Hello 출력, 3초 있다가World 출력, Hi 출력의 순서로 결과가 보여야 하지만 setTimeout을 사용해서 World가 출력되는 시간을 미뤄줬습니다.

이처럼 특정 로직이 끝날 때까지 기다려주지 않고 나머지 로직을 우선 연산해 주는 것을 비동기 처리라고 합니다.

우리는 이 비동기처리를 학습목적으로 axios에 대해 잠시 살펴볼 것입니다.

비동기 처리 살펴보기

우선 axios 라이브러리를 사용하려면 노드 패키지를 사용하여 npm install axios하거나 cdn을 통해 문서에 삽입해주어야 합니다.

현재 0.24버전이 최신버전이며 웹문서에 다음과 같이 추가하여 사용할 수도 있습니다.

<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.24.0/axios.min.js"></script>

구글에 검색어를 넘겨주는 html문서를 작성한다고 가정하면

<script>
  const btn = document.getElementById("searchBtn");
  const url = "https://www.google.com/search";
  let search = document.getElementById("search").value;

  const onClick = () => {
    axois.get(url + search)
    .then((res) => console.log(res));
  };

  btn.addEventListener("click", onClick);
</script>

아마 404에러가 생기면서 작동은 안할겁니다.

그렇지만 이렇게 해주면 axiosget 메서드 방식으로 정보를 넘겨줄 것이고 성공했다면 콘솔에 결과를 출력해줄 것입니다.

스크립트 태그 내에 있는 것 중 onClick은 우선 처리하지 않고 사용자가 사용하고자 할 때만 호출해줍니다.

오늘 포스팅은 여기까지입니다.

profile
tried ? drinkCoffee : keepGoing;

0개의 댓글