DB 없이 끝까지 데이터를 흘려본다
Part 2-3 에서 우리는 부분 부분을 배웠습니다:
하지만 — 한 번도 끝까지 연결해본 적이 없다.
v1 에서 바로 DB 까지 한꺼번에 만들면 — 어디서 막혀도 원인을 찾기 어렵습니다.
흐름이 길수록 디버깅 거리도 길다. 그래서 오늘은 짧은 종단간 한 번 → 다음 Part 에서 DB 추가.
학습 전략 — 「선(先) 흐름, 후(後) DB」.
폼에 이름 입력 → 제출 → 서버가 인사말 응답 → 화면에 표시. DB 없이도 4 박스가 한 줄로 이어지는 종단간.
<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<head><title>인사 받기</title></head>
<body>
<h2>인사 받기</h2>
<form action="/greet" method="post">
이름: <input type="text" name="name" />
<button type="submit">인사받기</button>
</form>
</body>
</html>
action="/greet" — 어디로 보낼지method="post" — POST 로 보냄 (바디에 데이터 숨김)name="name" — 컨트롤러 메서드 파라미터 이름과 정확히 일치해야 함JSP 의 name 속성과 컨트롤러 매개변수 이름이 한 글자라도 다르면 — 매개변수가 null. 그런데 에러는 안 남.
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class GreetService {
public String getGreeting(String name) {
if (name == null || name.isBlank()) {
return "안녕하세요, 손님!";
}
return "안녕하세요, " + name + "님!";
}
}
👉 「하드코딩」 — 다음 Part 에서 이 자리에 DB 호출이 들어옵니다.
이 정도는 Controller 에 두어도 되지 않을까? — 처음엔 그렇게 보입니다. 하지만:
처음부터 Service 로 분리해두면, 로직이 자라도 Controller 가 흔들리지 않음.
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.example.demo.service.GreetService;
@Controller
public class GreetController {
@Autowired
private GreetService service;
// ... 메서드는 다음 슬라이드
}
@Controller
public class GreetController {
@Autowired private GreetService service;
@GetMapping("/greet/form")
public String form() {
return "greet/form"; // 입력 폼 화면
}
@PostMapping("/greet")
public String greet(String name, Model model) {
String msg = service.getGreeting(name);
model.addAttribute("msg", msg);
return "greet/result"; // 결과 화면
}
}
@Controller → "이 클래스는 Bean 이고 요청을 처리한다"@Autowired GreetService service → 컨테이너에서 자동 주입@GetMapping("/greet/form") → GET /greet/form 요청 처리@PostMapping("/greet") → form 의 method="post" 와 짝String name → form 의 name="name" 입력값이 자동 바인딩Model model → JSP 로 전달할 데이터 그릇return "greet/result" → ViewName, ViewResolver 가 JSP 파일로 변환
<!-- /WEB-INF/views/greet/result.jsp -->
<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<body>
<h1>${msg}</h1>
<a href="/greet/form">다시 인사받기</a>
</body>
</html>
👉 ${msg} = Controller 가 model.addAttribute("msg", ...) 한 그 키.
EL (Expression Language) 문법. JSP 의 모델 데이터에 접근하는 표준 방법.
| 위치 | 데이터의 형태 |
|---|---|
| 브라우저 input | HTML 폼의 텍스트 |
| HTTP 메시지 바디 | name=홍길동 |
| Controller 매개변수 | String name = "홍길동" |
| Service 매개변수 | String name = "홍길동" |
| Service 반환값 | String "안녕하세요, 홍길동님!" |
| Model 의 속성 | { "msg" : "안녕하세요, 홍길동님!" } |
| JSP 의 ${msg} | HTML 안의 텍스트로 변환 |
| 응답 HTTP 바디 | 완성된 HTML 문서 |
| 브라우저 화면 | "안녕하세요, 홍길동님!" 글자 |
JSP 의 name="name" 을 name="userName" 으로 바꾸고 제출해보면?
<input type="text" name="userName" />
화면에 "안녕하세요, 손님!" 이 뜸 (Controller 의 name 매개변수가 null).
에러는 안 나지만 데이터가 안 들어옴 — 가장 찾기 어려운 버그.
// @Service ← 주석!
public class GreetService {
public String getGreeting(String name) { ... }
}
Tomcat 시작 시 에러:
NoSuchBeanDefinitionException:
No qualifying bean of type 'GreetService' available
컨테이너에 등록 안 된 Bean 은 주입 불가 → 시작부터 실패.
return "greet/result_xxx"; // 이런 JSP 없음
화면에 에러:
HTTP 404 Not Found
또는
"Could not resolve view with name 'greet/result_xxx'"
ViewResolver 가 prefix + ViewName + suffix 로 파일을 찾는데 그 파일이 없음.
<h1>${message}</h1> <!-- 모델은 "msg" 키로 넣었는데 -->
화면에 빈 <h1></h1> 만 보임. 에러 없음.
EL 은 없는 키를 만나도 빈 문자열로 처리. 가장 찾기 까다로운 종류의 버그.
| 실험 | 증상 | 의심할 곳 |
|---|---|---|
| ① name 속성 불일치 | 매개변수 null | JSP name ↔ Controller 매개변수 |
| ② @Service 빼기 | NoSuchBean 에러 | 등록 어노테이션 |
| ③ ViewName 깨기 | 404 또는 ViewResolver 에러 | JSP 파일 경로 |
| ④ EL 키 깨기 | 빈 화면 (조용) | model 키 ↔ JSP ${...} |
👉 4 가지 자리를 외워두면 디버깅이 빨라집니다.
오늘이 우리 프로젝트의 두 번째 마일스톤입니다.
v0.5 의 「하드코딩」 자리는 v1 에서 그대로 DB 호출로 교체됩니다. 흐름은 같고, 한 자리만 채워질 뿐.
Controller·Service·View 가 무엇인지 따로따로 안다.
한 번도 끝까지 연결해본 적 없음.
폼 → Controller → Service → JSP 까지 데이터를 처음 끝까지 흘려봤다.
일부러 끊어보며 4 가지 단절 자리도 외운다.
다음 Part — Service 의 하드코딩 자리에 DB 호출이 들어옵니다.
다음 Part: 데이터와 MyBatis — Service 의 하드코딩 자리를 진짜 DB 로 채웁니다.