- 파일 다운로드 기능 마무리
- 다수의 파일 업로드, 다운로드
- DBCP 적용 : 싱글톤 패턴, JNVI
지난 시간까지 파일 다운로드를 할 때, <a>
태그를 동적 바인딩해서 파일의 주소로 다운로드를 할 수 있도록 설정했다.
하지만 이 방식을 사용하게 되면, 브라우저가 서버를 통해 파일로 직접 요청으로 끌고 와서 중간에 Controller를 통한 비즈니스 로직 구현이 불가능하다. (다운로드 통계)
//다운로드의 기본 전제는 서버가 클라이언트가 어떤 파일을 선택했는지 알고 있다는 것이다.
<a>
태그의 속성 변경anker.attr("href", "/download.file");
anker.attr("href", "/download.file?sysName="+resp[i].sysName+"&oriName="+resp[i].oriName);
String sysName = request.getParameter("sysName");
String oriName = request.getParameter("oriName");
oriName = new String(oriName.getBytes("utf8"), "ISO-8859-1"); // 파일 이름 깨짐 방지
리얼 패스 내부에 파일이 존재하는 경로를 가져온다.
String filePath = request.getServletContext().getRealPath("files");
File target = new File(filePath+"/"+sysName);
response.reset();
response.setHeader("Content-Disposition", "attachment;filename=\""+ oriName +"\"");
response 객체의 Content-Disposition
속성의 값을 세팅하는데, 이는 클라이언트에서 다운받을 때, 해당 파일의 이름을 정하는 것이다.
해당 파일의 이름을 oriName이 아닌, sysName으로 해버리면 둘의 차이로 인해 클라이언트는 자신이 원한 파일을 받았다고 인식을 못할 수 있다. 따라서 파일이름은 oriName(클라이언트에서 정한 파일명)을 사용한다.,
(1) 하드디스크 → 램
byte[] fileContents = new byte[(int)target.length()];
DataInputStream dis = new DataInputStream(new FileInputStream(target));
(2) RAM → 네트워크 → 클라이언트 하드디스크
ServletOutputStream sos = response.getOutputStream();
// RAM으로 로딩 된 파일의 내용을 네트워크(response)로 전송하기 위한 스트림
dis.readFully(fileContents);
sos.write(fileContents);
sos.flush(); // 버퍼 비우기
앞서 구현한 기능은 하나의 파일만 올리고, 내려받을 수 있다. 그러나 실 서비스는 여러 파일을 자유롭게 올리고, 조건에 따라 압축파일로 변환해서 내려받는다.
그래서 여러 개의 파일을 다루는 기능을 구현하고자 한다. 일단 두 가지 방법이 있는데, 첫째는 ‘아파치 라이브러리’를 바꾸기만 하면 된다. 그리고 업로드 시, <input type=“file” name=file mutiple>
로 보내면 요청을 분석해서 관리할 수 있다.
하지만 현재 쓰는 COS.jar
는 업로드는 가능하지만, 이를 분석해서 응용할 수가 없다. 즉, 들어온 파일 정보를 DAO를 통해 DB에 입력할 수 없다. 그래서 불편하지만 다음과 같이 사용한다.
<form action=“upload.file” method=“post” enctype=“multipart/form-data”>
<input type=“text” name=“file1”>
<input type=“text” name=“file2”>
<input type=“text” name=“file3”>
</form>
이렇게 3개를 넣고 싶으면, 3개의 <input>
을 만들고 name
으로 각 파일을 구분해줘야 한다. 더 나아가 가변적으로 업로드 파일을 늘리려면, 동적 바인딩을 한다.
무튼 넘어온 파일을 이름으로 구분되고, 업그레이드한 리퀘스트 객체에서 뽑아서 DB에 입력한다.
String oriName1 = multi.getOriginalFileName(“file1”);
String sysName1 = multi.getFilesystemName(“file1”);
String oriName2 = multi.getOriginalFileName(“file2”);
String sysName2 = multi.getFilesystemName(“file2”);
String oriName3 = multi.getOriginalFileName(“file3”);
String sysName3 = multi.getFilesystemName(“file3”);
DB에 접속하는 연결량을 제한이 되어 있다. 그러나 서비스마다 DB가 할당되는 것이 아니라, 여러 서비스가 하나의 DB를 사용한다. 따라서 DB 연결을 끊어지지 않게 다루는 것이 매우 중요하다.
하지만 여러 서비스-다수의 사용자가 합쳐지면, 우리가 개발 단계에서 생각하지 못한 사건 사고들이 일어난다. 개발자들은 그런 상황을 최소화하기 위한 답을 찾아냈으니, 그것이 ‘DBCP’이다.
DBCP는 애플리케이션 실행 시, 일정량의 연결을 미리 생성하여 사용자와 DB의 연결을 순차적으로 처리하는 기법이다. 예를 들어 30개의 연결을 미리 생성하고 60명의 사용자가 있다면, 1-30등을 먼저 연결하고 끝날 때마다 31~60등을 순차적으로 연결해준다.
하지만 기존에 배웠던 DBCP는 문제점이 있다.
private BasicDataSource bds = new BasicDataSource();
private CoffeeDAO() { // 1. 애초에 생성자를 못쓰게 private로 막아버린다.
this.bds = new BasicDataSource(); // lib에 라이브러리를 넣어줘야됨
bds.setInitialSize(30);
bds.setUsername("ID");
bds.setPassword("PW ");
bds.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
bds.setDriverClassName("oracle.jdbc.driver.OracleDriver");
}
private Connection getConnection() throws Exception{
return bds.getConnection();
}
설명을 쉽게 하기 위해 접속량을 ‘연결선’으로 대체하겠다.
위 방식은 30개의 연결선만 사용했다. 그래서 DB 접속 자체는 문제가 없다. 그러나 연결선을 가진 소유자인 인스턴스(BasicDataSource)가 사용자가 DAO 인스턴스를 생성할 때마다 함께 만들어지도록 했다.
결과적으로 30개의 연결선은 DAO가 3번 만들어지면 90개가 되고, 4번이면 120개, 이런 식으로 증가하게 되어, 과접속 문제는 여전하다.
이를 해결하기 위한 방법은 단순하다.
사용자가 서비스를 사용할 때마다, DAO가 만들어졌다면 이제는 하나의 DAO를 모든 사용자가 사용하도록 만든다.
바로 그게 ‘싱글톤 패턴’이다. 근데 어떻게 한 개만 생성하도록 해야 될까?
우리가 인스턴스를 생성하려면, new
를 통해서 heap 메모리 위에 올려야 하는데 말이다.
이제 관점을 바꿔보자. 우리는 1개의 인스턴스만 있으면 되고, 문제는 서비스 실행마다 new
가 동작하는 것이다.
그럼 new가 반복적으로 사용되는 것만 막으면 문제가 해결되지 않을까? 맞다.
// DB 접속량을 관리하는 객체
private BasicDataSource bds = new BasicDataSource();
private DAO() {
this.bds = new BasicDataSource();
bds.setInitialSize(30);
bds.setUsername("kh");
bds.setPassword("kh");
bds.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
bds.setDriverClassName("oracle.jdbc.driver.OracleDriver");
}
이렇게 만들어두면, 외부에서 생성자를 사용할 수 없게 된다. 오직 클래스 내부에서만 인스턴스를 생성할 수 있다.
private static DAO instance = null;
위 변수는 클래스 변수이기 때문에, 인스턴스화 없이 언제나 사용될 수 있다. 동시에 인스턴스를 사용하기 위한 참조변수이다.
public synchronized static DAO getInstance() {
if(instance == null) {
instance = new DAO();
}
return instance;
}
위 메서드 역시, 클래스 메서드이기 때문에 인스턴스화 없이 사용할 수 있다. 그래서 원래 DAO외부에서 DAO를 생성하는 시점에서 해당 메서드를 호출하면 된다.
그리고 위의 instance
변수가 null
인 경우 DAO를 생성하여 반환하다. 따라서 하나의 인스턴스를 모두가 사용할 수 있게 되었다.
여기서 중요한 포인트가 하나 있다. 바로 synchronized
이다.
일반적으로 컴퓨터가 순차적으로 작업을 처리하지만, 멀티 스레드 환경에서는 이전 작업이 끝나기 전에, 실행이 될 수 있는 ‘동시성 이슈’가 존재한다. 즉, 직전 작업의 결과가 적용되지 않았다는 것이다.
그래서 DAO 인스턴스를 생성했음에도, 그 결과가 반영되지 않아 또 생성될 수 있다. 따라서 직전 작업 결과가 반영되었을 때, 다음 작업이 실행되도록 synchronized
를 통해 block I/O 구조를 적용한다.
여기까지만 하면 다한 것처럼 보이지만, 마지막 한 문제가 있다.
일단 싱글톤으로 DAO는 한 개만 생성되고, 이 인스턴스가 연결선들을 갖는다. 그러나 프로젝트에는 DAO가 여러 개다. 이제는 이름과 역할이 다른 DAO가 동일 DB의 연결선들을 갖게 된다. 따라서 연결 객체(데이터 소스) 또한 1개만 존재해야 한다.
그럼 뭐가 문제일까..일단 DAO마다 연결객체를 만드는데, 그 안에는 DB접근을 위한 정보들이 담겨있다. 이걸 뭐 다른 곳을 돌려야 하는데, DAO 말고 어디에 둬야할 것인가? 그리고 연결이 빠지면 솔직히 앞선 문제들이 모두 필요가 없어진다.
이 문제를 극복하기 위해, 개발자들은 연결 객체(이하 데이터 소스) DAO가 아닌 톰캣 서버 자체로 넘겨보인다. 즉, 요청을 직접적으로 받는 서버가 연결도 만들어서 DAO로 던져주라는 것이다.
<Resource auth="Container"
name="jdbc/orcl"
driverClassName="oracle.jdbc.driver.OracleDriver"
username="ID"
password="PW"
url="jdbc:oracle:thin:@localhost:1521:xe"
type="javax.sql.DataSource"
/>
서버를 관리하는 정보가 담긴 XML 파일로 이동한다. XML은 데이터를 전달하는 문서로서, 위처럼 직렬화하여 DAO에게 전해주는 것이다.
그리고 DAO에서는 다음과 같이 데이터를 역직렬화 하여 사용한다.
private Connection getConnection() throws Exception{
Context ctx = new InitialContext(); // 톰캣의 환경 설정 정보를 가지고 있는 객체
// object로 반환함. 다운캐스팅 필요
DataSource dSource = (DataSource)ctx.lookup("java:comp/env/jdbc/orcl");
return dSource.getConnection();
}