토비의 스프링 1. 오브젝트와 의존관계
1.1 초난감 DAO
자바 개발에서 "아티팩트(Artifact)"라는 용어는 빌드 과정에서 생성되는 파일이나 결과물을 의미합니다. 이 아티팩트는 소스 코드, 설정 파일, 라이브러리, 컴파일된 클래스 파일 등 다양한 형태를 가질 수 있으며, 자바 개발의 중요한 산출물입니다.
자바 아티팩트의 종류
1. JAR 파일 (Java Archive File): JAR 파일은 자바 클래스 파일과 해당 애플리케이션 또는 라이브러리에 필요한 리소스 파일들을 압축한 아카이브 파일입니다. - 이는 자바 애플리케이션을 배포하거나 라이브러리를 공유할 때 흔히 사용됩니다.
2. WAR 파일 (Web Application Archive File): 웹 애플리케이션의 배포를 위해 사용되는 아티팩트입니다. - JSP, 서블릿, 자바 클래스, XML, HTML, JavaScript 파일 등 웹 애플리케이션을 구성하는 모든 파일을 포함합니다.
3. EAR 파일 (Enterprise Application Archive File): 주로 엔터프라이즈 애플리케이션을 위해 사용되며, 여러 개의 모듈(웹 모듈, EJB 모듈 등)을 하나의 아카이브로 묶습니다. 대규모 시스템에서 다양한 애플리케이션 구성 요소를 통합하기 위해 사용됩니다.
4. 클래스 파일: - 자바 소스 코드가 컴파일된 후 생성되는 바이너리 파일입니다. 이 파일들은 JAR, WAR, EAR 파일로 패키징되기 전 단계의 산출물입니다.
아티팩트의 중요성
- 재사용성과 공유: 아티팩트는 다른 프로젝트나 팀원들과 코드나 라이브러리를 공유하기 위한 표준화된 방법을 제공합니다.
- 버전 관리: 아티팩트는 특정 버전으로 관리되어, 소프트웨어의 버전 관리와 배포 과정을 효과적으로 수행할 수 있게 합니다.
- 자동화된 빌드 및 배포: 현대의 개발 환경에서는 아티팩트의 생성과 배포가 자동화 도구(예: Maven, Gradle)를 통해 관리됩니다. 이는 개발 과정을 효율적으로 만들어 줍니다.
자바 아티팩트는 개발 과정에서 매우 중요한 역할을 하며, 소프트웨어의 구성, 배포, 버전 관리에 있어 핵심적인 요소입니다.
DAO
DAO (Data Access Object)는 자바에서 데이터베이스의 데이터에 접근하기 위한 객체를 의미합니다. 이 패턴은 데이터 액세스 로직과 비즈니스 로직을 분리하여 구현하는데 사용됩니다. DAO의 주요 목적은 다음과 같습니다:
1. 데이터 접근 추상화 및 캡슐화: DAO는 데이터베이스와의 모든 상호작용을 캡슐화하여, 나머지 애플리케이션에 데이터 소스로부터의 데이터 접근 방식을 숨깁니다. 이는 데이터베이스 작업을 수행하는 API를 제공함으로써, 나머지 애플리케이션이 데이터베이스에 직접 접근하는 것을 방지합니다.
2. 데이터 소스로부터의 독립성: DAO를 사용하면 애플리케이션은 특정 데이터베이스나 저장소 기술에 종속되지 않습니다. 이를 통해, 데이터베이스 시스템을 변경하거나 다른 종류의 저장소로 마이그레이션할 때 애플리케이션의 나머지 부분에 미치는 영향을 최소화할 수 있습니다.
3. 재사용 및 유지보수의 용이성: 공통된 데이터 접근 로직을 DAO에 구현함으로써, 코드 중복을 줄이고 재사용성을 높일 수 있습니다. 또한, 데이터 접근 로직을 한 곳에 집중시켜 두면 유지보수가 훨씬 용이해집니다.
DAO 패턴을 구현하는 일반적인 방법은 다음과 같습니다:
- DAO 인터페이스: 이 인터페이스는 데이터베이스 작업을 수행하는 데 필요한 메서드를 정의합니다 (예: save, get, update, delete 등).
- DAO 구현 클래스: 이 클래스는 DAO 인터페이스를 구현하며, 실제 데이터베이스 연결과 데이터 조작 로직을 포함합니다.
- DTO (Data Transfer Object): 이 객체는 데이터베이스의 데이터를 나타내며, DAO를 통해 데이터베이스와 애플리케이션 간에 데이터를 전송하는 데 사용됩니다.
예를 들어, 사용자 정보를 관리하는 애플리케이션에서는 UserDAO 인터페이스가 정의될 수 있고, 이 인터페이스는 getUserById, saveUser, updateUser, deleteUser와 같은 메서드를 포함할 수 있습니다. UserDAOImpl 클래스는 이 인터페이스를 구현하고 실제 데이터베이스 로직을 처리합니다.
DAO 패턴은 데이터베이스 관련 코드의 가독성과 유지보수성을 향상시키며, 테스트와 데이터 소스 교체를 용이하게 만듭니다.
사용자 정보 저장용 자바빈 User 클래스
package springbook.user.domain;
public class User {
String id;
String name;
String password;
public String getld() {
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;
}
}
자바빈
자바빈(JavaBean)은 자바에서 재사용 가능한 컴포넌트를 만들기 위한 일종의 규약 또는 표준입니다. 자바빈은 다양한 자바 애플리케이션, 특히 웹 애플리케이션과 데스크톱 애플리케이션에서 데이터를 표현하고 관리하는 데 널리 사용됩니다. 자바빈은 특정 규칙을 따르는 단순한 자바 클래스입니다.
자바빈의 특징
1. 캡슐화: 자바빈은 데이터(프로퍼티)를 private 필드로 저장하고, 이에 접근하기 위해 public 메서드인 getter와 setter를 제공합니다. 이를 통해 데이터의 접근과 수정을 제어할 수 있으며, 객체의 상태와 행동을 적절히 분리합니다.
2. 직렬화 가능성: 자바빈은 Serializable 인터페이스를 구현함으로써, 객체의 상태를 파일이나 네트워크를 통해 전송 가능한 형태로 변환할 수 있습니다. 이는 자바빈이 퍼시스턴스(persistence) 및 원격 전송에 적합하게 만듭니다.
3. 기본 생성자: 자바빈은 매개변수 없는 기본 생성자를 가집니다. 이는 자바빈을 쉽게 인스턴스화하고, 프레임워크나 라이브러리가 리플렉션을 사용해 객체를 자동으로 생성할 수 있게 합니다.
4. 재사용 가능성: 표준화된 디자인 패턴을 사용함으로써, 자바빈은 다양한 환경에서 재사용될 수 있습니다. 이는 개발자가 일관된 방식으로 객체를 사용하고 관리할 수 있게 해줍니다.
자바빈의 장점
1. 유연성: 자바빈은 코드의 재사용성을 높이고 유지보수를 용이하게 합니다. 표준화된 구조 덕분에 다른 개발자도 쉽게 이해하고 활용할 수 있습니다.
2. 통합성: 다양한 자바 기반 프레임워크 및 라이브러리와 쉽게 통합됩니다. 예를 들어, Spring 프레임워크는 자바빈을 사용하여 의존성 주입과 같은 고급 기능을 제공합니다.
3. 데이터 바인딩과 처리 용이성: 웹 애플리케이션에서 자바빈은 폼 데이터를 쉽게 취급할 수 있게 해줍니다. 사용자 인터페이스와 백엔드 데이터 처리 사이의 연결 고리 역할을 합니다.
실제 사용 예
웹 애플리케이션에서 자바빈을 사용하는 경우를 예로 들어보겠습니다. 가정해보면, 사용자의 입력을 받아서 처리하는 웹 폼이 있을 수 있습니다. 여기서 사용자는 이름과 이메일을 입력하고, 이 데이터는 자바빈의 인스턴스로 매핑됩니다. 다음은 간단한 자바빈의 예입니다:
public class UserBean implements Serializable {
private String name;
private String email;
public UserBean() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
위 자바빈은 사용자의 입력을 받아서 처리하는 데 사용될 수 있으며, 다양한 비즈니스 로직에서 재사용될 수 있습니다. 이처럼 자바빈은 간단하면서도 매우 강력한 도구로, 자바 기반 애플리케이션의 개발을 촉진합니다.
1.2 DAO의 분리
관심사의 분리
객체지향 기술의 핵심 장점 중 하나는 가상의 추상 세계를 효과적으로 모델링하고, 이를 자유롭게 변경, 발전시키고 확장할 수 있다는 것입니다. 이는 객체들 간의 관계와 상호작용을 유연하게 정의할 수 있기 때문에 가능합니다. 또한, 객체지향 설계는 시스템의 각 부분을 독립적인 객체로 취급하여, 한 부분의 변경이 전체 시스템에 불필요한 영향을 미치지 않도록 합니다. 이는 시스템의 유지보수성과 확장성을 크게 향상시킵니다.
시스템의 변경이 필요할 때 최소한의 수정으로 원하는 결과를 얻기 위해서는, 분리와 확장을 고려한 설계가 필수적입니다. 이는 소프트웨어 아키텍처에서의 각 컴포넌트가 단일 책임을 가지고, 서로 간의 결합도는 낮으면서도 필요에 따라 쉽게 확장할 수 있는 구조를 갖추는 것을 의미합니다. 적절한 추상화 수준과 인터페이스의 사용은 변경에 대한 유연한 대응을 가능하게 하며, 시스템 전반의 안정성과 유지보수성을 보장하는 핵심 요소가 됩니다.
결론적으로, 객체지향 설계와 프로그래밍은 변화하는 요구사항과 기술 환경에 효과적으로 대응할 수 있는 강력한 방법론을 제공합니다. 이를 통해 개발자는 보다 유연하고 확장 가능한 소프트웨어를 구축할 수 있으며, 장기적으로는 시스템의 안정성과 유지보수성을 높일 수 있습니다.
변화에서의 관심사
예를 들어, 이전에 작성된 초난감 DAO 코드의 경우를 살펴보면, 데이터베이스의 계정과 비밀번호와 같은 한 가지 관심사에 대한 변경이 필요할 때, 이러한 변경을 수용하기 위해 모든 DAO 코드를 전면적으로 수정해야 할 수도 있습니다. 이는 초난감 DAO가 데이터베이스 연결 정보와 같은 중요한 정보를 하드코딩하거나, 재사용성이 낮은 방식으로 구현되었기 때문입니다.
이러한 문제를 해결하기 위한 전략은 프로그램의 각 부분이 하나의 관심사에만 집중하도록 하는 것입니다. 즉, 특정 데이터베이스 계정 정보의 변경과 같은 단일 관심사에 대한 수정이 필요할 때, 해당 변경을 수용하기 위해 오직 한 곳의 코드만 수정하면 되도록 만드는 것입니다. 이를 위해서는 각 클래스와 모듈이 단일 책임 원칙을 준수하도록 설계해야 하며, 공통적인 기능이나 정보는 재사용 가능한 컴포넌트로 분리하는 것이 중요합니다.
결론적으로, 소프트웨어의 각 부분이 독립적으로 존재하고, 변화가 필요한 경우 최소한의 수정으로 대응할 수 있는 구조를 갖추는 것이 바람직합니다. 이는 프로그램의 유연성을 높이고, 유지보수를 용이하게 하며, 장기적인 안정성을 보장하는 데 핵심적인 역할을 합니다.
관심사 분리의 핵심
더 구체적으로는, 관심사가 유사한 기능들은 하나의 객체나 모듈 내부에 집중되어야 하며, 이는 코드의 재사용성과 유지보수성을 높이는 데 기여합니다. 반면에, 서로 다른 관심사를 가진 기능들은 가능한 한 서로 독립적으로 유지되어야 하며, 이는 시스템 전체의 복잡성을 줄이고 각 컴포넌트의 명확한 역할과 책임을 정의하는 데 도움이 됩니다.
이러한 관심사의 분리는 프로젝트 초기에 완벽하게 이루어지기 어렵습니다. 대부분의 경우, 소프트웨어는 초기에 단순한 형태로 개발되며, 시간이 지남에 따라 필요에 따라 지속적으로 리팩토링되고 개선됩니다. 이 과정에서 중요한 것은 개발 초기에 코딩을 단순화하더라도, 지속적으로 코드를 검토하고 개선하여 관심사를 명확히 분리하는 것입니다. 이러한 접근 방식은 시스템의 확장성과 유연성을 보장하고, 장기적으로 소프트웨어의 품질을 높이는 데 기여합니다.
public void add(User user) throws SQLException, ClassNotFoundException {
// 1. 커넥션 가져오기
Class.forName("org.postgresql.Driver");
String user = "postgres";
String password = "password";
Connection c = DriverManager.getConnection(
"jdbc:postgresql://localhost/toby_spring"
, user
, password
);
// 2. SQL 문장을 담을 Statement를 만들고 실행하기
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();
// 3. 리소스 반납하기
ps.close();
c.close();
}
- DB와 연결하기 위한 커넥션을 얻어오는 것
- DB에 전송할 SQL 문장을 가지는 Statement를 생성하고 실행하는 것
- 공유 리소스를 시스템에 반환하는 것
따라서, 코드의 중복을 최소화하는 것이 중요합니다. 중복된 코드를 별도의 메소드로 추출하는 리팩토링(refactoring) 기법을 적용하면, 이러한 문제를 효과적으로 해결할 수 있습니다. 코드를 메소드로 추출하면, 해당 코드가 필요한 모든 장소에서 이 메소드를 재사용할 수 있게 됩니다. 이는 코드의 재사용성을 높이고, 변경이 필요할 때 한 곳에서만 수정하면 모든 관련 부분이 자동으로 업데이트되는 이점을 제공합니다.
또한, 이러한 접근 방식은 소프트웨어 설계의 관심사 분리 원칙을 적용하는 것으로, 각 메소드나 클래스가 단일 책임을 갖도록 함으로써 코드의 명확성과 유지보수성을 향상시킵니다. 메소드 추출을 통한 리팩토링은 코드의 가독성을 높이고, 오류 가능성을 줄이며, 향후 시스템 확장이나 변경에 용이하게 대응할 수 있는 효과적인 방법입니다.
public void add(User user) throws SQLException, ClassNotFoundException {
// 1.2.2 중복 코드의 메소드 추출
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();
}
public User get(String id) throws SQLException, ClassNotFoundException {
// 1.2.2 중복 코드의 메소드 추출
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;
}
// 커넥션 얻어오기 관심사
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.postgresql.Driver");
String user = "postgres";
String password = "password";
Connection c = DriverManager.getConnection(
"jdbc:postgresql://localhost/toby_spring"
, user
, password
);
}
위 코드 예제는 데이터베이스 연결 과정을 중복 없이 관리하는 효과적인 방법을 보여줍니다. add와 get 메소드에서 공통적으로 사용되는 데이터베이스 연결 로직을 getConnection 메소드로 추출함으로써, 중복 코드를 제거하고 코드의 가독성과 유지보수성을 크게 향상시켰습니다.
이러한 리팩토링 접근 방식은 데이터베이스 연결 설정에 대한 변경이 필요할 때, 단일 메소드인 getConnection의 내용만 수정함으로써 전체 애플리케이션의 데이터베이스 연결 설정을 간편하게 관리할 수 있게 합니다. 예를 들어, 데이터베이스의 ID, 비밀번호, URL 또는 사용하는 JDBC 드라이버가 변경되었을 때, getConnection 메소드만 업데이트하면, 모든 데이터베이스 연결 로직이 자동으로 새로운 설정을 반영하게 됩니다.
이 접근 방식의 핵심은 데이터베이스 연결에 대한 구체적인 정보를 단일 지점에서 관리하고, 나머지 애플리케이션은 이 메소드를 통해 데이터베이스 연결을 가져오는 것입니다. 이를 통해 데이터베이스 연결 방식의 변경에 대응하는 데 필요한 작업을 최소화하고, 이러한 변경이 애플리케이션의 다른 부분에 불필요한 영향을 미치지 않도록 할 수 있습니다. 또한, 이러한 방식은 코드의 재사용성을 높이며, 비즈니스 로직과 데이터베이스 연결 로직 간의 결합도를 낮춰, 시스템의 전체적인 유연성과 확장성을 증가시킵니다.
변경사항에 대한 검증하기: 리팩토링과 테스트
코드의 변화, 특히 데이터베이스와 같이 중요한 기능에 관련된 변경이 있었을 때, 그것이 예상대로 작동하는지 철저하게 검증하는 것은 필수적입니다. 리팩토링이나 기능 개선 후에는 반드시 해당 변경사항이 올바르게 적용되었는지 확인하기 위해 충분한 테스트를 수행해야 합니다. 이러한 테스트 과정은 개발된 기능이 의도한 대로 동작하는지 검증하는 것은 물론, 부수적인 버그나 예상치 못한 문제점들을 사전에 발견하고 수정하는 데 중요한 역할을 합니다.
예를 들어, 초난감 DAO의 경우, `main` 메소드는 현재까지 간단한 테스트의 목적으로 활용되었을 수 있습니다. 이 메소드는 데이터베이스에 사용자를 등록하고 검색하는 기본적인 기능을 검증하는 데 유용합니다. 그러나, 이 테스트 방식에는 한계가 있습니다. 예를 들어, 테스트 과정에서 동일한 기본키를 가진 회원을 데이터베이스에 등록하려고 할 경우, 기본키의 중복으로 인해 에러가 발생할 수 있습니다. 이러한 상황은 실제 애플리케이션 운영에서 발생할 수 있는 문제점을 반영하는 것이므로, 테스트 과정에서 이러한 경우를 고려하여 보다 체계적이고 신뢰할 수 있는 테스트를 구현하는 것이 중요합니다.
이를 위해, 자동화된 단위 테스트 또는 통합 테스트를 도입하는 것을 고려할 수 있습니다. 이러한 테스트는 개발 과정에서 발생할 수 있는 다양한 시나리오를 자동으로 검증하고, 시스템의 안정성을 지속적으로 확보하는 데 큰 도움이 됩니다. 또한, 테스트 코드 작성은 소프트웨어의 품질을 보장하고, 장기적인 유지보수를 용이하게 하는 중요한 개발 관행입니다.
public static void main(String[] args) throws SQLException, ClassNotFoundException {
UserDao dao = new UserDao();
User user = new User();
user.setId("1");
user.setName("제이크");
user.setPassword("jakejake");
dao.add(user);
System.out.println(user.getId() + " register succeeded");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " query succeeded");
}
DB 커넥션을 2개로 독립시켜보기
public abstract class UserDao {
public void add(User user) throws SQLException, ClassNotFoundException {
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();
}
public User get(String id) throws SQLException, ClassNotFoundException {
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;
}
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
이러한 접근 방식은 설계의 유연성을 높이고 코드의 재사용성을 향상시킵니다. 각 하위 클래스는 getConnection() 메소드를 자신의 필요에 맞게 구현하여, 다양한 종류의 데이터베이스 또는 연결 설정에 적응할 수 있습니다. 예를 들어, 한 하위 클래스는 PostgreSQL 데이터베이스에 연결하는 방식을 구현할 수 있으며, 다른 하위 클래스는 MySQL 또는 Oracle 데이터베이스에 특화된 연결 방식을 제공할 수 있습니다.
이와 같은 추상화는 데이터베이스 연결 로직을 중앙화하고 표준화하는 동시에, 필요에 따라 다른 데이터베이스와의 연결을 유연하게 처리할 수 있는 능력을 제공합니다. 또한, 이는 데이터베이스 연결 방식의 변경이 필요할 때 UserDao의 코드를 수정하지 않고도, 하위 클래스에서만 변경을 적용할 수 있도록 하여 코드의 유지보수를 용이하게 합니다.
추상 클래스를 사용하는 이러한 설계는 객체지향 프로그래밍의 중요한 원칙 중 하나인 개방-폐쇄 원칙(Open-Closed Principle)을 따릅니다. 이 원칙은 클래스가 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다는 개념을 말하며, UserDao와 같은 추상 클래스 설계는 이 원칙을 실현하는 효과적인 방법입니다.
public class NUserDao extends UserDao{
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.postgresql.Driver");
String user = "postgres";
String password = "password";
Connection c = DriverManager.getConnection(
"jdbc:postgresql://localhost/toby_spring"
, user
, password
);
return c;
}
}
DUserDao
public class DUserDao extends UserDao {
@Override
public Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("org.postgresql.Driver");
String user = "postgres";
String password = "password";
Connection c = DriverManager.getConnection(
"jdbc:postgresql://localhost/toby_spring"
, user
, password
);
return c;
}
}
위와 같이 두 개의 클래스로 나눌 수 있다.

UserDao 클래스를 추상 클래스로 전환한 후, NUserDao와 DUserDao 같은 구체적인 하위 클래스들이 이를 상속받아서 사용하는 새로운 구조가 도입되었습니다. 이러한 변경은 하위 클래스들이 `getConnection()` 메소드를 자신들의 필요에 맞게 구현할 수 있는 유연성을 제공합니다.
이러한 접근 방식은 객체지향 설계의 상속 개념을 활용하여, 공통의 로직은 상위 클래스에 유지하면서, 개별적인 행동은 하위 클래스에서 구체화할 수 있도록 합니다. 예를 들어, NUserDao와 DUserDao는 각각 다른 데이터베이스 연결 방식을 구현할 수 있습니다. 이들 각각의 클래스는 UserDao의 추상 메소드인 `getConnection()`을 오버라이딩하여, 특정 데이터베이스에 접속하기 위한 고유한 로직을 구현합니다.
이 구조는 다음과 같은 여러 가지 이점을 가지고 있습니다:
1. 재사용성과 확장성: UserDao의 공통 로직은 모든 하위 클래스에서 재사용됩니다. 새로운 데이터베이스 연결 방식이 필요할 때마다 새로운 하위 클래스를 추가하기만 하면 됩니다.
2. 유지보수의 용이성: 공통 로직의 변경이 필요한 경우, UserDao 클래스만 수정하면 모든 하위 클래스에 자동으로 적용됩니다. 이는 유지보수를 단순화하고, 오류의 가능성을 줄입니다.
3. 분리와 캡슐화: 데이터베이스 연결에 관한 구체적인 구현은 각 하위 클래스에 캡슐화되어 있어, 시스템의 다른 부분과의 결합도가 낮아지고, 코드의 명확성이 향상됩니다.
이와 같이, NUserDao와 DUserDao의 도입은 소프트웨어 설계의 원칙을 효과적으로 적용하여, 시스템의 유연성과 확장 가능성을 크게 향상시키는 동시에, 관리의 복잡성을 줄이는 방법을 제공합니다.
디자인 패턴과 상속을 이용한 확장에 쓰이는 패턴들
요약
1. 변화에 대한 전략적 대응과 관심사의 분리: 제시된 UserDao 클래스의 예시는 객체지향 설계에서 변화에 대응하는 방법의 중요성을 보여줍니다. 초기에는 DB 연결, SQL 문 생성, 리소스 해제 등의 다양한 관심사가 하나의 메소드 안에 결합되어 있었습니다. 이러한 설계는 변경에 취약하고 유지보수가 어렵습니다. 관심사의 분리 원칙을 적용하여 이러한 기능들을 별도의 메소드로 분리함으로써, 코드는 더 유연하고 확장 가능해집니다.
2. 리팩토링을 통한 코드의 진화: 초기 add() 및 get() 메소드에 중복된 DB 연결 코드는 리팩토링을 통해 getConnection() 메소드로 추출됩니다. 이로써 각 메소드는 자신의 핵심 기능에만 집중할 수 있게 되며, DB 연결 방식의 변경이 필요할 때 하나의 메소드만 수정하면 되는 이점이 생깁니다. 이것은 리팩토링의 핵심 목표 중 하나인 '변경의 용이성'을 달성하는 좋은 예시입니다.
3. 상속과 확장성: UserDao를 추상 클래스로 전환하고, getConnection() 메소드를 추상 메소드로 선언한 뒤, 이를 상속받는 NUserDao와 DUserDao 클래스에서 구체적인 구현을 제공하는 방식은 상속의 유용성을 보여줍니다. 이는 템플릿 메소드 패턴의 일종으로, 상속을 통해 기능을 확장하고 변화에 대응하는 전략입니다.
4. 디자인 패턴의 적용: 여기에서 사용된 팩토리 메소드 패턴은 생성 로직을 서브 클래스에 위임하여, 객체 생성 과정의 변화에 따른 영향을 최소화합니다. 이는 객체 생성에 관련된 관심사를 분리하고, 코드의 재사용성과 유지보수성을 높이는 방법입니다.
5. 리팩토링과 테스트의 중요성: 코드에 변화를 주고 나서는 반드시 테스트를 수행해야 합니다. 이는 변경된 코드가 기존 기능을 해치지 않으면서도 더 나은 구조를 제공하는지 검증하는 과정입니다. 제시된 `main` 메소드에서의 간단한 테스트는 이러한 과정의 예시입니다.
6. 상속의 한계와 대안: 상속은 유용하지만 한계가 있습니다. 예를 들어, 자바에서는 다중 상속이 불가능하며, 강한 결합을 만들어 유지보수를 어렵게 할 수 있습니다. 이런 경우 인터페이스, 컴포지션, 전략 패턴 등을 고려할 수 있습니다.
이러한 방식으로 UserDao의 설계와 구현을 점진적으로 개선하는 과정은 객체지향 설계와 프로그래밍의 중요한 원칙들을 실제로 적용하는 좋은 예시입니다. 코드의 재사용성, 확장성, 유지보수성을 높이는 동시에, 변화에 효과적으로 대응하는 유연한 설계를 만들어 나가는 것이 핵심입니다.