1장 오브젝트와 의존관계(2)

개발 99·2025년 3월 24일

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

오브젝트의 동일성과 동등성

동일성은 ==, 동등성은 equals()이다.
두 오브젝트가 "동일"하다면, 사실 하나의 오브젝트만 존재하는 것이고,
두 개의 오브젝트 레퍼런스 변수를 갖고 있을 뿐이다.

두 개의 오브젝트가 동일하지는 않지만 동등한 경우에는
두 개의 각기 다른 오브젝트가 메모리상에 존재하는 것인데,
이는 정보가 같다는 것이지, 레퍼런스 변수는 다른다.

@Configuration
public class DaoFactory{
	@Bean public UserDao userDao(){
    	return new UserDao();
    }
}
ApplicationContext context = new
	AnnotationConfigApplicationContext(DaoFactory.class);
    
UserDao dao3 = context.getBean("userDao",UserDao.class);

UserDao dao4 = context.getBean("userDao",UserDao.class);

//System.out.println()을 할 경우, 동일한 결과값이 나온다.
//(동일하게 참조를 한다.)

스프링은 여러 번에 걸쳐 빈을 요청하더라도 매번 동일한 오브젝트를 돌려준다!

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

( 싱글톤을 저장하고 관리하는 싱글톤 레지스트리 )

스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 "싱글톤"으로 만든다.
( 구현방법은 다름 )

서버 애플리케이션과 싱글톤

매번 클라이언트 요청이 올 때마다 새로운 객체를 생성하는 것은 JVM의 메모리를 심하게 잡아먹는다.

서블릿 클래스당 하나의 오브젝트만 만들어두고, 사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용한다.

그런데 싱글톤이 항상 좋은 것은 아니다.(안티패턴)

( 참고로, 단일 오브젝트만 존재해야 하고, 이를 애플리케이션의 여러 곳에서 공유하는 경우에 주로 사용한다. )

싱글톤 패턴의 한계

public class UserDao{
	private static UserDao INSTANCE;
    
    private UserDao(ConnectionMaker connectionMaker){
    	this.connectionMaker = connectionMaker;
    }
    
    public static synchronized UserDao getInstance(){
    	if(INSTANCE == null) INSTANCE = new UserDao(???);
        return INSTANCE;
    }
}
// connectionMaker 어떻게 주입???

private로 바꿘 생성자는 외부에서 호출할 수가 없기 때문에 DaoFactory에서 UserDao를 생성하며 ConncetionMaker 오브젝트를 넣어주는 게 불가능해졌다.

  • private 생성자를 갖고 있기 때문에 상속할 수 없다.
    객체지향의 장점이 상속과 이를 이용한 다형성을 적용할 수 없다.

기술적인 서비스만 제공하는 경우라면 상관없지만,
애플리케이션 로직을 담고 있는 일반 오브젝트의 경우 객체지향 설계가 불가능하다.

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

초기화 과정에서 생성자 등을 통해 사용할 오브젝트를 다이내믹하게 주입하기가 힘들기 때문에
필요한 오브젝트는 직접 오브젝트를 만들어 사용할 수 밖에 없다.

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

클래스 로더를 어떻게 구성하고 있느냐에 따라 싱글톤 클래스임에도 하나 이상의 오브젝트가 만들어질 수 있다.

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

아무 객체난 자유롭게 접근하고 수정하고 공유할 수 있는 전역 상태는 객체지향 프로그래밍에서 권장되지 않는 모델이다.
(그럴바에 스태틱 필드와 메소드로만 구성된 클래스를 사용하는 편이 낫다.)

싱글톤 레지스트리( 이 부분을 적용해서 리팩토링에 적용하자 )

스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리한다.
그것이 바로 "싱글톤 레지스트리"이다.

스프링 컨테이너는 싱글톤을 생성,관리,공급하는 싱글톤 관리 컨테이너다.

장점은 스태틱 메소드와 private 생성자를 사용해야 하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 싱글톤으로 활용하게 해준다.
(평범한 자바 클래스라도 IoC방식의 컨테이너를 사용해서 제어권을 넘기면 손쉽게 싱글톤 방식으로 만들어져 관리할 수 있다.)

오브젝트 생성에 관한 모든 권한은 IoC 기능을 제공하는 applicationContext에 있으므로 가능하다.

또한 테스트 환경에서 자유롭게 오브젝트를 만들 수 있고,
테스트를 위한 Mock 오브젝트로 대체하는 것도 가능하다.

생성자 파라미터를 이용해서 사용할 오브젝트를 넣어주게 할 수 있다.

가장 중요한 점은 싱글톤 패턴과 달리 스프링이 지지하는 객체지향적인 설계 방식과 원칙, 디자인 패턴 등을 적용하는 데 아무런 제약이 없다!

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

기본적으로 싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는
상태정보를 갖고 있지 않은 무상태(stateless)방식으로 만들어져야 한다.
(다중 사용자의 요청을 한꺼번에 처리하는 스레드들이 동시에 싱글톤 오브젝트의 인스턴스 변수를 수정하는 것은 매우 위험하다.)

따라서 싱글톤은 인스턴스 필드의 값을 변경하고 유지하는 상태유지 방식으로 만들지 않는다.

stateless한 클래스를 만드는 경우 각 요청에 대한 정보 ,DB ,리소스로부터 생성한 정보는 어떻게 관리해야하나?

메소드 안에서 생성되는 로컬 변수는 매번 새로운 값을 저장할 독립적인 공간이 만들어지기 때문에 싱글톤이라고 해도 여러 스레드가 변수의 값을 덮어쓸 일은 없다.

public class UserDao{

	// 이 변수는 읽기전용이므로, 상관이 없다.
    // static final이나 final 권장.
	private ConncetionMaker connectionMaker;
    
    // 하위 변수들은 매번 바뀌므로, 1) 메소드의 로컬 변수로 쓰거나,
    // 2) 파라미터로 주고받아야 한다.
    private Conncetion c;
    private User user;
    
    public User get(String id) throws ...{
    	this.c = connectionMaker.makeConncetion();
        this.user = new User();
        ...
        return this.user;
    }
    
}

싱글톤 빈을 만들 때, 개별적으로 바뀌늰 정보는 로컬 변수로 정의하거나
파라미터로 주고받으면서 사용하게 해야한다.

1.6.3 스프링 빈의 스코프

스프링이 관리하는 오브젝트, 즉 빈이 생성되고, 존재하고, 적용되는 범위를
"빈의 스코프"라고 한다.
( 기본 스크로는 싱글톤임. )

싱글톤 스코프는 컨테이너 내에 1개의 오브젝트만 만들어져서,
강제로 제거하지 않는 한 스프링 컨테이너가 존재하는 동안 계속 유지된다.

싱글톤 외에도 프로토타입 스코프가 있다.
이는 컨테이너 빈을 요청할 때마다 매번 새로운 오브젝트를 만들어준다.

HTTP 요청이 생길 때마다 생성되는 요청 스코프도 있고,
웹의 세션과 스코프가 유사한 세션 스코프도 있다.
( 10장 참고. )

1.7 의존관계 주입(DI)

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

스프링 Ioc 컨테이너는 객체를 생성하고 관계를 맺어주는 작업을 담당한다.

스프링 IoC의 대표적인 동작원리는 주로 DI(의존관계 주입)이라고 불린다.

의존관계 주입, 의존성 주입, 의존 오브젝트 주입?

DI는 오브젝트 레퍼런스를 외부로부터 제공(주입)받고
이를 통해 여타 오브젝트와 다이내믹하게 의존관계가 만들어지는 것이 핵심이다.

1.7.2 런타임 의존관계 설정

의존관계

두 클래스가 "의존관계"이면 한 클래스는 "무조건 방향성"을 가진다.

의존하고 있다는 뜻은 타이틀,본문,댓글이 바뀌면 게시글이 바뀌는 것을 의미한다.
(한 클래스는 다른 클래스에 종속되어 있다.)

UserDao의 의존관계

인터페이스에 대해서만 의존관계를 만들어두면
인터페이스 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는 상태가 된다
(결합도가 낮다.)


아무리 DConncetionMaker를 바꾸더라도 UserDao는 영향을 받지 않는다.
(어차피 구체적인 클래스는 알 필요 없고, 인터페이스로 가져다가 쓰면 그만이다.)

그러나 런타임 의존관계도 존재한다.

(실제 사용대상인 오브젝트를 의존 오브젝트라고 한다.)

정리해서 의존관계 주입이란 다음과 같은 세 가지 조건을 충족한다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다.
    그러기 위해서는 인터페이스에만 의존하고 있어야 한다.

  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.

  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 주입해줌으로써 만들어진다.

의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 제 3의 존재가 있다.

ApplicationContext, BeanFactory, IoC container 등 모두 외부에서 오브젝트 사이의 런타임 관계를 맺어주는 제 3의 존재이다.

UserDao의 의존관계 주입

의존관계가 느슨할지라도 UserDao가 사용할 구체적인 클래스를 알고 있어야 한다.

public UserDao(){
	connectionMaker = new DConnectionMaker();
}

위 코드는 이미 런타임 시의 의존관계가 코드 속에 다 미리 결정되어 있다는 점이다.

그래서 Ioc 방식을 써서, 런타임 의존관계를 드러내는 코드를 제거하고
제 3의 존재에 런타임 의존관계 결정 권한을 위임한다.

그래서 아래와 같이 수정을 한다.

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

UserDao는 DConnectionMaker 오브젝트의 레퍼런스를 생성자 파라미터를 통해서
주입을 받는다.

DI 컨테이너는 자신이 결정한 의존관계를 맺어줄 클래스의 오브젝트를 만들고 이 생성장의 파라미터로 오브젝트의 레퍼런스를 전달해준다.

1.7.3 의존관계 검색과 주입

의존관계 검색이라는 방법도 있음
( 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾는다. )

런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과
오브젝트의 생성 작업은 외부 컨테이너에게 IoC로 맡기지만,
이를 가져올 때는 메소드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.

이 방법은 스스로 IoC 컨테이너에 요청을 하는 것이다.

public UserDao(){
	AnnotationConfigApplicationContext context = 
    	new AnnotationConfigApplicationContext(DaoFactory.class);
    this.connectionMaker = context.getBean("connectionMaker",ConnectionMaker.class);
}

의존 관계 주입의 거의 모든 장점을 가지고 있으나, 방법만 조금 다른다.

결론적으로 그냥 의존관계 주입 방식을 사용하는 편이 낫다.

그런데 의존관계 검색방식을 사용해야 할 때가 있다.

의존관계 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다는 점이다.

구체적으로
UserDao는 굳이 스프링이 만들고 관리하는 빈일 필요가 없고,
그냥 new UserDao()해서 만들어서 사용해도 된다.
이때는 ConnectionMaker만 스프링의 빈이기만 하면 된다.

반면에 의존관계 주입에서는 UserDao와 ConnectionMaker 사이에는 DI가 적용되려면 반드시 UserDao도 빈 오브젝트여야 한다.

DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈이 돼어야 한다.

DI 받는다.

DI에서 말하는 주입은 다이내믹하게 구현 클래스를 결정해서 제공받을 수 있도록 인터페이스 타입의 파라미터를 통해 이뤄줘야 한다.
(특정 클래스 타입으로 고정되어 있다면 DI가 일어날 수 없다.)

1.7.4 의존관계 주입의 응용

런타임 클래스에 대한 의존관계가 나타나지 않고,
인터페이스를 통해 결합도가 낮은 코드를 만들므로
다른 책임을 가진 사용 의존관계에 있는 대상이 바뀌거나 변경되더라도 자신은 영향을 받지 않으며,
변경을 통한 다양한 확장 방법에 자유롭다.

UserDao가 ConnectionMaker라는 인터페이스에만 의존하고 있다는 것은
ConnectionMaker를 구현하기만 하고 있다면 어떤 오브젝트든지 사용할 수 있음을 의미한다.

DI는 스프링의 핵심이다.

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

지금까지는 UserDao의 의존관계 주입을 위해 생성자를 사용했다.
그런데 반드시 꼭 생성자가 있어야 하는 것은 아니다.
일반메소드로도 가능하다.

  • 수정자(setter) 메소드를 이용한 주입
    파라미터로 전달된 값을 보통 내부의 인스턴스 변수에 저장하는 것이다.

  • 일반 메소드를 이용한 주입
    수정자 메소드처럼 set으로 시작해야 하고 한 번에 한 개의 파라미터만 가질 수 있다는 제약이 싫다면
    여러 개의 파라미터를 갖는 일반 메소드를 DI용으로 사용할 수 있다.

아래는 수정자 메소드 DI를 적용한 UserDao이다.

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

1.8 XML을 이용한 설정

XML은 단순한 텍스트 파일이고, 컴파일과 같은 별도의 빌드 작업이 없다.
환경이 달라져서 오브젝트의 관계가 바뀌는 경우에도 변경사항을 반영할 수 있다.

1.8.1 XML 설정

applicationContext는 XML에 담긴 DI 정보를 활용할 수 있다.

DI정보가 담긴 XML 파일은 beans 를 루트 엘리먼트로 사용한다.


  <beans>
    
    <bean>...</bean>
    
    <bean>...</bean>
    
    ...
  </beans>
  

이는 @Configuration -> , @Bean -> 으로 치환하는 것과 동일하다.

하나의 @Bean 메소드를 통해 얻을 수 있는 빈의 DI정보는 다음 3가지이다.

  • 빈의 이름 : @Bean 메소드 이름 = 빈의 이름. getBean()에서 사용
  • 빈의 클래스 : 빈 오브젝트를 어떤 클래스를 이용해서 만들지를 정의한다.
  • 빈의 의존 오브젝트 : 빈의 생성자나 수정자 메소드를 통해 의존 오브젝트를 넣어준다.
    의존 오브젝트도 하나의 빈이므로 이름이 있고, 그 이름에 해당하는 메소드를 호출해서 의존 오브젝트를 가져온다.
    (의존 오브젝트는 하나 이상일 수도 있다.)

connectionMaker() 전환

@Bean //---><bean
public ConncetionMaker{
  connectionMaker(){ //---> id="connectionMaker"
  	return new DConnectionMaker(); //--->class="springboot...DConnectionMaker"/>
  }

DI 컨테이너는 이 태그의 정보를 읽어서 connectionMaker()와 같은 작업을 진행한다.

userDao() 전환

수정자 메소드를 선호하는 이유 중에는 XML로 의존관계 정보를 만들 때 편리하다는 점이 있다.

자바빈의 관례에 따라서 수정자 메소드(setter)는 프로퍼티가 된다.
프로퍼티 이름은 메소드 이름에서 set을 제외한 나머지 부분을 사용한다.

seConnectionMaker() -> conncetionMaker라는 프로퍼티를 갖는다고 할 수 있다.

태그를 사용해 의존 오브젝트와의 관계를 정의한다.

property는 2개의 attribute를 갖는다.

  • name
    프로퍼티의 이름( 수정자 메소드를 알 수 있음 )
  • ref
    수정자 메소드를 통해 주입해줄 오브젝트의 빈 이름
userDao.setConnectionMaker(connectionMaker());
<property name="connectionMaker" ref="connectionMaker"/>
// 수정자를 이용해서 connectionMaker를 UserDao에 주입하겠다.
<bean id="userDao" class="springboot.dao.UserDao">
  <property name="connectionMaker" ref="connectionMaker"/>
</bean>
@Bean 
public UserDao userDao{
  connectionMaker(){ 
  UserDao userDao = 
  userDao.setConnectionMaker(connectionMaker()); 
  
  return userDao;
}

XML의 의존관계 주입 정보

  <beans>
    <bean id="connectionMaker" class="springboot.user.dao.DConnectionMaker"/>
    <bean id ="userDao" class="springboot.user.dao.UserDao">
      <property name ="connectionMaker" ref="connectionMaker"/>
    </bean>
  </beans>
  @Configuration
  public class DaoFactory{
  
  @Bean //---><bean
  public ConncetionMaker{
    connectionMaker(){ //---> id="connectionMaker"
      return new DConnectionMaker(); //--->class="springboot...DConnectionMaker"/>
  }
  
  @Bean 
  public UserDao userDao connectionMaker(){// id=userDao 
    UserDao userDao = 
    userDao.setConnectionMaker(connectionMaker()); 
	// property name="connectionMaker"
    return userDao;
  }
  
}

의 name은 수정자 메소드의 프로퍼티 이름이고, ref는 주입할 오브젝트를 정의한 빈의 ID다.

보통 프로퍼티 이름과 DI되는 빈의 이름이 같은 경우가 많다.
(보통 name도 인터페이스를 따르고, ref도 인터페이스를 따른다.)

때로는 같은 인터페이스를 구현한 의존 오브젝트를 여러개 정의해두고 그중에서 원하는 걸 골라서 DI하는 경우도 있다.

<beans>
  // id=메소드명, class=리턴타입
  <bean id="localDB..." class="...LocalDB..."/>
  <bean id="testDB..." class="...TestDB..."/>
  <bean id="productionDB..." class="...ProductionDB..."/>
  
  // setter = connectionMaker이고, 주입 객체는 localDB ...
  <bean id="userDao" class="...UserDao">
    <property name="connectionMaker" ref="localDBConncetionMaker"/>
  </bean>
</beans>

1.8.2 XML을 이용하는 applicationContext

GenericXmlApplicationContext로 XML에서 빈의 의존 관계 정보를 이용해서 IoC/DI 작업을 수행한다.

1.8.3 DataSource 인터페이스로 변환

DataSource 인터페이스 적용

DB 커넥션을 가져오는 오브젝트의 기능을 추상화해서 비슷하게 쓸 수 있다.

핵심은 getConnection 메소드 하나뿐이다.
이를 통해서 DB 커넥션을 가져오면 된다.

1.8.4 프로퍼티 값의 주입

  @Bean
  public DataSource dataSource(){
  	SimpleDriverDataSource dataSource = new Simple...();
  
  dataSource.setDriverClass(...);
  dataSource.setUrl(...);
  dataSource.setUsername(...);
  dataSource.setPassword(...);
  
  return dataSource;
  }
<bean id="dataSource" class="...">
  <property name ="driverClass" value="..."/>
  <property name ="url" value=".."/>
  ...
  <property name ="password" value="..."/>
</bean>

1.9 정리

  • 책임이 다른 코드를 분리해서 두 개의 클래스를 만든다(관심사의 분리, 리팩토링)
  • 바뀔 수 있는 쪽의 클래스는 인터페이스를 구현하고, 클래스에서 인터페이스를 통해서만 접근하도록 한다.
    (이렇게 함으로써 인터페이스를 정의한 쪽의 구현 방법이 달라져 클래스가 바뀌더라도, 그 기능을 사용하는 클래스의 코드는 같이 수정할 필요가 없다, 전략 패턴)
  • 이를 통해 자신의 책임 자체가 변경되는 경우 외에는 불필요한 변화가 발생하지 않도록 막아주고,
    자신이 사용하는 외부 오브젝트의 기능은 자유롭게 확장하거나 변경할 수 있다.(개방 폐쇄 원칙)
  • 결국 한쪽의 기능 변화가 다른 쪽의 변경을 요구하지 않아도 되며(낮은 결합도),
    자신의 책임과 관심사에만 순수하게 집중하면 된다(높은 응집도)
  • 오브젝트 생성 및 관계를 맺는 작업의 제어권을 별도의 오브젝트 팩토리로 넘겼다.
    IoC 컨테이너가 오브젝트가 자신이 사용할 대상의 생성이나 선택에 관한 책임으로부터 자유롭게 해줬다.(제어의 역전/IoC)
  • 싱글톤 패턴의 단점을 극복하는 "싱글톤 레지스트리"가 있다.
  • 클래스와 인터페이스간 느슨한 의존관계만 만들어놓고,
    런타임 시, DI 컨테이너에 의해서 오브젝트를 주입받는다.
  • 의존 오브젝트는 생성자/수정자 메소드를 이용하는 방법이 있다.
  • XML을 통해서 DI가 가능하다.
profile
구구구구구!

0개의 댓글