Last Modified 2022.2.14

자바 9 모듈

자바는 9 버전부터 모듈을 지원한다.
프로그래머는 모듈 단위로 프로그램을 구성할 수 있다.

모듈을 지원하기 위해, 자바는 먼저 자바 API를 모듈화해야 했다.
모듈 간 의존이 순환되지 않도록 패키지를 그룹화하는 작업은 쉽지 않았다.
A 모듈이 B 모듈을 의존하고 B 모듈이 C 모듈을 의존하고 C 모듈이 다시 A 모듈을 의존한다면, 이를 모듈 간 의존이 순환된다고 말한다.

모듈에는 모듈 디스크립터가 있다.
모듈 디스크립터는 모듈 이름과 모듈이 필요로 하는requires 외부 모듈 목록, 모듈이 익스포트exports하는 패키지 목록을 정의한다.

모듈 이름은 유일한 이름으로 짓는 게 좋다.
유일한 이름을 갖기 위해, 대부분 모듈은 도메인 역순으로 시작하는 이름을 가진다.

모듈은 이미지나 설정 파일--자바 프로퍼티, XML 파일--과 같은 리소스를 가질 수 있지만, 이들을 모듈 디스크립터에 정의하지 않는다.

자바 EE는 아직 모듈을 채택하지 않았다.
웹 프로젝트를 모듈화해야 한다는 부담을 가질 필요 없다.

커넥션 풀 소스를 모듈화하는 실습을 준비했다.
실습하기 전 아랫글을 읽어 보는 게 좋다.

모듈화 전

src/
├── net
│    └── java_school
│            └── db
│                └── dbpool
│                     ├── DBConnectionPool.java
│                     ├── DBConnectionPoolManager.java
│                     └── ConnectionManager.java
├── net
│    └── java_school
│            └── db
│                └── dbpool
│                     └── oracle
│                           └── OracleConnectionManager.java
├── net
│    └── java_school
│            └── db
│                └── dbpool
│                     └── mysql
│                           └── MySqlConnectionManager.java
├── net
│    └── java_school
│            └── test
│                  └── GetEmp.java
├── mysql.properties
├── oracle.properties
jars/
├── ojdbc6.jar
└── mysql-connector-java-8.0.28.jar

디렉터리 구조대로 파일을 생성한다.

DBConnectionPool.java
package net.java_school.db.dbpool; 

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Date;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

public class DBConnectionPool {
	private static final Logger logger = Logger.getLogger(DBConnectionPool.class.getName());
	
	// Number of connections currently in use
	private int checkedOut;

	// Free Connection List
	private Vector<Connection> freeConnections = new Vector<Connection>();

	// Maximum number of connections
	private int maxConn;

	// Waiting time (maximum time to wait when there is no connection in the pool)
	private int maxWait;

	// Connection Pool Name
	private String poolName;

	// DB Password
	private String passwd;

	// DB URL
	private String URL;

	// DB UserID
	private String userID;

	// Constructor
	public DBConnectionPool(String poolName, 
			String URL, 
			String userID, 
			String passwd, 
			int maxConn, 
			int initConn, 
			int waitTime) {

		this.poolName = poolName;
		this.URL = URL;
		this.userID = userID;
		this.passwd = passwd;
		this.maxConn = maxConn;
		this.maxWait = waitTime;

		for (int i = 0; i < initConn; i++) {
			freeConnections.addElement(newConnection());
		}
	}

	// Returning Connection
	// @param con : Connection to return
	public synchronized void freeConnection(Connection con) {
		freeConnections.addElement(con);
		checkedOut--;
		//Notify thread waiting to get Connection
		notifyAll();
	}

	// Get Connection
	@SuppressWarnings("resource")
	public synchronized Connection getConnection() {
		Connection con = null;
		//If Connection is in Free List, get the first of List
		if (freeConnections.size() > 0) {
			con = (Connection) freeConnections.firstElement();
			freeConnections.removeElementAt(0);

			try {
				//If the connection is closed by the DBMS, request again
				if (con.isClosed()) {
					logger.log(Level.SEVERE, "Removed bad connection from " + poolName);
					con = getConnection();
				}
			} //If strange connection occurs, request again
			catch (SQLException e) {
				logger.log(Level.SEVERE, "Removed bad connection from " + poolName);
				con = getConnection();
			}
		} //If Connection is not in Free List, create new
		else if (maxConn == 0 || checkedOut < maxConn) {
			con = newConnection();
		}

		if (con != null) {
			checkedOut++;
		}

		return con;
	}

	// Get Connection
	// @param timeout : Maximum Wait Time to Obtain a Connection
	public synchronized Connection getConnection(long timeout) {
		long startTime = new Date().getTime();
		Connection con;
		while ((con = getConnection()) == null) {
			try {
				wait(timeout * maxWait);
			} catch (InterruptedException e) {}
			if ((new Date().getTime() - startTime) >= timeout) {
				//Wait timeout
				return null;
			}
		}

		return con;
	}

	// Get Connection
	private Connection newConnection() {
		Connection con = null;
		try {
			if (userID == null) {
				con = DriverManager.getConnection(URL);
			} else {
				con = DriverManager.getConnection(URL, userID, passwd);
			}
			logger.info("Created a new connection in pool " + poolName);
		} catch (SQLException e) {
			StringBuffer sb = new StringBuffer();
			sb.append("Can't create a new connection for ");
			sb.append(URL);
			sb.append(" user: ");
			sb.append(userID);
			sb.append(" passwd: ");
			sb.append(passwd);
			logger.log(Level.SEVERE, sb.toString());
			return null;
		}

		return con;
	}

}
DBConnectionPoolManager.java
package net.java_school.db.dbpool;

import java.sql.Connection;
import java.util.Hashtable;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

public class DBConnectionPoolManager {
	private static final Logger logger = Logger.getLogger(DBConnectionPoolManager.class.getName());
	
	// To apply the singleton pattern to the DBConnectionPoolManager (keep only one instance), 
	// declare it as static
	static private DBConnectionPoolManager instance;
	private Vector<String> drivers = new Vector<String>();
	private Hashtable<String, DBConnectionPool> pools = new Hashtable<String, DBConnectionPool>();

	// Obtaining instance of DBConnectionPoolManager
	// @return DBConnectionManger
	static synchronized public DBConnectionPoolManager getInstance() {
		if (instance == null) {
			instance = new DBConnectionPoolManager();
		}

		return instance;
	}

	// Default Constructor
	private DBConnectionPoolManager() {}

	// Send current Connection to Free Connection List
	// @param name : Pool Name
	// @param con : Connection
	public void freeConnection(String name, Connection con) {
		DBConnectionPool pool = (DBConnectionPool) pools.get(name);
		if (pool != null) {
			pool.freeConnection(con);
		}

		logger.info("One Connection of " + name + " was freed");
	}

	// Obtain Open Connection.
	// Creates a new connection if there are no open connections and the maximum number 
	// of connections has not been reached.
	// Waits for the default wait time when there are no open connections currently 
	// and the maximum number of connections is in use.
	// @param name : Pool Name
	// @return Connection : The connection or null
	public Connection getConnection(String name) {
		DBConnectionPool pool = (DBConnectionPool) pools.get(name);
		if (pool != null) {
			return pool.getConnection(10);
		}
		return null;
	}

	// Create a Connection Pool
	// @param poolName : Name of Pool to create
	// @param url : DB URL
	// @param userID : DB UserID
	// @param passwd : DB Password
	private void createPools(String poolName, 
			String url, 
			String userID,
			String passwd, 
			int maxConn, 
			int initConn, 
			int maxWait) {

		DBConnectionPool pool = new DBConnectionPool(poolName, url, userID, passwd, maxConn, initConn, maxWait);
		pools.put(poolName, pool);
		logger.info("Initialized pool " + poolName);
	}

	// Initialization
	public void init(String poolName, 
			String driver, 
			String url,
			String userID, 
			String passwd, 
			int maxConn, 
			int initConn, 
			int maxWait) {

		loadDrivers(driver);
		createPools(poolName, url, userID, passwd, maxConn, initConn, maxWait);
	}

	// JDBC Driver Loading
	// @param driverClassName : The JDBC driver for the DB you want to use.
	private void loadDrivers(String driverClassName) {
		try {
			Class.forName(driverClassName);
			drivers.addElement(driverClassName);
			logger.info("Registered JDBC driver " + driverClassName);
		} catch (Exception e) {
			logger.log(Level.SEVERE, "Can't register JDBC driver: " + driverClassName);
		}
	}

	public Hashtable<String,DBConnectionPool> getPools() {
		return pools;
	}
	
	public int getDriverNumber() {
		return drivers.size();
	}

}
ConnectionManager.java
package net.java_school.db.dbpool;

import java.sql.Connection;

public abstract class ConnectionManager {

	protected DBConnectionPoolManager poolManager;
	protected String poolName;
	
	public ConnectionManager() {
		this.poolManager = DBConnectionPoolManager.getInstance();
	}
	
	public Connection getConnection() {
		return (poolManager.getConnection(poolName));
	}

	public void freeConnection(Connection con) {
		poolManager.freeConnection(poolName, con);
	}
	
	public abstract void initPoolManager(String poolName, String driver, String url, 
			String userID, String passwd, int maxConn, int initConn, int maxWait);
	
	public int getDriverNumber() {
        return poolManager.getDriverNumber();
    }

}
OracleConnectionManager.java
package net.java_school.db.dbpool.oracle;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import net.java_school.db.dbpool.ConnectionManager;

public class OracleConnectionManager extends ConnectionManager {
	private static final Logger logger = Logger.getLogger(OracleConnectionManager.class.getName());

	public OracleConnectionManager() {
		this.poolName = "oracle";
		String configFile = "oracle.properties";
		
		ClassLoader resource = this.getClass().getClassLoader();
		URL path = resource.getResource(configFile);
		
		try {
			Properties prop = new Properties();
			FileInputStream inputStream = new FileInputStream(new File(path.getFile()));
			prop.load(inputStream);
					
			String dbServer = prop.getProperty("dbServer");
			String port = prop.getProperty("port");
			String dbName = prop.getProperty("dbName");
			String userID = prop.getProperty("userID");
			String passwd = prop.getProperty("passwd");
			int maxConn = Integer.parseInt(prop.getProperty("maxConn"));
			int initConn = Integer.parseInt(prop.getProperty("initConn"));
			int maxWait = Integer.parseInt(prop.getProperty("maxWait"));
			String driver = "oracle.jdbc.driver.OracleDriver";
			String JDBCDriverType = "jdbc:oracle:thin";
			String url = JDBCDriverType + ":@" + dbServer + ":" + port + ":" + dbName;
			
			initPoolManager(this.poolName, driver, url, userID, passwd, maxConn, initConn, maxWait);
		} catch (IOException e) {
			logger.log(Level.SEVERE, "Error reading properties of " + configFile);
			throw new RuntimeException(e);
		}

	}

	@Override
	public void initPoolManager(String poolName, String driver, String url, 
			String userID, String passwd, int maxConn, int initConn, int maxWait) {
		this.poolManager.init(poolName, driver, url, userID, passwd, maxConn, initConn, maxWait);
	}

}
MySqlConnectionManager.java
package net.java_school.db.dbpool.mysql;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import net.java_school.db.dbpool.ConnectionManager;

public class MySqlConnectionManager extends ConnectionManager {
	
	private static final Logger logger = Logger.getLogger(MySqlConnectionManager.class.getName());

	public MySqlConnectionManager() {
		this.poolName = "mysql";
		String configFile = "mysql.properties";
		
		ClassLoader resource = this.getClass().getClassLoader();
		URL path = resource.getResource(configFile);

		try {
			Properties prop = new Properties();
			FileInputStream inputStream = new FileInputStream(new File(path.getFile()));
			prop.load(inputStream);
		
			String dbServer = prop.getProperty("dbServer");
			String port = prop.getProperty("port");
			String dbName = prop.getProperty("dbName");
			String userID = prop.getProperty("userID");
			String passwd = prop.getProperty("passwd");
			int maxConn = Integer.parseInt(prop.getProperty("maxConn"));
			int initConn = Integer.parseInt(prop.getProperty("initConn"));
			int maxWait = Integer.parseInt(prop.getProperty("maxWait"));
			String driver = "com.mysql.jdbc.Driver";
			String JDBCDriverType = "jdbc:mysql";
			String url = JDBCDriverType + "://" + dbServer + ":" + port + "/" + 
				dbName + "?useUnicode=true&characterEncoding=UTF-8&useSSL=false";
				
			initPoolManager(this.poolName, driver, url, userID, passwd, maxConn, initConn, maxWait);
		} catch (IOException e) {
			logger.log(Level.SEVERE, "Error reading properties of " + configFile);
			throw new RuntimeException(e);
		}

	}
	
	@Override
	public void initPoolManager(String poolName, String driver, String url, 
			String userID, String passwd, int maxConn, int initConn, int maxWait) {
		this.poolManager.init(poolName, driver, url, userID, passwd, maxConn, initConn, maxWait);
	}

}
oracle.properties
############################################ 
# Database Connection Properties for Oracle
############################################ 

# Database Server Name OR IP address 
dbServer = 127.0.0.1

# The port number your DB server listents to. 
port = 1521

# Database Name 
dbName = XE

# Database User 
userID = scott

# Database Password 
passwd = tiger

# Maximum Connection Number 
maxConn = 20

# Inital Connection Number 
initConn = 5

# Maximum Wait Time 
maxWait = 5
mysql.properties
############################################ 
# Database Connection Properties for MySQL
############################################ 

# Database Server Name OR IP address 
dbServer = localhost

# The port number your DB server listents to. 
port = 3306

# Database Name 
dbName = xe

# Database User 
userID = scott

# Database Password 
passwd = tiger

# Maximum Connection Number 
maxConn = 20

# Inital Connection Number 
initConn = 5

# Maximum Wait Time 
maxWait = 5

MySQL에 접속하여 다음을 실행한다.

mysql --user=root --password mysql

create user 'scott'@'%' identified by 'tiger';
grant all privileges on *.* to 'scott'@'%';

create database xe;
exit;

mysql --user=scott --password xe

CREATE TABLE DEPT (
    DEPTNO DECIMAL(2),
    DNAME VARCHAR(14),
    LOC VARCHAR(13),
    CONSTRAINT PK_DEPT PRIMARY KEY (DEPTNO) 
);
CREATE TABLE EMP (
    EMPNO DECIMAL(4),
    ENAME VARCHAR(10),
    JOB VARCHAR(9),
    MGR DECIMAL(4),
    HIREDATE DATE,
    SAL DECIMAL(7,2),
    COMM DECIMAL(7,2),
    DEPTNO DECIMAL(2),
    CONSTRAINT PK_EMP PRIMARY KEY (EMPNO),
    CONSTRAINT FK_DEPTNO FOREIGN KEY (DEPTNO) REFERENCES DEPT(DEPTNO)
);
CREATE TABLE SALGRADE ( 
    GRADE TINYINT,
    LOSAL SMALLINT,
    HISAL SMALLINT 
);
INSERT INTO DEPT VALUES (10,'ACCOUNTING','NEW YORK');
INSERT INTO DEPT VALUES (20,'RESEARCH','DALLAS');
INSERT INTO DEPT VALUES (30,'SALES','CHICAGO');
INSERT INTO DEPT VALUES (40,'OPERATIONS','BOSTON');
INSERT INTO EMP VALUES (7369,'SMITH','CLERK',7902,STR_TO_DATE('17-12-1980','%d-%m-%Y'),800,NULL,20);
INSERT INTO EMP VALUES (7499,'ALLEN','SALESMAN',7698,STR_TO_DATE('20-2-1981','%d-%m-%Y'),1600,300,30);
INSERT INTO EMP VALUES (7521,'WARD','SALESMAN',7698,STR_TO_DATE('22-2-1981','%d-%m-%Y'),1250,500,30);
INSERT INTO EMP VALUES (7566,'JONES','MANAGER',7839,STR_TO_DATE('2-4-1981','%d-%m-%Y'),2975,NULL,20);
INSERT INTO EMP VALUES (7654,'MARTIN','SALESMAN',7698,STR_TO_DATE('28-9-1981','%d-%m-%Y'),1250,1400,30);
INSERT INTO EMP VALUES (7698,'BLAKE','MANAGER',7839,STR_TO_DATE('1-5-1981','%d-%m-%Y'),2850,NULL,30);
INSERT INTO EMP VALUES (7782,'CLARK','MANAGER',7839,STR_TO_DATE('9-6-1981','%d-%m-%Y'),2450,NULL,10);
INSERT INTO EMP VALUES (7788,'SCOTT','ANALYST',7566,STR_TO_DATE('13-7-1987','%d-%m-%Y')-85,3000,NULL,20);
INSERT INTO EMP VALUES (7839,'KING','PRESIDENT',NULL,STR_TO_DATE('17-11-1981','%d-%m-%Y'),5000,NULL,10);
INSERT INTO EMP VALUES (7844,'TURNER','SALESMAN',7698,STR_TO_DATE('8-9-1981','%d-%m-%Y'),1500,0,30);
INSERT INTO EMP VALUES (7876,'ADAMS','CLERK',7788,STR_TO_DATE('13-7-1987', '%d-%m-%Y'),1100,NULL,20);
INSERT INTO EMP VALUES (7900,'JAMES','CLERK',7698,STR_TO_DATE('3-12-1981','%d-%m-%Y'),950,NULL,30);
INSERT INTO EMP VALUES (7902,'FORD','ANALYST',7566,STR_TO_DATE('3-12-1981','%d-%m-%Y'),3000,NULL,20);
INSERT INTO EMP VALUES (7934,'MILLER','CLERK',7782,STR_TO_DATE('23-1-1982','%d-%m-%Y'),1300,NULL,10);
INSERT INTO SALGRADE VALUES (1,700,1200);
INSERT INTO SALGRADE VALUES (2,1201,1400);
INSERT INTO SALGRADE VALUES (3,1401,2000);
INSERT INTO SALGRADE VALUES (4,2001,3000);
INSERT INTO SALGRADE VALUES (5,3001,9999);
COMMIT;
exit;
GetEmp.java
package net.java_school.test;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import net.java_school.db.dbpool.mysql.MySqlConnectionManager;
import net.java_school.db.dbpool.oracle.OracleConnectionManager;

public class GetEmp {

	public static void main(String[] args) {

		OracleConnectionManager oracleConnectionManager = new OracleConnectionManager();
		
		Connection con = null;
		PreparedStatement stmt = null;
		ResultSet rs = null;

		String sql = "SELECT * FROM EMP";

		try {
			con = oracleConnectionManager.getConnection();
			stmt = con.prepareStatement(sql);
			rs = stmt.executeQuery();

			while (rs.next()) {
				String empno = rs.getString(1);
				String ename = rs.getString(2);
				String job = rs.getString(3);
				String mgr = rs.getString(4);
				String hiredate = rs.getString(5);
				String sal = rs.getString(6);
				String comm = rs.getString(7);
				String depno = rs.getString(8);

				System.out.println(empno + " : " + ename + " : " + job + " : " + 
					mgr + " : " + hiredate + " : " + sal + " : " + comm + " : " + depno);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				rs.close();
			} catch (SQLException e) {}
			try {
				stmt.close();
			} catch (SQLException e) {}
			oracleConnectionManager.freeConnection(con);
		}
		
		MySqlConnectionManager mysqlConnectionManager = new MySqlConnectionManager();
		
		try {
			con = mysqlConnectionManager.getConnection();
			stmt = con.prepareStatement(sql);
			rs = stmt.executeQuery();

			while (rs.next()) {
				String empno = rs.getString(1);
				String ename = rs.getString(2);
				String job = rs.getString(3);
				String mgr = rs.getString(4);
				String hiredate = rs.getString(5);
				String sal = rs.getString(6);
				String comm = rs.getString(7);
				String depno = rs.getString(8);

				System.out.println(empno + " : " + ename + " : " + job + " : " + 
					mgr + " : " + hiredate + " : " + sal + " : " + comm + " : " + depno);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				rs.close();
			} catch (SQLException e) {}
			try {
				stmt.close();
			} catch (SQLException e) {}
			mysqlConnectionManager.freeConnection(con);
		}
		
		System.out.println("Driver Number: " + mysqlConnectionManager.getDriverNumber());
		
	}
}

Oracle JDBC Driver Download
MySQL JDBC Driver Download
각 링크에서 내려받은 JDBC 드라이버를 jars/ 폴더에 복사한다.

윈도에서 테스트

dir 명령을 실행할 때 아래처럼 보이는 디렉터리에서 이어지는 명령을 실행한다.

C:\ Command Prompt
 Volume in drive H has no label.
 Volume Serial Number is 26D8-3E15

 Directory of H:\this-is-not-modules-example

03/05/2020  05:41 AM    <DIR>          .
03/05/2020  05:41 AM    <DIR>          ..
03/05/2020  05:35 AM    <DIR>          jars
03/05/2020  05:35 AM    <DIR>          src

바이트 코드가 만들어지는 디렉터리를 생성한다. --리눅스에서 javac는 -d 옵션 다음에 나오는 디렉터리가 없으면 만들지만, 윈도에서 javac는 그렇지 않다--

C:\ Command Prompt
mkdir out

컴파일

C:\ Command Prompt
javac -d out src/net/java_school/db/dbpool/*.java
javac -d out src/net/java_school/db/dbpool/oracle/OracleConnectionManager.java
javac -d out src/net/java_school/db/dbpool/mysql/MySqlConnectionManager.java
javac -d out src/net/java_school/test/GetEmp.java

프로퍼티 파일을 out 디렉터리에 복사

C:\ Command Prompt
xcopy src\*.properties out

실행

C:\ Command Prompt
set classpath=jars/ojdbc6.jar;jars/mysql-connector-java-8.0.28.jar;out
java net.java_school.test.GetEmp

리눅스에서 테스트

ls 명령을 실행할 때 아래처럼 출력되는 디렉터리에서 이어지는 명령을 실행한다.

jars  src

컴파일

javac -d out -sourcepath src $(find src -name "*.java")

프로퍼티 파일을 out 디렉터리에 복사

cp $(find src -name '*.properties') out

실행

CP=jars/ojdbc6.jar
CP+=:jars/mysql-connector-java-8.0.28.jar
java -cp $CP:out net.java_school.test.GetEmp
Sep 17, 2019 11:46:41 AM net.java_school.db.dbpool.DBConnectionPoolManager loadDrivers
INFO: Registered JDBC driver oracle.jdbc.driver.OracleDriver
Sep 17, 2019 11:46:42 AM net.java_school.db.dbpool.DBConnectionPool newConnection
INFO: Created a new connection in pool oracle
Sep 17, 2019 11:46:42 AM net.java_school.db.dbpool.DBConnectionPool newConnection
INFO: Created a new connection in pool oracle
Sep 17, 2019 11:46:42 AM net.java_school.db.dbpool.DBConnectionPool newConnection
INFO: Created a new connection in pool oracle
Sep 17, 2019 11:46:42 AM net.java_school.db.dbpool.DBConnectionPool newConnection
INFO: Created a new connection in pool oracle
Sep 17, 2019 11:46:42 AM net.java_school.db.dbpool.DBConnectionPool newConnection
INFO: Created a new connection in pool oracle
Sep 17, 2019 11:46:42 AM net.java_school.db.dbpool.DBConnectionPoolManager createPools
INFO: Initialized pool oracle
7369 : SMITH : CLERK : 7902 : 1980-12-17 00:00:00 : 800 : null : 20
7499 : ALLEN : SALESMAN : 7698 : 1981-02-20 00:00:00 : 1600 : 300 : 30
7521 : WARD : SALESMAN : 7698 : 1981-02-22 00:00:00 : 1250 : 500 : 30
7566 : JONES : MANAGER : 7839 : 1981-04-02 00:00:00 : 2975 : null : 20
7654 : MARTIN : SALESMAN : 7698 : 1981-09-28 00:00:00 : 1250 : 1400 : 30
7698 : BLAKE : MANAGER : 7839 : 1981-05-01 00:00:00 : 2850 : null : 30
7782 : CLARK : MANAGER : 7839 : 1981-06-09 00:00:00 : 2450 : null : 10
7788 : SCOTT : ANALYST : 7566 : 1987-07-13 00:00:00 : 3000 : null : 20
7839 : KING : PRESIDENT : null : 1981-11-17 00:00:00 : 5000 : null : 10
7844 : TURNER : SALESMAN : 7698 : 1981-09-08 00:00:00 : 1500 : 0 : 30
7876 : ADAMS : CLERK : 7788 : 1987-07-13 00:00:00 : 1100 : null : 20
7900 : JAMES : CLERK : 7698 : 1981-12-03 00:00:00 : 950 : null : 30
7902 : FORD : ANALYST : 7566 : 1981-12-03 00:00:00 : 3000 : null : 20
7934 : MILLER : CLERK : 7782 : 1982-01-23 00:00:00 : 1300 : null : 10
Sep 17, 2019 11:46:42 AM net.java_school.db.dbpool.DBConnectionPoolManager freeConnection
INFO: One Connection of oracle was freed
Sep 17, 2019 11:46:42 AM net.java_school.db.dbpool.DBConnectionPoolManager loadDrivers
INFO: Registered JDBC driver com.mysql.jdbc.Driver
Sep 17, 2019 11:46:43 AM net.java_school.db.dbpool.DBConnectionPool newConnection
INFO: Created a new connection in pool mysql
Sep 17, 2019 11:46:43 AM net.java_school.db.dbpool.DBConnectionPool newConnection
INFO: Created a new connection in pool mysql
Sep 17, 2019 11:46:43 AM net.java_school.db.dbpool.DBConnectionPool newConnection
INFO: Created a new connection in pool mysql
Sep 17, 2019 11:46:43 AM net.java_school.db.dbpool.DBConnectionPool newConnection
INFO: Created a new connection in pool mysql
Sep 17, 2019 11:46:43 AM net.java_school.db.dbpool.DBConnectionPool newConnection
INFO: Created a new connection in pool mysql
Sep 17, 2019 11:46:43 AM net.java_school.db.dbpool.DBConnectionPoolManager createPools
INFO: Initialized pool mysql
7369 : SMITH : CLERK : 7902 : 1980-12-17 : 800.00 : null : 20
7499 : ALLEN : SALESMAN : 7698 : 1981-02-20 : 1600.00 : 300.00 : 30
7521 : WARD : SALESMAN : 7698 : 1981-02-22 : 1250.00 : 500.00 : 30
7566 : JONES : MANAGER : 7839 : 1981-04-02 : 2975.00 : null : 20
7654 : MARTIN : SALESMAN : 7698 : 1981-09-28 : 1250.00 : 1400.00 : 30
7698 : BLAKE : MANAGER : 7839 : 1981-05-01 : 2850.00 : null : 30
7782 : CLARK : MANAGER : 7839 : 1981-06-09 : 2450.00 : null : 10
7788 : SCOTT : ANALYST : 7566 : 1987-06-28 : 3000.00 : null : 20
7839 : KING : PRESIDENT : null : 1981-11-17 : 5000.00 : null : 10
7844 : TURNER : SALESMAN : 7698 : 1981-09-08 : 1500.00 : 0.00 : 30
7876 : ADAMS : CLERK : 7788 : 1987-07-13 : 1100.00 : null : 20
7900 : JAMES : CLERK : 7698 : 1981-12-03 : 950.00 : null : 30
7902 : FORD : ANALYST : 7566 : 1981-12-03 : 3000.00 : null : 20
7934 : MILLER : CLERK : 7782 : 1982-01-23 : 1300.00 : null : 10
Sep 17, 2019 11:46:43 AM net.java_school.db.dbpool.DBConnectionPoolManager freeConnection
INFO: One Connection of mysql was freed
Driver Number: 2

모듈로 분해

모듈로 나누는 데 있어 정답은 없다.
예제를 아래처럼 모듈화할 것이다.

1st module : net.java_school.db.dbpool (Module Name)
DBConnectionPool.java
DBConnectionPoolManager.java
ConnectionManager.java
2nd module : net.java_school.db.dbpool.oracle
OracleConnectionManager.java
oracle.properties
3rd module : net.java_school.db.dbpool.mysql
MySqlConnectionManager.java
mysql.properties
4th module : main.app
GetEmp.java
5th module
ojdbc6.jar
6th module
mysql-connector-java-8.0.28.jar

디렉터리 구조를 아래와 같이 변경한다.
src/ 바로 아래, 모듈 이름의 폴더가 있어야 한다.

src/
├── net.java_school.db.dbpool (Module Name)
│   ├── net
│   │   └── java_school
│   │       └── db
│   │           └── dbpool
│   │               ├── DBConnectionPool.java
│   │               ├── DBConnectionPoolManager.java
│   │               └── ConnectionManager.java
│   └── module-info.java
├── net.java_school.db.dbpool.oracle
│   ├── net
│   │   └── java_school
│   │       └── db
│   │           └── dbpool
│   │               └── oracle
│   │                   └── OracleConnectionManager.java
│   ├── module-info.java
│   └── oracle.properties
├── net.java_school.db.dbpool.mysql
│   ├── net
│   │   └── java_school
│   │       └── db
│   │           └── dbpool
│   │               └── mysql
│   │                   └── MySqlConnectionManager.java
│   ├── module-info.java
│   └── mysql.properties
├── main.app
│   ├── net
│   │   └── java_school
│   │       └── test
│   │            └── GetEmp.java
│   └── module-info.java
jars/
├── ojdbc6.jar
└── mysql-connector-java-8.0.28.jar

1st Module : net.java_school.db.dbpool

src/net.java_school.db.dbpool 폴더에 모듈 디스트립터를 다음과 같이 생성한다.

module-info.java
module net.java_school.db.dbpool {
	requires java.logging;
	requires transitive java.sql;
	
	exports net.java_school.db.dbpool;
}

모듈 디스크립터는 클래스 파일이지만 일반적인 클래스 파일은 아니다.
모듈 디스크립터 내용은 module로 시작한다. module 다음에 모듈 이름이 온다.
모듈 이름 다음에 블록{ }이 시작된다.
블록 안에 이 모듈이 필요로 하는 다른 모듈과 이 모듈이 익스포트 하는 패키지를 정의한다.

requires modulename;
이 모듈이 사용하는 외부 모듈

requires transitive java.sql;
이 모듈은 java.sql 모듈을 사용한다.
거기에 더하여, 외부 모듈이 이 모듈을 사용하면requires, 그 외부 모듈은 java.sql 모듈도 자동으로 사용할 수 있게 된다.

exports package;
이 모듈이 익스포트 하는 패키지

A 모듈이 B 모듈을 requires 한다고 해도, A 모듈은 B 모듈이 exports 하는 패키지의 public 요소만 사용할 수 있다.

2nd Module : net.java_school.db.dbpool.oracle

src/net.java_school.db.dbpool.oracle 폴더에 모듈 디스트립터를 생성한다.

module-info.java
module net.java_school.db.dbpool.oracle {
	requires net.java_school.db.dbpool;
	
	exports net.java_school.db.dbpool.oracle;
}

3rd Module : net.java_school.db.dbpool.mysql

src/net.java_school.db.dbpool.mysql 폴더에 모듈 디스트립터를 생성한다.

module-info.java
module net.java_school.db.dbpool.mysql {
	requires net.java_school.db.dbpool;
	
	exports net.java_school.db.dbpool.mysql;
}

4th Module : main.app

src/main.app 폴더에 모듈 디스트립터를 생성한다.

module-info.java
module main.app {
	requires java.sql;
	requires net.java_school.db.dbpool.oracle;
	requires net.java_school.db.dbpool.mysql;
}

이후 설명 중에 나오는 모듈 이름에서 도메인 부분(net.java_school.)은 생략하겠다.
모듈 디스크립터에 의해 모듈 간 관계는 아래 그림처럼 설정된다.
java modules 1

모듈에 속한 클래스로부터 모듈을 대표하는 java.lang.Module 을 얻을 수 있다.
Module은 절대 경로를 사용하므로 경로에 슬래시/를 사용하지 않는다.
모듈 루트에 설정 파일이 있으면 경로에 파일 이름만 입력하면 된다.
oracle.properties 와 mysql.properties 파일은 모듈의 루트 디렉터리에 있다.
프로퍼티 파일을 로딩하는 코드를 아래처럼 수정한다.

OracleConnectionManager.java
public OracleConnectionManager() {
	this.poolName = "oracle";
	String configFile = "oracle.properties";

	Class<?> clazz = OracleConnectionManager.class;
	Module m = clazz.getModule();

	try {
		InputStream inputStream = m.getResourceAsStream(configFile);
		Properties prop = new Properties();
		prop.load(inputStream);
		
		//..Omit..

MySqlConnectionManager.java
public MySqlConnectionManager() {
	this.poolName = "mysql";
	String configFile = "mysql.properties";

	Class<?> clazz = MySqlConnectionManager.class;
	Module m = clazz.getModule();

	try {
		InputStream inputStream = m.getResourceAsStream(configFile);
		Properties prop = new Properties();
		prop.load(inputStream);
		
		//..Omit..

윈도에서 테스트

컴파일

C:\ Command Prompt
javac -d out --module-source-path src ^
-m main.app,net.java_school.db.dbpool, ^
net.java_school.db.dbpool.oracle,net.java_school.db.dbpool.mysql

--module-source-path : 모듈 소스 위치
-m : 컴파일 대상 모듈 리스트

프로퍼티 파일 복사

C:\ Command Prompt
copy src\net.java_school.db.dbpool.oracle\oracle.properties ^
out\net.java_school.db.dbpool.oracle\
copy src\net.java_school.db.dbpool.mysql\mysql.properties ^
out/net.java_school.db.dbpool.mysql\

실행

C:\ Command Prompt
java -p jars:out -m main.app/net.java_school.test.GetEmp

-p : 모듈 패스
-m : 실행할 모듈, -m 다음에는 모듈 이름/실행될 클래스 가 온다. 모듈형 jar는 모듈 이름만 입력한다.

모듈 패스에 있는 모듈형 jar가 아닌 jar는 모두 자동 모듈Automatic Modules이 된다.
자동 모듈은 다른 모든 모듈에 대해서 requires transitive 다른_모듈; 관계라고 해석된다.
자동 모듈은 자신의 모든 패키지를 익스포트 한다.

리눅스에서 테스트

컴파일

javac -d out --module-source-path src $(find src -name "*.java")

프로퍼티 파일 복사

cp src/net.java_school.db.dbpool.oracle/oracle.properties \
out/net.java_school.db.dbpool.oracle/
cp src/net.java_school.db.dbpool.mysql/mysql.properties \
out/net.java_school.db.dbpool.mysql/

실행

java -p jars:out -m main.app/net.java_school.test.GetEmp

ServiceLoader 사용하기

모듈을 채택하면서 모듈 시스템을 지원하는 기능이 추가되었다.
새로운 ServiceLoader를 사용하면 서비스와 서비스 구현을 각각의 모듈로 구성할 수 있다.
여기서 서비스는, 스프링 프로젝트에서 서비스 컴포넌트를 떠올리면 쉽게 이해할 수 있다.
서비스는 대부분 인터페이스다.
모듈 디스크립터를 편집해, ServiceLoader로 하여금 런타임에 서비스 구현을 로드하도록 할 수 있다.

main.app 모듈은 uses 키워드를 사용해 ConnectionManager를 서비스로 사용한다고 선언한다. --ConnectionManager는 추상 클래스다. 추상 클래스나 구현 클래스도 서비스가 될 수 있다--

module main.app {
  requires net.java_school.db.dbpool;
  
  uses net.java_school.db.dbpool.ConnectionManager;
}

db.dbpool.oracle 모듈은 provides 서비스 with 구현 클래스 를 사용해, ConnectionManager 서비스의 구현으로 OracleConnectionManager를 제공한다고 선언한다.

module net.java_school.db.dbpool.oracle {
  requires net.java_school.db.dbpool;
  
  provides net.java_school.db.dbpool.ConnectionManager 
      with net.java_school.db.dbpool.oracle.OracleConnectionManager;
}

마찬가지로, db.dbpool.mysql 모듈 디스크립터를 수정한다.

module net.java_school.db.dbpool.mysql {
  requires net.java_school.db.dbpool;
  
  provides net.java_school.db.dbpool.ConnectionManager 
      with net.java_school.db.dbpool.mysql.MySqlConnectionManager;
}

모듈 디스크립터에 의해 모듈 간 관계는 아래 그림처럼 설정된다.
java modules 2

서비스 구현을 제공하는 모듈에서 자신의 어떤 패키지도 익스포트하지 않았다는 점에 주목하자.
main.app 모듈의 GetEmp 클래스는 이전과 달리 서비스 구현 모듈의 어떤 타입도 코드에 사용할 수 없다.
수정된 GetEmp 클래스에서 확인한다.

package net.java_school.test;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ServiceLoader;

import net.java_school.db.dbpool.ConnectionManager;

public class GetEmp {

  public static void main(String[] args) {

    Iterable<ConnectionManager> managers = ServiceLoader.load(ConnectionManager.class);
		
    for (ConnectionManager manager : managers) {
      Connection con = null;
      PreparedStatement stmt = null;
      ResultSet rs = null;

      //.. Omit ..
      			
    }

  }

}

이제 서비스와 구현이 각각의 모듈로 완벽하게 분리되어 있다.
GetEmp 클래스 코드는 db.dbpool.oracle과 db.dbpool.mysql 모듈의 어떤 타입도 사용하지 않는다.
db.dbpool.oracle과 db.dbpool.mysql 모듈 중 하나만 있어도 main.app 모듈을 실행할 수 있다.
실행은 안 되지만 구현 모듈을 모두 제거해도 컴파일엔 성공한다.
모듈 패스에 Microsoft SQL Server 데이터베이스를 다루는 모듈을 만든다면, main.app 모듈은 수정 없이 동작한다.

java.sql 모듈 디스크립터에는 uses java.sql.Driver; 라고 선언되어 있다.
JDBC 드라이버 모듈은 디스크립터에 provides java.sql.Driver with 구현_클래스; 라고 선언해야 한다.
JDBC 드라이버 모듈을 모듈 패스에 두면 구현_클래스는 서비스 바인딩 된다.
서비스 바인딩이란 런타임에 SerivceLoader가 모듈 디스크립터를 참조해 구현을 인스턴스 화하는 것을 말한다.
그런데 예제에서 사용한 JDBC 드라이버는 모두 모듈이 아니다.
그런데도 서비스 바인딩이 된다.
이 점에서 자바가 편의를 제공했다고 생각한다.
모듈화했는데 데이터베이스 연동이 안 된다면 자바에 대한 신뢰가 무너질 것이다.
JDBC 드라이버를 모듈 패스가 아닌 클래스 패스에 둬도 예제는 실행된다.

서비스 필터링

ServiceLoader를 사용하면 모듈 패스에 있는 서비스 구현을 모두 서비스 바인딩한다. 이 동작을 컨트롤할 수 있는 기능이 ServiceLoader에 없다. GetEmp에서 서비스 바인딩 대상인 OracleConnectionManager 와 MySqlConnectionManager 중 OracleConnectionManager 만 사용하고 싶다면, 서비스 구현이 인스턴스 화하기 전에 서비스를 필터링하면 된다. 인스턴스 화하기 전 필터링이므로 클래스 차원의 정보를 서비스 구현 클래스에 추가해야 한다.

db.dbpool 모듈에 다음 어노테이션을 생성한다.
이 어노테이션을 OracleConnectionManager 서비스 구현 클래스에 추가되는 클래스 차원의 정보로 사용하려 한다.

package net.java_school.db.dbpool;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Oracle {
  public boolean value() default true;
}

db.dbpool.oracle 모듈의 OracleConnectionManager 클래스에 Oracle 어노테이션을 추가한다.

import net.java_school.db.dbpool.Oracle;

@Oracle
public class OracleConnectionManager extends ConnectionManager {
  //..
}

GetEmp 클래스를 수정한다.

import net.java_school.db.dbpool.ConnectionManager;
import net.java_school.db.dbpool.Oracle;

public class GetEmp {

	public static void main(String[] args) {

		ServiceLoader<ConnectionManager> managers = ServiceLoader.load(ConnectionManager.class);
		
		ConnectionManager manager = managers.stream()
				.filter(provider -> isOracle(provider.type())) //1.
				.map(ServiceLoader.Provider::get).findAny().get(); //2.

        //.. Omit.. 
	}
	
	private static boolean isOracle(Class<?> clazz) {
		return clazz.isAnnotationPresent(Oracle.class)
				&& clazz.getAnnotation(Oracle.class).value() == true;
	}
	
}
  1. type() 메소드를 통해서 클래스 정보java.lang.Class를 얻는다. 클래스 정보를 isOracle() 메소드에 전달해 서비스 구현을 필터링한다.
  2. ServiceLoader의 Provider 클래스를 사용해 필터링 된 서비스 구현을 인스턴스 화한다.

Oracle 어노테이션이 db.dbpool 모듈에 있어야 한다는 점이 마음에 들지 않는다.
다음 예에서 이 점을 개선하자.

인터페이스 추가

db.dbpool.api 모듈을 새로 만들고, ConnectionManageable 인터페이스를 아래와 같이 생성한다.

ConnectionManageable.java
package net.java_school.db.dbpool.api;

import java.sql.Connection;

public interface ConnectionManageable {
	
  public Connection getConnection();

  public void freeConnection(Connection con);

  public int getDriverNumber();

}

db.dbpool 모듈의 ConnectionManager 추상 클래스가 db.dbpool.api 모듈의 ConnectionManageable 인터페이스를 구현하게 한다.

package net.java_school.db.dbpool;

import java.sql.Connection;

import net.java_school.db.dbpool.api.ConnectionManageable;

public abstract class ConnectionManager implements ConnectionManageable {

  protected DBConnectionPoolManager poolManager;
  protected String poolName;

  public ConnectionManager() {
    this.poolManager = DBConnectionPoolManager.getInstance();
  }

  @Override	
  public Connection getConnection() {
    return (poolManager.getConnection(poolName));
  }

  @Override
  public void freeConnection(Connection con) {
    poolManager.freeConnection(poolName, con);
  }

  public abstract void initPoolManager(String poolName, String driver, String url, 
      String userID, String passwd, int maxConn, int initConn, int maxWait);

  @Override	
  public int getDriverNumber() {
    return poolManager.getDriverNumber();
  }

}

db.dbpool.api 모듈의 모듈 디스크립터를 생성한다.

module net.java_school.db.dbpool.api {
  requires transitive java.sql;
  
  exports net.java_school.db.dbpool.api;
}

기존 모듈 디스크립터를 수정한다.

module net.java_school.db.dbpool {
  requires transitive net.java_school.db.dbpool.api;
  
  exports net.java_school.db.dbpool;
}
module net.java_school.db.dbpool.mysql {
  requires net.java_school.db.dbpool;
  
  provides net.java_school.db.dbpool.api.ConnectionManageable
      with net.java_school.db.dbpool.mysql.MySqlConnectionManager;
}
module net.java_school.db.dbpool.oracle {
  requires net.java_school.db.dbpool;
  
  provides net.java_school.db.dbpool.api.ConnectionManageable
      with net.java_school.db.dbpool.oracle.OracleConnectionManager;
}
module main.app {
  requires net.java_school.db.dbpool.api;
  
  uses net.java_school.db.dbpool.api.ConnectionManageable;
}

모듈 간 관계는 아래 그림처럼 설정된다.
java modules 3

어노테이션 Oracle.java를 db.dbpool 모듈에서 db.dbpool.api 모듈로 옮긴 후 패키지를 수정한다.

package net.java_school.db.dbpool.api;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Oracle {
  public boolean value() default true;
}

db.dbpool.oracle 모듈의 OracleConnectionManager 클래스를 수정한다.

package net.java_school.db.dbpool.oracle;

import net.java_school.db.dbpool.ConnectionManager;
import net.java_school.db.dbpool.api.Oracle;

@Oracle
public class OracleConnectionManager extends ConnectionManager {

main.app 모듈의 GetEmp 클래스를 수정한다.

import net.java_school.db.dbpool.api.ConnectionManageable;
import net.java_school.db.dbpool.api.Oracle;

public class GetEmp {

  public static void main(String[] args) {

    ServiceLoader<ConnectionManageable> managers = ServiceLoader.load(ConnectionManageable.class);

    ConnectionManageable manager = managers.stream()
        .filter(provider -> isOracle(provider.type()))
        .map(ServiceLoader.Provider::get).findAny().get();
			

이제 우리의 예제는 좋은 코드의 모습을 가진다.
공급자와 소비자는 서비스 타입으로 ConnectionManageable 인터페이스만 공유한다.
공급자는 어떤 패키지도 익스포트 하지 않는다. --db.dbpool.oracle과 db.dbpool.mysql 모듈은 공급자Provider다. main.app 모듈은 소비자Consumer다--

최종 소스: https://github.com/kimjonghoon/java-module-test

관련 글 참조