스프링이 관심 갖는 대상인 오브젝트의 설계와 구현, 동작원리에 집중하여 아래 예제를 통해 스프링이 무엇인지 이해해보자.
1. 간단한 DAO로 문제점을 파악해보자
❓ DAO (Data Access Object)
DB를 사용해 데이터를 조회, 조작하는 기능을 전담하도록 만든 오브젝트.
사용자 정보를 Jdbc API를 통해 DB에 저장하고 조회할 수 있는 간단한 DAO를 만들어보자.
1-1. User Class 생성
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; } }
- id, name, password 세 개의 프로퍼티를 가진 User 클래스이다.
- 자바빈 규약을 따르는 오브젝트를 생성하였다.
❓ 자바빈 : 아래의 조건을 충족하는 오브젝트를 가리킨다.
- 디폴트 생성자가 있어야 한다. (툴이나 프레임워크에서 리플랙션을 이용해 오브젝트를 생성하기 때문에 필요하다)
- 프로퍼티는 setter, getter 접근메서드를 통해 수정, 조회가 가능해야 한다.
- 프로퍼티는 자바빈이 노출하는 이름을 가진 속성이다.
1-2. UserDao 생성
- 사용자 정보를 관리하는 UserDao를 만든다.
- 사용자 생성(add), 사용자 정보 조회(get)
public class UserDao { public void add(User user) throws ClassNotFoundException, SQLException { Class.forName("com.mysql.jdbc.Driver"); Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book"); 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(); } // get() 생략 }
작업 순서
- DB Connection 가져오기
- SQL 담은 Statement(Prepared Statement) 만들기
- Statement 실행
- 조회의 경우 실행 결과를 ResultSet 으로 받아 오브젝트에 옮긴다 (User)
- 작업 후 Connection, Statement, ResultSet 닫기
DAO를 테스트 해보려면 어떻게 해야할까?
- DAO 기능을 사용하는 웹 애플리케이션을 만들고 웹 브라우저를 통해 DAO 기능을 사용한다. → UserDao만 확인하고 싶은데 배보다 배꼽이 더 크다. 😖
- main()을 이용한 DAO 테스트 코드를 작성한다. : 만든 DAO 내부에 main 메서드를 만들고 User 오브젝트 생성, 프로퍼티 값 넣고 add() 메서드 이용해 DB를 등록한다. 각 단계별로 콘솔 메시지도 출력해볼 수 있다.
but! 2번 방법에도 문제점이 많다. 왤까?
잘 동작하는 코드를 구지 개선하는 이유가 뭘까? 장점은? 스프링을 사용하는 개발에서 무슨 차이가 있을까? 아래 과정을 통해 문제점을 해결하며 리팩토링 해보자.
2. 관심사의 분리
왜 해야할까?
한 가지 관심에 대해 문제가 일어나지만 그에 따른 작업은 한 곳에 집중되지 않는 경우가 많다. DB 접속용 암호를 바꾸려고 DAO 클래스 수백 개를 모두 수정한다면? 트랜잭션 기술을 교체한다고 비즈니스 로직 코드 구조를 모두 변경해야 한다면? 등의 상황을 대비하기 위함이다.
관심이 같은 것 끼리 모으고, 관심이 다른 것은 따로 떨어져 있게 해야한다.
2-1. 커넥션 만들기의 추출
UserDao의 관심사항
- DB 연결을 위한 Connection 오브젝트를 가져온다.
- 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행한다.
- 리소스를 시스템에 반납한다.
중복 코드의 메서드 추출
public void add(User user) throws ClassNotFoundException, SQLException { Connection c = getConnection(); // .... } public void get(String id) throws ClassNotFoundException, SQLException { Connection c = getConnection(); // .... } private Connection getConnection() throws ClassNotFoundException, SQLException { Class.forName("com.mysql.jdbc.Driver"); Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book"); return c; }
- getConnection() 로 1번 관심사항을 분리시켰다. add(), get() 에서 중복되는 코드는 분리된 메서드를 호출하게 된다.
2-2. 커넥션 만들기의 독립
문제 상황 도출
- N사와 D사에게 UserDao 를 납품해야 한다.
- 각 사는 다른 종류의 DB를 사용하고 있다.
- DB 커넥션을 가져오는데 있어 독자적으로 만든 방법을 적용하고 싶어한다.
- 구매 이후에도 커넥션 방법이 변경될 수 있다.
→ 어떻게하면 UserDao 소스코드를 N,D사에 제공해주지 않고 고객 스스로 원하는 DB 커넥션 생성 방식을 적용해가면서 UserDao를 사용하게 할 수 있을까?
상속을 통한 확장
getConnection() 내 코드를 제거하고 추상메서드로 만든다. 추상 메서드라 add(), get() 코드 내 getConnection()는 그대로 있게 된다. NUserDao, DUserDao 서브클래스를 만들고 UserDao 클래스를 상속해서 getConnection()을 각 사별로 원하는 방식대로 구현한다.
→ UserDao 소스코드를 제공해서 수정해 쓰지 않아도 원하는 방식으로 확장하여 UserDao 기능과 함께 사용할 수 있다.
관심사가 클래스 레벨로 분리되었다.
- UserDao : 어떻게 데이터를 등록, 가져올 것인가(SQL 작성, 파라미터 바인딩, 쿼리 실행, 검색정보 전달)
- NUserDao, DUserDao : DB 연결 방법은 어떻게 할 것인가
수정된 로직에는 두 가지 디자인 패턴이 적용되었다고 볼 수 있다.
- 템플릿 메소드 패턴
- 팩토리 메소드 패턴
템플릿 메소드 패턴
- 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경,확장되는 기능은 서브클래스로 확장하여 만드는 것이다.
- 슈퍼클래스에 미리 추상 메소드/오버라이드 가능 메소드(훅메소드)를 정의해두고 이를 담고있는 기본 템플릿 메소드를 만든다.
- 훅메소드 : 슈퍼클래스에서 디폴트 기능을 정의해두거나, 비웠다가 서브클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메소드 (선택적으로 오버라이드 가능)
- 추상 메소드 : 슈퍼클래스에 정의만 해두고 상속 시 반드시 구현을 강제한 메소드 (반드시 구현)
public static class Super { public void templateMethod() { // 기본 골격을 담고있는 메소드 // 기본 알고리즘 코드 hookMethod(); abstractMethod(); } protected void hookMethod() {} // 선택적으로 오버라이드 가능 public abstract void abstractMethod(); // 서브클래스에서 반드시 구현 } public class Sub1 extends Super { // Super 클래스의 기능을 확장한다. protected void hookMethod() { ... } public void abstractMethod() { ... } }
팩토리 메소드 패턴
- 팩토리 메소드 : 서브클래스에서 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메소드
- 팩토리 메소드 패턴 : 위 방식을 통해 오브젝트 생성 방법을 나머지 로직, 즉 슈퍼클래스의 기본 코드에서 독립시키는 방법
❓ 뭐지..? 두 패턴이 뭐가 다른거지?
→ 상속을 통해 독립적으로 변경하고 기능을 확장한다는 의미는 같다. 템플릿 메소드 패턴은 기본 규격을 갖춘 템플릿을 만들고 그 중간에 들어가는 로직을 갈아끼우는 것이다. 팩토리 메소드 패턴은 서브 클래스에서 오브젝트를 생성 방법을 결정하고 그 로직을 분리하여 슈퍼 클래스의 기본 코드로 부터 독립시키는 것이다.
상속을 했을 때의 한계점
- UserDao 가 다른 목적을 위해 상속을 사용하고 있다면?
- 다중 상속이 불가하다.
- 상속을 통한 상하위 클래스 관계를 생각보다 밀접하다.
- 서브클래스는 슈퍼클래스의 기능을 직접 사용할 수 있다. 슈퍼클래스 내부 변경이 있을 때 모든 서브클래스를 함께, 수정/개발 해야할 수도 있다.
- 다른 DAO 클래스들이 만들어진다면?
- getConnection() 구현 코드가 매 DAO 클래스마다 중복될 것이다.
✔ To do
- abstract의 public, protected 붙는 차이
'개발 독서 > 토비의 스프링3' 카테고리의 다른 글
[토비의 스프링3] 8-4. 스프링의 기술(2) - AOP, PSA (0) | 2021.03.25 |
---|---|
[토비의 스프링3] 8-4. 스프링의 기술(1) - IoC/DI (0) | 2021.03.25 |
[토비의 스프링3] 8-3. POJO 프로그래밍 (0) | 2021.03.23 |
[토비의 스프링3] 8.1~2 스프링의 정의, 목적 (0) | 2021.03.23 |