remagine
토비의 스프링 3.1 읽기 (1) 본문
토비의 스프링 3.1을 읽으면서 주요내용을 나열해 보았습니다.
나름의 요약 정리입니다.
1. 간단한 dao 구조를 만들어 보았다.
1.1 postgresql을 사용해서 table을 만들고 jdbc로 set,get해 보았다.
1.2 성공
2. 관심사의 분리
2.1 확장,변경이 용이하기 위해서 관심사에 따라 객체를 나눠야 한다
2.2.1 DB 연결을 위한 커넥션을 어떻게 가져올 것인가.
2.2.2 DB로 보낼 Statement를 만들고 실행하는 것
2.2.3 작업이 끝나면 사용한 Statement와 Connection 객체를 Close해서 공유 리소스를 시스템에 돌려준다.
3 중복코드의 메소스 추출 / Refactoring과 Test
3.1 커넥션 코드의 중복 = 스파게티 소스. 커넥션 코드 변경하면 수백개의 DAO를 일일이 변경해야 하는 문제
1 2 | Class.forName("org.postgresql.Driver"); Connection c = DriverManager.getConnection("jdbc:postgresql://localhost:5431/arthur", "arthur", "arthur"); | cs |
부분을 드래그 한 후 Refactor 에서 Extract Method를 실행한다.
그 후 getConnection으로 Method이름을 지정해 주면
해당 소스는
1 2 3 4 5 6 7 8 | private Connection getConnection() throws ClassNotFoundException, SQLException { Class.forName("org.postgresql.Driver"); Connection c = DriverManager.getConnection("jdbc:postgresql://localhost:5431/arthur", "arthur", "arthur"); return c; } public User get(String id) throws ClassNotFoundException, SQLException { Connection c = getConnection(); | cs |
위와 같이 모두 변경된다. 굿굿
3.2 DB 커넥션 만들기 독립
3.2.1 상속을 통한 확장
슈퍼클래스에 기본적인 로직의 흐름(커넥션 가져오기, SQL 생성, 실행, 반환...)을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected메소드 등으로 만든뒤 서브클래스에서필요에 맞게 구현해서 사용하도록 하는 방법을 템플릿 메소드 패턴이라 한다.
한 줄 요약 : UserDao에 팩토리 메소드 패턴을 적용해서 getConnection()을 분리합시다
3.2.2 템플릿 메소드 패턴
상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법이다. 변하지 않는 기능은 슈퍼클래스에 만들어두고, 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 한다. 슈퍼클래스에서는 미리 추상 메소드 또는 오버라이드 가능한 메소드를 정의해두고 이를 활용해 코드의 기본 알고리즘을 담고 있는 템플릿 메소드를 만든다. 서브클래스에서는 추상 메소드를 구현하거나, 훅 메소드를 오버라이드 하는 방법을 통해 기능의 일부를 확장한다.
3.2.3 팩토리 메소드 패턴
3.2.3 문제점
상속을 통한 확장에는 문제가 있다. 자바는 클래스의 다중상속을 허용하지 않는다. 상속은 부모자식클래스간 긴밀한 연결을 제공한다. 그래서 슈퍼클래스 내부가 변경될 때 서브 클래스 변경이 필요할 수 있다. 또한 UserDao외에 다른 클래스가 만들어진다면 상속을 통해 만들어진 getConnection() 구현코드가 DAO클래스마다 중복된다.
4. 상속은 문제가 있는 확장패턴이다.
4.1 독립된 메소드로 분리 --> 상하위 상속 클래스로 분리 --> 완전한 독립적인 클래스로 분리
4.2 가장 깔끔한 분리 방법이다.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | package springbook.user.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import springbook.user.domain.User; import java.sql.*; public class UserDao { private SimpleConnectionMaker simpleConnectionMaker; public UserDao(){ simpleConnectionMaker = new SimpleConnectionMaker(); } public static void main(String[] args) throws ClassNotFoundException, SQLException{ UserDao dao = new UserDao(); User user = new User(); user.setId("id"); user.setName("name"); user.setPassword("spring"); dao.add(user); System.out.println(user.getId() + "등록 성공"); User user2 = dao.get(user.getId()); System.out.println(user2.getName()); System.out.println(user2.getId() + "조회 성공"); } public void add(User user) throws ClassNotFoundException, SQLException { Connection c = getConnection(); PreparedStatement ps = c.prepareStatement("insert into users(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(); } private Connection getConnection() throws ClassNotFoundException, SQLException { Class.forName("org.postgresql.Driver"); Connection c = DriverManager.getConnection("jdbc:postgresql://localhost:5431/arthur", "arthur", "arthur"); return c; } public User get(String id) throws ClassNotFoundException, SQLException { Connection c = getConnection(); PreparedStatement ps = c.prepareStatement("select * from users 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; } } | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package springbook.user.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class SimpleConnectionMaker { public Connection makeNewConnection() throws ClassNotFoundException, SQLException{ Class.forName("org.postgresql.Driver"); Connection c = DriverManager.getConnection("jdbc:postgresql://localhost:5431/arthur", "arthur", "arthur"); return c; } } | cs |
상속을 통하지 않고, 관심사에 따라 클래스를 분리해서 사용하게 만들었다. 이런 리팩토링이 이뤄진 후 반드시 '검증'이 거쳐져야한다. 기능의 변화없이 구조만 변화되었다고 하지만, 그것은 Test를 통해 확인이 필요하다.
5. 추가적으로 필요한 사항이 생겼다. 저런 방식으로 사용하게 되면, connection을 가져오는 방식이 변할 때마다 바꿔줘야 하고, makeNewConnection이 아닌 다른 이름을 사용하게 되면 일일이 변경해 줘야 한다.
6. 인터페이스의 도입
이렇게 두 개의 클래스가 긴밀하게 연결되 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어 주는 방법이 있다. 자바가 추상화를 위해 제공하는 가장 유용한 도구는 바로 인터페이스이다. 인터페이스는 자신을 구현한 클래스에 대한 구체적인 정보는 모두 감춰버린다.
6.1 인터페이스의 장점
오브젝트를 만드려면 구체적인 클래스 하나를 선택해야 한다. 인테페이스로 추상화해놓은 최소한의 통로를 통해 접근하는 쪽에서는 오브젝트를 만들 때 사용할 클래스가 무엇인지 몰라도 된다. 즉 인터페이스를 통해 접근하게 하면 실제 구현 클래스를 바꾸도 신경 쓸 일이 없다.
6.2 바꿔진 코드리뷰
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | package springbook.user.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import springbook.user.domain.User; import java.sql.*; public class UserDao { private ConnectionMaker connectionMaker; // 인터페이스를 통해 오브젝트에 접근하므로 구체적인 클래스 정보를 알 필요가 없다. //private SimpleConnectionMaker simpleConnectionMaker; public UserDao(){ //simpleConnectionMaker = new SimpleConnectionMaker(); connectionMaker = new DConnectionMaker(); // 하지만 생성자에는 여전히 특정 Connection 생성방법 클래스네임이 나오고 있다. } public static void main(String[] args) throws ClassNotFoundException, SQLException{ UserDao dao = new UserDao(); User user = new User(); user.setId("id"); user.setName("name"); user.setPassword("spring"); dao.add(user); System.out.println(user.getId() + "등록 성공"); User user2 = dao.get(user.getId()); System.out.println(user2.getName()); System.out.println(user2.getId() + "조회 성공"); } public void add(User user) throws ClassNotFoundException, SQLException { Connection c = connectionMaker.makeConnection();// 인터페이스에 정의된 메소드를 사용하므로 클래스가 바뀐다고 해도 메소드 이름이 변경될 걱정은 없다. PreparedStatement ps = c.prepareStatement("insert into users(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(); } private Connection getConnection() throws ClassNotFoundException, SQLException { Class.forName("org.postgresql.Driver"); Connection c = DriverManager.getConnection("jdbc:postgresql://localhost:5431/arthur", "arthur", "arthur"); return c; } public User get(String id) throws ClassNotFoundException, SQLException { Connection c = connectionMaker.makeConnection(); PreparedStatement ps = c.prepareStatement("select * from users 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; } } | cs |
인터페이스에 정의된 메소드를 통해 Connection을 생성하므로 다양한 Connection 방식의 클래스를 하나의 생성방법으로 통일 할 수 있다.
하지만 여전히 생성자에는 특정 방식으로 생성해야 되는 생성클래스 네임이 지정되고 있다.
7. 관계설정 책임의 분리
여전히 UserDao에는 어떤 ConnectionMaker 구현 클래스를 사용할지 결정하는 코드가 남아 있다. 이 때문에 인터페이스를 이용한 분리에도 불구하고 여전히 UserDao 변경 없이는 DB 커넥션 기능의 확장이 자유롭지 못하다.
그 이유는 UserDao안에 분리되지 않은 또 다른 관심사항이 존재하고 있기 때문이다.
그것은 UserDao가 어떤 ConnectionMaker 구현 클래스의 오브젝트를 이용할 것인지 결정하는 것이다. 즉 UserDao와 UserDao가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계를 설정해주는 것에 관한 관심이다. 이 관심사를 담은 코드를 UserDao에서 분리하지 않으면 UserDao는 결코 독립적으로 확장 가능한 클래스가 될 수 없다.
7.1 UserDao의 클라이언트에서 UserDao를 사용하기 전에 , 먼저 UserDao가 어떤 ConnectionMaker의 구현 클래스를 사용할 지 결정하도록 만들자
관계에서 UserDao가 독립적이려면 인터페이스 말고는 다른 어떤 클래스와도 직접적으로 연결되어선 안된다.
7.2 오브젝트 사이 간에 다이내믹한 관계
클래스 사이 간의 관계 = 코드내에 다른 클래스 이름이 나타나기 때문에 만들어진다
다형성을 통한 다이내믹한 관계 = 해당 클래스가 구현한 인터페이스를 받아서 사용. 다형성을 통한 가능
7.3 코드리뷰
관계설정의 책임을 UserDao의 클라이언트인 main() 메소드에게 떠넘겨 보았다.
UserDaoTest클래스로 main클래스를 이동한다.
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 | package springbook.user.dao; import java.sql.SQLException; import springbook.user.domain.User; public class UserDaoTest { public static void main(String[] args) throws ClassNotFoundException, SQLException { ConnectionMaker connectionMaker = new DConnectionMaker(); UserDao dao = new UserDao(connectionMaker); User user = new User(); user.setId("id"); user.setName("name"); user.setPassword("spring"); dao.add(user); System.out.println(user.getId() + "등록 성공"); User user2 = dao.get(user.getId()); System.out.println(user2.getName()); System.out.println(user2.getId() + "조회 성공"); } } | cs |
1 2 3 4 | public UserDao(ConnectionMaker connectionMaker){ //simpleConnectionMaker = new SimpleConnectionMaker(); this.connectionMaker = connectionMaker; } | cs |
UserDao 메소드는 클라이언트가 지정한 connectionMaker 인터페이스를 통해 생성된 DConnectionMaker()를 인자로 받는다.
이로써 UserDao는 관심사가 모두 분리되었다.
이로써 사용자는 UserDao에는 전혀 손대지 않고도 모든 고객이 만족스럽게 DB 연결 기능을 확장해서 사용할 수 있게 되었다.
DB커넥션을 가져오는 방법을 어떻게 변경하든 UserDao 코드는 아무런 영향을 받지 않는다.
8. 원칙과 패턴
8.1 개방 패쇄 원칙 (OCP)
클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.
8.2 객체 지향 설계 원칙 (SOLID)
생략
8.3 높은 응집도와 낮은 결합도
응집도가 높다는 것은
하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻이다.
불필요하거나 직접 관련이 없는 외부의 관심과 책임이 얽혀 있지 않으며, 하나의 공통 관심사는 한 클래스에 모여 있다.
변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것이다.
응집도가 높을 수록 변경이 일어날 모듈안에서만 수정이 일어나며, 다른 부분에 영향을 끼칠 것을 걱정하지 않아도 된다.
낮은 결합도 . 책임과 관심사가 다른 오브젝트 또는 모듈과는 느슨하게 연결된 형태를 유지하는 것이 바람직하다. 느슨한 연결은 관계를 유지하는데 꼭 필요한 최소한의 방법만 간접적인 형태로 제공하고, 나머지는 서로 독립적이고 알 필요도 없게 만들어주는 것이다. 결합도가 낮아지면 변화에 대응하는 속도가 높아지고, 확장하기에 용이하다.
결합도란 "하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도"
8.4 전략 패턴
개선된 UserDaoTest - UserDao - ConnectionMaker 구조를 디자인 패턴의 시작으로 보면 전략 패턴(Strategy Pattern)에 해당한다고 볼 수 있다.
전략 패턴은 자신의 기능 맥락(Context)에서 , 필요에 따라 변경이 필요한 알고리즘을 인턴페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.
'Spring' 카테고리의 다른 글
토비의 스프링 3.1 읽기 (2) (0) | 2016.09.27 |
---|---|
spring.io에서 spring 따라 만들기 (0) | 2016.09.24 |