728x90

초난감 DAO

DAO (Data Access Object)

데이터베이스를 사용해 데이터를 조회 및 조작하는 기능을 전담하는 오브젝트

 

JavaBean (Bean)

비주얼 틀에서 조작 가능한 컴포넌트

파라미터가 없는 디폴트 생성자를 갖고 있어야 함

Getter와 Setter를 통해 빈을 수정/조회 할 수 있어야 함

 

그냥 캡슐화된 객체라고 생각하면 편함

기존 JDBC를 활용한 초난감 DAO의 문제점

public class UserDao {
	public void add(User user) throws ClassNotFoundException, SQLException {
		Class.forName("com.mysql.cj.jdbc.Driver");
		Connection c = DriverManager.getConnection("jdbc:mysql://localhost/toby?characterEncoding=UTF-8&serverTimezone=Asia/Seoul", "DB계정명",
				"비밀번호");

		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 ClassNotFoundException, SQLException {
		Class.forName("com.mysql.cj.jdbc.Driver");
		Connection c = DriverManager.getConnection("jdbc:mysql://localhost/toby?characterEncoding=UTF-8&serverTimezone=Asia/Seoul", "DB계정명",
				"비밀번호");
		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 static void main(String[] args) throws ClassNotFoundException, SQLException {
		UserDao dao = new UserDao();

		User user = new User();
		user.setId("test");
		user.setName("한글");
		user.setPassword("abcd");

		dao.add(user);

		System.out.println(user.getId() + "등록성공");

		User user2 = dao.get(user.getId());
		System.out.println(user2.getName());
		System.out.println(user2.getPassword());

		System.out.println(user2.getId() + "조회성공");
	}

}

간단한 SQL CRUD 작업을 수행하는 코드이다.

 

초난감이라는 말에서 알 수 있듯이 DAO 코드가 많이 난감하다는 것인데

코드가 긴 것도 문제지만 코드의 유지보수성이 떨어진다.

 

객체지향설계는 유연하고 확장성 있는 코드로 미래를 생각하며 작성하는 것이 중요한데

초난감 DAO는 이러한 객체지향설계에 맞지 않는 방식이다.

객체지향적으로 해결하기 : 분리

기능이 분리가 되어있지 않다면

사용하는 데이터베이스가 바뀌거나 사용자 정보가 바뀌는 경우 등

기존에 코드들을 하나하나 모두 수정해야 하는 경우가 발생

 

관심이 같은 것들끼리만 모으고 같지 않는 것들은 떨어트려

서로 영향을 주지 않도록 분리하여 코드를 작성해야 함

 

DAO 코드에 어떤 기능들이 있는지를 살펴보고

관심사항을 추출해야 한다.

객체지향적으로 해결하기 : 관심사항 추출

DAO 코드의 관심사항은 아래와 같이 크게 3가지가 있다.

  1. 데이터베이스 연결에 사용할 커넥션 가져오기
  2. 데이터 추가 및 조회에 사용되는  SQL Statement 생성 및 실행
  3. 커넥션과 Statement 오브젝트 종료

또한 해당 관심사항들은 add와 get 메서드에서 반복적으로 실행 되는 것도 알 수 있는데

DAO 작업을 계속 하다보면 이러한 중복되는 코드들이 갈수록 많아질 것이기 때문에

이 각각의 관심사항들을 하나씩 나눠서 재사용성을 높이는 코드를 작성해야 한다.

객체지향적으로 해결하기 : 중복 코드 추출

인텔리제이에서 기본적으로 제공하는 리팩토링 기능처럼

중복되는 코드들을 하나의 메서드로 추출할 것인데

Class.forName("com.mysql.cj.jdbc.Driver");
Connection c = 
	DriverManager.getConnection(
		"jdbc:mysql://localhost/toby?characterEncoding=UTF-8&serverTimezone=Asia/Seoul", 
        "db 사용자 id","pw");

우선 add와 get 메서드에서 중복적으로 사용되는

커넥션을 가져오는 코드를 분리하여 하나의 메서드로 만든다.

private Connection getConnection() throws ClassNotFoundException, SQLException {
    Class.forName("com.mysql.jdbc.Driver");
    Connection c =
    	DriverManager.getConnection(
        	"jdbc:mysql://localhost/toby?characterEncoding=UTF-8&serverTimezone=Asia/Seoul", 
        	"db 사용자 id","pw");
    return c;
}

위와 같이 중복되는 부분을 하나의 메서드로 추출하여 해당 기능이 필요할 때만

해당 메서드를 호출하여 사용하도록 하는 것을 통해

public User get(String id) throws ClassNotFoundException, SQLException {
    Connection c = getConnection();
}

코드의 수정이 필요한 경우 해당 메서드의 코드만 수정하면 되기 때문에

코드가 간결해지고 유연해졌다.

객체지향적으로 해결하기 : 추출한 메서드의 독립 (상속)

해당 메서드의 코드를 공개하지 않고 상대방에게 제공하고 싶은 경우에는

상속을 사용하여 추상 메서드와 구현으로 분리하는 방법이 있다.

abstract protected Connection getConnection() throws ClassNotFoundException, SQLException;

이렇게 메서드를 독립시키면 하나의 getConnection 메서드를 상황에 맞게 작성된 코드로 사용할 수 있다.

 

즉, 기존의 mysql이 아니라 오라클이나 다른 데이터베이스를 사용해야 하는 경우에는

해당 추상 클래스를 구현한 다른 클래스의  getConnection 메서드를 사용하면 되는 것이고

새로운 데이터베이스를 사용해야 하는 경우 손쉽게 확장해서 사용할 수 있다.

템플릿 메서드 패턴

위와 같이 슈퍼 클래스에서 기본적인 로직의 흐름(변하지 않는 기능)만 제어하고

기능의 일부를 추상 메서드나 오버라이딩이 가능한 protected 메서드로 만들어

서브 클래스에서 실질적인 기능(자주 변경되거나 확장되는 기능)을 구현하여 상황에 맞게 사용하는 디자인 패턴

 

보통 서브 클래스에서 반드시 구현해야 하는 부분들은 추상 메서드로 만들고

선택적으로 구현해야 하는 것은 protected등의 접근 제어자를 통해 메서드를 오버라이딩 할 수 있게 한다.

팩토리 메서드 패턴

서브 클래스에서 구체적인 객체 생성 방법을 결정하게 하는 디자인 패턴

객체지향적으로 해결하기 : 상속의 문제점

하지만 상속을 통한 메서드의 독립은 간단하지만 좋은 방법은 아닌데,

자바는 다중 상속을 지원하지 않기 때문에 추가적인 상속을 받을 수 없고

상속을 통해 연결된 관계는 결합이 강하기 때문이다.

 

현재까지 중복되는 코드를 하나의 메서드로 분리하고

이를 다시 상하위 클래스로 분리하는 작업을 했는데,

이제는 다시 이 상하위 클래스를 서로 독립적인 클래스로 만들어야 한다.

객체지향적으로 해결하기 : 클래스의 분리

서로 독립적인 클래스로 만드는 것은 간단한데

그저 새로운 클래스를 하나 만들고 커넥션 관련 코드를 옮기는 것이다.

public class SimpleConnectionMaker {
	public Connection getConnection() throws ClassNotFoundException,
			SQLException {
		Class.forName("com.mysql.jdbc.Driver");
		Connection c = DriverManager.getConnection("jdbc:mysql://localhost/toby?characterEncoding=UTF-8", "id",
				"pw");
		return c;
	}
}

이렇게 별도의 클래스를 가져다 사용하기만 하면 된다.

public abstract class UserDao {
	private SimpleConnectionMaker simpleConnectionMaker;
	
	public UserDao() {
		this.simpleConnectionMaker = new SimpleConnectionMaker();
	}
}

SimpleConnectionMaker 클래스에서 커넥터를 가져오는 메서드를 사용하기 위해

SimpleConnectionMaker 객체를 가져와서 필드에 멤버로 등록하고

UserDao 클래스의 생성자를 통해 해당 인스턴스를 생성해준다.

Connection c = this.simpleConnectionMaker.getConnection();

만들어진 SimpleConnectionMaker 인스턴스를 통해 getConnection 메서드를 사용하면

기존에 상하위 클래스로 상속 받던 관계가 완전히 분리되었다.

객체지향적으로 해결하기 : 여전히 존재하는 문제점

하지만 클래스를 분리하니 새로운 문제가 생기는데

상속을 통해 메서드를 분리했을 때는 상황에 맞게 코드를 사용할 수 있었지만

지금은 UserDao 클래스가 SimpleConnectionMaker에 종속되었기 때문에 

새로운 데이터베이스를 사용하는 경우에 대한 커넥션 관련 코드들을 변경할 수 없다

객체지향적으로 해결하기 : 인터페이스

이러한 문제를 해결하기 위해 자바에서는 인터페이스를 사용할 수 있다.

 

인터페이스는 추상 메서드의 집합으로 다중 상속을 지원하지 않는 자바에서

다중 상속과 비슷한 역할을 수행할 수 있게 해준다.

 

공통적, 필수적으로 구현되어야 하는 기능들을 인터페이스에 정의하고

이 인터페이스를 사용하여 클래스들간 연결을 해주면서도 느슨한 관계를 유지시킬 수 있다.

 

즉, 기존에는 클래스들 간 밧줄로 강하게 묶여 있었다면 인터페이스를 통해 느슨하게 묶여 있는 것이다.

객체지향적으로 해결하기 : 인터페이스 적용하기

데이터베이스 커넥션 기능을 정의할 인터페이스를 생성 후

해당 인터페이스의 구현 클래스들이 구현해야 할 추상 메서드를 선언해준다.

public interface ConnectionMaker {
	public abstract Connection makeConnection() throws ClassNotFoundException, SQLException;
}

해당 인터페이스를 통해 상황에 맞게 실행되어야 할 코드들을 각각의 클래스에 구현한다.

public class DConnectionMaker implements ConnectionMaker {
	public Connection makeConnection() throws ClassNotFoundException, SQLException {
		// 커넥션 생성 구현 코드
	}
}

UserDao에서는 인터페이스를 사용하여

public class UserDao {
	private ConnectionMaker connectionMaker;
	
	public UserDao() {
		ConnectionMaker = new DConnectionMaker();
	}
    
    Connection c = connectionMaker.makeConnection();
}

이렇게되면 해당 인터페이스의 구현 클래스들이 서로 달라도 해당 인터페이스를 통해

makeConnection이라는 메서드를 상황에 맞게 사용할 수 있다.

 

하지만 아직도 UserDao의 생성자를 살펴보면 특정 객체의 생성자를 호출하여 사용하고 있다.

이러면 여전히 코드를 수정해야 하는 경우 여러 부분을 똑같이 수정해야 하는 것은 변하지 않았다.

객체지향적으로 해결하기 : 책임 분리

여전히 코드가 독립적이지 못한 이유는

어떤 커넥션 구현 클래스를 사용할지 직접 결정해줘야하기 때문인데,

이러한 부분을 제거해줘야 독립적이고 확장 가능한 클래스가 될 수 있다.

 

즉, 어떤 커넥션을 적용할지 결정하는 책임을 분리해야하는데

이러한 책임을 분리하기 위해서는 다른 클래스에 책임을 넘기는 것이 좋다.

 

UserDao라는 클래스(서비스)를 사용하는 클래스(클라이언트)에 이러한 책임을 넘겨서

클라이언트가 서비스에 어떠한 기능을 적용하라고 지정해주는 것이다.

 

고객(클라이언트)이 주문을 하면 해당 주문에 맞는 음식을 가게(서비스)가 제공하는 것과 같다.

객체지향적으로 해결하기 : 책임 분리의 원리

이제 클라이언트 오브젝트와 서비스 오브젝트의 관계를 설정해주면 되는데

서비스 오브젝트는 메서드나 생성자의 파라미터를 통해 외부에서 오브젝트를 전달 받고

클라이언트 오브젝트는 서비스 오브젝트의 파라미터에 맞게 오브젝트를 전달해준다.

 

현재 클래스들은 직접적으로 연결 되어 있지 않고 인터페이스를 통하여 간접적으로 연결되어 있기 때문에

파라미터로 전달되는 오브젝트의 클래스는 해당 인터페이스의 구현 클래스면 어떤 것이든 상관이 없다.

 

클래스 사이에 관계가 생기는 이유는 코드에 다른 클래스가 등장하기 때문이니

코드에 다른 클래스가 직접적으로 등장하지 않게 만들면 클래스 사이의 관계가 생기지 않는다.

 

하지만 오브젝트 사이의 관계는 특정 클래스가 등장하지 않아도 해당 클래스가 구현한

인터페이스를 사용했다면, 해당 인터페이스 타입으로 받아서 사용할 수 있다. (다형성)

 

즉, 모델링 시점(코드를 작성하는 시점)에는 존재하지 않던 관계

런타임 시에 클라이언트 오브젝트에서 관계를 주입 받아 생기게 되는 것이다.

객체지향적으로 해결하기 : 책임 분리 적용하기

클라이언트 클래스를 새로 생성하여 UserDao 클래스에 있던 main 메서드를 옮긴다.

이때 클라이언트의 메인 메서드의 UserDao의 생성자에 어떤 커넥션을 적용할지 전달한다.

public class Client {
	public static void main(String[] args) throws ClassNotFoundException, SQLException {
		ConnectionMaker connectionMaker = new DConnectionMaker();
		UserDao dao = new UserDao(connectionMaker);

		//기존의 main 메서드 코드
	}
}

UserDao의 생성자는 아래와 같이 구현 클래스를 직접 상속 받지 않고

클라이언트로부터 구현 클래스를 전달 받게 한다.

public UserDao(ConnectionMaker connectionMaker) {
    this.connectionMaker = connectionMaker;
}

드디어 인터페이스를 통해 완벽한 독립적인 코드 작성이 끝났다.

이제는 만약 데이터베이스를 다른 플랫폼으로 바꾸고자 한다면 메인 메서드의

생성자 부분의 코드만 수정하면 된다.

'Back-End > Spring' 카테고리의 다른 글

싱글톤 레지스트리 / 오브젝트 스코프  (0) 2023.06.07
스프링 IoC  (0) 2023.06.05
IoC(Inversion of Control, 제어의 역전)  (0) 2023.06.05
초난감 DAO와 객체지향  (0) 2023.06.05
Spring Framework  (0) 2023.06.05

+ Recent posts