토비의 스프링 1장 정리

8 minute read

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

두개의 오브젝트가 완전히 동일한 것: 동일성 (== 연산자로 확인)

동일한 정보를 담고 있는 것: 동등성 (equals 메소드로 확인)

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

2개의 오브젝트가 동일하지는 않지만, 동등한 경우 두 개의 각기 다른 오브젝트가 메모리상에 존재하는 것이다.

즉 동일 → 동등(참) / 동등 → 동일 (거짓)이다.

알아보고 싶은 것은 DaoFactory를 여러 번 호출했을 때 동일한 오브젝트로 반환되는가? 코드에 답이 있다.

DaoFactory factory = new DaoFactory();
UserDao1 dao1 = factory.userDao();
UserDao1 dao2 = factory.userDao();

System.out.println(dao1);
System.out.println(dao2);

---
  
springbook.dao.userDao@118f375
springbook.dao.userDao@117a7bd

출력값을 보면 이들은 동등하다. 즉, 동일하지 않다. 따라서 UserDao를 사용하면 새로운 오브젝트가 만들어질 것이다.

이번에는 스프링의 애플리케이션 켄텍스트에 DaoFactory를 설정 정보로 등록하고 getBean() 메소드를 이용해 userDao라는 이름으로 등록된 오브젝트를 가져와 보자. 여기서도 애플리케이션 켄텍스트가 DaoFactory의 userDao()메소드를 호출해서 UserDao 타입 오브젝트를 만드는 건 동일하다. 하지만 다음 코드를 실행해보면 다른 결과가 나온다.

ApplicationContext context = new
  AnnotationConfigApplicationContext(DaoFactory.class);

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

System.out.println(dao3);
System.out.println(dao4);

---
  
springbook.dao.userDao@ee22f7
springbook.dao.userDao@ee22f7

동일한 오브젝트로 기록된 것을 확인할 수 있다. 즉 getBean() 메소드로 두 번 호출해서 가져온 오브젝트가 동일하다. 확실히 하려면 dao3 == dao4를 확인하면 된다. true라고 나올 것이다. 다르게 말하면 getBean()을 실행할 때마다 userDao()메소드를 호출하고 매번 new에 의해 새로운 UserDao가 만들어지지 않는다는 뜻이다. 왜 그럴까?

ApplicationContext는 우리가 만들었던 Factory와 비슷한 방식으로 동작하는 IOC 컨테이너이다. 그러면서 동시에 애플리케이션 켄텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리(singleton refistry)이기도 하다. 스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 Bean 오브젝트를 모두 싱글톤으로 만든다. 여기서의 싱글톤은 디자인 패턴에서의 싱글톤과 비슷하지만 구형 방법은 확연히 다르다.


IoC(Inversion of Control)의 컨테이너

스프링 프레임워크도 객체에 대한 생성 및 생명주기를 관리할 수 있는 기능을 제공하고 있음. 즉, IoC 컨테이너 기능을 제공한다.

  • IoC 컨테이너는 객체의 생성을 책임지고, 의존성을 관리한다.
  • POJO의 생성, 초기화, 서비스, 소멸에 대한 권한을 가진다.
  • 개발자들이 직접 POJO를 생성할 수 있지만 컨테이너에게 맡긴다.

  • 싱글톤 패턴의 원리: 애플리케이션 안에 제한된 수, 대개 한 개의 오브젝트만 만들어서 사용하는 것
  • 싱글톤 패턴:
    • 어떤 클래스를 애플리켕션 내에서 제한된 인스턴스 개수, 이름처럼 주로 하나만 존재하도록 강제하는 패턴이다.(디자인 패턴 중 가장 자중 활용되는 패턴이기도 함) 이렇게 하나만 만들어지는 클래스의 오브젝트는 애플리케이션 내에서 전역적으로 접근이 가능하다.
    • 오로지 단일 오브젝트만 존재해야 하고, 이를 애플리케이션의 여러 곳에서 공유하는 경우에 주로 사용한다.

하지만, 구현이 어렵고 여러 문제점이 있는데, 이러한 싱글톤 패턴을 피해야 할 패턴이라는 의미로 안티패턴으로 불리기도 함.

싱글톤 패턴의 한계

싱글톤 구현은 다음과 같다.

  • 클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private으로 만든다.
  • 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의한다.
  • 스태칙 팩토리 메소드인 getInstance()를 만들고 이 메소드가 최초로 호출되는 시점에서 한 번만 오브젝트가 만들어지게 한다. 생성된 오브젝트는 스태틱 필드에 저장된다. 또는 스태틱 필드의 초기값으로 오브젝트를 미리 만들어둘 수도 있다.
  • 한번 오브젝트(싱글톤)가 만들어지고 난 후에는 getInstance() 메소드를 통해 이미 만들어져 스태틱 필드에 저장해둔 오브젝트를 넘겨준다.

UserDao를 싱글톤 패턴을 이용하여 만들면 아래와 같다.

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;
  }
  ...
}

private으로 만든 생성자는 외부에서 호출하지 못하기 때문에 DaoFactory에서 UserDao를 생성하며 ConnectionMaker 오브젝트를 넣어주는 게 이제는 불가능하다. 일반적으로 싱글톤 패턴 구현 방식엔느 다음과 같은 문제가 있다.

  1. private 생성자를 갖고 있기 때문에 상속할 수 없다.
  2. 싱글톤은 테스트하기가 힘들다.
  3. 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
  4. 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.

자바의 기본적인 싱글톤 패턴의 구현 방식은 여러 가지 단점이 있기 때문에, spring에서는 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. (싱글톤 레지스트리: singleton registry) 스프링 컨테이너는 싱글톤을 생성하고, 관리하고, 공급하는 싱글톤 관리 컨테이너이기도 하다. 즉 비정상적 클래스(static 메소드 또는 private 생성자를 사용해야 하는)가 아니라 평범한 자바 클래스를 싱글톤으로 활용하게 해준다는 점이다.

따라서 싱글톤 방식의 애플리케이션 클래스라도 public 생성자를 가질 수 있다.

앞에서 코드를 통해 확인했듯이 이미 UserDao는 스프링 IoC를 적용하면서 싱글톤으로 만들어진다. 그래서 getBean()을 여러 번 호출해서 UserDao를 요청하더라도 매번 동일한 오브젝트를 받게 된다.

싱글톤 오브젝트의 상태는 동일한 메모리에서 동일한 오브젝트를 갖고오기 때문에 데이터 중복이라는 문제가 발생할 수 있어 기본적으로 인스턴스 필드의 값을 변경하고 유지하는 상태유지 방식으로 만들지는 않는다. 그렇다면 상태가 없는 방식으로 클래스를 만드는 경우에 정보나 DB나 서버의 리소스로 부터 생성한 정보는 어떻게 다룰까? 이때는 parameter, local variable, return value를 이용하면 된다. 메소드 파라미터나 메소드 안에서 생서외는 로컬 변수는 매번 새로운 값을 저장할 독립적인 공간이 만들어지기 때문에 싱글톤이라도 여러 스레드가 변수의 값을 덮어쓸 일은 없다.


스레드(thread)는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다. 일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다.


public class UserDao {
  private ConnectionMaker connectionMaker; //  초기에 설정하면 사용 중에는 바뀌지 않는 읽기전용 인스턴스 변수
  // 매번 새로운 값으로 바뀌는 정보를 담은 인스턴스 변수 → 심각한 문제가 발생
  private Connection c;
  private User user;
  //
  
  public User get(String id) throws ClassNotFoundException, SQLException {
    this.c = connectionMaker.makeConnection();
    ...
    this.user = new User();
    this.user.setId(rs.getString("id"));
    this.user.setName(rs.getString("name"));
    this.user.setPassword(rs.getString("password"));
    ...
    return this.user;
  }
}

기존과 다른 점은 UserDao를 로컬변수로 선언하고 사용했던 connection과 User를 클래스의 인스턴스 필드로 선언했다는 것이다. 따라서 싱글톤으로 만들어져서 멀티스레드 환경에서 사용하면 위에서 설명한 대로 심각한 문제가 발생한다. 따라서 스프링의 싱글톤 Bean으로 사용되는 클래스를 만들 때는 기존의 UserDao처럼 처음 개별적으로 바뀌는 정보는 로컬 변수로 정의하거나, 파라미터로 주고 받으면서 사용하게 해야 한다.

그런데 기존의 UserDao에도 인스턴스 변수로 정의해서 사용한 것이 있는데, 바로 ConnectionMaker의 connectionMaker이다. 이건 인스턴스 변수로 사용해도 상관없는데, 이유는 읽기전용의 정보이기 때문이다.(읽기전용은 INSTANCE로 사용해도 상관없다. ConnectionMaker 타입의 싱글톤 오브젝트가 들어 있다.) 만약에 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도라면 인스턴스 변수를 사용해도 좋다. 즉 스프링이 한 번 초기화해주고 나면 이후에는 수정되지 않기 때문에 멀티스레드 환경에서 사용해도 아무런 문제가 없다.물론 동일하게 읽기전용의 속성을 가진 정보라면 싱글톤에서 인스턴스 변수로 사용해도 좋다. 그러나 단순한 읽기 전용이라면 static final, final로 선언하는 편이 나을 것이다.

스프링 빈의 스코프 (여러 가지 스코프가 존재하지만, 기본적인 것은 싱글톤 스코프, 이외에 프로토타입, 웹, 세션 등이 있음)

빈의 생성, 존재, 적용되는 범위에 대해서 알아보자. 이를 빈의 스코프라고 한다.

스프링 빈의 기본 스코프는 싱글톤이다.

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

하지만 경우에 따라서 싱글톤 외의 스코프를 갖을 수 있다. 대표적으로 프로토타입 스코프가 있다.

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

의존관계 주입(DI)

제어의 역전과 의존관계 주입

IoC는 소프트웨어 나타나는 가장 일반적인 개념이다.(객체지향, 디자인 패턴, 컨테이너에서 동작하는 서버 기술을 사용한다면, 자연스럽게 IoC를 적용하거나 그 원리로 동작하는 기술을 사용) DaoFactory처럼 객체를 생성하고 관계를 맺어주는 등의 작업을 담당하는 기능을 일반화 한 것이 스프링의 IoC 컨테이너이다. 여기서 IoC의 정확한 개념을 알아보자.

스프링 IoC 기능이 대표적인 동작원리는 주로 의존 관계 주입이라고 불린다. 초기에는 주로 IoC 컨테이너, 현재는 의존관계 주입 컨테이너 또는 DI 컨테이너라고 불린다. 사실 오브젝트는 다른 오브젝트에 주입될 수 있는게 아니라, reference가 전달될 뿐이다.

즉 DI는 오브젝트 Reference를 외부로부터 제공받고 이를 통해 여타 오브젝트와 다이내믹하게 의존관계가 만들어지는 것이 핵심이다.

그렇다보니 의존관계 설정이라는 용어로 사용해도 괜찮다.

의존은 무엇일까? B가 바뀌면 A에 영향을 미친다는 뜻이다. 의존관계는 방향성이 있다. A가 B에 의존하고 있으면 반대는 성립하지 않는다. 의존하지 않는다.는 말은 B는 A의 변화에 영향을 받지 않는다는 이야기이다.

또한 프로그램이 시작되고 UserDao 오브젝트가 만들어지고 나서 런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트(dependent object)라고 말한다. 중요한 것은 의존관계 주입이란 다음과 같이 3가지 조건을 충족하는 작업을 말한다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
  • 의존관계는 사용할 오브벡트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.
// 의존관계 주입을 위한 코드
public class UserDao {
  private ConnectionMaker connectionMaker;
  
  public UserDao(ConnectionMaker connectionMaker) {
    this.connectionMaker = connectionMaker
  }
  ...
}

이런식으로 코드를 작성하면 두 개의 오브젝트 UserDao, ConnectionMaker 간에 의존관계가 만들어진다. DI 컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 Reference를 전달받는 과정이 마치 method(생성자)를 통해 DI 컨테이너가 UserDao에게 주입해주는 것과 같다고 해서 이를 의존관계 주입이라고 부른다. DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점엥서 IoC와 개념이 잘 들어맞는다. 스프링 컨테이너의 IoC는 주로 의존관계 주입 또는 DI라는 데 초점이 맞춰져 있다. 그래서 스프링을 IoC 컨테이너 외에도 DI 컨테이너 또는 DI 프레임워크라고 부르는 것이다.

의존관계 검색과 주입

의존관계 검색은 자신이 필요로 하는 의존 오브젝트를 능적으로 찾는다. 물론 자신이 어떤 클래스의 오브젝트를 이용할지 결정하지 않는다(만약에 그렇다면 IoC 컨테이너라고 할 수 없을 것).

// DaoDactory를 이용하는 생성자
public UserDao() {
  DaoFactory daoFactory = new DaoFactory();
  this.connectionMaker = daoFactory.connectionMaker();
}

위 코드를 보면, UserDao는 여전히 자신이 어떤 connectionMaker 오브젝트를 사용할지 미리 알지 못한다. 여전히 코드의 의존대상은 ConnectionMaker 인터페이스뿐이다. 런타임 시에 DaoFactory가 만들어서 돌려주는 오브젝트와 다이내믹하게 런타임 의존관계를 맺는다. 따라서 IoC 개념을 잘 따르고 있으며, 그 혜택을 받고 있는 코드다.

하지만 적용방법은 외부로부터의 주입이 아니라 스스포 IoC 컨테이너인 DaoFactory에서 요청하는 것이다. 이때 요청은 미리 정해놓은 이름을 전달하고 그 이름에 해당하는 오브젝트를 찾게 된다. 따라서 일종의 검색으로 볼 수 있다. 또한 그 대상이 런타임 의존관계를 가질 오브젝트이므로 의존관계 검색이라고 부르는 것이다.

스프링의 IoC 컨테이너의 애플리케이션 컨텍스트는 getBean()이라는 메소드를 제공한다. 바로 이 메소드가 의존관계 검색에 사용되는 것이다. UserDao는 아래와 같이 애플리케이션 컨텍스트를 사용해서 의존관계 검색 방식으로 ConnectionMaker 오브젝트를 가져오게 만들 수도 있다.

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

의존관계 검색 방법은 코드 안에 오브젝트 팩토리 클래스나 스프링 API가 나타난다. 애플리케이션 컴포넌트가 컨테이너와 같이 성격이 다른 오브젝트에 의존하게 되는 것이므로 그다시 바람직하지는 않다.(훨씬 복잡해지기 떄문에) 따라서 대개는 의존관계 주입 방식을 사용하는 편이 낫다. 하지만 의존관계 검색을 사용해야할 때가 있다. 의존관계 검색 방식인 getBean()을 사용했을 경우 스프링의 IoC와 DI 컨테이너를 적용했다고 하더라도 애플리케이션의 기동시점에서 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 한다. 스태틱 메소드인 main()에서는 DI를 이용해서 오브젝트를 주입받을 방법이 없기 때문이다. 서버에서도 마찬가지이다. 서버에는 main()과 같은 기동 메소드는 없지만 사용자의 요청을 받을 때마다 main() 메소드와 비슷한 역할을 하는 서블릿에서 스프링 컨테이너에 담긴 오브젝트를 사용하려면 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 한다. 다행히 이런 서블릿은 스프링이 미리 만들어서 제공하기 때문에 직접 구현할 필요는 없다.

또한 의존관계 검색과 주입을 적용할 때 발견할 수 있는 중요한 차이점이 하나 있는데, 의존관계 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다는 것이다. UserDao에 스프링의 getBean()을 사용한 의존관계 검색 방법을 적용했다고 하더라도 UserDao는 굳이 스프링이 만들고 관린는 빈이 필요는 없다. 단지 어딘가에서 직접 new UserDao()해서 만들어서 사용해도 된다. 이때는 ConnectionMaker만 스프링의 빈이기만 하면 된다.

하지만 의존관계 주입에서는 UserDao와 ConnectionMaker 사이에 DI가 적용되려면 UserDao도 반드시 컨테이너가 만드는 빈 오브젝트여야 한다. 컨테이너가 UserDao에 ConnectionMaker 오브젝트를 주입해주려면 UserDao에 대한 생성과 초기화 권한을 갖고 있어야 하고, 그러려면 UserDao는 IoC 방식으로 컨테이너에서 생성되는 오브젝트, 즉 Bean이어야 하기 때문이다. 이런 점에서 DI와 DL(의존관계 검색)은 적용 방법에 차이가 있다.

DI(의존성 주입)를 원하는 오브젝트는 먼저 자기 사진이 컨테이너가 관리하는 빈이 돼야 한다는 사실을 잊지말자.

DI를 받는다는 의미

사실 DI를 받는다는 건은 이름 그대로 외부로부터의 주입이다. 하지만 단지 외부에서 파라미터로 오브젝트를 넘겨줬다고 해서, 즉! 주입해줬다고 해서 다 DI는 아니다(???) 주입받는 메소드 파라미터가 `이미 특정 클래스 타입으로 고정`되어 있다면 DI가 일어날 수 없다. DI에서 말하는 주입은 다이내믹하게 구현 클래스를 결정해서 제공받을 수 있도록 인터페이스 타입의 파라미터를 통해 이뤄져야 한다.

그래서 DI 원리를 지켜며 외부에서 오브젝트를 제공받는 방법을 단순히 "주입받는다"라고 하는 대신 "DI 받는다"라고 표현하기도 한다. 좀 어색한 표현일 수도 있지만 단순한 오브젝트 주입이 아니라 DI 개념을 따르는 주입임을 강조하는 것으로 생각하면 된다.