Back-end/Spring

Spring 싱글톤과 IoC컨테이너

이안_ian 2019. 5. 31. 01:52
반응형

스프링 환경에서는 특별한 설정을 하지않으면 싱글톤 레지스트리로 생성된다. 왜냐하면 매번 인스턴스를 생성할 경우에 초당 100건의 요청이 있을 때 시간 단위로만 놓고봐도 엄청난 양의 인스턴스가 생겨 과부하가 걸리기 마련이기 때문이다. 하지만 자바의 기본적인 싱글톤 패턴의 구현 방식은 여러 가지 단점이 있기 때문에, 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공하는데 그것이 싱글톤 레지스트리다.

 

싱글톤 레지스트리의 장점

스태틱 메소드와 private 생성자를 사용해야 하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 싱글톤으로 활용하게 해준다는 점이다. 덕분에 싱글톤 방식으로 사용될 애플리케이션 클래스라도 public 생성자를 가질 수 있다.

 

싱글톤으로 만들어지기 때문에 주의할 점

싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있다. 기본적으로 싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는 상태정보를 내부에 갖고 있지 않은 무상태 방식으로 만들어져야 한다. 다중 사용자의 요청을 한꺼번에 처리하는 스레드들이 동시에 싱글톤 오브젝트의 인스턴스 변수를 수정하는 것은 매우 위험하다. 저장할 공간이 하나뿐이니 서로 값을 덮어쓰고 자신이 저장하지 않은 값을 읽어올 수 있기 때문이다.

 

그렇다면 상태가 없는 방식으로 클래스를 만드는 경우에 각 요청에 대한 정보나, DB나 서버의 리소스로부터 생성한 정보는 어떻게 다뤄야할까? 이때 파라미터와 로컬변수, 리턴 값을 이용하면된다. 이것들은 매번 새로운 값을 저장할 독립적인 공간이 만들어지기 때문에 싱글톤이라고 해도 덮어쓸 일이 없다.

위에서 connectionMaker는 인스턴스 변수로 사용해도 상관없다. 왜냐면 읽기전용의 정보이기 때문이다. 이 변수에는 ConnectionMaker 타입의 싱글톤 오브젝트가 들어 있다. 이 변수도 DaoFactory에 @Bean을 붙여서 만들었으니 스프링이 관리하는 빈이 될 것이고, 별다른 설정이 없다면 기본적으로 오브젝트 한 개만 만들어져서 UserDao의 인스턴스 필드에 저장된다. 이렇게 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도라면 인스턴스 변수를 사용해도 좋다.

스프링 빈의 스코프

빈이 생성되고, 존재하고, 적용되는 범위에 대해 알아보자. 스프링에서는 이것을 빈의 스코프라고 한다. 스프링 빈의 기본 스코프는 싱글톤이다. 싱글톤 스코프는 컨테이너 내에 한 개의 오브젝트만 만들어져서, 강제로 제거하지 않는 한 스프링 컨테이너가 존재하는 동안 계속 유지된다. 스프링에서 만들어지는 대부분의 빈은 싱글톤 스코프를 갖는다. 경우에 따라서는 다른 스코프를 가질 수 있는데 대표적으로 프로토타입 스코프가 있다.

 

프로토타입은 컨테이너에 빈을 요청할 때마다 매번 새로운 오브젝트를 만들어준다. 그 외에도 웹을 통해 새로운 HTTP 요청이 생길 때마다 생성되는 요청 스코프가 있고, 웹의 세션과 스코프가 유사한 세션 스코프도 있다.

의존관계 설정 or 주입(DI)

기본 동작원리가 모두 IoC방식이라고 할 수 있지만, 스프링이 여타 프레임워크와 차별화되서 제공해주는 기능은 의존관계 주입이라는 새로운 용어를 사용할 때 분명하게 드러난다. 그래서 초기에는 주로 IoC 컨테이너라고 불리던 스프링이 지금은 의존관계 설정 컨테이너 또는 그 약자를 써서 DI 컨테이너라고 더 많이 불리고 있다. 

 

그리고 엄밀하게 말해서 오브젝트는 다른 오브젝트에 주입할 수 있는게 아니다. 오브젝트의 레퍼런스가 전달될 뿐이다. DI는 오브젝트 레퍼런스를 외부로부터 제공(주입)받고 이를 통해 여타 오브젝트와 다이내믹하게 의존관계가 만들어지는 것이 핵심

 

의존관계

먼저 의존관계란 무엇인지 생각해보자.

두 개의 클래스 또는 모듈이 의존관계에 있다고 말할 때는 항상 방향성을 부여해줘야 한다. 즉 누가 누구에게 의존하는 관계에 있다는 식이어야 한다. UML 모델에서는 두 클래스의 의존관계를 다음과 같이 점선으로 된 화살표로 표현한다.

A가 B에 의존하고 있음을 나타낸다.

그렇다면 의존하고 있다는 것은 무슨 의미일까? 의존한다는 건 의존대상, 즉 여기서는 B가 변하면 그것이 A에 영향을 미친다는 뜻이다. B의 기능이 추가되거나 변경되거나, 형식이 바뀌거나 하면 그 영향이 A로 전달된다는 것이다. 하지만 반대로 B는 A에 의존하지 않는다. 의존하지 않는다는 말은 B는 A의 변화에 영향을 받지 않는다는 뜻이다.

UserDao의 의존관계

다음 그림에서 UserDao는 ConnectionMaker 인터페이스에만 의존하고 있다. 따라서 ConnectionMaker 인터페이스가 변한다면 그 영향을 UserDao가 직접적으로 받게 된다. 하지만 인터페이스를 구현한 클래스, 즉 DConnectionMaker 등이 다른 것으로 바뀌거나 그 내부에서 사용하는 메소드에 변화가 생겨도 UserDao에 영향을 주지 않는다. 결합도가 낮다고 설명할 수 있다. 의존관계란 한쪽의 변화가 다른 쪽에 영향을 주는 것이라고 했으니, 인터페이스를 통해 의존관계를 제한해주면 그만큼 변경에서 자유로워지는 셈이다.

이 그림에서 알 수 있듯이 USerDao 클래스는 ConnectionMaker 인터페이스에게만 직접 의존한다. 심지어 DConnectionMaker라는 클래스의 존재도 알지 못한다. 그런데 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계 말고, 런타임 시에 오브젝트 사이에서 만들어지는 의존관계도 있다. 런타임 의존관계 또는 오브젝트 의존관계인데, 설계 시점의 의존관계가 실체화된 것이라고 볼 수 있다. 런타임 의존관계는 모델링 시점의 의존관계와는 성격이 분명히 다르다.

의존관계 검색과 주입

스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 것이 아니다. 코드에서는 구체적인 클래스에 의존하지 않고, 런타임 시에 의존관계를 결정한다는 점에서 의존관계 주입과 비슷하지만, 의존관계를 맺는 방법이 외부로부터의 주입이 아니라 스스로 검색을 이용하기 때문에 의존관계 검색이라 불리는 것도 있다.

 

의존관계 검색은 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾는다. 물론 자신이 어떤 클래스의 오브젝트를 이용할지 결정하지는 않는다. 그러면 IoC라고 할 수 없을 것이기 때문이다. 의존관계를 맺을 오브젝트를 결정하고 생성하는 작업은 외부 컨테이너에게 IoC로 맡기지만, 이를 가져올 때는 메소드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용!

 

하지만 적용 방법은 외부로부터의 주입이 아니라 스스로 IoC 컨테이너인 DaoFactory에게 요청을 하는 것이다. DaoFactory의 경우라면 미리 준비된 메소드를 호출하면 되니까 단순히 요청으로 보이겠지만, 이런작업을 일반화한 스프링의 애플리케이션 컨텍스트라면 미리 정해놓은 이름을 전달해서 그 이름에 해당하는 오브젝트를 찾게 된다. 일종의 검색이라 볼 수 있다.

의존관계 검색은 기존 의존관계 주입의 거의 모든 장점을 지니고 있다. IoC 원칙에도 잘 들어맞는다. 단, 방법만 조금 다를 뿐이다. 그렇다면 주입과 검색중 어떤 것이 더 나을까? 코드를 보면 느껴지겠지만 주입쪽이 훨씬 단순하고 깔끔하다. 사용자에 대한 DB정보를 어떻게 가져올 것인가에 집중해야 하는 UserDao에서 스프링이나 오브젝트 팩토리를 만들고 API를 사용하는 코드가 섞여 있는 것은 어색하다. 그런데 의존관계 검색 방식을 사용해야하는 때가 있다. 테스트를 사용할 때 스프링의 IoC와 DI 컨테이너를 적용했다고 하더라도 애플리케이션 기동시점에서 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야한다. 스태틱 메소드인 main()에서는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문이다. 서버에서도 마찬가지다. 서버에는 main()과 같은 기동 메소드는 없지만, 사용자의 요청을 받을 때마다 main()메소드와 비슷한 역할을 하는 서블릿에서 스프링 컨테이너에 담긴 오브젝트를 사용하려면 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야한다.

 

의존관계 검색과 주입 방식의 차이점은 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다는 점이다. userDao에 스프링의 getBean()을 사용한 의존관계 검색 방법을 적용했다고 해보자. 이 경우 UserDao는 굳이 스프링이 만들고 관리하는 빈일 필요가 없다. 그냥 어딘가에서 직접 new UserDao()해서 만들어서 사용해도 된다. 이땐 ConnectionMaker만 빈이기만 하면 된다.

 

반면에 주입에서는 UserDao와 ConnectionMaker 사이에 DI가 적용되려면 UserDao도 반드시 컨테이너가 만드는 빈 오브젝트여야 한다. 컨테이너가 UserDao에 ConnectionMaker 오브젝트를 주입해주려면 UserDao에 대한 생성과 초기화 권한을 갖고 있어야 하고, 그러려면 UserDao는 IoC 방식으로 컨테이너에서 생성되는 오브젝트, 즉 빈이어야한다.

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

지금까지는 UserDao의 의존관계 주입을 위해 생성자를 사용했다. 생성자에 파라미터를 만들어두고 이를 통해 DI 컨테이너가 의존할 오브젝트 래퍼런스를 넘겨주도록 만들었다. 그런데 생성자 방식 말고도 다른 방법 2가지가 있다.

 

수정자 메소드를 이용한 주입

수정자 메소드는 외부에서 오브젝트 내부의 애트리뷰트 값을 변경하려는 용도로 주로 사용된다. 메소드는 항상 set으로 시작한다. 간단히 수정자라고 불리기도 한다. 수정자 메소드의 핵심기능은 파라미터로 전달된 값을 보통 내부의 인스턴스 변수에 저장하는 것이다. 부가적으로, 입력 값에 대한 검증이나 그 밖의 작업을 수행할 수도 있다. 수정자 메소드는 외부로부터 제공받은 오브젝트 래퍼런스를 저장해뒀다가 내부의 메소드에서 사용하게 하는 DI 방식에서 활용하기에 적당하다.

 

일반 메소드를 이용한 주입

여러 개의 파라미터를 갖는 일반 메소드를 DI용으로 사용할 수도 있다. 생성자가 수정자 메소드보다 나은 점은 한 번에 여러 개의 파라미터를 받을 수 있다는 점이다. 하지만 파라미터의 개수가 많아지고 비슷한 타입이 여러 개라면 실수하기 쉽다. 임의의 초기화 메소드를 이용하는 DI는 적절한 개수의 파라미터를 가진 여러 개의 초기화 메소드를 만들 수도 있기 때문에 한 번에 모든 필요한 파라미터를 다 받아야 하는 생성자보다 낫다.

 

스프링은 전통적으로 수정자 메소드를 가장 많이 사용해왔다. 뒤에서 보겠지만, DaoFactory 같은 자바코드 대신 XML을 사용하는 경우에는 자바빈 규약을 따르는 수정자 메소드가 가장 사용하기 편리하다. 수정자 메소드 DI를 사용할 때는 메소드의 이름을 잘 결정하는게 중요하다. 가능한한 의미 있고 단순한 이름을 사용하자.

 

UserDao도 수정자 메소드를 이용해 DI 하도록 만들어보자. 기존 생성자는 제거한다. 생성자를 대신한 setConnectionMaker()라는 메소드를 하나 추가하고 파라미터로 ConnectionMaker타입의 오브젝트를 받도록 선언한다.

UserDao를 수정자 메소드 DI 방식이 가능하도록 변경했으니 Di를 적용하는 DaoFactory의 코드도 함께 수정해줘야한다. 단지 의존관계를 주입하는 시점과 방법이 달라졌을 뿐 결과는 동일하다.

XML방식에서 의존성 주입 하는 방법

때로는 같은 인터페이스를 구현한 의존 오브젝트를 여러 개 정의해두고 그중에서 원하는 걸 골라서 DI 하는 경우도 있다. 이때는 각 빈의 이름을 독립적으로 만들어두고 ref 애트리뷰트를 이용해 DI 받을 빈을 지정해주면 된다.

DataSource 인터페이스로 변환

ConnectionMaker는 DB 커넥션을 생성해주는 기능 하나만을 정의한 매우 단순한 인터페이스다. IoC와 DI의 개념을 설명하기 위해 직접 이 인터페이스를 정의하고 사용했지만, 사실 자바에서는 DB 커넥션을 가져오는 오브젝트의 기능을 추상화해서 비슷한 용도로 사용할 수 있게 만들어진 DataSource라는 인터페이스가 이미 존재한다. 따라서 실전에서 인터페이스를 만들어서 사용할 일은 없을 것이다. 단, DataSource는 getConnection()이라는 DB커넥션을 가져오는 기능 외에도 여러 개의 메소드를 갖고 있어서 인터페이스를 직접 구현하기는 부담스럽다.

UserDao를 리팩토링 해보자. ConnectionMaker에서 DataSource로 변경한다. 그리고 DB 커넥션을 가져오는 코드를 makeConnection()에서 getConnection() 메소드로 바꿔준다.

다음은 DataSource 구현 클래스가 필요하다. 앞에서 만들었던 DriverManager를 사용하는 SimpleConnectionMaker처럼 단순한 DataSource 구현 클래스를 하나 가져다 사용하자.

 

자바코드 설정 방식으로 할 것인데, 먼저 DaoFactory 설정 방식을 이용해보자. 기존의 connectionMaker()를 dataSource()로 변경하고 SimpleDriverDataSource의 오브젝트를 리턴하게 한다. 이 오브젝트를 넘기기 전에 DB 연결과 관련된 정보를 수정자 메소드를 이용해 지정해줘야 한다.

이번에는 XML 설정 방식으로 변경하자

먼저 id가 connectionMaker인 <bean>을 없애고 dataSource라는 이름의 <bean>을 등록한다. 그리고 클래스를 변경해주면 아래와 같이 <bean>설정이 만들어진다.

 

 

 

반응형