DispatcherServlet ~ View 의 분업 지도
@RequestMapping("/board/list")
public String list() {
// ① DB 직접 연결
Connection c = DriverManager.getConnection(...);
// ② SQL 실행
PreparedStatement ps = c.prepareStatement("SELECT...");
// ③ 결과 가공
while(rs.next()) { ... }
// ④ HTML 조립
StringBuilder html = new StringBuilder("<html>...");
// 한 메서드 안에 4 가지 일이 다 있다
}
화면 디자인 한 번 바꾸려고 SQL 도 손대야 한다면? 부서지기 쉽고, 테스트 불가능, 새 사람이 와서 손대기 어려움.
큰 식당이 손님 한 명을 응대하는 흐름과 정확히 같습니다. 각 사람은 자기 일만 하고, 다른 사람의 일은 알지 못해도 됩니다.
책임: 모든 HTTP 요청을 가장 먼저 받는다. 어느 컨트롤러가 처리할지 결정해서 넘긴다.
web.xml 에 한 번만 등록책임: 요청을 받아 검증하고, 적절한 Service 에게 일을 시킨다. 결과를 모델에 담아 View 로 보낸다.
@Controller
public class BoardController {
@Autowired
private BoardService service; // 셰프
@GetMapping("/board/view")
public String view(@RequestParam int id, Model model) {
Board b = service.findById(id); // 셰프에게 부탁
model.addAttribute("board", b); // 그릇에 담아
return "board/view"; // 식탁(JSP)으로
}
}
👉 컨트롤러는 「중개자」 — SQL 도 모르고, HTML 도 안 만든다.
책임: 실제 요리(비즈니스 로직)를 한다. 트랜잭션도 책임진다.
@Service
public class BoardService {
@Autowired
private BoardMapper mapper; // 창고 관리자
public Board findById(int id) {
Board b = mapper.findById(id);
if (b == null) {
throw new IllegalArgumentException("글이 없습니다");
}
return b;
}
}
@Service
public class BoardService {
@Autowired private BoardMapper mapper;
@Autowired private FileService fileService;
@Transactional
public void writeWithFile(Board b, MultipartFile file) {
mapper.insert(b); // 글 저장
fileService.save(file); // 파일 저장
// 둘 중 하나 실패하면 둘 다 롤백
}
}
@Transactional 로 원자성 보장Service 가 「두툼해야」 한다 — Controller 와 Mapper 는 얇게.
책임: DB 접근 SQL 만 작성. 비즈니스 로직 없음.
// 인터페이스 — 자바
@Mapper
public interface BoardMapper {
Board findById(int id);
List<Board> findAll();
void insert(Board b);
void update(Board b);
void delete(int id);
}
// SQL — XML
<mapper namespace="...BoardMapper">
<select id="findById" resultType="Board">
SELECT * FROM board WHERE id = #{id}
</select>
</mapper>
👉 「DAO」 와 「Repository」 는 사실 같은 일. Spring 권장은 @Repository.
책임: 계층 사이 데이터 운반. 로직은 거의 없는 「데이터 그릇」.
public class Board {
private int id;
private String title;
private String content;
private int writerId;
private LocalDateTime createdAt;
// getter / setter
public int getId() { return id; }
public void setId(int id) { this.id = id; }
// ...
}
👉 DB 한 행 ↔ JSP 의 ${board.title} 사이를 오가는 그릇.
| 이름 | 주된 의미 | 특징 |
|---|---|---|
| DTO | Data Transfer Object | 계층 간 운반. 보통 getter/setter |
| VO | Value Object | 「값」으로서 의미. 불변(immutable) 권장 |
| Entity | DB 의 한 행을 표현 | JPA 등에서. 본 과정에서는 미사용 |
👉 본 과정에선 「DTO 와 VO 는 거의 같다」 정도로 받아들여도 OK. 정확한 구분은 Part 3 의 ◇ DTO·VO·Model 차시에서.
책임: 모델 데이터를 받아 사용자에게 보여줄 화면을 그린다.
<!-- /WEB-INF/views/board/view.jsp -->
<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<body>
<h1>${board.title}</h1>
<p>작성자: ${board.writerName}</p>
<div>${board.content}</div>
</body>
</html>
👉 View 는 자바 코드를 거의 안 씀. 표현(${...})만.
| 계층 | 한 줄 책임 | 이런 코드는 여기 없음 |
|---|---|---|
| DispatcherServlet | 요청 분배 | (우리가 안 짬) |
| Controller | 요청·응답 중개 | SQL, 비즈니스 규칙 |
| Service | 비즈니스 로직 + 트랜잭션 | HTML, 직접 SQL |
| DAO/Repository | SQL 만 | 비즈니스 규칙 |
| DTO/VO | 데이터 운반 | 비즈니스 메서드 |
| View | 화면 표시 | SQL, 비즈니스 처리 |
👉 화살표가 한 방향이라 의존 관계가 단순. 한 계층 변경이 위로만 영향.
| 증상 | 의심할 계층 |
|---|---|
| 404 Not Found | DispatcherServlet 매핑 / Controller URL |
| "No qualifying bean of type 'XService'" | Service 의 @Service 누락 |
| NullPointerException at mapper | DAO 의 @Autowired 누락 |
| SELECT 결과 모두 null | DTO 필드 매핑 (mapUnderscoreToCamelCase) |
| "Could not resolve view" | ViewName 오타 또는 JSP 위치 |
| 한글 깨짐 | web.xml CharacterEncodingFilter |
"분업하라" 는 큰 그림은 안다. 6 계층의 이름은 처음 들어봄.
6 계층의 이름·책임·의존 방향을 한 그림으로 외운다. 다음 차시들에서 각 계층을 깊이 파고들 토대 완성.
다음: DispatcherServlet 깊이 보기 — URL 분배의 마법.