일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 다익스트라
- 기술면접
- onclick
- 직무면접
- compiler
- spring boot
- top-down
- 우선순위큐
- 코딩테스트
- Android Studio
- kmeans
- BufferedReader
- Django
- scikit-learn
- 플로이드 와샬
- 유니온 파인드
- 최단경로
- Controller
- 거쳐가는 정점
- Java
- Python
- union-find
- 엔테크서비스
- bottom-up
- 음수가 포함된 최단경로
- 벨만 포드 알고리즘
- clean code
- 동적계획법
- dto
- disjoint set
- Today
- Total
춤추는 개발자
[Spring] 유연하고 확장하기 쉬운 코드 만들기! 코드의 분리와 확장 본문
Spring 프레임워크에서 가장 많은 관심을 두는 대상은 오브젝트 즉, 객체입니다. 어플리케이션에서 오브젝트가 생성되면 다른 오브젝트와 관계를 맺고, 사용되고, 소멸되기까지 전 과정을 진지하게 생각해볼 필요가 있습니다.
특히, 오브젝트의 기술적인 특징과 사용 방법 등을 고민하는 설계 단계부터 시작하여 디자인 패턴, 리팩토링, 단위 테스트와 같은 구현 단계까지 다양한 응용 기술과 지식이 요구됩니다.
이번 포스팅에서는 DAO코드를 작성하며 오브젝트의 설계과 구현, 개선 방향까지 단계적으로 나아가는 시간을 갖겠습니다.
📢DAO란?
DB에 접근하여 데이터를 조회하거나 수정하는 기능을 담당하는 오브젝트
✅ DAO 코드 작성하기
먼저, 예제로 사용될 코드를 작성하겠습니다. 사용자 정보를 저장할 User클래스를 아래와 같이 만들겠습니다.
public class User {
String id;
String name;
String password;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
User 오브젝트에 담긴 정보가 실제로 보관될 DB 테이블도 필요하지만, 이는 생략하도록 하겠습니다. 실제로 DB 테이블을 구성할 때는 User 클래스의 프로퍼티와 동일하게 구성해야 합니다.
필드명 | 타입 | 설정 |
Id | VARCHAR(10) | Primary Key |
Name | VARCHAR(20) | Not Null |
Password | VARCHAR(20) | Not Null |
다음은 DAO클래스를 작성합니다. 일단 기본적으로 add, get 메서드를 아래와 같이 작성합니다. 코드가 너무 길어지기 때문에 편의상 import문을 제외합니다.
public class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:url", "userName", "userPassword");
PreparedStatement ps = c.prepareStatement(
"INSERT INTO user(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
ps.executeUpdate();
ps.close();
c.close();
}
public User get(String id) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:url", "userName", "userPassword");
PreparedStatement ps = c.prepareStatement(
"SELECT * FROM user WHERE id = ?");
ps.setString(1, id);
ResultSet rs = ps.executeQuery();
rs.next();
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
rs.close();
ps.close();
c.close();
return user;
}
}
마지막으로 작성할 코드는 위에 작성한 DAO의 메서드를 테스트할 수 있는 main() 메서드를 작성합니다.
main() 메서드를 실행하면 DAO의 add, get 메서드가 실행됩니다. 만약, 출력문이 제대로 나온다면 코드가 정상적으로 작성됐음을 확인할 수 있습니다.
public static void main(String[] ars) throws ClassNotFoundException, SQLException {
UserDao dao = new UserDao();
User user = new User();
user.setId("dev");
user.setName("heon9u");
user.setPassword("123123");
dao.add(user);
System.out.println(user.getId() + "가입 완료");
User user2 = dao.get(user.getId());
System.out.println(user2.getName() + ", " + user2.getPassword());
}
지금까지 작성한 코드는 말 그대로 그냥 동작만 확인할 수 있는 정도의 DAO 오브젝트입니다. 한눈에 봐도 수정해야할 부분이 보이며, API가 많아지거나 요구사항이 바뀌는 경우, 시간 낭비하기 딱 좋은 스파게티 코드가 완성될 것입니다. 이제부터 DAO를 객체지향 원리에 충실한 코드로 개선해보는 작업을 진행하겠습니다.
✅ DAO 분리하기
유지보수가 용이한 or 미래 지향적인 코드란 변화에 빠르게 대처할 수 있는 코드라고 생각합니다. 즉, 비즈니스적으로 특정 기능을 추가, 수정, 삭제하는 경우, 코드를 변경하는 폭을 최소한으로 줄이는 것입니다. 단 몇 줄의 코드만 변경함으로써 요구사항에 대응하고, 나머지 기능에 문제를 주지 않는다면 더할 나위없이 좋은 코드입니다.
이러한 코드를 작성하기 위해서는 분리와 확장을 고려한 설계가 필요합니다. 먼저 코드를 분리하기 위해 코드들이 어떤 오브젝트를 다루는지, 어떤 계층에 가까운지, 무슨 기능을 담당하는지 등을 파악해야 합니다. 즉, 코드들을 관심사에 따라 분리하는 것입니다.
1. 중복된 코드 추출하기
앞서 작성했던 DAO코드를 살펴보면 add() 메서드를 3가지의 관심사로 분리할 수 있습니다.
- DB연결을 위한 Connection 가져오기
- DB에 보낼 SQL 쿼리를 담을 Statement를 만들고 실행하기
- 작업 중 사용한 리소스(Statement, Connection) 닫기
먼저, 첫번째 관심사를 살펴보겠습니다. DAO의 get, add 메서드를 보면 커넥션을 가져오는 중복된 코드를 발견할 수 있습니다.
여기서 만약, 수천개의 메서드가 작성되는 상황이 생긴다면? 커넥션을 가져오는 중복된 코드 또한 수천개 작성되는 불필요함을 가져오게 됩니다. 이러한 상황을 해결하기 위해 아래와 같은 코드를 작성하며 커넥션을 가져오겠습니다.
private Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:url", "userName", "userPassword");
return c;
}
이제 메서드가 수천개씩 늘어나도 getConnection() 메서드를 호출하면 됩니다. 또한, DB 연결관련 부분이 수정되어도 getConnection() 메서드만 수정하면 됩니다. 이러한 작업을 리팩토링이라고 하며, 메서드별로 중복된 기능을 뽑아내는 것을 메서드 추출기법이라고 부릅니다.
2. 커넥션 만들기의 독립
이번에는 다른 상황을 가정하겠습니다. 만약, 메서드별로 다른 DB 커넥션을 가져와야 한다면 그만큼 getConnection() 메서드가 많아지게 됩니다. 물론, 파라미터를 통해 접근할 수 있습니다.
하지만, 외부 어플리케이션에서 UserDao에 접근하길 원하고, DB 커넥션을 가져올 때 독자적인 방법을 사용하고 싶어하는 경우가 발생할 수 있습니다. 게다가, DB 커넥션을 접근 시, 보안상 지켜야할 정보가 포함되있다면 메서드의 파라미터로 적용하길 원치 않을 수 있습니다.
UserDao의 소스 코드를 외부에 넘겨주고 알아서 코드를 수정하여 사용하라고 할 수 있지만, 코드에 포함된 기술을 노출시키고 싶지 않을 수 있습니다.
이러한 복합적인 상황에 대응하고자 UserDao 코드를 한단계 더 분리하여 상속을 통한 확장을 진행하겠습니다. 일단 DB의 커넥션을 가져오는 메서드를 추상 메서드로 만들고 구현 코드를 삭제합니다.
그러면 외부에서 접근 시, UserDao를 상속하는 서브 클래스를 만들고, getConnection() 메서드를 구현할 수 있습니다. 아래 그림은 A, B라는 외부 어플리케이션에서 UserDao의 기능을 자유롭게 확장하여 사용하는 방법을 보여줍니다.
결국, 앞서 분리했던 코드를 리팩토링하면 아래와 같습니다.
public abstract class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = getConnection();
...
}
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
public class A_UserDao extends UserDao {
public Connection getConnection() throws ClassNotFoundException, SQLException {
// A 어플리케이션의 DB 커넥션 생성 코드
}
}
public class B_UserDao extends UserDao {
public Connection getConnection() throws ClassNotFoundException, SQLException {
// B 어플리케이션의 DB 커넥션 생성 코드
}
}
이렇게 메인 클래스에 기본적인 로직의 흐름(커넥션 가져오기, SQL 생성, 실행, 반환)을 만들고, 그 기능의 일부를 추상 메서드나 오버라이딩이 가능한 protected 메서드 등으로 만든 뒤, 서브 클래스에서 필요에 맞게 구현하여 사용하며 확장할 수 있습니다.
이러한 방법을 디자인 패턴에서 템플릿 메서드 패턴이라고 합니다. 또한, 서브클래스에서 구체적인 오브젝트를 어떻게 생성할 것인지 결정하게 하는 것을 팩토리 메서드 패턴이라고 부릅니다.
UserDao는 Connection이 인터페이스 타입의 오브젝트라는 것 외에는 관심이 없습니다. 그저 Connection 인터페이스에 정의된 메서드를 사용할 뿐입니다. A_UserDao와 B_UserDao에서는 어떤 식으로 Connection 기능을 제공하는지에 관심을 두고 있을 뿐입니다.
즉, UserDao는 Connection 오브젝트가 만들어지는 방법과 내부 동작 방식에는 관심이 없고, 필요한 기능을 Connection 인터페이스에서 사용하기만 할 뿐입니다.
아래 그림은 UserDao에 적용된 팩토리 메서드 패턴입니다.
하지만, 이 역시 상속을 사용했다는 단점이 있습니다. 자바 특성상 다중 상속이 불가능하기 때문에 커넥션 오브젝트를 가져오는 방법을 상속 구조로 만들어버리면 다른 목적으로 UserDao에 상속을 적용하기 어려워집니다.
이외에도 상속을 통한 상하위 클래스는 밀접하기 때문에 메인 클래스 내부의 변경이 있는 겨우, 모든 서브클래스를 함께 수정하거나 다시 개발해야할 수도 있습니다.
확장된 기능인 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에 적용할 수 없다는 단점도 있습니다.
✅ DAO 확장하기
지금까지 DAO코드를 관심사에 따라 분리하기 위해 점진적으로 진행했습니다. 처음에는 독립된 메서드를 만들어서 분리했고, 다음에는 상하위 클래스로 나눠 상속을 통한 확장을 구현했습니다.
이번에는 완전히 독립적인 클래스로 만들어서 UserDao와 DB 커넥션과 관련된 부분을 분리해보겠습니다.
DB 커넥션을 생성하는 클래스인 ConnectionMaker라는 클래스를 생성합니다. 그리고 UserDao에서 new 연산자를 통해 ConnectionMaker의 오브젝트를 생성하며 DB 커넥션을 가져올 수 있습니다. 이러한 방식으로 리팩토링한 코드는 아래와 같습니다.
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao() {
connectionMaker = new ConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeNewConnection();
...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeNewConnection();
...
return user;
}
}
public class ConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:url", "userName", "userPassword");
return c;
}
}
위처럼 별도의 클래스로 ConnectionMaker를 만든다면 관심사에 따라 코드를 분리할 수 있습니다. 하지만 앞서, 외부 어플리케이션에서 UserDao에 접근하는 것처럼, DB 커넥션 기능을 확장하는 방법이 불가능해졌습니다. 왜냐하면 UserDao의 코드가 ConnectionMaker라는 특정 클래스에 종속되었고, UserDao 코드의 수정 없이 DB 커넥션을 가져오는 기능을 변경할 방법이 없어졌기 때문입니다.
이렇게 클래스를 분리한 경우에도 상속을 이용했을 때처럼 자유로운 확장이 가능하게 하려면 2가지 문제를 해결해야 합니다.
- ConnectionMaker의 메서드 문제로 만약, A_UserDao에서 makeNewConnection()이 아닌 openConnection() 메서드를 사용하는 경우
- DB 커넥션에서 제공하는 클래스가 어떤 것인지 UserDao가 구체적으로 알고있어야 합니다. 즉, ConnectionMaker라는 클래스에 대한 정보를 일일히 알고있어야 하기 때문에 UserDao는 DB 커넥션을 가져오는 방법에 종속되어 버립니다.
1. 인터페이스의 도입
위와 같은 상황을 해결하기 위한 가장 좋은 해결책은 두 개의 클래스 사이에 추상적인 무언가를 만들어주는 것입니다. 즉, 자바에서는 인터페이스를 사용하는 것입니다.
UserDao에서 ConnectionMaker 오브젝트를 만들 때, 인터페이스로 접근한다면 사용할 클래스가 무엇인지 몰라도 되고, 실제 구현 클래스를 바꿔도 신경 쓸 일이 없습니다.
아래 그림처럼 UserDao는 자신이 사용할 클래스가 무엇인지 몰라도 됩니다. 단지, 인터페이스를 통해 원하는 기능만 사용하면 됩니다.
실제 코드에서 인터페이스란 어떤 일을 하겠다는 기능만 정의해놓은 것입니다. 해당 인터페이스를 구현한 클래스들이 상황에 맞게 적절한 로직을 구현하면 되기 때문에 확장에 용이합니다.
public interface ConnectionMaker {
public Connection makeNewConnection() throws ClassNotFoundException, SQLException;
}
이후에 A_UserDao에서 사용할 ConnectionMaker를 아래와 같이 작성해주면 됩니다.
public class A_ConnectionMaker implements ConnectionMaker {
public Connection getConnection() throws ClassNotFoundException, SQLException {
// A 어플리케이션의 DB 커넥션 생성 코드
}
}
위의 과정들을 진행하여 리팩토링한 UserDao 코드는 아래와 같습니다.
public class UserDao {
private ConnectionMaker connectionMaker;
public UserDao() {
connectionMaker = new A_ConnectionMaker();
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeNewConnection();
...
}
public User get(String id) throws ClassNotFoundException, SQLException {
Connection c = connectionMaker.makeNewConnection();
...
return user;
}
}
하지만, UserDao의 생성자를 보면 A_ConnectionMaker()라는 클래스 이름이 남아있습니다. 이대로는 UserDao와 ConnectionMaker의 종속성이 남아있기 때문에 확장이 자유롭지 못하게 됩니다.
즉, UserDao가 어떤 ConnectionMaker 구현 클래스의 오브젝트를 이용하게 할지 결정하는 것이라는 충분히 독립적인 관심사를 담고 있습니다. 해당 관심사를 분리하기 위해 UserDao 오브젝트를 생성하기 전에, 먼저 어떤 ConnectionMaker의 구현 클래스르 사용할지 결정하도록 만들 것입니다.
UserDao 오브젝트를 생성 시, 생성자를 사용하게 됩니다. 이때, 생성자에 ConnectionMaker를 파라미터로 받음으로써 어떤 ConnectionMaker 구현 클래스를 사용할지 결정해주는 것입니다.
public UserDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
위처럼 UserDao와 ConnectionMaker 구현 클래스의 오브젝트 간 관계를 맺는 책임을 생성자로 넘겨주는 것입니다. 이렇게 한다면 외부 어플리케이션에서 각자 인터페이스로 구현한 ConnectionMaker를 생성자 파라미터로 넘겨주며 DB 연결 기능을 확장해서 사용할 수 있게 됩니다.
결과적으로 인터페이스를 도입하고, 생성자를 활용함으로써 유연하고 확장성이 용이한 코드를 작성하게 되었습니다. 위에서는 언급하지 않았지만, main() 메서드에서 UserDao의 오브젝트를 만들고 ConnectionMaker를 파라미터로 넘겨주는 구조는 아래와 같습니다.
🙋♂️ 느낀점
상황에 맞게 코드를 분리하고 확장하는 과정들의 중요성을 느낄 수 있었다. 알고리즘 문제를 풀 때도 중복된 코드를 줄이기 위해 모듈화를 자주 이용했다. 이처럼 이번 포스팅에서 DAO코드를 개선하기위해 메서드 추출, 디자인 패턴, 추상화, 생성자 파라미터 등의 리팩토링을 사용했다.
추상화라는 개념이 크게 와닿지 않았지만, 이번에 공부하면서 확실히 알게 됐다. 두 클래스 간의 종속성 또는 결합도를 낮추기 위한 방법으로 어떤 기능을 수행하는지에 대해 정의만 함으로써 관심사를 철저하게 분리하는 것이다. 결국, 외부에서 추상 메서드 또는 인터페이스를 재구현함으로써 원하는 동작을 수행하게 한다면 그만큼 확장성이 용이한 코드가 되는 것이다.
마지막에 생성자에 파라미터를 주입하는 방식은 실제 알고리즘 문제를 풀거나 Java로 프로젝트를 진행하면서 많이 했던 작업이다. 또한, 이러한 방식이 객체 간의 의존성을 주입하여 변경의 폭을 최소한으로 만들기 때문에 의존성을 주입하는 Annotation인 @AutoWired보다 권장하고 있다.
'Developer's_til > 스프링 프레임워크' 카테고리의 다른 글
[Spring] 하나의 오브젝트로! 스프링에서의 싱글톤(Singleton) (0) | 2021.08.18 |
---|---|
[Spring] 스프링의 핵심! 제어의 역전(IoC) (0) | 2021.08.18 |
[Spring boot] Nginx를 통한 무중단 배포 (0) | 2021.07.28 |
[Spring boot] Travis CI를 통한 배포 자동화 (0) | 2021.07.27 |
[Spring boot] 소셜 로그인 (Spring Security와 OAuth2.0) (0) | 2021.07.27 |