반응형

1. 서론

 

이번 포스팅에서는 토비의 스프링 3.1의 1장인 오브젝트와 의존관계 에 대해 알아보도록 하겠습니다.

 

2. 초난감 DAO

스프링은 자바 언어 기반의 프레임워크입니다.

때문에, 객체지향 프로그래밍이 제공하는 폭넓은 혜택을 누릴 수 있도록 기본으로 돌아가자는 것이 스프링의 핵심입니다.

스프링은 객체지향 설계와 구현을 특정하도록 강요하지는 않습니다.

다만, 효과적으로 설계와 구현을 할 수 있도록 기준을 마련하여 개발자에게 편리함을 제공합니다.

 

아래는 책에 나오는 DB에 데이터를 추가 및 조회할 수 있는 예제입니다.

 

@Setter
@Getter
public class User {
    private String id;
    private String name;
    private String password;
}

 

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 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.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
        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;
    }

}

 

위 예제코드는 정상적으로 동작하나 사실은 문제가 많은 코드입니다.

개발자가 실제로 이러한 코드를 본다면... 경악을 금치 못할것입니다.

 

이제 아래 DAO의 분리와 DAO의 확장을 통해 위의 코드를 리팩토링하며 좀 더 나은 객체지향 설계로 변경하도록 하겠습니다.

 

3. DAO의 분리

위 예제를 리팩토링하기 전에, 먼저 관심사 분리에 대해서 설명하겠습니다.

 

자바 프로그램은 객체지향 장점으로 지속적인 변화에 매우 유용한 언어입니다.

그렇다면, 객체지향 장점이 그럼 무엇일까요?

바로, 관심사의 분리입니다.

 

즉, 관심이 같은 것끼리는 하나로 관심이 다른 것은 다르게 하여 실제 세계의 개념들을 객체라는 것으로 만들 수 있는 것을 의미합니다.

 

그렇다면, 위 초난감 DAO의 관심사항은 무엇일까요?

 

아마, 아래와 같은 관심사항으로 정의할 수 있습니다.

 

  1. DB와 연결을 위한 커넥션을 어떻게 가져올까라는 관심.
  2. 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 관심
  3. 작업이 끝나면 리소스인 Statement와 Connection 오브젝트를 닫아, 리소스 낭비를 일으키지 않는 관심

 

먼저, 중복 코드인 DB 연결 코드를 메소드로 추출하는 작업을 해보겠습니다.

현재는 2개의 메소드 밖에 없지만, 메소드가 10개 100개가 된다면 메소드를 만들 때 마다 Connection 소스가 중복으로 존재하게 됩니다.

 

아래는 getConnection이라는 메소드를 통해 DB 연결을 하는 중복코드를 제거한 코드입니다.

 

public class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {
        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 ClassNotFoundException, SQLException {
        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;
    }
    
    private Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
        return c;
    }

}

 

위와같이 변경함으로, 이제 DB 종류 혹은 접속 정보가 변경되더라도 getConnection 메소드만 수정하여 문제를 해결할 수 있게 되었습니다.

 

 

만약, UserDao를 소스를 외부에 제공하기 위해서는 메소드 추출만으로는 유연한 코드가 될 수 없습니다.

UserDao를 사용하는 클라이언트 입장에서 자신들이 사용하는 DB와 접속 정보에 따라 UserDao 코드를 수정해야 하기 때문입니다.

 

좀 더 유연한 UserDao를 만들기 위해 이번에는 자바에서 제공되는 상속을 사용하여 코드를 리팩토링 하겠습니다.

 

public abstract class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {
        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 ClassNotFoundException, SQLException {
        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;
}

 

UserDao 클래스를 추상 클래스로, getConnection을 추상 메서드로 만들어 외부에서는 자신들이 사용하는 DB에 맞게 getConnection을 오버라이딩하여 사용할 수 있도록 만들었습니다.

 

위와 같인 슈퍼클래스에 기본적인 로직의 흐름을 만들고, 중간에 서브클래스가 제정의한 것을 사용하여 동작하도록 만드는 것이
템플릿 메소드 패턴입니다.

 

추가로, 위 예제에서는 Connection이라는 인터페이스를 통해 UserDao는 어떤 기능을 사용할지에 대한 관심만을 가지게 하였고.

서브 클래스들은 어떤 식으로 Connection 기능을 제공하는지에 관심을 가지도록 나누었습니다.

 

하지만, 위와 같은 코드도 문제점은 존재합니다. 바로 상속을 사용한 문제입니다.

자바는 기본적으로 다중 상속을 지원하지 않습니다.

 

때문에, UserDao의 서브 클래스는 UserDao가 아닌 클래스는 상속을 못받는 문제가 생깁니다.

 

4. DAO의 확장

위 getConnection은 DB 연결에 대한 관심으로 UserDao와는 관심이 다르기 때문에, 아예 별도의 클래스로 분리하겠습니다.

 

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

 

public class UserDao {

    private SimpleConnectionMaker simpleConnectionMaker;

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

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
        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 {
        Connection c = simpleConnectionMaker.makeNewConnection();
        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;
    }
}

 

관심사 분리를 적용하여 클래스 분리까지는 좋았습니다.

다만, 위 코드는 외부제공 시 가져야하는 connection에 대한 유연성 문제를 해결하지 못한 코드입니다.

 

이런 경우, 인터페이스를 통하여 2개의 관심사를 느슨하게 연결할 수 있습니다.

 

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

 

public class UserDao {

    private ConnectionMaker connectionMaker;

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

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();
        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 {
        Connection c = connectionMaker.makeConnection();
        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;
    }
}

 

ConnectionMaker라는 인터페이스를 하나 만들어, UserDao가 인스턴스 시 해당 인터페이스의 구현체를 전달받아 사용하도록 하는 코드로 변경하였습니다.

 

사실상, 위 코드는 UserDao를 사용하는 제3의 클라이언트가 런타임 시 UserDao와 ConnectionMaker와의 관계를 갖도록 책임을 위임한 코드입니다.

 

그렇다면, 클라이언트는 ConnectionMaker의 구현체를 만들어 new UserDao(connectionImpl); 같은 코드로 프로그래밍해야 합니다.

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

5. 제어의 역전(IoC)

일반적으로, 팩토리는 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 일을 합니다.

 

위 예제에서는 제 3의 클라이언트가 팩토리 기능을 구현해야 하는 것입니다.

이것 또한, UserDao, ConnectionMaker와는 별개로 객체의 관계 설정이라는 관심사로 분리되어 별도 클래스로 추출이 가능합니다.

 

public class DaoFactory {
    
    public UserDao userDao() {
        ConnectionMaker connectionMaker = () -> {
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
            return c;
        };
        UserDao userDao = new UserDao(connectionMaker);
        return userDao;
    }
}

 

DaoFactory를 분리했을 때 얻을 수 있는 장점은 아래와 같습니다.

 

어플리케이션의 컴포넌트 역할을 하는 오브젝트와 어플리케이션의 구조를 결정하는 오브젝트를 분리.

 

이러한, 객체간의 관계를 제 3의 클라이언트 혹은 Factory 클래스에게 위임하는것이 바로 객체지향에서의 Ioc(제어의 역전)입니다.

 

제어의 역전에서는 아래와 같은 기능들을 제공해야 합니다.

 

  • 어플리케이션 컴포넌트의 생성과 관계설정
  • 컴포넌트 사용
  • 컴포넌트 생명주기 관리

 

6. 스프링의 IoC

 

이제 Ioc 역할인 DaoFactory를 스프링에서 사용하는 방식으로 변경하도록 하겠습니다.

 

스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(bean)이라고 부릅니다.

또한, 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말입니다.

 

스프링에서는 DaoFactory와 같이 IoC 오브젝트를 빈 팩토리라고 부르며, 동시에 어플리케이션 컨테스트라고도 일컫습니다.

실제 코드상에서는 ApplicationContext는 BeanFactory의 서브 인터페이스입니다.

 

이제 위 DaoFactory의 기능을 스프링으로 변경하겠습니다.

 

@Configuration
public class DaoFactory {

    @Bean
    public UserDao userDao() {
        UserDao userDao = new UserDao(connectionMaker());
        return userDao;
    }
    
    @Bean
    public ConnectionMaker connectionMaker() {
        return () -> {
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
            return c;
        };
    }
}

 

위 코드의 @Configuration과 @Bean은 아래와 같습니다.

 

  1. @Configuration은 DaoFactory 클래스가 오브젝트 설정을 담당하는 클래스라는 것을 스프링이 인식할 수 있도록 하는 설정 표시.
  2. @Bean은 오브젝트 생성을 담당하는 IoC용 메소드라는 표시

 

이제 DaoFactory를 설정정보로 사용하는 어플리케이션 컨텍스트는 아래와 같습니다.

 

public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
    UserDao userDao = context.getBean("userDao", UserDao.class);
    ...
}

 

DaoFactory라는 설정정보를 담고있는 클래스를 통하여 어플리케이션 컨텍스트를 생성하였습니다.

이러한, 어플리케이션 컨텍스트는 getBean 메서드를 통해 설정정보 클래스에 정의한 오브젝트를 가져올 수 있습니다.

 

getBean 메서드에서 사용한 "userDao"는 가져올 빈 이름을 의미합니다.

이 이름은 DaoFactory에서 @Bean 어노테이션을 붙인 메서드 명이 자동적으로 UserDao 빈의 이름으로 등록되어 집니다.

 

굳이 이름을 통하여 빈을 가져와야 하는 이유는, 스프링에서는 동일 타입의 인스턴스들을 빈으로 등록할 수 있기 때문입니다.

 

 

어플리케이션 컨텍스트는 DaoFactory와는 다르게 어플리케이션 전체에 IoC를 적용하여 모든 오브젝트에 대한 생성과 관계설정을 담당하는 것을 알았습니다.

 

이러한 스프링의 어플리케이션 컨텍스트 장점은 아래와 같습니다.

 

  1. 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없습니다.
  2. 어플리케이션 컨텍스트는 종합 IoC 서비스를 제공해줍니다.
  3. 어플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공합니다.

 

7. 싱글톤 레지스트리와 오브젝트 스코프

 

스프링 어플리케이션 컨텍스트는 bean을 저장하고 관리하는 IoC 컨테이너라고 했습니다.

이때, bean은 싱글톤으로서 하나의 객체를 생성하여 bean으로 등록하여 재사용합니다.

 

싱글톤으로 만들어 사용하는 이유로는 스프링이 서버에서 주로 사용되는 프레임워크이기 때문입니다.

계속적인 객체가 생성되어 리소스 낭비가 심해지는것을 방지하기 위해서 입니다.

 

자바 디자인 패턴 중 싱글톤 패턴과는 한개의 객체를 사용하도록 한다는 개념은 같지만 약간 사용성이 다릅니다.

 

자바 싱글톤 패턴은 아래와 같습니다.

 

public class UserDao {

    private static UserDao INSTANCE;

    private UserDao() {
    }

    public static synchronized UserDao getInstance() {
        if (INSTANCE == null) INSTANCE = new UserDao();
        return INSTANCE;
    }
}

 

스프링의 bean의 싱글톤은 아래와 같이 정의할 수 있습니다.

 

public class UserDao {

    private static UserDao INSTANCE;

    public UserDao() {
    }

    public static UserDao getInstance() {
        if (INSTANCE == null) INSTANCE = new UserDao();
        return INSTANCE;
    }
}

 

차이점으로는 아래와 같습니다.

 

  1. 생성자 = bean의 경우 private 생성자가 아니기 때문에, 객체 생성에 대한 제약이 있지 않습니다.
  2. 동기화 코드 = bean의 경우 synchronized 선언이 되어 있지 않습니다. 이는 synchronized로 인해 발생하는 성능이슈를 방지하기 위함입니다.

 

차이점에서 두번째인 동기화 코드로 인해 bean 사용시 유의해야 할 점이 있습니다.

그건 바로, stateful 방식의 bean이 아닌 stateless 방식으로 빈을 생성해야 한다는 것입니다.

 

이유는 stateful 방식으로 bean을 사용하면 멀티쓰레드 환경에서 동시접근으로 인해 정확한 상태를 가질 수 없기 때문입니다.

 

 

8. 의존관계 주입(DI)

스프링에서 제공하는 또 하나의 큰 기능을 말하라고 한다면 DI(Dependency Injection) 를 말할 수 있습니다.

 

DI는 말 그대로 의존관계 주입으로 위에서 DaoFactory가 UserDao와 ConnectionMaker간의 의존성 관계를 주입하는 것을 의미합니다.

 

의존관계에는 방향성이 존재합니다.

 

위 UserDao와 ConnectionMaker의 의존관계를 표현한다면 UserDao -> ConnectionMaker로 표현할 수 있으며,

UserDao는 ConnectionMaker에 의존하고 있다고 말할 수 있습니다.

 

즉, 이 경우 UserDao는 ConnectionMaker에 의존하고 있기 때문에, ConnectionMaker 변경에 영향을 받습니다.

하지만, ConnectionMaker는 UserDao 에 의존하고 있지 않기 때문에 UserDao 변경에 영향을 받지 않습니다.

 

스프링에서는 의존관계 주입을 3가지의 방법을 통해 정의할 수 있습니다.

 

  1. 어노테이션 - @Autowired를 사용
  2. 생성자 주입
  3. setter 주입

 

위의 경우 UserDao는 생성자에 ConnectionMaker를 받고 있기 때문에 생성자 주입방식을 사용한 것을 알 수 있습니다.

 

스프링 Document에서는 DI 방식으로 생성자 주입을 권장하고 있습니다.
다만, 의존관계가 많은 클래스의 경우 생성자 코드가 커져 장황한 코드가 나올 수 있는 단점이 있습니다.
이는 lombok 과 같은 써드파티 라이브러리를 사용한다면 해결 가능합니다.

 

추가로 스프링은 DL(Dependency Lookup) 도 제공하고 있습니다.

DL은 의존관계 검색으로서 위에서 살펴본 getBean을 DL이라고 볼 수 있습니다.

 

DI는 객체간의 의존관계를 주입하기 위해 제어권을 스프링에게 넘겨야 하며, DI의 관련 객체들은 모두 bean으로 스프링 컨테이너에 등록되어야 합니다.

 

하지만 DL은 스프링 컨테이너에 등록된 bean을 검색하는 용도로,

DL을 사용하는 클래스는 bean으로 등록될 필요가 없다는 차이점을 가지고 있습니다.

 

9. 마무리

이번 포스팅에서는 오브젝트와 의존관계에 대해 간단한 소개와 설명을 했습니다.

다음에는 테스트에 대해 포스팅하겠습니다.

반응형

'Framework > Spring' 카테고리의 다른 글

(2) 테스트  (2) 2020.06.21

+ Recent posts