자바 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 PromptVolume 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 Promptmkdir out
컴파일
C:\ Command Promptjavac -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 Promptxcopy src\*.properties out
실행
C:\ Command Promptset 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.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 Promptjavac -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 Promptcopy 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 Promptjava -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
참조