수업 복습

Spring Boot + JSP 로그인부터 게시판 조회

_김영인 2026. 1. 20. 22:07

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로 구역을 나누는 것이 안전하다