본문 바로가기

Spring/토비의 스프링 3.1

1.3 DAO의 확장


1.3 DAO의 확장

데이터 액세스 로직 + DB 연결 : 상하위 클래스로 분리

  1. UserDao
    • 데이터 액세스 로직을 담당
    • 어떤 API를 사용하여 DB와 통신할 것인가
    • 어떤 SQL을 만들 것인가
    • 어떤 오브젝트를 통해 DB에 저장할 정보를 전달받고 넘겨줄 것인가
  2. NUserDao / DUserDao
    • DB 연결을 담당
    • DB 연결 방법이 그대로면 코드의 변경이 없음
    • DB 연결 방법이 바뀌면 NUserDao, DUserDao의 코드만 바뀜

그러나 여전히 상속이라는 방법은 불편하다.

1.3.1 클래스의 분리

두 개의 관심사를 본격적으로 독립시키면서 동시에 손쉽게 확장 가능하도록 분리하자.
일단 두 개의 클래스를 아예 별도의 클래스로 분리하자.

  1. SimpleConnectionMaker 클래스를 생성 - DB 생성 기능
  2. UserDao는 new를 사용하여 SimpleConnectionMaker 오브젝트를 생성
  3. add(), get() 메소드에서 사용
    • 각각의 메소드에서 매번 새로운 오브젝트를 생성하는 것이 아니라 한번만 생성하면 된다.

UserDao는 다음과 같이 변경한다.

public class UserDao {

    private SimpleConnectionMaker simpleConnectionMaker;

    public UserDao() {
        // 한 번만 만들어 인스턴스 변수에 저장해두고 메소드에서 사용하게 한다.
        simpleConnectionMaker = new SimpleConnectionMaker();
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
        // ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
        // ...
    }

    public static void main(String[] args) throws ClassNotFoundException, SQLException{
        // ...
    }
}

SimpleConnectionMaker는 다음과 같다.

public class SimpleConnectionMaker {
    public Connection makeNewConnection() throws ClassNotFoundException, SQLException{
        Class.forName("oracle.jdbc.driver.OracleDriver");
        Connection c = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:xe", "spring", "book");

        return c;
    }
}

main() 메소드 실행 후 테스트를 통과하는 지 확인해보자.

1.3.1의 문제점

  • DB 커넥션 기능을 확장해서 사용하는 것이 불가능
  • SimpleConnectionMaker가 특정 클래스에 종속
  • 문제점
    1. SimpleConnectionMaker의 메소드 문제
      • 만약 makeNewConnection() 메소드명이 openConnection()으로 바뀐다면?
    2. DB 커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알아야한다.
      • 클라이언트가 다른 클래스를 구현하면 UserDao를 수정해야 한다.

1.3.2 인터페이스의 도입

추상화 : 어떤 것들의 공통적인 성격을 뽑아내어 따로 분리해내는 작업

  • 두 개의 클래스가 긴밀하게 연결되어 있지 않도록
  • 추상적인 느슨한 연결고리를 생성(인터페이스)
  • 최소한의 통로를 통해 접근하는 쪽에서 오브젝트를 만들 때 사용할 클래스를 몰라도 됨

ConnectionMaker 인터페이스

  • makeConnection()을 호출하기만 하면 Connection 타입의 오브젝트를 리턴할 것이라 기대
public interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}

ConnectionMaker 구현 클래스

public class DConnectionMaker implements ConnectionMaker{
    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException{
        Class.forName("oracle.jdbc.driver.OracleDriver");
        Connection c = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:xe", "spring", "book");

        return c;
    }
}

UserDao

public class UserDao {
    // 인터페이스를 통해 오브젝트에 접근하므로 구체적인 클래스 정보 알 필요 없음
    private ConnectionMaker connectionMaker;

    public UserDao() {
        // 그러나 여기에 클래스 이름이 나옴
        connectionMaker = new DConnectionMaker();
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        // 인터페이스에 정의된 메소드를 사용하므로 클래스가 바뀌어도 걱정없음
        Connection c = connectionMaker.makeConnection();
        // ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();
        // ...
    }

    public static void main(String[] args) throws ClassNotFoundException, SQLException{
        // ...
    }
}

1.3.2의 문제점

N사와 D사가 DB 접속용 클래스를 다시 만들더라도 UserDao의 코드를 수정하지 않아도 되!ㄹ 줄 알았지?
최초에 한 번은 어떤 클래스를 사용할 지 결정하는 생성자의 코드에 클래스의 이름이 등장한다.
즉, UserDao 코드를 함께 제공해야만 한다.

1.3.3 관계 설정 책임의 분리

문제점

  1. 문제의 원인
    • UserDao 내에 분리되지 않은 또 다른 관심사항이 존재
    • new DConnection() : UserDao가 어떤 ConnectionMaker 구현 클래스의 오브젝트를 이용할지를 결정
  2. 해결책
    • UserDao의 클라이언트 오브젝트에 해당 기능을 분리
    • 클래스와 클래스의 관계 설정이 아닌, 오브젝트와 오브젝트의 관계를 설정
    • 오브젝트의 관계 설정 방법 : 직접 생성이 아닌 메소드 파라미터로 전달

UserDao의 모든 코드는 ConnectionMaker 인터페이스 외에는 어떤 클래스와도 관계를 가져서는 안 된다.

  1. UserDao의 클라이언트
    • UserDao 오브젝트를 사용하는 main() 메소드
    • ConnectionMaker 구현 클래스를 선택하고, 오브젝트를 생성하여 UserDao와 연결
    • 관심과 책임이 다르므로 새로운 클래스로 분리(UserDaoTest)
  2. UserDao
    • 생성자에 ConnectionMaker 오브젝트를 전달받을 수 있는 파라미터를 추가

UserDao

  • SQL 생성, 실행에만 집중
public class UserDao {
    private ConnectionMaker connectionMaker;

    public UserDao(ConnectionMaker connectionMaker) {
        // 구체적인 구현 클래스의 이름이 사라짐
        this.connectionMaker = connectionMaker;     
    }

    // ...
}

UserDaoTest

  • 런타임 오브젝트 의존관계를 설정하는 책임을 담당
  • 원래 자신의 책임이던 테스트 작업 수행
public class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException{
        // UserDao가 사용할 ConnectionMaker 구현 클래스를 결정하고 오브젝트를 생성
        ConnectionMaker connectionMaker = new DConnectionMaker();

        // 1. UserDao 생성
        // 2. 사용할 ConnectionMaker 타입의 오브젝트 제공
        // 결국 두 오브젝트 사이의 의존관계 설정 효과
        UserDao dao = new UserDao(connectionMaker);

        // ...
    }
}

1.3.3 결론

  • 인터페이스를 사용함으로써 상속에 비해 훨씬 유연해 짐
  • 각각의 클래스는 자신의 책임만을 다루고 있음
  • DAO가 아무리 많아져도, DB 연결이 그대로면 ConnectionMaker는 유지됨
  • DB가 아무리 변경되어도, UserDao는 그대로임
  • N사, D사 외 다른 고객들이 생겨도 확장에 유연함

1.3.4 원칙과 패턴

개방 폐쇄 원칙(OCP; Open-Closed Priciple)

'클래스나 모듈은 확장에는 열려 있어야 하고, 변경에는 닫혀있어야 한다.'

객체지향 설계 원칙 (SOLID)

객체 지향의 특징을 잘 살릴 수 있는 설계의 특징

  • 단일 책임 원칙(SRP; The Single Responsibility Principle)
  • 개방 폐쇄 원칙(OCP; The Open Closed Principle)
  • 리스코프 치환 원칙(LSP; The Liskov Substitution Principle)
  • 인터페이스 분리 원칙(ISP; The Interface Segregation Principle)
  • 의존관계 역전 원칙(DIP; The Dependency Inversion Principle)

높은 응집도와 낮은 결합도

  1. 높은 응집도
    • 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다.
    • 작업은 항상 전체적으로 일어나고 무엇을 변경할지 명확
    • 다른 클래스의 수정을 요구하지 않음
    • 기능에 영향을 주지 않음
  2. 낮은 결합도
    • 책임과 관심사가 다른 오브젝트 또는 모듈과는 느슨하게 연결되어야 한다.
    • 결합도 : 변경이 일어날 때 관계를 맺고 있는 다른 오브젝트에 변화를 요구하는 정도
    • 변경에 대한 요구가 전파되지 않는 상태

UserDao는 자신의 책임에 대한 응집도가 높으며, ConnectionMaker와는 인터페이스를 통해 매우 느슨하게 연결되어 있다.

전략 패턴(Strategy Pattern)

  • UserDaoTest-UserDao-ConnectionMaker의 구조
  • 전략 패턴 : 디자인 패턴의 꽃
  • 자신의 기능 맥락(Context)에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 함
  • 컨텍스트(UserDao)를 사용하는 클라이언트(UserDaoTest)는 컨텍스트가 사용할 전략(ConnectionMaker를 구현한 DConnectionMaker)을 컨텍스트의 생성자 등을 통해 제공해주는 것이 일반적


' Spring > 토비의 스프링 3.1' 카테고리의 다른 글

1.6 싱글톤 레지스트리와 오브젝트 스코프  (0) 2019.01.09
1.5 스프링의 IoC  (0) 2019.01.07
1.4 제어의 역전(IoC)  (0) 2019.01.07
1.2 DAO의 분리  (0) 2019.01.03
1.1 초난감 DAO  (0) 2019.01.03