[스프링] 3. 웹 앱 개발 - IoC with Java & Spring

JM·2022년 11월 27일
0
post-thumbnail

참고자료

  • 토비의 스프링 3.1 1장_오브젝트와 의존관계
  • 토비의 스프링 3.1 (95p~110p)

순수 자바를 사용한 IoC 예제

일반적으로 프로그램의 흐름은 main() 메소드와 같이 프로그램이 시작되는 지점에서 다음에 사용할 오브젝트를 결정하고, 만들어진 오브젝트의 메소드를 호출하는 과정들을 반복한다. 즉, 자신이 사용할 오브젝트를 자신이 직접 결정하고 생성한다.

그러나 IoC(제어의 역전)에서는 이 제어의 흐름이 거꾸로 뒤집힌다. 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임한다. main()을 제외하고 모든 오브젝트에서는 이렇게 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정된다. IoC의 예시로는 서블릿, 템플릿 메소드 패턴, 프레임워크 등이 있다.


예제 코드 설명

UserDao : DB에 접근하여 User모델을 다루는 클래스

ConnectionMaker : DB연결을 처리하는 클래스를 만드는 인터페이스

DConnectionMaker : ConnectionMaker의 구현체로, DB와 관련한 실제 정보를 담고있는 클래스

DaoFactory : ConnectionMaker의 구현체를 생성하여 이를 Dao에 주입하여 객체 생성후 Dao를 제공하는 클래스

UserDaoTest : UserDao의 인스턴스를 사용하여 실행 테스트하는 클래스

위 코드들은 IoC 구조를 가지고 있다. UserDao클래스는 데이터 접근에만 관심을 두며 DB접속에는 관심을 두지 않는다. DB접속을 담당하는 Connection의 객체를 필요로 하지만, 직접 생성하지 않고 ConnectionMaker 인터페이스에게 위임한다. 권한을 위임함으로써 DB접근 코드 수정 시 UserDao의 코드는 수정되지 않는다. 다만, ConnectionMaker를 구현한 구현체의 코드만 달라질 뿐이다.

Connection c = connectionMaker.makeConnection();

또한, main()함수를 실행시키는 UserDaoTest 클래스는 직접 UserDao의 객체를 생성하지 않는다. UserDao의 객체를 생성하려면, UserDao의 생성자에 ConnectionMaker의 구현체를 주입시켜야 한다.

만약 직접 주입한다면, UserDao의 기능을 테스트하기 위한 목적으로 설계된 UserDaoTest의 기능이 흘들리게 된다. 또 다른 책임까지 떠맡고 있는 것은 문제가 있다. 따라서 DaoFactory클래스를 통해 UserDao와 ConnectionMaker 구현 클래스의 오브젝트를 만드는 것과,그렇게 만들어진 두 개의 오브젝트가 연결돼서 사용될 수 있도록 관계를 맺게 한다.

예제 코드

  • User.java
public class User {
    String id;
    String name;
    String password;

    public String getId(){
        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;
    }
}
  • UserDao.java
/**
 * DB에 접근하여 User모델을 처리하는 클래스
 */
public class UserDao {

    private ConnectionMaker connectionMaker;

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

    public void add(User user) throws SQLException, ClassNotFoundException {
        Connection c = connectionMaker.makeConnection();
        PreparedStatement ps = c.prepareStatement(
                "insert into user(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 user 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.setName(rs.getString("password"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }
}
  • ConnectionMaker.java
/**
 * DB 연결을 처리하는 클래스를 만드는 인터페이스
 */
public interface ConnectionMaker {
    Connection makeConnection() throws ClassNotFoundException, SQLException;
}
  • DConnectionMaker.java
/**
 * ConnectionMaker의 구현체로, DB와 관련한 실제 정보를 담고있는 클래스
 */
public class DConnectionMaker implements ConnectionMaker{
    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/spring?useSSL=false",
                "root",
                "1111");
        return c;
    }
}
  • DaoFactory.java
/**
 * ConnectionMaker의 구현체를 생성하여 이를 DAO에 주입하여 객체 생성 후 DAO를 제공하는 클래스
 */
public class DaoFactory {
    public UserDao userDao(){
        UserDao userDao = new UserDao(connectionMaker());
        return userDao;
    }

    public ConnectionMaker connectionMaker(){
        return new DConnectionMaker();
    }
}
  • UserDaoTest.java
/**
 * UserDao의 인스턴스를 사용하여 실행 테스트하는 클래스
 */
public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        UserDao dao = new DaoFactory().userDao();

        User user = new User();
        user.setId("1");
        user.setName("KJM");
        user.setPassword("123");

        dao.add(user);

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

        User user2 = dao.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getId() + "조회 성공");
    }
}



스프링을 이용한 IoC 예제

Spring IoC

스프링의 핵심은 빈 팩토리(Bean factory) 또는 애플리케이션 컨텍스트(Application context)이다. 이 두 가지는 IoC_with_Java의 DaoFactory를 일반화한 것으로 볼 수 있다.

스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(bean)이라고 부른다. 자바빈 또는 EJB에서 말하는 빈과 비슷한 오브젝트 단위의 애플리케이션 컴포넌트를 말한다. 동시에 스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 IoC(제어의 역전)가 적용된 오브젝트를 가리키는 말이다.

  • 빈 팩토리 : 스프링에서 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트.
  • 애플리케이션 컨텍스트 : 빈 팩토리를 좀더 확장한 것

스프링에서는 애플리케이션 컨텍스트를 IoC 컨테이너라 하기도 하고, 스프링 컨테이너라고 하기도 한다. 애플리케이션 컨텍스트는 빈 팩토리를 상속했기 때문에 빈 팩토리라고 부르기도한다.

@Configuration이 붙은 DaoFactory는 이 애플리케이션 컨텍스트가 활용하는 IoC 설정정보이다. 내부적으로는 애플리케이션 컨텍스트가 DaoFactory의 userDao() 메소드를 호출해서 오브젝트를 가져온 것을 클라이언트가 getBean()으로 요청할 때 전달해준다.

단편적으로 볼 때에는 순수 자바로 구현한 오브젝트 팩토리와 애플리케이션 컨텍스트가 차이가 없는 것처럼 보일 수 있다.

그러나, 애플리케이션 컨텍스트는 다음과 같은 장점을 가지고 있다.

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.(클라이언트는 Bean의 이름만 알면된다.)
  • 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.
  • 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.

또한 스프링은 여러 번에 걸쳐 빈을 요청하더라도 매번 동일한 오브젝트를 돌려준다. 애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이다. 싱글톤 레지스트리는 일반적인 싱글톤 패턴과 다르다.

싱글톤 패턴은 private 생성자를 사용하기 때문에 상속이 불가능하다. 또한 만들어지는 방식이 제한적이여서 테스트가 힘들다. 그 외에도 서버에서 싱글톤 1개만 생성되는 것을 보장하지 않으며, 객체 지향관점에서 전역 상태를 갖는것은 바람직하지 않다.

싱글톤 레지스트리는 이러한 단점들을 해결해준다. 빈을 생성하는 클래스는 public 생성자를 갖는다. 다만, 스프링이 빈을 싱글톤으로 생성되도록 관리하는 것이다. 스프링의 싱글톤 빈으로 사용되는 클래스를 만들 때는 기존의 UsreDao처럼 개별적으로 바뀌는 정보는 로컬 변수로 정의하거나, 파라미터로 주고받으면서 사용하게 해야 한다. 만약 멤버변수로 정의할 경우 멀티쓰레드 환경에서 데이터가 뒤죽박죽이 되는 상황에 직면할 수 있다.

만약 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도라면 인스턴스 변수를 사용해도 좋다. 또한 읽기전용의 변수도 인스턴스 변수로 사용해도 된다.

코드 설명

소스코드는 IoC_with_Java와 거의 유사하다. 다만, DaoFactory와 UserDaoTest가 다르다.

DaoFactory.java

@Configuration // 애플리케이션 컨텍스트 또는 빈 팩토리가 사용할 설정정보라는 표시
public class DaoFactory {
    @Bean // 오브젝트 생성을 담당하는 IoC용 메소드라는 표시
    public UserDao userDao(){
        UserDao userDao = new UserDao(connectionMaker());
        return userDao;
    }

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

코드는 동일하지만, 어노테이션을 붙인다는 점에서 차이가 있다. @Configuration은 애플리케이션 컨텍스트 혹은 빈 팩토리가 사용할 설정정보라는 표시이다. @Bean은 오브젝트 생성을 담당하는 IoC용 메소드라는 표시이다. 해당 어노테이션을 붙임으로서 스프링 컨테이너는 클라이언트가 getBean()을 통해 Bean 호출시, 해당 클래스와 메소드를 찾거나 빈 목록을 검색하게 된다.

UserDaoTest.java

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

DaoFactory로 부터 직접적으로 UserDao 객체를 받던 코드는 애플리케이션 컨텍스트에 있는 Bean을 받는 것으로 수정되었다. userDao는 애플리케이션 컨텍스트에 저장된 bean의 이름이며, 이는 UserDao의 객체를 리턴하는 메소드의 이름이기도 하다. 원래 getBean()메소드는 Object type을 리턴한다. 따라서 이를 형변환해주기 위해 2번째 파라미터로 UserDao.class를 넣어주는 것이다.

profile
나는 사는데로 생각하지 않고, 생각하는데로 살겠다

0개의 댓글