JAVA 웹개발 과정 국비 29일차 정리

29일차

※ 배우는 과정이므로 정확하지 않을 수 있습니다.
잘못된 점은 알려주시면 수정하도록 하겠습니다!


어제 만든 프로그램에 방명록 시스템을 더해보았다.
로그인을 하고 나면 방명록 시스템을 이용할 수 있다.
대부분 어제 했던 부분의 연장선이었으므로 중복된 내용은 생략하고 새롭게 배운 부분만 정리한다.

1. 방명록 시스템 - 예제

1) 서버에서의 if문 안에 if문 사용

방명록 시스템은 로그인 한 회원 한정으로 사용 가능하다.
따라서 클라이언트 클래스에서는 로그인 성공한 if문 안에 방명록 시스템을 출력하였다.

int loginResult = dis.readInt();
if (loginResult>0) {
	System.out.println("로그인 성공!");
	
	while(true) {
		System.out.println("== 방명록 시스템 ==");
		System.out.println("1. 새 글 등록");
		System.out.println("2. 방명록 보기");
		System.out.println("3. 방명록 삭제");
		System.out.println("4. 방명록 검색");

그렇다면 서버도 똑같이 login if문 안에 작성해줘야할까?
사실 그렇게해도 상관 없지만, 아래와 같이 서브 메뉴 (방명록 시스템 메뉴)를 받을 때도 else if문으로 밖으로 빼주는 것이 좋다.
서브 메뉴가 계속 if문을 타고 내려가면 가독성도 좋지 않고 메뉴를 리셋하는 while문을 어디서 어디까지 써야하는지가 헷갈릴 수 있기 때문이다.

2) 서버 사이드 렌더링 & 클라이언트 사이드 렌더링

2번 방명록 보기 기능을 구현하는 중에 두 가지 방법 중 어느것이 더 좋을지 고민을 했다.
SELECT 문의 결과를 ArrayList로 받아서 서버로 가져온 것까지는 좋았는데, 인스턴스 변수를 각각 받아와서 서버에서 클라이언트로 넘기는 방법이 나은지 String으로 한번에 묶어서 그 문자열을 클라이언트로 넘기는 것이 나은지 말이다.

처음에 생각했을 때는 서버에 최대한 부하를 주지 않는 방법이라면 String으로 한번에 묶어서 클라이언트로 내보내는게 한번만 값을 전달하는 것이기 때문에 더 좋지않을까? 라고 생각했는데, 바로 여기서 서버 사이드 렌더링과 클라이언트 사이드 렌더링 개념을 배웠다.

  • 서버 사이드 렌더링
    서버에서 출력값을 만들어서 클라이언트로 넘기는 방법
// Sercer Class
for (GuestbookDTO dto : list) {
	dos.writeUTF(dto.getSeq()+":"+dto.getWriter()+":"+
				dto.getMessage()+":"+dto.getWrite_date());
	dos.flush();
}
// Client Class
for (int i=0;i<listSize;i++){
	System.out.println(dis.readUTF());
}
  • 클라이언트 사이드 렌더링
    서버에서는 값만 넘기고, 클라이언트에서 값을 가공하여 출력하는 방법
// Server Class
for (GuestbookDTO dto : list) {
	dos.writeInt(dto.getSeq());
	dos.writeUTF(dto.getWriter());
	dos.writeUTF(dto.getMessage());
	dos.writeUTF(dto.dateToString(dto.getWrite_date()));
	dos.flush();
}
// Client Class
for (int i=0;i<listSize;i++) {
	int seq = dis.readInt();
	String writer = dis.readUTF();
	String message = dis.readUTF();
	String write_date = dis.readUTF();
	System.out.println(seq+":"+writer+":"+message+":"+write_date);
}

클라이언트 사이드 렌더링이 코드도 길고 복잡해보이지만, 서버에는 부하가 적기 때문에 최근에 많이 사용하고 있다고 한다.

2. DBCP (DataBase Connection Pool) Library

1) Connection Pool 이란?

DBMS는 Connection의 제한이 약 100개 정도라고 생각하면 된다. 한명의 사용자가 Connection을 이용하는 시간은 0.1초 정도로 아주 짧은 순간이기 때문에 다시 말하면 이 짧은 순간에 동시에 100명이 넘는 사람들이 한꺼번에 Connection을 이용해야 문제가 생길정도로 아주 넉넉한 것이다.

그러나 정~말 많은 사람들이 이용하는 시스템이라면? Connection 제한 수를 넘어섰을 때 어떻게 될까?

  1. 만 명이 서비스를 이용하는 와중에 100번째 Connection까지 다 써버림.
  2. 101번째 고객이 기능을 사용하려고 한다.
  3. 시스템의 DBMS는 예외를 발생시킨다.
  4. 101번 사용자는 에러페이지를 마주하게 된다.
  5. 101번 사용자는 당황했지만 다시 접속하려고 재시도를 한다.
  6. 그 때 100개의 Connection 중에 자리가 있다면 접속 성공, 없다면 또 에러페이지로 넘어간다.
  7. 이런 상황이 여러 사용자에게 발생한다.
  8. 고객들은 짜증이난다. 아예 안될꺼면 안되던가 왜 되다 안되다 하냐!!!
  9. 시스템은 엉망진창이 되고.. 망한다…

이 상황에서 가장 좋은 방법은 101번의 사용자의 접속을 delay시키는 것이다.
그리고 100명 중에 한명이 Connection 접속을 해제하는 순간 101번 사용자에게 Connection을 전달해주는 것이다.

이 상황을 해결할 수 있는 것이 바로 Connection pool 이다.
Connection pool은 미리 Connection을 특정 개수(예를 들면 30개)로 만들어놓고 울타리에 넣어놓고 접속자가 생기면 접속자가 Connection을 한 개 꺼내서 접속하러 가고, 접속을 해제하면 다시 Connection은 울타리에 넣어놓게 만듦으로써 Connection을 제어할 수 있는 환경을 만들어준다.

이 기능은 직접 만들 필요가 없이, Library로 구현되어있기 때문에 가져다 쓰기만 하면 된다.
이 Library의 Full Name이 DataBase Connection Pool, 즉 DBCP이다.

2) 현재 코드의 문제점

현재 배워서 사용하고 있는 getConnection 메소드는 아래와 같다.

private Connection getConnection() throws Exception {

	Class.forName("oracle.jdbc.driver.OracleDriver");
	String url = "jdbc:oracle:thin:@localhost:1521:xe";
	String userName = "practice";
	String passWord = "practice";

	return DriverManager.getConnection(url,userName,passWord);
}

지금 쓰고 있는 이 getConnection은 쉽게 말하면 Connection을 막 찍어내는 기능이다.
막 찍어내다가 Connection 개수가 부족하면 만들다가 예외 전가로 넘어가버린다.
따라서 이 부분 코드를 수정해야 한다.
코드를 수정하려면 DBCP 라이브러리를 가져와야 한다.

3) DBCP 라이브러리 가져오기

① DBCP 다운로드

  1. 구글에 maven repository를 검색해서 들어간다. maven repository는 나중에 배울 것이기 때문에 지금은 그냥 라이브러리를 모아놓은 사이트 정도로만 알고 있자.
  2. 제일 처음 나오는 Apache Commons DBCP 를 들어간다. 선호하는 버전(보통 가장 최근 버전 바로 전 버전) jar 파일을 다운받는다.
  3. 프로젝트에 라이브러리를 추가해준다.

② getConnection 메소드 바꾸기

private Connection getConnection() throws Exception {
	
	BasicDataSource bds = new BasicDataSource(); // Connection Pool
	bds.setDriverClassName("oracle.jdbc.driver.OracleDriver");
	bds.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
	bds.setUsername("practice");
	bds.setPassword("practice");
	bds.setInitialSize(30); // Connection Pool 안에 몇 개 Connection을 만들어 둘 것인지?
	
	return bds.getConnection();
}

③ 에러 확인

그리고나서 실행을 했더니 바로 에러가 뜬다. 확인해보면
NoClassDefFoundError : org/apache/commons/pool2/SwallowedExceptionListener
클래스를 찾을 수 없는 예외이다. 위 경로에 있는 클래스가 없다는 것이다.
받은 라이브러리를 열어서 확인해보니 pool2로 시작되는 패키지가 없는 것을 확인할 수 있다.
왜 이런 상황이 생겼을까?

DBCP는 혼자서 사용할 수 있는 라이브러리가 아니다. 다른 라이브러리를 이용해서 만든 라이브러리이다.
따라서 이용한 다른 라이브러리도 함께 받아줘야 사용할 수 있다.
이런 라이브러리를 의존성(Dependency) 라이브러리라고 한다.

④ 라이브러리 추가

다시 1번 과정으로 돌아가서, pool2를 검색해서 다운 받은 후 라이브러리를 추가한다.
그리고 실행해본다.

⑤ 또 에러 확인

NoClassDefFoundError : org/apache/commons/logging/LogFactory
이번엔 다른 클래스가 없다고 한다.
logging을 검색해서 라이브러리를 또 추가해준다.

⑥ 실행 확인

그리고 실행해주면 이제 잘 동작하는 것을 확인할 수 있다.
라이브러리를 추가하고 난 뒤에 에러가 뜨는 것을 두려워하지 말자.
자연스러운 과정으로 하나씩 해결하고 나면 별거아니기 때문이다!!

4) Connection Pool 개선하기

① 멤버필드/생성자/메소드로 나누기

사실 DBCP를 이용하여 만든 getConnection 메소드는 완전하지 않다.
getConnection을 부를 때마다 Connection Pool 인스턴스가 새로 계속 생성되는 상황이기 때문이다.
Connection Pool이 30개, 다른 Connection Pool이 30개… 이런식으로 Connection Pool을 만든 것이지 Connection을 만든 것이 아니다.
즉, 인스턴스 생성 코드는 한번만! 만들어지면 되므로 메소드에서 빼고 멤버필드로 생성해주면 된다.

private BasicDataSource bds = new BasicDataSource(); 

private Connection getConnection() throws Exception {
	bds.setDriverClassName("oracle.jdbc.driver.OracleDriver");
	bds.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
	bds.setUsername("practice");
	bds.setPassword("practice");
	bds.setInitialSize(30); 
	
	return bds.getConnection();
}

근데 메소드에 남아있는 코드들도 환경설정이기 때문에 계속 불러올 필요가 없다.
따라서 메소드가 실행될 때 한번만 실행되도록 생성자에 넣어주면 된다.

private BasicDataSource bds = new BasicDataSource(); 

public MessageDAO() {
	bds.setDriverClassName("oracle.jdbc.driver.OracleDriver");
	bds.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
	bds.setUsername("practice");
	bds.setPassword("practice");
	bds.setInitialSize(30); 
}

private Connection getConnection() throws Exception {
	return bds.getConnection();
}

② Singleton Design Pattern

클래스의 인스턴스가 한 개 이상 생성되지 않게 통제하는 기법

사실 ① 번 코드도 완전한 코드는 아니다.
Connection Pool 인스턴스는 MessageDAO 클래스 안에 멤버필드로 존재한다.
다르게말하면 MessageDAO의 인스턴스가 여러개 생성된다면 Connection Pool 인스턴스도 여러개가 생겨서 똑같은 상황이 되버릴 수 있는 것이다..

따라서 MessageDAO의 인스턴스가 한 개 이상 만들어질 수 없게 Singleton Design Pattern을 사용하면 된다.

  • bds 멤버필드에 private 해주는 이유?
    MessageDAO 클래스 내에서만 사용할 것이기 때문
  • instance 멤버필드에 private static 해주는 이유?
    getInstance 메소드가 static 메소드이기 때문에 static을 붙여주고, getInstance 메소드 내에서만 쓰니까 private을 붙여준다.
  • getInstance 메소드에 public static 해주는 이유?
    생성자를 private로 막아서 인스턴스 생성을 할 수 없으므로 getInstance 메소드도 부를 수 없다. 따라서 getInstance 메소드를 쓰려면 static을 써줘야한다.
  • 생성자에 private 해주는 이유?
    인스턴스 생성을 막기 위해
private BasicDataSource bds = new BasicDataSource();

private static MessageDAO instance = null;
public static MessageDAO getInstance() {
	if(instance == null) {
		instance = new MessageDAO();
	}
	return instance;
} 

private MessageDAO() {
	bds.setDriverClassName("oracle.jdbc.driver.OracleDriver"); 
	bds.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
	bds.setUsername("practice");
	bds.setPassword("practice");
	bds.setInitialSize(30); 
}

③ Static 변수 선언

Connection Pool 인스턴스를 생성하는 변수 bds를 private static 변수로 생성하는 것도 하나의 방법이 될 수 있다.
그러나 아래와 같은 이유로 쓰는 것을 추천하지 않는다.

  1. static 변수는 멀티 쓰레드 환경에서 문제가 생기기 쉽다.
  2. 생성자에 넣은 환경설정 코드는 어쨌든 계속 실행된다.
private static BasicDataSource bds = new DasicDataSource();

public MessageDAO() {
	bds.setDriverClassName("oracle.jdbc.driver.OracleDriver");
	bds.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
	bds.setUsername("practice");
	bds.setPassword("practice");
	bds.setInitialSize(30); 
}

private Connection getConnection() throws Exception {
	return bds.getConnection();
}

댓글남기기