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

2023. 9. 26. 03:49Books

1.1 초난감 DAO

DAO 란?

- Data Access Object는 DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담한 오브젝트이다

1.1.2 UserDao

JDBC의 일반적인 순서

  1. DB Connection 생성
  2. SQL Statement(또는 PreparedStatement) 생성
  3. Statement 실행
  4. 실행결과를 ResultSet으로 받아서 오브젝트에 옮겨준다
  5. 작업 중 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후 반드시 닫아준다. (close)
  6. JDBC API 가 만드는 예외 처리

1.1.3 main()을 이용한 DAO 테스트 코드

책에 있는 코드를 테스트해보기 위해서 직접 웹이나, DB를 띄워서 테스트해 볼 수 있지만 이는 배보다 배꼽이 더 큰 것이다. 그래서 우리는 추후 테스트 코드를 작성하여 테스트를 할 것이지만 여기서는 간단하게 main 메서드를 통해 테스트를 하고 있다.

앞선 코드의 테스트는 성공하였지만 이 책을 읽으며 객체지향 설계의 원칙을 생각하며 코드를 개선해나가는 연습을 할 것이다.

1.2 DAO의 분리

1.2.1 관심사의 분리

객체지향 세계에서는 많은 것이 변하고 우리는 항상 그 변화에 대비를 해야 한다. 기존 절차지향적 프로그래밍에 비해 초기에 좀 더 많은, 번거로운 작업을 요구하는 이유는 객체지향 기술 자체가 변화에 효과적으로 대처할 수 있다는 특징 때문이다. 또한 객체지향 프로그래밍은 변경, 발전, 확장을 더욱 쉽게 할 수 있다.

이러한 변화가 일어날 때 우리는 더욱 정확하고 빠른 작업을 위해서 (유연한 대처를 위해) 분리와 확장을 고려한 설계를 해야 한다.

 

관심사의 분리 (Separation Of Concerns) 라는 개념은 객체지향 관점에서 관심이 같은 것 끼리는 하나의 객체 안으로 모으고, 관심이 다른 객체는 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것이다. 이런 식으로 분리를 해서 설계하면 추후 변화가 들어와도 관심사가 분리되어 있기 때문에 특정 코드만 변경해 주면 된다.

1.2.2 커넥션 만들기의 추출

이 책에서는 UserDao 라는 예제를 들어 설명하고 있다.

UserDao에서의 관심사항을 보면

  1. DB 연결과 관련된 관심
    1. 어떤 DB를 쓰는가
    2. 어떤 드라이버를 사용할 것인가
    3. 어떤 로그인 정보를 쓰는가
    4. Connection을 어떻게 생성할 것인가
  2. 사용자 등록을 위한 SQL Statement를 만들고 실행시키는 관심
    1. 파라미터로 넘어온 사용자 정보를 Statement에 바인딩
    2. Statement에 담긴 SQL 쿼리를 DB를 통해 실행
  3. 사용한 리소스인 Statement와 Connection 오브젝트를 닫아주는 관심

이를 위해 UserDao 에서 add, get 메소드들에 대해 Connection을 가져오는 중복 코드를 따로 추출하여 사용할 것이다.

중복 코드의 메소드 추출 (Extract Method)

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

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

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 Connection을 호출할 일이 정말 많을 것이다. 메소드 분리 이전에는 DB Connection에 대한 변경사항이 생겼을 때 모든 메소드의 코드를 직접 변경해줘야 할 테지만 위와 같이 중복 코드를 메소드로 추출한다면 추후 Connection을 가져오는 변경사항에 대해서는 getConnection 메소드만 변경을 해주면 다른 코드는 변경하지 않아도 된다.

1.2.3 DB 커넥션 만들기의 독립

책의 가정상황은 추후 이 UserDao 코드를 N사, D사에서 사용하고 싶다하지만 우리는 내부 코드를 제공하기는 싫다는 것이다. 이에 우리는 getConnection 코드만 추상메소드로 변경하여 제공하고자 한다.

public abstract class UserDao {

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

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

    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}

이런 식으로 UserDao를 추상클래스로 만들어 getConnection 메소드를 추상메소드로 변경해 준다면 N, D사에서는 UserDao를 상속받고 getConnection 메소드만 각자 구현하면 된다.

 

public class NUserDao extends UserDao{

    @Override
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        // N사 DB connection 생성 코드
    }
}

public class DUserDao extends UserDao{

    @Override
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        // D사 DB connection 생성 코드
    }
}

위와 같이

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

"템플릿 메소드 패턴 (Template Method Pattern)"라고 한다.

 

그리고

UserDao의 서브클래스의 getConnection 메소드는 어떤 Connection 클래스의 오브젝트를 어떻게 생성할 것인지 결정하는 방법으로 볼 수도 있는데

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

팩토리 메소드 패턴 (Factory Method Pattern)이라고 한다.

1.3 DAO의 확장

1.3.1 클래스의 분리

처음에는 메소드를 분리했고, 다음에는 상하위 클래스로 분리했다.

이제는 아예 클래스로 분리해서 composition으로 사용해 보자.

이런 식으로 바꾸면 상속을 쓰지 않아도 되고 abstract를 쓰지 않아도 된다. add, get 메소드에서 DB 커넥션을 가져오면 된다.

 

public class Userdao() {

    private SimpleConnectionMaker simpleConnectionMaker;

    public Userdao() {
        simpleConnectionMaker = new SimpleConnectionMaker;
    }

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

    public void get(String id) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewconnection();
        ...
    }
}

하지만 이런 방법으로는 여러 가지 문제가 아직 있다. add, get 등의 메소드가 여러 개가 된다면 작업량이 많아지고 Connection 이 바뀌면 모든 코드를 바꿔주어야 한다. 또한 UserDao 가 어떤 Connection인지 알고 있어야 한다는 점이다.

1.3.2 인터페이스의 도입

이를 해결하기 위해 클래스 간의 연결 사이에 추상적인 연결고리, 인터페이스를 두는 것이다.

하지만 이렇게 변경하더라도 결국 UserDao에서 어떤 ConnectionMaker를 사용하는지 정해주어야 한다.

connectionMaker = new DConnectionMaker();

결국 아직도 어떤 ConnectionMaker를 쓰냐에 따라서 UserDao 내부의 코드가 바뀌게 된다.

1.3.3 관계설정 책임의 분리

이를 해결하기 위해서 우리는 클래스 간의 의존이 아닌 오브젝트를 다이내믹하게(런타임) 관계를 맺어주면 된다. 클라이언트(사용하는 오브젝트)가 인터페이스 및 다형성을 이용하여 직접 주입해 준다면 UserDao는 어떤 connectionMaker를 사용하든 코드가 변경될 필요가 없게 된다.

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

UserDao 생성자 파라미터에서 ConnectionMaker 인터페이스를 주입받는다. 우리는 UserDao에 들어갈 구현체만 클라이언트에서 넘겨주면 된다. 스프링에서는 스프링 컨테이너가 이 역할을 해주지만 우리는 자바만으로 하기 위해서 main 메소드에 한번 넣어보자.

public class UserDaoTest {

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

        UserDao userDao = new UserDao(connectionMaker);
        ...
    }
}

UserDaoTest는 UserDao와 ConnectionMaker 구현 클래스와의 런타임 오브젝트 의존관계를 설정하는 책임을 담당한다. 그래서 UserDao 생성자 파라미터를 통해 두 개의 오브젝트를 연결해 준다.

 

이제 DConnectionMaker에서 NConnectionMaker로 바꾸기 위해서는 UserDao 코드의 변경 일체 없이 UserDaoTest에서

ConnectionMaker connectionMaker = new NConnectionMaker();

이렇게 바꿔주면 된다!

1.3.4 원칙과 패턴

개방 폐쇄 원칙

2023.04.12 - [OOP] - 객체지향 설계 5원칙 - SOLID

 

이전에 SOLID 원칙에 대해 다른 포스트가 있다. 자세한 건 참고 바란다.

 

OCP (Open-Closed Principle) 개방 폐쇄 원칙 : 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.

기능 확장은 자유롭게 할 수 있되, 그런 변화에 영향을 받지 않고 핵심 기능은 유지되어야 한다. 우리는 ConnectionMaker라는 인터페이스를 이용했기 때문에 여러 가지 Connection으로 확장할 수 있고 이런 변화에도 UserDao는 ConnectionMaker 인터페이스와만 결합되어 있기 때문에 이때 변화에 영향을 받지 않는다.

높은 응집도와 낮은 결합도

High coherence and Low coupling 이라고도 불린다.

 

높은 응집도라는 것은 하나의 모듈 혹은 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻이다. 다른 예시를 하나 들어보자면, 어떤 Car 클래스에 주행, 정비의 기능이 모두 들어 있는 게 아니라 주행 클래스, 정비 클래스를 따로 두어 응집도를 높여야 한다는 것이다.

 

낮은 결합도는 책임과 관심사가 다른 오브젝트 또는 모듈과는 느슨한 연결을 유지한다는 것이다. 하나의 오브젝트에 변경이 생길 때 관계를 맺는 다른 오브젝트에 영향이 최대한 적어야 한다는 것이 낮은 결합도이다. 위의 코드에서 ConnectionMaker의 구현체가 바뀌더라도 UserDao 에는 영향이 전혀 없다. 이때 ConnectionMaker와 UserDao는 낮은 결합도를 가지고 있다고 말할 수 있다.

 

전략패턴(Strategy Pattern)

전략패턴은 자신의 기능 맥락에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 분리시키고 필요에 따라 구현체 클래스를 바꿔 사용할 수 있게 하는 디자인 패턴이다. 위의 코드에서 ConnectionMaker라는 DB 연결 방식 알고리즘은 NConnectionMaker 혹은 DConnectionMaker의 구현체들로 바꿔 사용할 수 있게 해 둔 것이 이런 예시이다.

1.4 제어의 역전 (IoC)

1.4.1 오브젝트 팩토리

팩토리(Factory) : 객체 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것

우리가 위에서 UserDaoTest로 임시 main 메소드를 이용해서 팩토리 역할을 해주고 있었다.

팩토리 기능을 담당할 클래스를 DaoFactory로 만들어보자.

public class DaoFactory {

    public UserDao userDao() {
        ConnectionMaker connectionMaker = new DConnectionMaker();
        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는 그들의 관계를 정의해 주는 설계도라고 볼 수 있다.

DaoFactory라는 팩토리를 이용함으로써 우리는 DConnectionMaker에서 NConnectionMaker로 변경하더라도 핵심 로직이 담긴 컴포넌트를 변경하는 것이 아닌 설계도인 팩토리에서만 변경해 주면 된다!

1.4.2 오브젝트 팩토리의 활용

public class DaoFactory {

    public UserDao userDao() {
        return new UserDao(new DConnectionMaker());
    }

    public AccountDao accountDao() {
        return new AccountDao(new DConnectionMaker());
    }

    public MessageDao messageDao() {
        return new MessageDao(new DConnectionMaker());
    }

}

AccountDao와 MessageDao라는 것도 생겼다고 가정하자. 우리가 DConnectionMaker의 DB 연결방식을 모두 넣어줘야 하기 때문에 이도 한번 분리해 줄 수 있다.

public class DaoFactory {

    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }

    public AccountDao accountDao() {
        return new AccountDao(connectionMaker());
    }

    public MessageDao messageDao() {
        return new MessageDao(connectionMaker());
    }

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

이렇게 되면 추후 NConnectionMaker로 바꾸더라도 함수 하나만 변경해 주면 된다!

1.4.3 제어권의 이전을 통한 제어관계 역전

제어의 역전(Inversion Of Control) : 프로그램의 제어 흐름 구조가 뒤바뀌는 것

우리가 초기에 만든 UserDao에서 테스트용 main 메소드를 생각해보자. main 메소드는 UserDao 클래스의 오브젝트를 "직접" 생성하고, 만들어진 오브젝트의 메소드를 사용한다. UserDao 또한 자신이 사용 할 ConnectionMaker의 구현체를 직접 결정했고, 그 오브젝트를 필요 시점에 생성해 두었고 각 메소드에서 이를 사용했다.

-> 모든 작업을 사용하는 쪽에서 제어하는 구조

 

제어의 역전은 이러한 제어 흐름을 뒤집는 것이다. 여기서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하거나 생성하지 않는다. 외부에서 누군가 정해주는 것이다. 앞서 탬플릿 메소드 패턴에서 추상 클래스 UserDao를 상속한 서브클래스는 getConnection()을 구현한다. 하지만 이 서브클래스는 본인의 getConnection이 언제 호출되는지 알 수 없다. 슈퍼클래스인 UserDao의 템플릿 메소드인 add(), get()에서 필요할 때 호출하는 것이다. 이 처럼 제어권이 상위 템플릿 메소드에 넘겨져 있다.

-> 템플릿 메소드는 제어의 역전이라는 개념을 활용해 문제를 해결하는 디자인 패턴이다.

 

프레임워크들도 마찬가지로 제어의 역전의 개념을 이용한다. 내가 이전에 이용해 본 Django 프레임워크는 urls.py에서 적절한 view 함수를 호출한다. 이런 것도 클라이언트단에서 건드린 적이 없지만 Django가 제어 권한을 가지고 있는 것이다.

 

라이브러리는 애플리케이션 코드에서 필요할 때 능동적으로 호출하여 사용하는 것이므로 제어의 역전은 아니다. 반면 프레임워크는 애플리케이션 코드가 프레임워크에 의해서 작동하므로 제어의 역전인 것이다.

1.5 스프링의 IoC

1.5.1 오브젝트 팩토리를 이용한 스프링 IoC

빈(Bean) : 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트

-> 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해 주는 제어의 역전이 적용된 오브젝트

 

빈 팩토리(Bean Factory) : 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트

애플리케이션 컨텍스트(Application Context) : 빈 팩토리 + α (스프링에서 사용하는 부가기능)

 

앞서 만든 DaoFactory를 애플리케이션 컨텍스트로 만들어보자.

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

 

 

  • @Configuration : 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식한다. (스프링이 설계도라고 인식하는 클래스에 붙여준다.)
  • @Bean : 오브젝트를 만들어주는 메소드에 붙인다.
public class UserDaoTest {

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

DaoFactory를 설정정보로 사용하는 애플리케이션 컨텍스트를 만들고 ApplicationContext의 getBean 메소드를 통해 UserDao 오브젝트를 가져온 것이다. getBean 메소드의 첫번째 파라미터는 @Bean으로 지정한 메소드의 이름을 적어주면 된다. 두 번째 파라미터는 리턴 타입을 적어주면 된다.

1.5.2 애플리케이션 컨텍스트의 동작방식

기존 DaoFactory 팩토리는 스프링에서

애플리케이션 컨텍스트   IoC 컨테이너  스프링 컨테이너 빈 팩토리

오브젝트 팩토리 대신 애플리케이션 컨텍스트를 사용하면 장점은 다음과 같다.

 

1. 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.

  • 애플리케이션이 커지면 IoC를 적용한 오브젝트도 계속 추가될 텐데 팩토리 클래스를 사용하면 그때마다 해당 클래스를 찾아야 한다. 하지만 애플리케이션 컨텍스트를 사용하면 일관된 방식으로 원하는 오브젝트를 가져올 수 있다.
  • 자바 코드뿐만 아니라 XML처럼 단순한 방법을 사용해서 IoC 설정정보를 만들 수도 있다.

2. 애플리케이션 콘텍스트는 종합 IoC 서비스를 제공한다.

  • 애플리케이션 컨텍스트는 오브젝트 생성, 관계설정 뿐만 아니라 오브젝트를 효과적으로 활용할 수 있는 다양한 기능을 제공한다. (오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수도 있고, 자동생성, 오브젝트에 대한 후처리, 정보의 조합, 설정방식의 다변화, 인터셉팅 등)
  • 빈이 사용할 수 있는 기반기술 서비스나 외부 시스템과의 연동 등을 컨테이너 차원에서 제공한다

3. 애플리케이션 컨텍스트는 빈을 검색하는 다양항 방법을 제공한다.

  • getBean() 메소드는 빈의 이름을 이용해 빈을 찾아준다.
  • 타입만으로 검색하거나 특별한 애노테이션 설저이 되어 있는 빈을 찾을 수도 있다.

1.5.3 스프링 IoC의 용어 정리

1️⃣ 빈(Bean)

빈 또는 빈 오브젝트는 스프링이 IoC 방식으로 관리하는 오브젝트라는 뜻이다. managed object라고 불리기도 한다. 참고로 스프링에서 모든 오브젝트가 빈은 아니고 스프링이 직접 생성과 제어를 담당하는 오브젝트만을 빈이라고 한다.

 

2️⃣ 빈 팩토리(Bean Factory)

스프링의 IoC 컨테이너를 가리킨다. 빈을 등록, 생성, 조회, 돌려주고 그 외에 부가적인 빈을 관리하는 기능을 한다. getBean 메소드가 정의되어 있다.

( 참고 : BeanFactory는 인터페이스이고 ApplicationContext 인터페이스가 이를 상속받는다. 우리는 ApplicationContext 인터페이스의 구현체를 이용한다.)

 

3️⃣ 애플리케이션 컨텍스트(Applicaton Context)

빈 팩토리를 확장한 IoC 컨테이너이다. 빈 팩토리와 같은 기능을 제공하고 추가적인 기능도 제공한다.

 

4️⃣ 설정정보/ 설정 메타 정보(Configuration Metadata)

스프링의 설정정보란 애플리케이션 컨텍스트가 IoC를 적용하기 위해 사용하는 메타정보를 말한다.

 

5️⃣ 컨테이너 또는 IoC 컨테이너

IoC 컨테이너, 스프링 컨테이너, 애플리케이션 컨텍스트 , 스프링 등 다양하게 가리키는 말이다.

 

6️⃣ 스프링 프레임워크

위의 것들을 모두 포함하고 스프링이 제공하는 모든 기능을 통틀어 말할 때 주로 사용한다.

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

동일성 vs 동등성

동일성은 == 연산자 / 동등성은 equals() 메소드를 사용한다.

동일하다는 것은 두 오브젝트가 같은 레퍼런스를 가지고 있다는 것이고

동등하다는 것은 두 오브젝트가 다른 메모리에 저장이 되어 있지만 가지고 있는 정보가 동일하다는 것이다.

 

기존 DaoFactory에서 userDao() 메소드를 호출한 것과 @Configuration을 이용해서 userDao() 메소드를 호출했을 때 반환하는 것은 차이가 있다.

전자는 호출 시 두 오브젝트가 다르고 후자는 같은 오브젝트(동일한 오브젝트)가 반환된다.

1.6.1 싱글톤 레지스트리로서의 애플리케이션 컨텍스트

애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이다. 스프링은 빈을 싱글톤으로 만든다. 싱글톤 컨테이너에 대한 자세한 내용은 예전에 작성한 글을 참고하자.

2023.05.12 - [Spring] - 싱글톤 컨테이너

 

싱글톤 구현 방법

  • 클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private으로 만든다.
  • 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 static 필드를 정의한다.
  • 스태틱 팩토리 메소든인 getInstance()를 만들고 이 메소드가 최초로 호출되는 시점에서 한 번만 오브젝트가 만들어지게 한다.
  • 한번 오브젝트가 만들어진 이후에는 getInstance() 메소드를 통해 이미 만들어져 스태틱 필드에 저장해 둔 오브젝트를 넘겨준다.
public class MySingleton {
    // private으로 생성자를 선언하여 외부에서 직접 객체를 생성하지 못하도록 합니다.
    private MySingleton() {
        // 생성자 내용 (예: 초기화 로직)
    }

    // 싱글톤 객체를 저장할 private static 필드를 정의합니다.
    private static MySingleton instance;

    // 스태틱 팩토리 메소드를 만들어 싱글톤 객체를 반환합니다.
    public static synchronized MySingleton getInstance() {
        // 처음 호출될 때만 객체를 생성합니다.
        if (instance == null) {
            instance = new MySingleton();
        }
        return instance;
    }
}

하지만 이러한 싱글톤 패턴은 단점도 많기 때문에 안티패턴이라고 불리기도 한다.

 

1. private 생성자를 갖고 있기 때문에 상속할 수 없다.

private 생성자를 사용한다는 것은 객체지향의 장점인 상속, 다형성 등을 이용할 수 없다는 것이다.

 

2. 싱글톤은 테스트하기가 힘들다.

 

3. 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.

 

4. 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.

 

 

자바에서 직접 구현하는 싱글톤 패턴은 여러 단점이 있기 때문에, 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 덕분에 public 생성자도 사용가능하고 테스트 환경에서도 용이하게 사용할 수 있다. 또한 객체지향, 디자인 패턴의 설계를 적용하는데 아무런 제약이 없다.

1.6.2 싱글톤과 오브젝트의 상태

이전 글에도 설명되어 있지만 싱글톤은 무상태(stateless) 방식으로 만들어져야 한다. 다중 사용자의 요청이 많아지면 매우 위험할 수 있기 때문이다. 이에 상태를 저장하지 않고 파라미터나 로컬 변수, 리턴 값 등을 이용하여 정보를 다룬다.

 

읽기 전용 인스턴스 변수는 사용해도 괜찮다. 하지만 매번 새로운 값으로 바뀌는 정보를 담은 인스턴스 변수들은 심각한 문제가 발생할 수 있다.

1.6.3 스프링 빈의 스코프

빈의 스코프란 빈이 생성되고, 존재하고, 적용되는 범위이다. 기본적으로는 싱글톤 스코프를 갖는다. 경우에 따라서는 싱글톤 외의 스코프를 갖는다.

1.7 의존관계 주입 (DI)

1.7.1 제어의 역전(IoC)와 의존관계 주입

스프링 IoC 기능의 의도를 명확하게 드러나도록 하기 위해 의존관계주입 (Dependency Injection)라는 이름을 사용하기 시작했다. 이에 IoC 컨테이너 대신 DI 컨테이너라고 더 많이 불리고 있다.

1.7.2 런타임 의존관계 설정

의존관계

UML 모델에서는 두 클래스의 의존관계를 점선 화살표로 표현한다. 의존한다는 것은 무슨 의미일까? 위 다이어그램에서는 B가 변하면 A에 영향을 미친 다는 것이다. 내부적으로 B가 변화한다면 A도 수정하거나 추가해줘야 하는 것이다.

 

의존관계에는 방향성이 있다. A가 B에 의존하고 있지만, 반대로 B는 A에 변화해도 영향이 전혀 가지 않는다.

UserDao의 의존관계

위 다이어그램에서 ConnectionMaker 인터페이스가 변한다면 UserDao에 영향이 갈 것이다. 하지만 DConnectionMaker, 즉 ConnectionMaker의 구현체는 변화하더라도 UserDao와 느슨한 결합으로 이루어져 있어서 변화에 영향을 덜 끼친다.

 

하지만 런타임시에는 다르다. 설계에서는 UserDao는 DConnectionMaker의 존재조차 모르지만 런타임 시에는 UserDao는 DConnectionMaker을 의존하고 있다. 실제 사용대상인 오브젝트를 의존 오브젝트라고 말한다.

 

의존관계 주입이란 다음 세 가지 조건을 충족한다

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. -> 인터페이스에만 의존하고 있어야 한다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 주입해 준다.

여기서 제3의 존재란 DaoFactory 같은 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너 등이다.

 

DI 컨테이너는 주입을 위해서 생성자 파라미터를 통해 전달해 주는 방법을 많이 쓴다.

public class UserDao {
    private ConnectionMaker connectionMaker;

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

1.7.3 의존관계 검색과 주입

스프링에서는 의존관계 주입뿐만 아니라 스스로 검색을 이용하는 의존관계 검색도 한다. 런타임 시 의존관계를 맺을 오브젝트를 결정, 생성은 IoC에게 맡기지만 이를 가져올 때는 스스로 컨테이너에게 요청하는 방법을 사용한다.

그것은 이전에 설명한 getBean() 메소드를 통해 가져오는 것이다.

1.7.4 의존관계 주입의 응용

이러한 DI는 언제 적용할까? 예를 들어 LocalDB와 ProductionDB를 구분해야 할 때 우리는 DI 방식을 적용하면 이득을 볼 수 있다. 

@Bean
public class ConnectionMaker connectionMaker {
    return new LocalDBConnectionMaker();
}
@Bean
public class ConnectionMaker connectionMaker {
    return new ProductionDBConnectionMaker();
}

DI 컨테이너의 코드 한 줄만 바꾸면 되기 때문에 나머지 코드는 수정할 필요가 없다. 앞서 이야기한 내용들이기 때문에 넘어가겠다.

1.7.5 메소드를 이용한 의존관계 주입

지금까지는 생성자 파라미터를 통해 의존관계를 주입했지만 다른 방법도 있다.

  • 수정자 메소드(setter)를 이용한 주입
  • 일반 메소드를 이용한 주입
public class UserDao {
    private ConnectionMaker connectionMaker;

    public void setConnectionMaker(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}
@Bean
public UserDao userDao() {
    UserDao userDao = new UserDao();
    userDao.setConnectionMaker(connectionMaker);
    return userDao;
}