SPRING

토비의 스프링 1. 오브젝트와 의존관계

짜비 2022. 5. 27. 21:54
public class UserDao {
    public void add(User user) throws ClassNotFoundException, SQLException {

        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection(
                "jdbc:mysql://localhost/springbook", "spring", "book"
        );

        PreparedStatement ps = connection.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();
        connection.close();
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection(
                "jdbc:mysql://localhost/springbook", "spring", "book"
        );

        PreparedStatement ps = connection.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();
        connection.close();

        return user;
    }
}

관심사의 분리

  • 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것.
  • UserDao 의 add() 메소드의 관심사항
    • DB 커넥션
    • SQL 만들고 실행
    • 리소스 반환

중복 코드의 메서드 추출

private Connection getConnection() throws ClassNotFoundException, SQLException {
    Class.forName("com.mysql.jdbc.Driver");
    Connection connection = DriverManager.getConnection(
            "jdbc:mysql://localhost/springbook", "spring", "book"
    );
    return connection;
}
class NUserDao extends UserDao {
    @Override
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        //N사 DB Connection 생성 코드
    }
}

템플릿 메소드 패턴

  • 슈퍼클래스에 기본적인 로직의 흐름을 만들어 두고, 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법.

팩토리 메소드 패턴

  • 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것.

상속이 아닌 독립적인 클래스로 분리

public class SimpleConnectionMaker {

    public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection(
                "jdbc:mysql://localhost/springbook", "spring", "book"
        );
        return connection;
    }
}


public class UserDao {

    private SimpleConnectionMaker simpleConnectionMaker;

    public UserDao() {
        simpleConnectionMaker = new SimpleConnectionMaker();
    }

    public void add(User user) throws ClassNotFoundException, SQLException {

        //Connection connection = getConnection();
        Connection connection = simpleConnectionMaker.makeNewConnection();

        PreparedStatement ps = connection.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();
        connection.close();
    }

}

위 코드의 문제점

  • userDao가 DB 커넥션을 가져오는 클래스에 대해 너무 많은 정보를 알고 있다. (e.g. 어떤 클래스가 쓰일지, 커넥션을 가져오는 메소드의 이름이 무엇인지 …)

해결 방법 : 인터페이스의 도입

  • 중간에 추상적인 느슨한 연결고리를 만들어 주는 것.
  • userDao는 자신이 사용할 클래스가 어떤 것인지 몰라도 된다. 인터페이스를 통해 원하는 기능을 사용하기만 하면 된다.
public interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}

public class NConnectionMaker implements ConnectionMaker{
    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        //N사의 독자적인 Connection 생성하는 코드
        return null;
    }
}


public class UserDao {

    private ConnectionMaker connectionMaker;

    public UserDao() {
        NConnectionMaker connectionMaker = new NConnectionMaker();
//구체적인 클래스 이름이 나옴 ... 
    }

  public void add(User user) throws ClassNotFoundException, SQLException {

    Connection connection = connectionMaker.makeConnection();

    PreparedStatement ps = connection.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();
    connection.close();
  }
}

관계설정 책임의 분리

  • (참고) 두 개의 오브젝트가 있고 한 오브젝트가 다른 오브젝트의 기능을 사용한다면, 사용되는 쪽 : 서비스 / 사용하는 쪽 : 클라이언트
  • 클라이언트가 ConnectionMaker의 구현 클래스를 선택하고, 선택한 클래스의 오브젝트를 생성해서 UserDao와 연결하도록 수정.
    • 위 코드에서는 main() 메소드가 클라이언트 역할 수행 중
public class UserDaoTest {

    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        ConnectionMaker connectionMaker = new NConnectionMaker();

        UserDao dao = new UserDao(connectionMaker);
    }
}



public class UserDao {
    private ConnectionMaker connectionMaker;

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

    public void add(User user) throws ClassNotFoundException, SQLException {


        Connection connection = connectionMaker.makeConnection();

        //...

}

개방 폐쇄 원칙

  • 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.
  • UserDao : DB 연결 방법 확장 가능 & 자신의 핵심 기능을 구현한 코드는 변화에 영향을 받지 않고 그대로 유지.

높은 응집도

  • 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있음.
  • 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다. 따라서 작업은 항상 전체적으로 일어나고 무엇을 변경해야할 지 명확하다.

낮은 결합도

  • 하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도
  • e.g. ConnectionMaker 인터페이스 도입으로 DB 연결 기능을 구현한 클래스가 바뀌더라도 DAO 코드는 변경될 필요가 없게 됨.

전략 패턴

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

제어의 역전(IoC)

오브젝트 팩토리

  • 팩토리 : 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것.
  • 기존 UserDaoTest 내 1) ConnectionMaker 구현 클래스의 오브젝트를 만드는 것 & 2) userDao 기능 테스트. 두 가지 관심사를 분리.
public class DaoFactory {
    public UserDao userDao() {
        NConnectionMaker connectionMaker = new NConnectionMaker();
        UserDao userDao = new UserDao(connectionMaker);
        return userDao;
    }
}

public class UserDaoTest {

    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        UserDao dao = new DaoFactory().userDao();
        //...
    }
}
  • userDao, ConnectionMaker : 실질적인 로직을 담당하는 컴포넌트
  • DaoFactory : 어플리케이션을 구성하는 컴포넌트의 구조와 관계를 정의한 설계도

제어의 역전

  • 일반적인 프로그램의 흐름 : 모든 오브젝트가 능동적으로 자신이 사용할 클래스를 결정하고, 언제 어떻게 그 오브젝트를 만들지를 스스로 관장. 다시 말해 모든 종류의 작업을 사용하는 쪽에서 제어하는 구조.
  • 제어의 역전 : 위 흐름을 거꾸로 뒤집는 것. 즉, 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하거나 생성하지 않음. 자신 또한 어떻게 만들어지고 어디서 사용되는지 알 수 없음.
  • 라이브러리 vs 프레임워크
    • 전자 : 애플리케이션 코드가 흐름을 직접 제어
    • 후자 : 어플리케이션 코드가 프레임워크에 의해 사용됨.

스프링의 IoC

어플리케이션 컨텍스트와 설정정보

bean

  • 스프링이 제어권을 가지고 직접 만들고, 관계를 부여하는 오브젝트. 혹은 오브젝트 단위의 어플리케이션 컴포넌트. 혹은 제스프링 컨테이너가 제어하는 제어의 역전이 적용된 오브젝트.

bean factory

  • 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트

application context

  • bean factory를 확장한 것.

DaoFactory를 사용하는 어플리케이션 컨텍스트

@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao() {
        UserDao userDao = new UserDao(connectionMaker());
        return userDao;
    }

    @Bean
    public ConnectionMaker connectionMaker() {
        return new NConnectionMaker();
    }
}

public class UserDaoTest {

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

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao dao = context.getBean("userDao", UserDao.class);

        //...
    }
}
  • getBean() : ApplicationContext가 관리하는 오브젝트를 요청하는 메소드

Application Context 동작 방식

  1. DaoFactory 클래스를 설정정보로 등록
  2. @Bean이 붙은 메소드 이름을 가져와 빈 목록 생성
  3. 클라이언트가 getBean() 호출시 빈 목록에서 요청한 이름이 있는지 찾고, 있다면 빈을 생성하는 메소드를 호출해서 오브젝트 생성 후 반환

Application Context 사용 장점

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요 없음
  • 오브젝트 생성, 관계설정 이외의 다양한 기능 제공
    • e.g. 오브젝트 생성 방식, 시점 다변화, 오브젝트 후처리 등…
  • bean 검색하는 다양한 방법 제공

싱글톤

  • 어프리케이션 안에 제한된 수, 대개 한 개의 오브젝트만 만들어서 사용.
  • 스프링은 여러 번에 걸쳐 빈을 요청하더라도 매번 동일한 오브젝트를 돌려준다.
    • e.g. getBean() 여러 번 호출 -> 모두 같은 오브젝트
  • 이유
    • 스프링은 엔터프라이즈 시스템을 위해 고안된 기술. 대부분 서버 환경에서 사용됨.
    • 서버는 하나의 요청 처리를 위해 다양한 오브젝트들이 참여하는 계층형 구조가 대부분. 만약 클라이언트에게 요청이 올 때마다 오브젝트가 새로 만들어진다면 서버가 부하를 감당하기 힘들 것.

일반적인 싱글톤 패턴 구현의 문제점

  • private 생성자를 가짐. -> 상속 불가능
  • 테스트 어려움. (생성자를 통한 오브젝트 주입 불가능)
  • 전역 상태로 사용되기 쉬움.

싱글톤 레지스트리

  • 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공.
  • static, private 생성자를 사용하지 않고도 클래스를 싱글톤으로 활용 가능.

싱글톤 주의사항

  • 싱글톤 오브젝트는 ‘무상태’로 만들어야 함.
  • 파라미터, 로컬 변수, 리턴 값을 활용해서 정보를 다뤄야 함.
public class UserDao {

    //OK
    private ConnectionMaker connectionMaker;

  //매번 새로운 값으로 바뀌는 정보를 담은 인스턴스 변수
    private Connection c;
    private User user;

    public User get(String id) throws ClassNotFoundException, SQLException {
        this.c = connectionMaker.makeConnection();
        this.user = new User();
        this.user.setId(rs.getString("id"));
        //...

    }

}

의존관계 주입(Dependency Injection)

  • 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업.
  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않음. 이를 위해서는 인터페이스에만 의존하고 있어야 함.
  • 런타임 시의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정.
public class UserDao {

    private ConnectionMaker connectionMaker;

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

}

의존관계 검색(dependency lookup, DL)

  • 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾는다.
public UserDao(ConnectionMaker connectionMaker) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
    this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}

DI vs DL

  • DI 의 코드가 더 깔끔하다.
    • DL 의 경우, 오브젝트 팩토리 클래스 혹은 스프링의 API가 나타남.
  • 그럼에도, DL을 사용해야만 하는 경우가 있음.
    • 어플리케이션 기동 시점 -> DI를 통해 오브젝트를 주입받을 방법이 없음. -> 의존관계 검색을 통해 오브젝트를 가져옴.
    • e.g. main 메소드, 서블릿
  • DI : DI를 받으려면 자기 자신이 컨테이너가 관리하는 빈이 되어야 함.
  • DL : 자신이 스프링 빈일 필요 없음.

DI 응용

부가기능 추가

  • 예시 : Dao 에서 DB를 연결하는 횟수를 카운팅할 때.
  • 해결방법 : Dao - DB 커넥션 만드는 오브젝트 사이에 연결횟수 카운팅 오브젝트를 추가.
public class CountingConnectionMaker implements ConnectionMaker{

    int counter = 0;
    private ConnectionMaker realConnectionMaker;

    public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
        this.realConnectionMaker = realConnectionMaker;
    }

    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        return realConnectionMaker.makeConnection();
    }

    public int getCounter() {
        return counter;
    }
}
@Configuration
public class CountingDaoFactory {
    @Bean
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }

    @Bean
    public ConnectionMaker connectionMaker() {
        return new CountingConnectionMaker(realConnectionMaker());
    }

    @Bean
    public ConnectionMaker realConnectionMaker() {
        return new NConnectionMaker();
    }
}
public class UserDaoConnectionCountTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CountingDaoFactory.class);
        UserDao dao = context.getBean("userDao", UserDao.class);

        //...

        CountingConnectionMaker ccm = context.getBean("connectionMaker", CountingConnectionMaker.class);
        System.out.println("ccm.getCounter() = " + ccm.getCounter());
    }
}


수정자(Setter) 를 통한 의존관계 주입

  • outdated

XML 을 이용한 의존관계 설정

  • outdated

DataSource 인터페이스로 전환

  • DB connection을 가져오는 오브젝트의 기능을 추상화해서 사용할 수 있도록 만들어진 인터페이스.
public class UserDao {

    private DataSource dataSource;

    public UserDao(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection connection = dataSource.getConnection();
        //...
}
@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao() {
        UserDao userDao = new UserDao(dataSource());
        return userDao;
    }


    @Bean
    public DataSource dataSource() {
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
        // dataSource 설정 정보
        return dataSource;
    }
}