본문 바로가기
Spring&SpringBoot/<토비의 스프링 3.1 Vol1.1>, 이일민

애플리케이션 컨텍스트 도입하기 : 의존성 해결과 싱글톤 관리

by 민휘 2023. 3. 2.

이번 장에서는 의존성 해결 책임을 애플리케이션 차원에서 관리하기 위해 스프링이 지원하는 애플리케이션 컨텍스트를 도입해본다. 사용 책임만 남은 객체는 외부에서 의존성을 주입받아야 하는데, 이를 의존성 해결이라고 한다. 의존성 해결을 위해서는 구현을 선택하고, 객체를 생성하고, 생성된 객체를 요청에 따라 주입하는 기능이 필요하다.

 

구현 선택 정보는 환경 설정 파일에서, 객체의 생성과 주입 및 관리는 애플리케이션 컨텍스트가 담당한다. 책임을 더 작게 쪼개서 유연한 설계를 얻는다. 애플리케이션 컨텍스트를 사용하면 일관된 방식으로 오브젝트를 생성하고 주입할 수 있다. 또 내부적으로 싱글톤을 지원하므로 제약 없이 객체 지향 설계를 적용할 수 있다.

 

예제는 의존성 해결의 책임의 이동에 따라 전개된다. 책임은 테스트 메소드, DaoFactory, 애플리케이션 컨텍스트로 이동한다. 각 지점으로 이동할 때 제어의 역전과 추상화가 적용된다. 그리고 싱글톤으로 관리되는 오브젝트에서 주의해야 하는 상태 변경을 다룬다.

 

테스트 메소드의 의존성 해결

이 코드를 보면 테스트 메소드에서 UserDao의 의존성을 해결하고 있다. UserDao가 필요로 하는 ConnectionMaker의 구현을 선택하고 생성해서 주입하고 있다. 지금 테스트 메소드에 dao의 생성과 사용 책임이 함께 존재하고 있다. 관심사 분리를 위해 의존성 주입하는 책임을 외부의 객체에게 할당하자.

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

    UserDao dao = new UserDao(new NConnectionMaker());
  // dao 사용 코드

 

 

DaoFactory의 의존성 해결

ConnectionMaker의 구현을 선택하고 생성해서 주입하는 책임을 DaoFactory에게 할당했다. 이제 테스트 메소드는 DaoFactory에게 의존성을 주입해달라는 메시지로 UserDao를 사용할 수 있게 된다. 테스트 메소드는 더이상 UserDao의 생성 방식을 신경쓰지 않아도 된다.

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

    private NConnectionMaker connectionMaker() {
        return new NConnectionMaker();
    }
}

public static void main(String[] args) throws ClassNotFoundException, SQLException {
    UserDao dao = new DaoFactory().userDao();
    // dao 사용 코드

UserDao의 생성 방식을 결정하는 제어권이 테스트 메소드에서 DaoFactory으로 이동했다. 이제 테스트 메소드는 더이상 UserDao의 생성 방식에 관여할 수 없고, 관여할 필요도 없다. 이를 제어의 역전이 발생했다고 한다. 제어의 역전을 통해 테스트 메소드는 시스템 동작 확인에, DaoFactory는 UserDao 생성에만 집중할 수 있다. 설계가 깔끔해지고 유연성이 증가하고 확장성이 좋아졌다.

 

제어의 역전은 제어권이 이동해서 기존의 제어 관계가 역전되는 현상을 말한다. 서블릿 컨테이너, 템플릿 메소드 패턴, 프레임워크 등 다양한 분야에 적용되는 개념이다.

 

애플리케이션 컨텍스트

스프링은 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리라고 한다. 애플리케이션 컨텍스트는 생성과 관계설정 뿐만 아니라 애플리케이션 전반에서 모든 구성요소의 제어를 담당한다.

 

이제부터 스프링을 적용해본다. 다음 라이브러리들을 인텔리제이에 추가하자.

 

[IntelliJ] 외부 라이브러리 추가하기

 

[IntelliJ] 외부 라이브러리 추가하기

외부 라이브러리 추가하기 1. File > Project Structure (Ctrl + Alt + Shift + S) 2. Modules > 외부 라이브러리를 추가할 프로젝트를 선택 > Dependencies > '+' 버튼을 클릭 > '1 JARs or directories...' 클릭 3. 외부 라이브

tychejin.tistory.com

위의 라이브러리 파일을 블로그 글을 보고 추가했다.

 

기존의 DaoFactory는 설정 정보를 선택하는 책임을 가지게 된다. 이 설정 정보는 애플리케이션 컨텍스트가 빈 생성과 관리할 때 사용한다.

@Configuration은 스프링에게 이 클래스가 설정 정보임을 알려주는 애노테이션이다. @Bean은 스프링이 관리해야하는 빈을 알려주는 애노테이션이다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

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

 

이제 테스트 메소드에서 애플리케이션 컨텍스트에게 빈을 주입해달라고 요청하자. 애노테이션을 사용하여 설정 정보를 등록했으므로 AnnotationConfigApplicationContext를 선택하여 context를 생성한다. context에게 메시지를 요청하여 설정 정보에 따라 생성되고 관리되는 UserDao 객체를 받아온다.

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

 

테스트가 성공적으로 실행되었다.

 

 

애플리케이션 컨텍스트는 DaoDactory 클래스를 설정정보로 등록하고 @Bean이 붙은 메소드의 이름을 가져와 빈 목록을 만든다. 클라이언트가 빈을 주입해달라는 메시지를 보내면 자신이 관리하고 있는 빈을 찾아 반환한다. 설정 정보를 만드는 책임은 애플리케이션을 개발하는 개발자가 직접 수행한다. 설정 정보를 바탕으로 객체를 생성하고 클라이언트에게 제공하는 책임은 스프링이 수행한다. 의존성 해결을 더 작은 책임으로 쪼개고, 일부는 개발자가 구현하도록 하여 개발자가 원하는 대로 유연하게 설계할 수 있다.

 

제어권의 이전 측면에서 살펴보면, 객체를 생성하고 주입하는 제어권이 DaoFactory(개발자가 작성)에서 ApplicationContext(스프링)으로 이전했다고 볼 수 있다. 결과적으로 개발자가 원하는 대로 빈 관계를 설정할 수 있고 일관적인 방식으로 오브젝트를 주입받을 수 있으니 책임 분할이 잘 이루어진 것이다.

 

그런데 사실 코드만 보면 IoC 오브젝트로써의 DaoFactory나 설정 파일로써의 DaoFactory나 별 차이가 없어보인다. 당장 구현 코드만 보면 그렇지만, 만약 IoC 오브젝트가 매우 많아진다고 생각해보자. 클라이언트는 자신이 주입 받을 빈을 생성하는 IoC 오브젝트가 누구인지 알고 있어야만 의존성 주입을 요청할 수 있다. 구현에 의존하면 변경에 취약해진다. 반면 애플리케이션 컨텍스트는 다양한 IoC 오브젝트를 추상화한다. IoC 오브젝트가 가지고 있던 다양한 객체 생성과 관리 책임을 추상화한 것이다. 그래서 클라이언트는 일관적인 방식으로 오브젝트를 사용할 수 있다.

 

애플리케이션 컨텍스트의 오브젝트 관리 방식

애플리케이션이 가지는 책임은 객체의 생성과 주입이다. 내부적으로 수행하는 방식은 외부 객체에게 숨긴다. 하지만 우리는 공부를 하고 있으므로 애플리케이션 컨텍스트가 어떻게 더 효과적인 방법으로 객체를 관리하는지 알아보자.

 

  1. 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공한다. 오브젝트 생성과 관계설정 뿐만 아니라 오브젝트가 만들어지는 방식, 시점, 전략을 다르게 가져갈 수 있다. 자동 생성, 오브젝트 후처리, 정보 조합, 설정 방식의 다변화, 인터셉팅 등 오브젝트를 효과적으로 활용할 수 있는 다양한 기능을 제공한다.
  2. 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다. 빈의 이름, 타입, 어노테이션으로 빈을 검색할 수 있다.
  3. 애플리케이션 컨텍스트는 빈을 싱글톤 방식으로 관리한다. 서버 애플리케이션은 사용자의 요청을 처리하기 위해 다양한 객체의 협력이 필요하다. 협력에 참여하는 객체를 모든 요청에 대해 생성하면 금방 부하가 발생한다. 그래서 이런 서비스 객체는 싱글톤으로 관리한다. 그런데 코드로 구현하는 싱글톤 패턴은 객체지향 설계를 해칠 수 있으므로 대신해서 애플리케이션 컨텍스트가 객체를 싱글톤으로 생성해준다. (스프링아 고마워) 코드로 구현한 싱글톤이 안티 패턴인 이유는 전역 상태를 만들어 결합도를 높이고, 상속도 안되고, 테스트 객체로 대체가 어렵고, JVM이 달라지면 싱글톤을 보장하지 못하기 때문이다.
  4. 애플리케이션 컨텍스트는 빈의 생명 주기를 관리한다. 빈의 기본 스코프는 싱글톤이다. 컨테이너에 빈을 요청할 때마다 새로 생성하는 스코프는 프로토타입, 웹을 통해 HTTP 요청이 들어올 때 새로 생성하는 요청 스코프, 웹의 세션과 유사한 세션 스코프 등을 제공한다.

 

설정 정보에 등록한 UserDao 역시 스프링에 의해 관리되는 싱글톤 빈이다. 싱글톤 오브젝트를 설계할 때 가장 중요한 것은 무상태를 유지하는 것이다. 인스턴스 필드의 값을 변경할 목적으로 사용하면 안된다. (읽기는 가능) 상태를 사용해야 한다면 메소드 안에서만 유효하도록 파라미터나 로컬 변수를 사용하자.

 

UserDao에 인스턴스 필드를 추가해서 필드 값을 변경하도록 코드를 수정했다. 이러면 나중에 서버 올렸을 때 큰일 나는 것이다. 정 필요하다면 메소드 매개변수로 넘겨서 메소드 안에서만 상태를 가지도록 제한하자.

public class UserDao {
    ConnectionMaker connectionMaker; // 기존 필드 - 읽기 전용
    Connection c; // 추가한 필드

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

    public void add(User user) throws ClassNotFoundException, SQLException {
//        Connection c = connectionMaker.makeConnection(); // 읽기니까 괜찮음
        this.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();
    }