5.12 DB 커넥션풀
1. 싱글 커넥션
- 지금까지 우리가 만든 애플리케이션이 바로 싱글 커넥션 시스템이다.
- 싱글 커넥션을 사용하면 여러 개의 DAO가 같은 커넥션을 바라보게 됨
- 하나라도 롤백 작업이 일어나면 모든 작업이 취소가 됨
2. 싱글 커넥션 문제점과 해결책
1) 문제점
- 싱글 커넥션을 사용하면 같은 커넥션을 이용하는 DAO들의 작업에 영향을 준다.
2) 해결책1
- 하나의 DAO 객체마다 새로운 커넥션을 할당받아서 사용하는 시스템
- 이 방식의 문제점 : 일을 하지 않을 때도 커넥션을 물고 있어서 자원 낭비
3) 해결책2 (진짜 해결책)
- 한정된 자원을 효율적으로 사용하는 방법 : 대여 = 풀링(pooling) 기법
- DB 커넥션풀에다가 DB커넥션을 여러 개 만들어놓고 커넥션 요청이 올 때마다 빌려주고 끝나면 반환받는 시스템
3. DB 커넥션풀
1. 커넥션 풀( Connection pool ) 의미
커넥션 풀이란 DB와 미리 connection( 연결 )을 해놓은 객체들을 pool( 웅덩이 )에 저장해두었다가
클라이언트 요청이 오면 커넥션을 빌려주고, 볼일이 끝나면 다시 커넥션을 반납 받아 pool에 저장하는 방식 (풀링 방식)
- ProjectDao가 커넥션을 요청해서 DB커넥션풀이 100번 커넥션을 빌려줌
- ProjectDao는 100번 커넥션으로 작업을 진행하고 도중에 MemberDao도 커넥션 요청을 하게 됨
- 이미 만들어진 101번 커넥션을 MemberDao에게 빌려줌
- ProjectDao가 작업이 끝나서 100번 커넥션 풀을 반납하면 DB커넥션풀은 100번 커넥션을 보관한다.
- TaskDao가 커넥션 요청을 한 경우 DB커넥션풀에 보관되어 있던 100번 커넥션을 TaskDao에게 빌려주게 됨
- 커넥션을 다시 쓸 수 있도록 하는게 바로 풀링 시스템.
4. DB 커넥션풀 실습
DBConnectionPool.java
package spms.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.ArrayList;
public class DBConnectionPool {
//미리 커넥션을 만들어 놓을 개수
final int PRE_POOL_SIZE = 10;
String url;
String username;
String password;
//커넥션을 담을 리스트
ArrayList<Connection> connList = new ArrayList<Connection>();
public DBConnectionPool(String driver, String url, String username, String password) throws Exception {
this.url = url;
this.username = username;
this.password = password;
Class.forName(driver);
//미리 PRE_POOL_SIZE만큼 커넥션 생성
for(int i = 0; i < PRE_POOL_SIZE; i++) {
connList.add(DriverManager.getConnection(url, username, password));
}
}
//Connection 객체 요청 시 Connection 대여
public Connection getConnection() throws Exception {
//현재 만들어진 커넥션 풀에 여유분이 존재하면
if(connList.size() > 0) {
Connection conn = connList.remove(0);
//DB 커넥션이 유효하면 꺼낸 커넥션 리턴
if(conn.isValid(10)) {
return conn;
}
}
//커넥션 풀에 여유분이 없을 경우 새로 커넥션을 만들어서 리턴
return DriverManager.getConnection(url, username, password);
}
//빌려준 커넥션을 반환
public void returnConnection(Connection conn) throws Exception {
if(conn != null && conn.isClosed() == false) {
connList.add(conn);
}
}
//어플리케이션 종료 시 모든 Connection 종료
public void closeAll() {
System.out.println("connList.size()============" + connList.size());
for(Connection conn : connList) {
try {
conn.close();
} catch(Exception e) {
e.printStackTrace();
}
}
}
}
ContextLoaderListener.java
package spms.listener;
import java.sql.DriverManager;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import spms.dao.MemberDAO;
import spms.util.DBConnectionPool;
public class ContextLoaderListener implements ServletContextListener {
// Connection conn;
// 원래 만들어놓은 커넥션을 커넥션풀로 만들어주기
DBConnectionPool connPool;
@Override
public void contextInitialized(ServletContextEvent sce) {
//웹 어플리케이션이 실행되면 자동으로 DB커넥션 생성 및 MemeberDAO객체 생성
try {
System.out.println("contextInitialized");
ServletContext sc = sce.getServletContext();
Class.forName(sc.getInitParameter("driver"));
// conn = DriverManager.getConnection(sc.getInitParameter("url"),
// sc.getInitParameter("username"),
// sc.getInitParameter("password"));
connPool = new DBConnectionPool(
sc.getInitParameter("driver"),
sc.getInitParameter("url"),
sc.getInitParameter("username"),
sc.getInitParameter("password"));
MemberDAO memberDAO = new MemberDAO();
memberDAO.setDBConnectionPool(connPool);
//생성된 MemberDAO객체를 ServletContext 데이터 보관소를 통해 서블릿끼리 공유
sc.setAttribute("memberDAO", memberDAO);
} catch(Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
try {
System.out.println("contextDestroyed");
connPool.closeAll();
} catch(Exception e) {
e.printStackTrace();
}
}
}
MemberDAO.java
package spms.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import spms.util.DBConnectionPool;
import spms.vo.Member;
public class MemberDAO {
/*
* Connection connection;
*
* //DAO객체는 servlet이 아니기 때문에 servletcontext에 있는 커넥션 직접 접근 불가능
* //memberlistServlet에서 커넥션을 객체를 DAO에 주입해줄 것 public void
* setConnection(Connection connection) { this.connection = connection; }
*/
DBConnectionPool connPool;
public void setDBConnectionPool(DBConnectionPool connPool) {
this.connPool = connPool;
}
public List<Member> selectlist() throws Exception {
Connection connection = null;
Statement stmt = null;
ResultSet rs = null;
String sqlSelect = "SELECT * FROM MEMBERS ORDER BY MNO ASC";
try {
connection = connPool.getConnection();
stmt = connection.createStatement();
rs = stmt.executeQuery(sqlSelect);
ArrayList<Member> members = new ArrayList<Member>();
while(rs.next()) {
members.add(new Member()
.setNo(rs.getInt("MNO"))
.setName(rs.getString("MNAME"))
.setEmail(rs.getString("EMAIL"))
.setCreateDate(rs.getDate("CRE_DATE")));
}
return members;
} catch(Exception e) {
throw e;
} finally {
try {
if(rs != null) {
rs.close();
}
} catch(Exception e) {
e.printStackTrace();
}
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
if(connection != null) {
connPool.returnConnection(connection);
}
}
}
//MemberAddServlet에서 입력 폼으로 입력받은 데이터를 member객체로 담아서
//DAO로 전달할 예정
public int insert(Member member) throws Exception {
int result = 0;
Connection connection = null;
PreparedStatement stmt = null;
final String sqlInsert = "INSERT INTO MEMBERS(EMAIL, PWD, MNAME, CRE_DATE, MOD_DATE)" +
"VALUES(?, ?, ?, NOW(), NOW())";
try {
connection = connPool.getConnection();
stmt = connection.prepareStatement(sqlInsert);
stmt.setString(1, member.getEmail());
stmt.setString(2, member.getPassword());
stmt.setString(3, member.getName());
//insert 성공하면 1 int 값 리턴
result = stmt.executeUpdate();
} catch(Exception e) {
throw e;
} finally {
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
if(connection != null) {
connPool.returnConnection(connection);
}
}
return result;
}
public int delete(int no) throws Exception {
int result = 0;
Connection connection = null;
Statement stmt = null;
final String sqlDelete = "DELETE FROM MEMBERS WHERE MNO=" + no;
try {
connection = connPool.getConnection();
stmt = connection.createStatement();
result = stmt.executeUpdate(sqlDelete);
} catch(Exception e) {
throw e;
} finally {
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
if(connection != null) {
connPool.returnConnection(connection);
}
}
return result;
}
//해당 멤버 데이터 조회
public Member selectOne(int no) throws Exception {
Member member = null;
Connection connection = null;
Statement stmt = null;
ResultSet rs = null;
final String sqlSelectOne = "SELECT * FROM MEMBERS WHERE MNO=" + no;
try {
connection = connPool.getConnection();
stmt = connection.createStatement();
rs = stmt.executeQuery(sqlSelectOne);
if(rs.next()) {
member = new Member()
.setNo(rs.getInt("MNO"))
.setEmail(rs.getString("EMAIL"))
.setName(rs.getString("MNAME"))
.setCreateDate(rs.getDate("CRE_DATE"));
} else {
throw new Exception("해당 번호의 회원을 찾을 수 없습니다.");
}
} catch(Exception e) {
throw e;
} finally {
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
if(connection != null) {
connPool.returnConnection(connection);
}
}
return member;
}
//해당 멤버 데이터 수정
public int update(Member member) throws Exception {
int result = 0;
Connection connection = null;
PreparedStatement stmt = null;
final String sqlUpdate = "UPDATE MEMBERS SET EMAIL=?, MNAME=?, MOD_DATE=NOW() WHERE MNO=?";
try {
connection = connPool.getConnection();
stmt = connection.prepareStatement(sqlUpdate);
stmt.setString(1, member.getEmail());
stmt.setString(2, member.getName());
stmt.setInt(3, member.getNo());
result = stmt.executeUpdate();
} catch(Exception e) {
throw e;
} finally {
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
if(connection != null) {
connPool.returnConnection(connection);
}
}
return result;
}
public Member exist(String email, String password) throws Exception {
Member member = null;
Connection connection = null;
PreparedStatement stmt = null;
ResultSet rs = null;
final String sqlExist = "SELECT * FROM MEMBERS WHERE EMAIL=? AND PWD=?";
try {
connection = connPool.getConnection();
stmt = connection.prepareStatement(sqlExist);
stmt.setString(1, email);
stmt.setString(2, password);
rs = stmt.executeQuery();
if(rs.next()) {
member = new Member()
.setName(rs.getString("MNAME"))
.setEmail(rs.getString("EMAIL"));
} else {
return null;
}
} catch(Exception e) {
throw e;
} finally {
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
if(connection != null) {
connPool.returnConnection(connection);
}
}
return member;
}
}
- 하나의 서버로 10명이 접속해보기
- 커넥션을 요청하면 커넥션리스트에서 하나씩 꺼내서 커넥션을 리턴해줌
- 커넥션풀에 여유분이 없으면 새로 만들어서 리턴
5. 데이터 소스 이용하여 톰캣에서 DB커넥션 관리 실습
- 데이터베이스 인터페이스를 상속받아 객체 풀링을 자동 처리함
- 장점 : 활용성이 좋음, 범용적
1) ContextLoaderListener.java
package spms.listener;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.apache.commons.dbcp.BasicDataSource;
import spms.dao.MemberDAO;
import spms.util.DBConnectionPool;
public class ContextLoaderListener implements ServletContextListener {
BasicDataSource ds;
@Override
public void contextInitialized(ServletContextEvent sce) {
//웹 어플리케이션이 실행되면 자동으로 DB커넥션 생성 및 MemeberDAO객체 생성
try {
System.out.println("contextInitialized");
ServletContext sc = sce.getServletContext();
Class.forName(sc.getInitParameter("driver"));
ds = new BasicDataSource();
ds.setDriverClassName(sc.getInitParameter("driver"));
ds.setUrl(sc.getInitParameter("url"));
ds.setUsername(sc.getInitParameter("username"));
ds.setPassword(sc.getInitParameter("password"));
MemberDAO memberDAO = new MemberDAO();
memberDAO.setDataSource(ds);
//생성된 MemberDAO객체를 ServletContext 데이터 보관소를 통해 서블릿끼리 공유
sc.setAttribute("memberDAO", memberDAO);
} catch(Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
try {
System.out.println("contextDestroyed");
if(ds != null) {
ds.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
2) MemberDAO.java
package spms.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import spms.vo.Member;
public class MemberDAO {
/*
* Connection connection;
*
* //DAO객체는 servlet이 아니기 때문에 servletcontext에 있는 커넥션 직접 접근 불가능
* //memberlistServlet에서 커넥션을 객체를 DAO에 주입해줄 것 public void
* setConnection(Connection connection) { this.connection = connection; }
*/
DataSource ds;
public void setDataSource(DataSource ds) {
this.ds = ds;
}
public List<Member> selectlist() throws Exception {
Connection connection = null;
Statement stmt = null;
ResultSet rs = null;
String sqlSelect = "SELECT * FROM MEMBERS ORDER BY MNO ASC";
try {
connection = ds.getConnection();
stmt = connection.createStatement();
rs = stmt.executeQuery(sqlSelect);
ArrayList<Member> members = new ArrayList<Member>();
while(rs.next()) {
members.add(new Member()
.setNo(rs.getInt("MNO"))
.setName(rs.getString("MNAME"))
.setEmail(rs.getString("EMAIL"))
.setCreateDate(rs.getDate("CRE_DATE")));
}
return members;
} catch(Exception e) {
throw e;
} finally {
try {
if(rs != null) {
rs.close();
}
} catch(Exception e) {
e.printStackTrace();
}
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
try {
if(connection != null) {
connection.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
//MemberAddServlet에서 입력 폼으로 입력받은 데이터를 member객체로 담아서
//DAO로 전달할 예정
public int insert(Member member) throws Exception {
int result = 0;
Connection connection = null;
PreparedStatement stmt = null;
final String sqlInsert = "INSERT INTO MEMBERS(EMAIL, PWD, MNAME, CRE_DATE, MOD_DATE)" +
"VALUES(?, ?, ?, NOW(), NOW())";
try {
connection = ds.getConnection();
stmt = connection.prepareStatement(sqlInsert);
stmt.setString(1, member.getEmail());
stmt.setString(2, member.getPassword());
stmt.setString(3, member.getName());
//insert 성공하면 1 int 값 리턴
result = stmt.executeUpdate();
} catch(Exception e) {
throw e;
} finally {
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
try {
if(connection != null) {
connection.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
return result;
}
public int delete(int no) throws Exception {
int result = 0;
Connection connection = null;
Statement stmt = null;
final String sqlDelete = "DELETE FROM MEMBERS WHERE MNO=" + no;
try {
connection = ds.getConnection();
stmt = connection.createStatement();
result = stmt.executeUpdate(sqlDelete);
} catch(Exception e) {
throw e;
} finally {
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
try {
if(connection != null) {
connection.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
return result;
}
//해당 멤버 데이터 조회
public Member selectOne(int no) throws Exception {
Member member = null;
Connection connection = null;
Statement stmt = null;
ResultSet rs = null;
final String sqlSelectOne = "SELECT * FROM MEMBERS WHERE MNO=" + no;
try {
connection = ds.getConnection();
stmt = connection.createStatement();
rs = stmt.executeQuery(sqlSelectOne);
if(rs.next()) {
member = new Member()
.setNo(rs.getInt("MNO"))
.setEmail(rs.getString("EMAIL"))
.setName(rs.getString("MNAME"))
.setCreateDate(rs.getDate("CRE_DATE"));
} else {
throw new Exception("해당 번호의 회원을 찾을 수 없습니다.");
}
} catch(Exception e) {
throw e;
} finally {
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
try {
if(connection != null) {
connection.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
return member;
}
//해당 멤버 데이터 수정
public int update(Member member) throws Exception {
int result = 0;
Connection connection = null;
PreparedStatement stmt = null;
final String sqlUpdate = "UPDATE MEMBERS SET EMAIL=?, MNAME=?, MOD_DATE=NOW() WHERE MNO=?";
try {
connection = ds.getConnection();
stmt = connection.prepareStatement(sqlUpdate);
stmt.setString(1, member.getEmail());
stmt.setString(2, member.getName());
stmt.setInt(3, member.getNo());
result = stmt.executeUpdate();
} catch(Exception e) {
throw e;
} finally {
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
try {
if(connection != null) {
connection.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
return result;
}
public Member exist(String email, String password) throws Exception {
Member member = null;
Connection connection = null;
PreparedStatement stmt = null;
ResultSet rs = null;
final String sqlExist = "SELECT * FROM MEMBERS WHERE EMAIL=? AND PWD=?";
try {
connection = ds.getConnection();
stmt = connection.prepareStatement(sqlExist);
stmt.setString(1, email);
stmt.setString(2, password);
rs = stmt.executeQuery();
if(rs.next()) {
member = new Member()
.setName(rs.getString("MNAME"))
.setEmail(rs.getString("EMAIL"));
} else {
return null;
}
} catch(Exception e) {
throw e;
} finally {
try {
if(stmt != null) {
stmt.close();
}
} catch(Exception e) {
e.printStackTrace();
}
try {
if(connection != null) {
connection.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
return member;
}
}
3) Servers > Tomcat > context.xml
<!-- 톰캣 데이터 소스 설정 -->
<Resource name="jdbc/studydb" auth="Container" type="javax.sql.DataSource"
MaxActive="10"
MaxIdle="3"
maxWait="10000"
username="study"
password="study"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc://mysql://localhost/studydb"
closeMethod="close">
</Resource>
- name : JNDI 이름. Context의 lookup() 메소드 사용하여 자원을 찾을 때 사용
- auth : 데이터 소스 관리 주체 (Container|Application)를 설정 (우리는 Container에서 관리)
- type : 자원의 타입 설정
- MaxActive : 최대 커넥션 개수 설정 (몇개까지 커넥션을 유지할지)
- maxIdle : 최대 유지 가능한 사용하지 않는 커넥션 개수.
설정한 개수보다 많은 커넥션이 반환되면 반환되는 커넥션은 종료
(3개 이상의 커넥션이 사용되지않을때 4번째 커넥션을 반환)
- maxWait : 연결 가능한 커넥션이 최대값에 도달했을 때 기다리는 시간(밀리초). 예) 10000 -> 10초
기다리는 시간동안 반환되는 커넥션이 없으면 예외를 던짐
- close : 톰캣 서버 종료 시 자원 해제를 위해 호출할 메소드 설정
* 사용자가 많지 않은 경우 3~4개의 커넥션으로도 충분
4) web.xml에 데이터소스 참조하는 코드인 resource-ref 추가
<!-- 데이터소스 참조 -->
<resource-ref>
<res-ref-name>jdbc/studydb</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
5) JNDI (Java Naming and Directory Interface) 의미
- 디렉터리 서비스에서 제공하는 데이터 및 객체를 발견(discover)하고 참고(lookup)하기 위한 자바 API다.
- WAS의 자원에 대한 고유 이름 정의
- 어플리케이션에서 서버 리소스를 접근할 때 사용하는 명명 규칙
* 1) java:comp/env - 응용 프로그램 환경 항목
* 2) java:comp/env/jdbc - JDBC 데이터 소스
* 3) java:comp/ejb - EJB 컴포넌트
* 4) java:comp/UserTransaction - UserTransaction 객첵
* 5) java:comp/env/mail - JavaMail 연결 객체
* 6) java:comp/env/url - URL 정보
* 7) java:comp/env/jms - JMS 연결 객체
6) ContextLoaderListener.java
package spms.listener;
import javax.naming.InitialContext;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import spms.dao.MemberDAO;
public class ContextLoaderListener implements ServletContextListener {
//datasource의 장점 커넥션풀을 톰캣에서 지원해주기 때문에
//개발자가 직접 connection pool 객체를 만들어 줄 필요가 없다
//BasicDataSource ds;
@Override
public void contextInitialized(ServletContextEvent sce) {
//웹 어플리케이션이 실행되면 자동으로 DB커넥션 생성 및 MemeberDAO객체 생성
try {
System.out.println("contextInitialized");
ServletContext sc = sce.getServletContext();
//톰캣 서버에서 자원(커넥션)을 찾기 위한 InitailContext 객체 생성
InitialContext initialcontext = new InitialContext();
/*
* JNDI 사용
* WAS의 자원에 대한 고유 이름 정의
* 어플리케이션에서 서버 리소스를 접근할 때 사용하는 명명 규칙(톰캣)
* 서버마다 명명 규칙은 다름 (제우스는 또다른 규칙이 있음)
*
* 1) java:comp/env - 응용 프로그램 환경 항목
* 2) java:comp/env/jdbc - JDBC 데이터 소스
* 3) java:comp/ejb - EJB 컴포넌트
* 4) java:comp/UserTransaction - UserTransaction 객첵
* 5) java:comp/env/mail - JavaMail 연결 객체
* 6) java:comp/env/url - URL 정보
* 7) java:comp/env/jms - JMS 연결 객체
*
* */
DataSource ds = (DataSource)initialcontext.lookup("java:comp/env/jdbc/studydb");
/*
* ds = new BasicDataSource();
* ds.setDriverClassName(sc.getInitParameter("driver"));
* ds.setUrl(sc.getInitParameter("url"));
* ds.setUsername(sc.getInitParameter("username"));
* ds.setPassword(sc.getInitParameter("password"));
*/
MemberDAO memberDAO = new MemberDAO();
memberDAO.setDataSource(ds);
//생성된 MemberDAO객체를 ServletContext 데이터 보관소를 통해 서블릿끼리 공유
sc.setAttribute("memberDAO", memberDAO);
} catch(Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
try {
System.out.println("contextDestroyed");
} catch(Exception e) {
e.printStackTrace();
}
}
}
7) 실행 화면
'👨💻 2. 웹개발_Back end > 2-4 JSP & Servlet' 카테고리의 다른 글
[JSP & Servlet] 6장 미니 MVC 프레임워크 만들기 (2) 페이지 컨트롤러의 진화 (0) | 2021.10.18 |
---|---|
[JSP & Servlet] 6장 미니 MVC 프레임워크 만들기 (1) 프런트 컨트롤러 (1) | 2021.10.18 |
[JSP & Servlet] 5장 MVC 아키텍처 (6) - ServletContextListener를 이용한 객체 공유 (0) | 2021.10.15 |
[JSP & Servlet] 5장 MVC 아키텍처 (5) - DAO객체 만들기 (0) | 2021.10.14 |
[JSP & Servlet] 5장 MVC 아키텍처 (4) - 뷰와 서블릿 분리 및 JSP 컨텍스트와 Action Tag (1) | 2021.10.14 |
댓글