자바 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/
├── ojdbc17.jar
└── mysql-connector-j-9.3.0.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.cj.jdbc.Driver";
String JDBCDriverType = "jdbc:mysql";
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);
}
}
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 명령을 실행할 때 아래처럼 보이는 디렉터리에서 이어지는 명령을 실행한다.
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는 그렇지 않다--
mkdir out
컴파일
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 디렉터리에 복사
xcopy src\*.properties out
실행
set classpath=jars/ojdbc17.jar;jars/mysql-connector-j-9.3.0.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/ojdbc17.jar CP+=:jars/mysql-connector-j-9.3.0.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.cj.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 ojdbc17.jar
6th module mysql-connector-j-9.3.0.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/
├── ojdbc17.jar
└── mysql-connector-j-9.3.0.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.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..
윈도에서 테스트
컴파일
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 : 컴파일 대상 모듈 리스트
프로퍼티 파일 복사
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\
실행
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;
}
모듈 디스크립터에 의해 모듈 간 관계는 아래 그림처럼 설정된다.
서비스 구현을 제공하는 모듈에서 자신의 어떤 패키지도 익스포트하지 않았다는 점에 주목하자.
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;
}
}
- type() 메소드를 통해서 클래스 정보java.lang.Class를 얻는다. 클래스 정보를 isOracle() 메소드에 전달해 서비스 구현을 필터링한다.
- 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;
}
모듈 간 관계는 아래 그림처럼 설정된다.
어노테이션 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
참조