Spring Boot (Spring MVC)에서 URL로 직접 접속 → 로그인 → 게시글 조회까지 흐름 정리
1. 목표 시나리오
이 글에서 설명하는 최종 사용자 흐름은 다음과 같다.
- 주소창에 직접 입력: http://localhost:8088/index
- 로그인 화면 (index.jsp)에서 아이디 / 비밀번호 입력 후 전송
- 서버에서 로그인 검증 후 게시글 목록을 조회
- main.jsp에서 게시글 테이블이 출력됨
실행 결과에서 중요한 포인트는 다음이다.
- 화면은 main.jsp가 뜨는데 브라우저 URL이 /login으로 남아 있을 수 있음
- 이는 POST /login 처리 후 return "main"이 redirect가 아니라 forward (뷰 렌더링) 이기 때문이다.
2. 프로젝트 구조 (계층) 한눈에 보기
이 코드는 “Spring MVC + Service/DAO 계층 분리”를 학습하는 전형적인 구조를 갖는다.
- Controller 계층
- 요청 (URL)을 받고 Service를 호출하고 View로 보낼 데이터를 Model에 담는다.
- 예: TestController
- Service 계층
- 비즈니스 로직의 진입점 (DAO 호출을 조합, 규칙 적용 등)
- 예: MemberServiceImpl, BoardServiceImpl
- DAO 계층
- SQL을 통해 DB에 직접 접근하고 결과를 DTO로 매핑한다.
- 예: MemberDAO, BoardDAO
- DTO 계층
- 데이터 전달 객체 (파라미터 바인딩, DB 결과 매핑)
- 예: MemberDTO, BoardDTO
- View 계층 (JSP)
- Controller가 넘긴 Model을 JSTL/EL로 렌더링한다.
- 예: index.jsp, main.jsp
3. 핵심 개념: Spring MVC는 “FrontController 패턴”을 프레임워크가 제공한다
Servlet/JSP에서 직접 FrontController를 만들던 시절에는:
- 사용자가 요청하면
- 내가 만든 FrontController (Servlet)가 받고
- 분기해서 Action/Service 호출하고
- JSP로 forward/redirect 해줬다
Spring MVC에서는 이 FrontController 역할을 DispatcherServlet이 담당한다.
Spring MVC 요청 흐름은 기본적으로 아래 파이프라인으로 굴러간다.
Client(브라우저)
→ DispatcherServlet(FrontController)
→ HandlerMapping(어떤 컨트롤러/메서드가 처리할지 찾음)
→ Controller(메서드 실행)
→ Service
→ DAO
→ DB
→ Model에 데이터 담기
→ ViewResolver(뷰 이름을 JSP 경로로 변환)
→ JSP 렌더링 결과 응답
4. 코드로 보는 URL별 동작
(1) /index로 직접 접속 (GET)
컨트롤러:
@Controller
public class TestController {
@GetMapping("/index")
public String index() {
System.out.println("로그01");
return "index";
}
}
여기서 핵심은 return "index"이다.
- Spring MVC에서 컨트롤러가 문자열을 리턴하면
- 그 문자열은 “뷰 이름 (view name)”으로 해석된다.
- ViewResolver가 index를 실제 JSP 경로로 바꾼다.
일반적인 JSP ViewResolver 규칙은 다음 형태이다.
- prefix: /WEB-INF/views/
- suffix: .jsp
따라서 최종적으로:
- "index" → /WEB-INF/views/index.jsp
사용자가 /index로 접속하면 서버는 index.jsp를 렌더링해서 응답한다.
(2) index.jsp에서 로그인 폼 전송 (POST /login)
index.jsp:
<form action="login" method="POST">
<input type="text" name="mid" required>
<input type="password" name="mpw" required>
<input type="submit" value="로그인">
</form>
- action이 "login" 이므로 실제 요청은 POST /login
- name이 mid, mpw이므로 서버는 mid=...&mpw=... 형태의 파라미터를 받는다.
5. Spring이 “파라미터를 DTO로 자동 바인딩 (Data Binding)”하는 원리
컨트롤러 로그인 메서드:
@PostMapping("/login")
public String login(MemberDTO mdto, Model model) {
System.out.println("로그02");
if(memberService.getMember(mdto) != null) {
model.addAttribute("datas", boardService.getBoardList(null));
return "main";
}
return "redirect:index";
}
여기서 MemberDTO mdto를 따로 request.getParameter()로 꺼내지 않았다.
그런데도 mid, mpw가 DTO에 들어가는 이유는:
- Spring MVC가 요청 파라미터 이름과 DTO의 필드 (Setter)를 매칭해서 자동으로 채워주기 때문이다.
- 즉 mid 파라미터 → setMid() 호출
- mpw 파라미터 → setMpw() 호출
이게 Spring MVC의 Data Binding이다.
Servlet/JSP 방식에서는 보통:
String mid = request.getParameter("mid");
String mpw = request.getParameter("mpw");
처럼 직접 꺼냈는데 Spring MVC에서는 그 작업을 프레임워크가 대신해준다.
6. DI/IoC: 왜 new를 안 하는가
TestController에는 아래가 있다.
@Autowired
private MemberService memberService;
@Autowired
private BoardService boardService;
이 말은:
- 컨트롤러가 new MemberServiceImpl() 같은 걸 직접 생성하지 않는다.
- Spring 컨테이너가 객체를 만들어서 (Bean 생성)
- 필요한 곳에 주입해준다 (Dependency Injection)
이 구조의 핵심 이점은 다음이다.
- 결합도 감소: Controller가 구현체를 모르게 된다 (인터페이스 중심)
- 테스트 / 확장 용이: Service 구현 교체가 쉬워진다
- 책임 분리: Controller는 요청/응답 흐름만 담당, DB 로직은 DAO로 내려간다
Service 구현체는 보통 이렇게 등록된다.
@Service("ms")
public class MemberServiceImpl implements MemberService {
@Autowired
private MemberDAO memberDAO;
}
DAO는 이렇게 등록된다.
@Repository
public class MemberDAO { ... }
그리고 @SpringBootApplication(scanBasePackages = "com.example") 때문에
com.example 하위 패키지의 @Controller/@Service/@Repository가 자동 스캔되어 Bean으로 올라간다.
7. 로그인 검증이 실제로 어떻게 되는가 (Service → DAO → DB)
TestController 로그인 처리에서 핵심은 이 부분이다.
if(memberService.getMember(mdto) != null) { ... }
(1) MemberServiceImpl
@Override
public MemberDTO getMember(MemberDTO dto) {
return memberDAO.getMember(dto);
}
(2) MemberDAO (SQL 실행)
MemberDAO는 다음 SQL을 사용한다.
- SELECT_ONE = "SELECT * FROM MEMBER WHERE MID=? AND MPW=?"
“아이디 / 비밀번호가 일치하는 회원이 있으면 1행 반환, 없으면 null” 구조이다.
이 부분이 Servlet/JSP 학습에서 자주 쓰던 로그인 검증 패턴과 동일하다.
8. 로그인 성공 시 “게시글 목록 조회”가 이어지는 이유
로그인 성공 분기에서:
model.addAttribute("datas", boardService.getBoardList(null));
return "main";
여기서 datas라는 이름으로 게시글 목록 (List<BoardDTO>)을 Model에 담는다.
(1) BoardDAO의 SELECT_ALL
BoardDAO는 다음 SQL을 사용한다.
- SELECT_ALL = "SELECT * FROM BOARD ORDER BY BID DESC"
최신 글이 먼저 나오도록 정렬된 목록을 가져와서 List<BoardDTO>로 반환한다.
9. View (Model 데이터)가 JSP로 전달되어 출력되는 과정
컨트롤러가:
model.addAttribute("datas", ...);
return "main";
을 수행하면
- view name: "main"
- ViewResolver: /WEB-INF/views/main.jsp로 변환
- main.jsp가 실행되면서 ${datas}를 읽는다
main.jsp:
<c:forEach var="data" items="${datas}">
<tr>
<td>${data.bid}</td>
<td>${data.title}</td>
<td>${data.writer}</td>
</tr>
</c:forEach>
Controller가 담아준 datas 리스트를 JSP가 반복 출력하여 테이블이 완성된다.
10. 왜 URL이 /login인데 화면은 main이 뜨는가 (forward vs redirect)
URL이 /login인데 게시판 테이블 (main.jsp)이 보이는 상황은 매우 자연스럽다.
이유:
- POST /login 요청이 들어옴
- 서버가 그 요청에 대한 응답으로 main.jsp 렌더링 결과를 바로 내려줌
- 브라우저는 “이 응답을 받은 URL이 /login”이라고 인식하므로 주소창이 유지된다
return "main"은 redirect가 아니라 forward (뷰 렌더링) 이다.
반대로 로그인 성공 후 주소창도 바꾸고 싶다면 다음처럼 바꾼다.
- 로그인 성공 → redirect:/main 같은 새로운 GET 요청을 유도 (POST-Redirect-GET 패턴)
현재는 학습 목적상 “로그인 성공하면 바로 main.jsp 렌더링” 구조로 이해하면 된다.
11. 로그가 여러 번 찍히는 이유 (로그01/로그02 반복)
로그는 아래 위치에서 찍힌다.
- 로그01: GET /index
- 로그02: POST /login
따라서 아래 상황이면 로그가 반복된다.
- 로그인 실패 시 return "redirect:index"로 다시 /index로 돌아감
- 새로고침/뒤로가기 등으로 /index 또는 /login 요청이 재발생
추가로 안정성을 높이려면 실패 redirect는 아래처럼 절대경로로 쓰는 것이 안전하다.
return "redirect:/index";
12. 스프링 핵심 요소
이 흐름을 가능하게 만든 핵심을 딱 정리하면 다음이다.
- DispatcherServlet
- 모든 웹 요청을 받는 FrontController
- HandlerMapping (@GetMapping/@PostMapping)
- URL과 컨트롤러 메서드를 연결
- Data Binding
- request 파라미터를 DTO에 자동 주입 (mid/mpw → MemberDTO)
- DI/IoC (@Autowired, @Service, @Repository)
- new 없이도 계층이 연결됨 (컨트롤러 → 서비스 → DAO)
- Model
- Controller에서 View로 넘길 데이터 보관소 (datas)
- ViewResolver
- "index" → /WEB-INF/views/index.jsp
- "main" → /WEB-INF/views/main.jsp
- JSTL/EL
- ${datas} 반복 출력로 화면 렌더링
13. 자주 언급되는 주의점: “매핑 충돌 (Ambiguous mapping)”
이 구조에서 가장 흔한 런타임 에러가 “같은 URL을 두 컨트롤러가 동시에 잡는 경우”이다.
예를 들어:
- TestController가 GET /index를 처리하는데
- 다른 컨트롤러도 GET /index를 처리하면
Spring은 어떤 메서드를 선택할지 모르게 되어 부팅 중에 즉시 실패한다.
이 경험은 실무에서도 매우 중요하다.
- URL 설계는 도메인 / 기능 기준으로 명확히 분리
- /index, /login, /logout처럼 역할이 섞이지 않게 네이밍을 유지
- 컨트롤러가 많아지면 /member/*, /board/*처럼 prefix로 구역을 나누는 것이 안전하다
'수업 복습' 카테고리의 다른 글
| Spring AOP 개념 / 용어 정리 (0) | 2026.01.23 |
|---|---|
| Spring MVC / JSP Q&A 정리 (0) | 2026.01.22 |
| 커맨드 객체 바인딩과 DI 흐름 정리 (0) | 2026.01.19 |
| FC 구조를 Spring MVC (DS)로 갈아끼우기 (0) | 2026.01.16 |
| FrontController에서 DispatcherServlet까지 (0) | 2026.01.16 |