Servlet_04) JSP/Servlet을 활용한 MVC 패턴
2025. 10. 2. 17:13

학습 목표

  • MVC 패턴의 계층별 역할과 책임 범위를 이해한다
  • Controller-Service-DAO-VO 4계층 구조를 실무 코드로 분석한다
  • 트랜잭션 관리와 예외 처리 전략을 학습한다
  • Forward vs Redirect 방식의 차이점과 사용 시나리오를 파악한다

<MVC 패턴의 계층 구조>

MVC 패턴은 애플리케이션을 논리적인 계층으로 분리하여 유지보수성과 확장성을 높이는 설계 패턴이다. 본 프로젝트에서는 4계층 구조를 채택하였다.

- 계층별 역할 정의

Controller 계층은 HTTP 요청을 받아 적절한 Service를 호출하고 응답 방식을 결정한다. Service 계층은 비즈니스 로직을 수행하고 트랜잭션 경계를 관리한다. DAO 계층은 데이터베이스 접근 로직을 캡슐화하고 SQL을 실행한다. VO 계층은 데이터를 담는 객체로서 계층 간 데이터 전달을 담당한다.

- 계층 간 데이터 흐름

사용자의 요청은 Controller에서 시작되어 Service를 거쳐 DAO로 전달된다. DAO는 데이터베이스 작업을 수행한 후 결과를 VO에 담아 반환하며, 이는 역순으로 Controller까지 전달되어 최종적으로 View에 전달된다.


<Controller 계층 구현>

Controller는 사용자의 요청을 받아 처리하는 진입점이다. @WebServlet 어노테이션을 활용하여 URL 매핑을 선언적으로 정의한다.

- 회원가입 Controller 구현

 
 
java
@WebServlet("/insert.me")
public class InsertController extends HttpServlet {
    
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        // 1. 요청 파라미터 추출
        String userId = request.getParameter("userId");
        String userPwd = request.getParameter("userPwd");
        String userName = request.getParameter("userName");
        String phone = request.getParameter("phone");
        String email = request.getParameter("email");
        String address = request.getParameter("address");
        String[] interestArr = request.getParameterValues("interest");
        
        // 2. 배열을 문자열로 변환
        String interest = "";
        if(interestArr != null) {
            interest = String.join(",", interestArr);
        }
        
        // 3. VO 객체 생성
        Member m = Member.insertCreateMember(userId, userPwd, userName, 
                                            phone, email, address, interest);
        
        // 4. Service 호출
        int result = new MemberService().insertMember(m);
        
        // 5. 응답 처리
        if(result > 0) {
            request.getSession().setAttribute("alertMsg", 
                "성공적으로 회원가입을 완료하였습니다.");
            response.sendRedirect(request.getContextPath());
        } else {
            request.setAttribute("errorMsg", "회원가입에 실패하였습니다.");
            request.getRequestDispatcher("/views/common/error.jsp")
                   .forward(request, response);
        }
    }
}

실행 결과:

 
 
성공 시: 메인 페이지로 리다이렉트 + 세션에 성공 메시지 저장
실패 시: error.jsp로 포워드 + 요청 영역에 에러 메시지 저장

- 로그인 Controller의 응답 처리 전략

 
 
java
@WebServlet("/login.me")
public class LoginController extends HttpServlet {
    
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        String userId = request.getParameter("userId");
        String userPwd = request.getParameter("userPwd");
        
        Member loginMember = new MemberService().loginMember(userId, userPwd);
        
        if(loginMember == null) {
            // 로그인 실패: Forward 방식
            request.setAttribute("errorMsg", "로그인에 실패하였습니다.");
            request.getRequestDispatcher("/views/common/error.jsp")
                   .forward(request, response);
        } else {
            // 로그인 성공: Redirect 방식
            HttpSession session = request.getSession();
            session.setAttribute("alertMsg", "로그인에 성공하였습니다.");
            session.setAttribute("loginMember", loginMember);
            response.sendRedirect(request.getContextPath());
        }
    }
}

실행 결과:

 
 
로그인 실패: URL은 /login.me로 유지되며 error.jsp 내용 표시
로그인 성공: URL이 /jsp로 변경되며 메인 페이지로 이동

Forward와 Redirect의 선택 기준은 요청의 성격에 따라 결정된다. 실패한 요청은 해당 URL을 유지하며 에러 정보를 표시하기 위해 Forward를 사용한다. 성공한 요청은 새로운 페이지로 완전히 이동하며 URL도 변경하기 위해 Redirect를 사용한다.


<Service 계층의 트랜잭션 관리>

Service 계층은 비즈니스 로직의 트랜잭션 경계를 관리하는 핵심 계층이다. Connection 객체의 생명주기를 제어하고 commit/rollback을 결정한다.

- 회원 정보 수정 Service 구현

 
 
java
public class MemberService {
    
    public Member updateMember(Member m) {
        // 1. Connection 획득
        Connection conn = getConnection();
        
        // 2. DAO 호출
        int result = new MemberDao().updateMember(m, conn);
        
        Member updateMember = null;
        
        // 3. 트랜잭션 처리
        if(result > 0) {
            commit(conn);
            // 4. 최신 정보 재조회
            updateMember = new MemberDao().selectMemberByUserId(m.getMemberId(), conn);
        } else {
            rollback(conn);
        }
        
        // 5. Connection 반납
        close(conn);
        
        return updateMember;
    }
}

실행 결과:

 
 
업데이트 성공: commit 수행 후 최신 Member 객체 반환
업데이트 실패: rollback 수행 후 null 반환

- 비밀번호 변경의 트랜잭션 처리

 
 
java
public Member updateMemberPwd(String memberId, String updatePwd) {
    Connection conn = getConnection();
    int result = new MemberDao().updateMemberPwd(memberId, updatePwd, conn);
    
    Member updateMember = null;
    if(result > 0) {
        commit(conn);
        updateMember = new MemberDao().selectMemberByUserId(memberId, conn);
    } else {
        rollback(conn);
    }
    
    close(conn);
    return updateMember;
}

실행 결과:

 
 
비밀번호 변경 성공: 새로운 비밀번호가 반영된 Member 객체 반환
비밀번호 변경 실패: 기존 데이터 유지 및 null 반환

Service 계층에서 재조회를 수행하는 이유는 데이터 정합성을 보장하기 위함이다. 수정 작업 후 최신 데이터를 조회하여 반환함으로써 세션의 로그인 정보와 데이터베이스의 실제 데이터가 동기화된다.


<DAO 계층의 SQL 실행>

DAO는 데이터베이스 접근 로직을 캡슐화하고 PreparedStatement를 활용하여 안전한 SQL 실행을 담당한다.

- Properties를 활용한 SQL 관리

 
 
java
public class MemberDao {
    private Properties prop = new Properties();
    
    public MemberDao() {
        String path = JDBCTemplate.class
            .getResource("/db/sql/member-mapper.xml").getPath();
        
        try {
            prop.loadFromXML(new FileInputStream(path));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

실행 결과:

 
 
member-mapper.xml 파일에서 SQL 쿼리를 로드하여 prop 객체에 저장
코드와 SQL을 분리하여 유지보수성 향상

- 회원 정보 수정 DAO 구현

 
 
java
public int updateMember(Member m, Connection conn) {
    int result = 0;
    PreparedStatement pstmt = null;
    
    String sql = prop.getProperty("updateMember");
    
    try {
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, m.getPhone());
        pstmt.setString(2, m.getEmail());
        pstmt.setString(3, m.getAddress());
        pstmt.setString(4, m.getInterest());
        pstmt.setString(5, m.getMemberId());
        
        result = pstmt.executeUpdate();
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        close(pstmt);
    }
    
    return result;
}

실행 결과:

 
 
SQL 실행 성공: 수정된 행의 개수(1) 반환
SQL 실행 실패: 0 반환 및 예외 로깅

- 로그인 검증 DAO 구현

 
 
java
public Member loginMember(String userId, String userPwd, Connection conn) {
    Member m = null;
    PreparedStatement pstmt = null;
    ResultSet rset = null;
    
    String sql = prop.getProperty("loginMember");
    
    try {
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, userId);
        pstmt.setString(2, userPwd);
        
        rset = pstmt.executeQuery();
        
        if(rset.next()) {
            m = new Member(
                rset.getInt("MEMBER_NO"),
                rset.getString("MEMBER_ID"),
                rset.getString("MEMBER_PWD"),
                rset.getString("MEMBER_NAME"),
                rset.getString("PHONE"),
                rset.getString("EMAIL"),
                rset.getString("ADDRESS"),
                rset.getString("INTEREST"),
                rset.getDate("ENROLL_DATE"),
                rset.getDate("MODIFY_DATE"),
                rset.getString("STATUS")
            );
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        close(rset);
        close(pstmt);
    }
    
    return m;
}

실행 결과:

 
 
일치하는 회원 존재: Member 객체 반환
일치하는 회원 없음: null 반환

PreparedStatement를 사용하는 이유는 SQL 인젝션 공격을 방지하고 쿼리 실행 계획을 재사용하여 성능을 향상시키기 위함이다.


<JDBCTemplate의 공통 기능>

반복적인 JDBC 작업을 메서드로 추상화하여 코드 중복을 제거하고 일관성을 유지한다.

- Connection 관리

 
 
java
public class JDBCTemplate {
    
    public static Connection getConnection() {
        Connection conn = null;
        Properties prop = new Properties();
        
        String path = JDBCTemplate.class
            .getResource("/db/driver/driver.properties").getPath();
        
        try {
            prop.load(new FileInputStream(path));
            Class.forName(prop.getProperty("driver"));
            
            conn = DriverManager.getConnection(
                prop.getProperty("url"),
                prop.getProperty("username"),
                prop.getProperty("password")
            );
            conn.setAutoCommit(false);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return conn;
    }
}

실행 결과:

 
 
Connection 객체 생성 및 AutoCommit을 false로 설정
수동 트랜잭션 관리 모드 활성화

- 트랜잭션 제어 메서드

 
 
java
public static void commit(Connection conn) {
    try {
        if(conn != null && !conn.isClosed()) {
            conn.commit();
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

public static void rollback(Connection conn) {
    try {
        if(conn != null && !conn.isClosed()) {
            conn.rollback();
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

실행 결과:

 
 
commit: 트랜잭션의 모든 변경사항을 데이터베이스에 영구 반영
rollback: 트랜잭션 시작 시점으로 데이터 복구

AutoCommit을 false로 설정하는 이유는 여러 SQL 작업을 하나의 트랜잭션으로 묶어 원자성을 보장하기 위함이다. Service 계층에서 비즈니스 로직의 성공 여부에 따라 명시적으로 commit 또는 rollback을 호출한다.


<세션을 활용한 상태 관리>

세션은 서버 측에서 사용자별 상태 정보를 유지하는 메커니즘이다. 로그인 정보와 임시 메시지를 저장하는 데 활용된다.

- 로그인 정보 세션 저장

 
 
java
HttpSession session = request.getSession();
session.setAttribute("loginMember", loginMember);
session.setAttribute("alertMsg", "로그인에 성공하였습니다.");

실행 결과:

 
 
세션 영역에 로그인 정보 저장
브라우저를 닫기 전까지 로그인 상태 유지

- 일회성 메시지 처리

 
 
jsp
<c:if test="${not empty alertMsg}">
    <script>
        alert("${alertMsg}");
    </script>
    <c:remove var="alertMsg"/>
</c:if>

실행 결과:

 
 
alertMsg가 존재하면 알림창 표시
표시 후 즉시 세션에서 제거하여 중복 표시 방지

세션과 리퀘스트 영역의 선택 기준은 데이터의 생명주기에 따라 결정된다. 여러 요청에 걸쳐 유지되어야 하는 로그인 정보는 세션에 저장하고, 단일 요청 내에서만 필요한 에러 메시지는 리퀘스트 영역에 저장한다.


정리 및 요약

MVC 패턴의 핵심은 관심사의 분리다. Controller는 요청 처리와 응답 방식 결정에 집중하고, Service는 비즈니스 로직과 트랜잭션 관리를 담당하며, DAO는 데이터 접근 로직을 캡슐화한다.

 

트랜잭션 관리는 Service 계층에서 Connection의 생명주기를 제어하며 commit/rollback을 통해 데이터 정합성을 보장한다.

 

Forward와 Redirect는 요청의 성격에 따라 선택하며, 실패는 Forward로 URL을 유지하고 성공은 Redirect로 URL을 변경한다.

 

Template는 반복적인 JDBC 코드를 static 메서드로 추상화하여 코드 중복을 제거하고 유지보수성을 향상시킨다.