Spring_02) JSP → MyBatis → Spring 계층별 구조 MVC패턴의 변화
2025. 10. 22. 14:26

학습 목표

  • JSP 프로젝트에서 Spring 프로젝트까지의 계층별 구조 변화를 이해한다
  • DAO, Service, Controller 계층의 역할과 구현 방식 변화를 파악한다
  • 각 기술 스택별 특징과 장단점을 비교 분석한다
  • 실무에서 적용 가능한 아키텍처 패턴을 학습한다

웹 애플리케이션 아키텍처의 진화

웹 애플리케이션 개발에서 가장 중요한 것은 관심사의 분리이다. 비즈니스 로직, 데이터 접근 로직, 사용자 인터페이스 로직을 명확히 구분하여 각각의 역할에 집중할 수 있도록 하는 것이 핵심이다.

MVC 패턴의 기본 원리

MVC 패턴은 Model-View-Controller의 약자로, 애플리케이션을 세 개의 주요 구성 요소로 분리한다. Model은 데이터와 비즈니스 로직을 담당하고, View는 사용자 인터페이스를 담당하며, Controller는 사용자 입력을 처리하고 Model과 View 사이의 조정 역할을 한다.

이러한 분리를 통해 각 계층은 독립적으로 개발하고 테스트할 수 있으며, 한 계층의 변경이 다른 계층에 미치는 영향을 최소화할 수 있다.

<JSP 프로젝트: 순수 JDBC 기반 구조>

DAO 계층의 특징

JSP 프로젝트에서는 순수 JDBC를 사용하여 데이터베이스와 연동한다. DAO 클래스는 직접 Connection, PreparedStatement, ResultSet을 관리해야 한다.

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();
        }
    }

    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;
    }
}
JSP 프로젝트의 DAO는 수동으로 모든 JDBC 리소스를 관리해야 한다.
- Connection 객체를 매개변수로 받아야 함
- PreparedStatement와 ResultSet을 직접 생성하고 관리
- try-catch-finally 블록으로 예외 처리
- 수동으로 리소스 해제 필요

Service 계층의 역할

Service 계층은 트랜잭션 관리비즈니스 로직을 담당한다. JDBCTemplate을 통해 Connection을 관리하고, DAO 메서드를 호출한 후 성공/실패에 따라 commit 또는 rollback을 수행한다.

public class MemberService {

    public Member loginMember(String userId, String userPwd) {
        Connection conn = getConnection();
        Member m = new MemberDao().loginMember(userId, userPwd, conn);

        close(conn);

        return m;
    }

    public int insertMember(Member m) {
        Connection conn = getConnection();

        int result = new MemberDao().insertMember(m, conn);
        if(result > 0) {
            commit(conn);
        } else {
            rollback(conn);
        }
        close(conn);

        return result;
    }
}

Controller 계층의 구현

Controller는 Servlet으로 구현되며, HTTP 요청을 처리하고 Service를 호출한 후 적절한 뷰로 포워딩하거나 리다이렉트한다.

@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) {
            request.setAttribute("errorMsg", "로그인에 실패하였습니다.");
            request.getRequestDispatcher("views/common/error.jsp").forward(request, response);
        } else {
            HttpSession session = request.getSession();
            session.setAttribute("loginMember", loginMember);
            session.setAttribute("alertMsg", "로그인 성공");

            response.sendRedirect(request.getContextPath());
        }
    }
}

<MyBatis 프로젝트: ORM 도입으로 인한 변화>

DAO 계층의 간소화

MyBatis 도입으로 DAO 계층이 크게 간소화되었다. SqlSession을 통해 SQL 매퍼 XML의 쿼리를 호출하는 방식으로 변경되었다.

public class MemberDao {
    public Member loginMember(SqlSession sqlSession, String userId, String userPwd) {
        HashMap<String, String> map = new HashMap<>();
        map.put("memberId", userId);
        map.put("memberPwd", userPwd);

        Member loginMember = sqlSession.selectOne("MemberMapper.loginMember", map);
        return loginMember;
    }

    public int insertMember(SqlSession sqlSession, Member m) {
        return sqlSession.insert("MemberMapper.insertMember", m);
    }
}

실행 결과:

MyBatis 프로젝트의 DAO는 SQL 매퍼 XML을 통해 쿼리를 실행한다.
- Connection 대신 SqlSession 사용
- SQL 쿼리는 XML 파일에 별도 관리
- ResultSet 매핑이 자동으로 처리됨
- 코드 라인 수가 크게 감소

Service 계층의 트랜잭션 관리

Service 계층에서는 SqlSession을 통해 트랜잭션을 관리한다. MyBatis의 수동 커밋 모드를 사용하여 트랜잭션을 제어한다.

public class MemberService {
    private MemberDao memberDao = new MemberDao();

    public int insertMember(Member m) {
        SqlSession sqlSession = Template.getSqlSession();

        int result = memberDao.insertMember(sqlSession, m);

        if(result > 0) {
            sqlSession.commit();
        } else {
            sqlSession.rollback();
        }
        sqlSession.close();
        return result;
    }
}

SQL 매퍼 XML의 활용

MyBatis의 핵심은 SQL 매퍼 XML이다. 복잡한 SQL 쿼리를 XML로 관리하고, 동적 쿼리와 ResultMap을 통해 객체 매핑을 자동화한다.

<!-- member-mapper.xml -->
<select id="loginMember" parameterType="map" resultType="Member">
    SELECT MEMBER_NO, MEMBER_ID, MEMBER_PWD, MEMBER_NAME, 
           PHONE, EMAIL, ADDRESS, INTEREST, ENROLL_DATE, 
           MODIFY_DATE, STATUS
    FROM MEMBER
    WHERE MEMBER_ID = #{memberId} AND MEMBER_PWD = #{memberPwd}
</select>

<Spring 프로젝트: 완전한 프레임워크 기반 구조>

Mapper 인터페이스의 도입

Spring에서는 DAO 대신 Mapper 인터페이스를 사용한다. @Mapper 어노테이션을 통해 MyBatis와 연동되며, 구현체는 프레임워크가 자동으로 생성한다.

@Mapper
public interface MemberMapper {
    Member getMemberById(@Param("memberId") String memberId);
    int getMemberCountById(@Param("memberId") String memberId);
    int addMember(Member member);
    int updateMember(Member member);
    int updatePwd(@Param("memberId") String memberId, @Param("newPwd") String newPwd);
    int deleteMember(@Param("memberId") String memberId);
}
Spring 프로젝트의 Mapper는 인터페이스로만 정의한다.
- @Mapper 어노테이션으로 MyBatis 연동
- 구현체는 프레임워크가 자동 생성
- @Param 어노테이션으로 매개변수 명시
- 트랜잭션 관리는 Spring이 자동 처리

Service 계층의 인터페이스 분리

Spring에서는 Service 계층을 인터페이스와 구현체로 분리한다. 이를 통해 의존성 주입과 테스트가 용이해진다.

public interface MemberService {
    Member getMemberById(String memberId);
    int getMemberCountById(String memberId);
    int addMember(Member member);
    int updateMember(Member member);
    int updatePwd(String memberId, String newPwd);
    int deleteMember(String memberId);
}

@Service
public class MemberServiceImpl implements MemberService {
    private final MemberMapper memberMapper;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    public MemberServiceImpl(MemberMapper memberMapper, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.memberMapper = memberMapper;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    public int addMember(Member member) {
        return memberMapper.addMember(member);
    }
}

Controller 계층의 어노테이션 기반 처리

Spring에서는 어노테이션 기반으로 Controller를 구현한다. @Controller, @PostMapping, @GetMapping 등의 어노테이션을 통해 간결하게 구현할 수 있다.

@Controller
public class MemberController {
    private final MemberService memberService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    public MemberController(MemberService memberService, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.memberService = memberService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @PostMapping("login.me")
    public ModelAndView login(String memberId, String memberPwd, HttpSession httpSession, ModelAndView mv) {
        Member loginMember = memberService.getMemberById(memberId);

        if(loginMember==null){
            mv.addObject("errorMSg", "아이디를 찾을 수 없습니다.");
            mv.setViewName("common/error");
        } else if(!bCryptPasswordEncoder.matches(memberPwd, loginMember.getMemberPwd())){
            mv.addObject("errorMSg", "비밀번호를 확인해주세요.");
            mv.setViewName("common/error");
        } else{
            httpSession.setAttribute("loginMember", loginMember);
            mv.setViewName("redirect:/");
        }
        return mv;
    }
}

계층별 구조 변화의 핵심 특징

의존성 주입의 도입

Spring에서는 의존성 주입(Dependency Injection)을 통해 객체 간의 결합도를 크게 낮췄다. 생성자 주입 방식을 통해 불변성을 보장하고 테스트를 용이하게 만들었다.

어노테이션 기반 설정

XML 설정 대신 어노테이션 기반 설정을 사용하여 코드의 가독성과 유지보수성을 향상시켰다. @Service, @Controller, @Mapper 등의 어노테이션으로 각 계층의 역할을 명확히 표현한다.

자동화된 트랜잭션 관리

Spring에서는 선언적 트랜잭션 관리를 통해 트랜잭션을 자동으로 처리한다. 개발자는 비즈니스 로직에만 집중할 수 있게 되었다.

실무 적용 및 주의사항

계층별 책임 분리

각 계층은 명확한 책임을 가져야 한다. Controller는 요청 처리와 응답 생성에만, Service는 비즈니스 로직에만, Mapper는 데이터 접근에만 집중해야 한다.

의존성 방향의 일관성

의존성은 항상 상위 계층에서 하위 계층으로만 향해야 한다. Controller → Service → Mapper 순서로 의존성이 흘러가야 하며, 역방향 의존성은 피해야 한다.

테스트 가능한 구조 설계

각 계층을 독립적으로 테스트할 수 있도록 설계해야 한다. Spring의 의존성 주입을 활용하면 Mock 객체를 쉽게 주입하여 단위 테스트를 작성할 수 있다.


결론 및 정리

  1. JSP 프로젝트: 순수 JDBC를 통한 직접적인 데이터베이스 제어로 이해하기 쉽다
  2. MyBatis 프로젝트: SQL 매퍼 XML을 통한 쿼리 관리로 유지보수성이 향상된다
  3. Spring 프로젝트: 완전한 프레임워크 기반으로 개발 생산성과 코드 품질이 크게 향상된다

웹 애플리케이션 아키텍처의 진화 과정을 통해 관심사의 분리가 얼마나 중요한지 깨달았다. 각 계층이 명확한 역할을 가지게 되면서 코드의 가독성, 유지보수성, 테스트 가능성이 모두 향상되었다.

기술의 변화를 단순히 새로운 문법을 배우는 것으로 생각하지 말고, 왜 그런 변화가 일어났는지에 대한 근본적인 이유를 이해하는 것이 중요하다. 각 기술 스택의 장단점을 파악하고 상황에 맞는 최적의 선택을 할 수 있는 능력을 가져야한다.

전체 코드 : GitHub 아이콘