✅ 클래스의 분리
지난 시간에서 추상클래스를 만들고 이를 상속한 서브클래스에서 변화가 필요한 부분을 바꿔서 쓸 수 있게 만들었습니다. 템플릿 메서드 패턴, 팩토리 메서드 패턴이 사용되었습니다. 하지만 여러가지 단점이 있는 “상속”이라는 방법이 불편하게 느껴집니다. 이를 보완하고자 상속 관계도 아닌 완전히 독립적인 클래스로 만드는 방법을 살펴보겠습니다.
- SimpleConnectionMaker
package com.jhcode.spring.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class SimpleConnectionMaker { //== Connection 객체를 생성하여 반환하는 메서드 ==// public Connection makeNewConnection() throws ClassNotFoundException, SQLException{ String className = "org.mariadb.jdbc.Driver"; String url = "jdbc:mariadb://localhost:3306/toby_study?characterEncoding=UTF-8"; String userId = "root"; String password = "1234"; Class.forName(className); Connection con = DriverManager.getConnection(url, userId, password); return con; } }
DB Connection 객체를 생성하는 로직을 가지고 있는 클래스를 따로 빼서 만들었습니다.
makeNewConnection()
메서드를 통해Connection
객체를 반환하여, 필요한 클래스에서Connection
객체를 사용할 수 있도록 합니다.
- UserDao
package com.jhcode.spring.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import com.jhcode.spring.domain.User; public class UserDao { private SimpleConnectionMaker simpleConnectionMaker; //생성자 public UserDao() { simpleConnectionMaker = new SimpleConnectionMaker(); } //== 상속을 통한 방식이 아닌, 클래스를 분리하는 방식을 사용했다 ==// //public abstract Connection getConnection() throws ClassNotFoundException, SQLException; public void add(User user) throws ClassNotFoundException, SQLException{ //Connection con = getConnection(); 기존코드 Connection con = simpleConnectionMaker.makeNewConnection(); //...생략
클래스를 분리하는 방식을 사용했으므로 UserDao는 더 이상 추상클래스일 이유가 없습니다.
abstract
를 제거합니다.UserDao
객체가 생성될 때,SimpleConnectionMaker
객체를 생성하여 필드에 저장했습니다. 이로써makeNewConnection()
메서드를 통해서Connection
객체를 반환받아 사용할 수 있습니다.➡️ 하지만 이렇게 코드를 변경하면 다음과 같은 문제가 생깁니다.
NUserDao
,DUserDao
가UserDao
에게 상속받아 사용했던 각각의 DB 커넥션 생성 코드를 사용할 수 없게 된다. UserDao 클래스가SimpleConnectionMaker
클래스에 종속되어SimpleConnectionMaker
클래스가 가지고 있는 메서드를 사용하지 않고서는Connection
객체를 사용할 수 없기 때문이다.
- 만약 D사에서
openConnection()
메서드를 만들어 DB 커넥션을 반환한다면, UserDao 클래스의add()
,get()
메서드에서Connection
객체를 가져오는 부분을 전부.openConnection()
메서드를 사용하는 것으로 변경해야하는 번거로움이 생긴다.
UserDao
는 DB 커넥션을 제공하는 구체적인 클래스와 메서드를 알고 있어야 한다. 필드에 해당 클래스를 정의해두었기 때문에, 만약 D사에서 다른 클래스, 다른 메서드로 DB 커넥션을 제공하는 코드를 구성하였다면 또 다시UserDao
코드를 전부 수정해야한다.
👉근본적인 원인은 UserDao
가 바뀔 수 있는 정보, DB 커넥션을 가져오는 클래스에 대해 너무 많이 알고 있기 때문입니다. DB 커넥션을 가져오는 클래스의 이름, 메서드의 이름이 뭔지 일일이 알고 있어야 Connection
객체를 가져올 수 있기 때문에 UserDao
는 SimpleConnectionMaker
클래스에 종속되어 버립니다. 책에서는 이러한 해결책으로 인터페이스를 설명하고 있습니다.
✅ 인터페이스의 도입
추상화란 공통적인 성격의 코드를 뽑아내어 따로 분리해내는 작업입니다. 자바에서 추상화를 위해 제공하는 가장 유용한 도구는 인터페이스입니다. 객체를 생성하기 위해선 인터페이스를 구현한 구체적인 클래스를 하나 선택해야하겠지만, 인터페이스로 접근을 하게 되면 사용할 클래스가 무엇인지 몰라도 됩니다.
인터페이스는 어떤 일을 하겠다는 기능만 정의해놓은 것입니다. 어떻게 동작하겠다는(구현부) 방법은 나타나 있지 않습니다. 이는 인터페이스를 구현한 구체적인 클래스에서 결정할 일입니다. 인터페이스의 메서드를 통해 사용할 수 있는 기능만 알고 있으면 되고, 이를 구현하는 구체적인 코드는 알 필요가 없어집니다. 이를 통해 클래스 간의 느슨한 연결이 가능해지는 것입니다.
- ConnectionMaker(인터페이스)
package com.jhcode.spring.dao; import java.sql.Connection; import java.sql.SQLException; public interface ConnectionMaker { //== 인터페이스는 사용할 기능만 정의한다 ==// public Connection makeConnection() throws ClassNotFoundException, SQLException; }
- NConnectionMaker, DConnectionMaker
package com.jhcode.spring.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class NConnectionMaker implements ConnectionMaker { @Override public Connection makeConnection() throws ClassNotFoundException, SQLException { //N,D사의 독자적인 방법으로 Connection을 생성하는 코드 String className = "org.mariadb.jdbc.Driver"; String url = "jdbc:mariadb://localhost:3306/toby_study?characterEncoding=UTF-8"; String userId = "root"; String password = "1234"; Class.forName(className); Connection nCon = DriverManager.getConnection(url, userId, password); return nCon; } }
- UserDao
package com.jhcode.spring.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import com.jhcode.spring.domain.User; public class UserDao { private ConnectionMaker connectionMaker; //생성자 public UserDao() { connectionMaker = new DConnectionMaker(); } public void add(User user) throws ClassNotFoundException, SQLException{ Connection con = connectionMaker.makeConnection(); //...생략
UserDao가 인터페이스를 사용하게 되면 인터페이스의 메소드를 통해 알 수 있는 기능에만 관심을 가지면 되므로, 그 기능을 어떻게 구현했는지에 대해서는 관심을 둘 필요가 없어졌습니다. 구체적인 클래스 정보를 알 필요도 없어지며, 인터페이스에 정의된 메서드를 사용함으로 구현 클래스가 바뀐다고 해도 메서드의 이름이 변경될 걱정은 없습니다.
하지만 여전히 인터페이스를 구현한 클래스의 객체를 생성하는 코드가 남아있습니다. DB 커넥션을 제공하는 클래스에 대한 구체적인 정보는 모두 제거했지만, 초기에 한 번 어떤 클래스의 객체를 사용할 것인지 결정하는 생성자의 코드는 제거되지 않았습니다. 결국 여전히 의존관계가 남아있고,
UserDao
는DconnectionMaker
의 정보를 가지고 있습니다.
✅ 관계설정 책임의 분리
초기에 한 번 어떤 클래스의 객체를 사용해야할 것인지 결정해야하기 때문에 UserDao
에서 생성자를 통해 객체를 생성하는 코드가 남아있습니다. 그러나 꼭 UserDao
에서 객체를 만들어야할까요? 자바에서는 인자를 통해 외부에서 생성된 객체도 가져올 수 있습니다. 외부에서 전달받으려면 메소드나 생성자의 파라미터를 이용하면 됩니다. 인터페이스를 구현한 클래스 객체는 다형성을 통해 인터페이스 타입으로 받을 수 있습니다.
- UserDao
package com.jhcode.spring.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import com.jhcode.spring.domain.User; public class UserDao { private ConnectionMaker connectionMaker; //생성자 public UserDao(ConnectionMaker connectionMaker) { this.connectionMaker = connectionMaker; }
어떤
ConnectionMaker
을 구현한 클래스를 사용할지는 UserDao 객체를 사용하는 클라이언트가 결정하도록합니다. 즉,UserDao
객체를 생성하여 사용할 때, 클라이언트는ConnectionMaker
을 구현한 클래스를 생성해UserDao
생성자의 파라미터로 전달 받도록 하는 것입니다.ConnectionMaker
을 구현한NConnectionMaker
,DConnectionMaker
객체는 다형성을 통해 조상 타입인ConnectionMaker
인터페이스 타입으로 받을 수 있습니다. 이제부터UserDao
객체를 생성하고 사용할 때, 클라이언트가 어떤 클래스를 사용할지 결정하고 생성하는 코드를 작성해보겠습니다.
- UserDaoTest
package com.jhcode.spring.dao; import java.sql.SQLException; import com.jhcode.spring.domain.User; public class UserDaoTest { public static void main(String[] args) throws ClassNotFoundException, SQLException { ConnectionMaker connectionMaker = new DConnectionMaker(); UserDao dao = new UserDao(connectionMaker); //...생략
지금까지
UserDao
가 사용할ConnectionMaker
객체를 직접 선택하고 생성했던 것을, 이제는UserDao
를 사용하는UserDaoTest
에서ConnectionMaker
객체를 선택하고 생성하게 만들었습니다. 이를 통해UserDao
는ConnecionMaker
인터페이스가 제공하는 기능만 알고 있으면 되고, 이를 구현하는 어떠한 정보도 몰라도 되게 되었습니다.UserDao
가 사용할ConnectionMaker
구현 클래스를 생성자를 통해 객체를 생성하고 인터페이스 타입으로 참조했습니다. 이를UserDao
가 생성자를 통해 자신의 필드에 해당 객체를 사용할 수 있도록 주입받았습니다.이를 통해
UserDao
클래스는 DB를 다양하게 연결할 수 있는 기능을 확장하는데는 열려있고,UserDao
자신의 핵심 기능을 구현한 코드는 그런 변화에 영향을 받지 않고 유지할 수 있으므로 개방 폐쇄 원칙(OCP)에 따라 설계되었다고 할 수 있습니다. 지금까지 진행해온 것을 이론적으로 더 깊게 살펴보겠습니다.
✅ 원칙과 패턴
- 개방 폐쇄 원칙 OCP (Open-Closed Principle)
소프트웨어의 구성 요소(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다는 원칙입니다. 즉, 기존의 코드를 변경하지 않으면서 새로운 기능을 추가할 수 있도록 설계해야 한다는 것을 의미합니다.
위 그림을 보면, 인터페이스를 통해 DB를 연결하는 기능 확장에는 활짝 개방되어 있습니다. 반면 인터페이스를 이용하는 클래스(
UserDao
)는 자신의 변화가 불필요하게 일어나지 않도록 굳게 폐쇄되어 있습니다. 자신이 필요한 기능을 가진 클래스를 직접 생성하지도, 정보를 알지 않아도, 해당 클래스를 전달받아 인터페이스를 통해 기능을 충분히 사용할 수 있는 것입니다.
- 높은 응집도
- 비슷한 기능을 수행하는 요소들이 모여 있고, 목적과 관심사가 명확하게 구분되어 있음
- 변경이 필요한 경우 해당 요소만 수정하면 되고, 코드의 가독성과 유지 보수성이 높음
ConnectionMaker
인터페이스를 통해 DB 연결 기능을 독립시켰다. 만약 DB의 연결방식이 변경되어 기존에 구현한DConnectionMaker
코드를 수정하더라도,UserDao
등 다른 클래스의 수정을 요구하지 않고, 기능에 영향을 주지 않는다. DB의 연결방식이 변경되었다고 다른 모든 코드들을 테스트할 필요도 없다, 단지 변경된DConnectionMaker
코드가 DB와 잘 연결되는지만 확인하면 된다. 이는ConnectionMaker
를 분리해서 응집도를 높인 덕분이다. 이에 반대는NUserDao
,DUserDao
로 구현한 낮은 응집도이다.- 낮은 응집도
- 다양한 기능을 수행하는 요소들이 하나의 모듈 또는 클래스에 포함되어 있고, 목적과 관심사가 혼합되어 있음
- 변경이 필요한 경우 여러 요소들을 수정해야 하고, 코드의 가독성과 유지 보수성이 낮음
- 낮은 응집도
- 낮은 결합도
결합도란 하나의 클래스가 변경이 일어날 때에 관계를 맺고 있는 다른 클래스의 변화를 요구하는 정도입니다. 낮은 결합도란 이러한 요구 수준이 낮기 때문에 의존도가 클래스 간의 의존도가 낮은 상태를 의미합니다.
ConnectionMaker
인터페이스로 DB 연결 기능을 구현한 클래스가 바뀌더라도 DAO의 코드는 변경될 필요가 없어졌습니다. 이는ConnectionMaker
와UserDao
의 결합도가 낮아진 것입니다. 또한ConnectionMaker
을 구현한 클래스를 결정하고 생성하는 것을UserDao
클래스에서 하는 것이 아닌,UserDao
객체를 사용하는UserDaoTest
로 옮김으로써 결합도가 더욱 낮아졌습니다.
- 전략 패턴
전략 패턴은 자신의 기능에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 클래스를 필요에 따라 바꿔서 사용할 수 있는 디자인 패턴입니다. DB연결 방식을
ConnectionMaker
인터페이스로 정의하고, 이를 구현한 클래스의 전략(구현부)를 바꿔가면서 사용할 수 있게 분리했습니다.UserDaoTest
-UserDao
-ConnectionMaker
구조로 설명할 수 있습니다. UserDao를 사용하는 사용자(UserDaoTest
)는UserDao
가 사용할 전략(ConnectionMaker
를 구현한 클래스)을UserDao
의 생성자를 통해 제공해주었습니다.
프로젝트 환경
- IDE : STS3 - 3.9.18.RELEASE
- SpringFramework : 5.3.20
- Java : 11
- Maven
📖토비 스프링 3.1 -p71~87
🚩jhcode33의 toby-spring-study.git으로 이동하기
Uploaded by N2T