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

susu·2022년 10월 10일
0
post-thumbnail

<토비의 스프링 3.1 vol.1>을 읽고 공부한 내용을 개인적으로 정리한 글입니다.

💡 theme

관심사 분리 OoP IoC DI

📌 JDBC

Java DataBase Connectivity의 약자.
자바 언어로 데이터베이스 프로그래밍을 하기 위한 라이브러리.
JDK에서 DBMS에 종속되지 않는 JDBC API를 제공하며,
각 DBMS 회사에서 제공하는 JDBC 드라이버를 통해 DBMS와 연동하게 된다.

연동 과정 (간단하게만)

자바 상에서 Package 로드

→ JDBC 드라이버 on
→ Connection, Statement 객체 생성
→ Query 수행
→ 쿼링의 결과로 반환된 Result 객체에서 데이터 추출
→ Result, Statemt, Connection 순으로 객체 close

📌 DAO

Data Access Object의 약자. 말 그대로 데이터에 접근하는 객체이다.
DB 계층과 서비스 계층을 이어주는 역할을 한다.

일반적인 Spring 웹 계층은 다음과 같은 구조로 이루어져 있다.
그렇다면, 서비스 계층에서는 왜 직접 레포지토리 계층에 접근하지 않을까?
지금부터 그 이유에 대해 알아보자.

📌 DAO의 분리와 확장

중심 키워드는 변화다.
변화가 일어날 때 필요한 작업을 최소화하고,
그 변경이 다른 곳에 문제를 일으키지 않게 하기 위해서는 분리확장을 고려한 설계가 필요하다.
회원의 정보를 관리하는 UserDao라는 클래스가 있다고 가정하고 하나씩 짚어보자.

❓ 관심사 분리

관심이 같은 것끼리는 하나의 객체 안으로, 또는 친한 객체로 모이게 하고,
관심이 다른 것은 따로 떨어뜨려서 서로 영향을 주지 않도록.
→ 같은 관심에 효과적으로 집중할 수 있게.

❓ 메소드 추출

UserDao에서 DB와의 연결을 위한 Connection 객체를 가져오는 코드는 다른 코드에서도 중복될 것이다.
이 커넥션을 가져오는 중복된 코드를 분리해서 독립적인 메소드 getConnection()으로 만들어준다.
관심 내용이 독립적이라는 점에서 수정이 매우 간단해진다.
공통의 기능을 담당하는 중복된 코드를 뽑아내는 것을 리팩토링에서는 메소드 추출 기법이라고 부른다.

💡 리팩토링

기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술.

  • 코드 내부의 설계가 개선되어 코드를 이해하기가 편해지고 (= 가독성 개선)
  • 변화에 효율적으로 대응할 수 있음 (= 유지보수 용이성)
  • 생산성 증대
  • 코드의 품질이 높아짐

📖 추천 서적 : <리팩토링> (마틴 파울러, 켄트 벡 공저)


이렇게 커넥션 메소드를 분리했지만, 연결하고 싶은 DB의 종류는 하나가 아닐 수도 있다.
하나의 코드로 오라클과 MySQL을 모두 대응하고 싶다.
그러기 위해선, 여기서 한 단계 더 분리가 이루어져야 한다.

❓ 상속을 통한 확장

위에서 제시한 상황을 해결하기 위해

  • 앞서 만든 getConnection() 메소드의 구현 코드를 제거하고
  • getConnection을 추상 메소드(abstract형)로 변환한다.
  • 메소드의 세부 구현은 서브클래스에게 넘긴다.

다음과 같은 리팩토링 과정을 거치고 나면 DB의 종류에 따라 서브클래스에서 슈퍼클래스를 상속해 확장해주기만 하면 된다.
이러한 기법들은 디자인 패턴의 일부이다.
디자인 패턴에 대해 좀 더 자세히 알아보자.

💡 디자인 패턴

주로 객체지향적인 설계 과정에서 사용되는 개념으로,
소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는,
재사용 가능한 솔루션을 의미한다.
이미 잘 알려진 패턴들이 있어서, 패턴의 이름을 언급하는 것만으로도 설계의 의도와 해결책을 함께 설명할 수 있다는 장점이 있다.

GoF(Gang of Fout) 디자인 패턴

  • 생성(Creational) 패턴
    객체 생성에 관련된 패턴.
    객체의 생성과 조합을 캡슐화해 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공한다.
    • Abstrac Factory
    • Builder
    • Factory Method
    • Prototype
    • Singleton
  • 구조 (Structural) 패턴
    클래스나 객체를 조합해 더 큰 구조를 만드는 패턴.
    예를 들어 서로 다른 인터페이스를 지닌 2개의 객체를 묶어 단일 인터페이스를 제공하거나
    객체들을 서로 묶어 새로운 기능을 제공하는 패턴이다.
    • Adapter
    • Bridge
    • Composite
    • Decorator
    • Facade
    • Flyweight
    • Proxy
  • 행위 (Behavioral) 패턴
    객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴.
    한 객체가 혼자 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하는지,
    또 그렇게 하면서도 객체 사이의 결합도를 최소화하는 것에 중점을 둔다.
    • Chain of Responsibility
    • Command
    • Interpreter
    • Mediator
    • Memento
    • Observer
    • State
    • Strategy
    • Template Method
    • Visitor

📖 추천 서적 : <GoF의 디자인 패턴> (에릭 감마 외), (에릭 프리먼)

🔗 참고 자료
[Design Pattern] 디자인 패턴 종류 - Heee's Development Blog

템플릿 메소드 패턴

이렇게 슈퍼클래스에서 기본적인 로직의 흐름(커넥션 가져오기, SQL 생성, 실행, 반환)을 만들고,
그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤
서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법.

즉 특정 작업을 서브 클래스로 캡슐화하여 전체적인 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내용을 바꾸도록 한다.
슈퍼 클래스에서는 미리 추상 메소드, 또는 오버라이드 가능한 메소드를 정의해두고, 이를 활용해 코드의 기본 알고리즘을 담고 있는 템플릿 메소드를 만든다.

슈퍼클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 서브클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메소드를 훅 메소드라고 하는데,
서브클래스에서는 추상 메소드를 구현하거나, 훅 메소드를 오버라이드하는 방법을 이용해 기능의 일부를 확장한다.

팩토리 메소드 패턴

객체 생성과 관련된 디자인 패턴.
서브클래스에서 구체적인 객체 생성 방법을 결정하게 하는 것.
템플릿 메소드 패턴과 마찬가지로 상속을 통해 기능을 확장하게 하는 패턴.


하지만 이 방법은 상속을 사용했다는 단점이 있다.

  • 자바는 클래스의 다중상속을 허용하지 않고,
  • 상속을 통한 상하위 클래스의 관계가 생각보다 밀접하다.

따라서 이렇게 상속으로 분리한다고 할지라도 슈퍼클래스 내부에서 변경이 일어나면 서브 클래스가 영향을 받지 않는다는 보장이 없다.

❓ 클래스 분리를 통한 확장

그렇다면 아예 두 관심사를 다른 클래스로 분리해보자.
하지만 이렇게 되면 상속을 통한 확장에서와 같이 자유로운 확장이 불가능해진다.

❓ 인터페이스의 도입

클래스를 분리하면서 자유로운 확장을 가능하게 하는 방식.
두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것이 바로 인터페이스다.

  • 인터페이스는 자신을 구현한 클래스에 대한 구체적인 정보를 모두 감춘다.
  • 어떤 일을 하겠다~ 는 기능만 정의해놓고, 어떻게 하겠다는 구현 방법은 나타나있지 않다,
  • 따라서 하위 클래스는 상위 클래스가 어떤 것인지 몰라도 된다. 단지 인터페이스를 통해 원하는 기능을 사용하기만 하면 된다.
// 인터페이스 클래스
public interface ConnectionMaker {
	public Connection makeConnection() throws ClassNotFoundException, SQLException;
}

// 실제 구현 클래스
public class ConnectionMakerImpl implements ConnectionMaker {
	...
	public Connection makeConnection() throws ClassNotFoundException, SQLException{
		// makeConnection에 대한 실제 구현 내용들...	
	}
}

하지만 여전히 UserDao에는 어떤 클래스와 오브젝트를 사용할지를 결정하는 생성자 코드에 구현 클래스의 이름이 남아있다.

connectionMaker = new ConnectionMakerImple();

❓ 관계설정 책임의 분리

UserDaoConnectionMaker이라는 두 개의 관심을 분리했다고 생각했는데,
여전히 UserDaoConnectionMakerImpl이라는 실제 인터페이스 구현체를 알아야만 객체를 생성할 수 있고,
이로 인해 DB 커넥션 기능의 확장을 위해선 UserDao 수정이 불가피하다.
둘 사이에 불필요한 의존관계가 남아있다는 뜻이다.

UserDaoConnectionMaker의 구현체 사이를 이어주는 방식에 대한 관심사가 아직 분리되지 않았기 때문이다.

이를 해결하기 위해선 UserDao를 사용하는 객체, 즉 클라이언트 객체의 개입이 필요하다.
UserDao와 특정 ConnectionMaker 구현체의 오브젝트 간 관계를 맺는 과정을 클라이언트 객체 단으로 넘기는 것이다.
이 과정을 관계설정 책임의 분리라고 부른다.

UserDao의 클라이언트를 UserDaoTest로 설정하고, 코드를 다음과 같이 수정한다.

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

		UserDao dao = new UserDao(connectionMaker)
	}
}

ConnnectionMaker 객체를 클라이언트 단에서 생성하고, 이를 UserDao의 파라미터로 넘기는 방식이다.
이를 통해 더 이상 UserDao를 수정하지 않고도 DB 연결 기능을 확장해서 사용할 수 있게 되었다.

❓ 원칙과 패턴

용어에 익숙해지는 것을 목표로!

객체지향 설계 원칙(SOLID)

객체지향이라는 특징을 잘 살릴 수 있는 설계의 특징.
디자인 패턴이 특별한 상황에서 발생하는 문제에 대한 좀 더 구체적인 솔루션이라고 한다면,
객체지향 설계 원칙은 좀 더 일반적인 상황에서 적용 가능한 설계 기준이라고 볼 수 있다.
객체지향 설계 원칙 SOLID는 다음의 5원칙 첫 글자를 따서 만든 단어다.

  • SRP(Single Responsibility Principle): 단일 책임 원칙
  • OCP(Open Closed Priciple): 개방 폐쇄 원칙
  • LSP(Listov Substitution Priciple): 리스코프 치환 원칙
  • ISP(Interface Segregation Principle): 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle): 의존 역전 원칙

📖 추천 서적 : <GoF의 디자인 패턴> (에릭 감마 외), (에릭 프리먼)

🔗 참고 자료
[Java] 객체지향 설계 5원칙 - SOLID란 무엇일까?

개방 폐쇄 원칙(Open-Cloesed Principle, OCP)

클래스나 모듈은 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.

높은 응집도와 낮은 결합도

개방 폐쇄 원칙을 설명하기 좋은 고전적인 SW 개념.
응집도가 높다 → 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다
낮은 결합도 → 높은 응집도보다 더욱 민감한 원칙으로, 책임과 관심사가 다른 객체들과는 느슨한 연결을 유지해야 한다.

cf. 결합도 → 하나의 객체에 변경이 일어날 때 관계를 맺고 잇는 다른 오브젝트에게 변화를 요구하는 정도

전략 패턴

위에서 보았던 디자인 패턴 중 행위 패턴의 Strategy 패턴에 해당한다.
자신의 기능 맥락Context에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 이용해서 통째로 외부에 분리해두고,
이를 구현한 구현체를 필요에 맞게 바꾸어 사용할 수 있도록 하는 디자인 패턴이다.
UserDao는 전략 패턴에서의 Context에 해당한다.

📌 제어의 역전 (Inversion of Control, IoC)

❓ 팩토리

객체의 생성 방법을 결정하고, 그렇게 만들어진 객체를 반환하는 역할을 한다.
이러한 역할을 담당하는 오브젝트를 흔히 팩토리라고 부른다.
(디자인 패턴에서의 팩토리와는 다르다…)

팩토리는 설계도의 역할을 한다.
클라이언트 객체(UserDaoTest)는 UserDao를 사용하고, UserDaoConnectionMakerImpl을 통해 ConnentionMaker를 사용하고 있다.
여기서 DaoFactory를 도입시켜서, UserDaoConnectionMakerImpl 객체의 생성을 지휘하도록 한다.
ConnectionMakerImpl 객체에 변경사항이 생기면 DaoFactory만 수정하면 되므로 여전히 UserDao는 보존되면서, 자유로운 확장이 가능하다.

❓ 제어의 역전, IoC

프로그램의 제어 흐름 구조가 뒤바뀌는 것.

  • 일반적인 프로그램의 흐름은 다음의 과정을 반복한다.
    1. 프로그램이 시작되는 시점에서 다음에 사용할 객체를 결정

    2. 결정한 객체를 생성

    3. 객체 내 메소드 호출

    4. 객체 메소드 내에서 다음에 사용할 것을 결정하고 호출

      모든 종류의 작업을, 그 객체를 사용하는 쪽에서 제어하는 구조이다.

  • 제어의 역전이란 이런 제어 흐름을 거꾸로 뒤집는 것을 의미한다.
    • 제어의 역전에서는 객체가 자신이 사용할 객체를 스스로 선택하지 않는다.
    • 당연히 생성하지도 않는다.
    • 또 자신도 어떻게 만들어지고 어디서 사용되는 지를 알 수 없다.
    • 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문이다.
  • 프로그램의 시작을 담당하는 main()과 같은 엔트리 포인트를 제외하면, 모든 객체는 이렇게 위임받은 제어 권한을 갖는 특별한 객체에 의해 결정되고 만들어진다.

IoC의 예를 들어보자.
앞서 만들었던 UserDao와 DaoFactory에도 제어의 역전 개념이 적용되어 있다.
ConnectionMaker의 구현체를 결정하고, 그 구현체를 생성하는 제어권은 UserDao(본인)가 아닌 DaoFactory에 있다.
UserDao 자신도 DaoFactory라는 팩토리에 의해 수동적으로 만들어지고,
자신이 사용할 객체도 DaoFactory가 만들어 넘겨주는 것을 수동적으로 사용하고 있다.
이것이 바로 IoC가 일어난 상황으로 볼 수 있다.
이처럼 제어의 역전에서는 컴포넌트의 생성과 관계 설정, 사용, 생명주기 관리 등을 관장해주는 특별한 존재가 필요하다.
→ 앞서 그 역할을 DaoFactory가 수행했다.

DaoFatory는 UserDao 수준의 매우 단순한 IoC 프레임워크라고 볼 수 있겠다.
스프링은 이 IoC를 모든 기능의 기초로 삼고 있으며, IoC를 극한까지 적용하고 있는 프레임워크다.
이어서, 스프링에서의 IoC에 대해 정리해보겠다.

📌 스프링에서의 IoC

스프링 빈(Bean)

스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 객체.
스프링 컨테이너에 의해 생성과 관계설정, 사용 등이 관리되므로 제어의 역전이 적용된 객체라 할 수 있다.
이처럼 스프링 빈을 제어하는 IoC 객체를 빈 팩토리라 부른다.

스프링을 사용하는 애플리케이션에서 만들어지는 모든 객체들이 다 빈은 아니다.
스프링이 직접 그 생성과 제어를 담당하는 객체만을 이라고 부른다.

빈 팩토리

스프링의 IoC를 담당하는 핵심 컨테이너.
빈을 등록하고, 생성하고, 조회하고, 돌려주고.
그 외에 부가적인 빈을 관리하는 여러 기능들을 담당한다.

보통은 이 빈 팩토리를 바로 사용하지 않고, 이를 확장한 애플리케이션 컨텍스트를 이용한다.
BeanFactory 인터페이스를 구현해야 하며, BeanFactory 내에는 getBean()과 같은 메소드가 정의되어 있다.

애플리케이션 컨텍스트

빈 팩토리보다 확장된 개념이지만, 빈 팩토리와 크게 구분하지 않고 사용한다.
빈 팩토리라고 부를 때는 주로 빈의 생성과 제어의 관점에서 이야기하는 것이고,
애플리케이션 컨텍스트라고 할 때는 스프링이 제공하는 애플리케이션 지원 기능을 모두 포함해 이야기하는 것이라고 보면 된다.
별도의 정보를 참고해 빈 객체의 생성, 관계설정 등의 제어 작업을 총괄하는 역할을 한다.

설정정보(이 객체가 어디서 만들어지는지, 어디서 사용할 것인지, 어떻게 연결해줄 것인지…)를 직접 담고 있지는 않지만,
이를 활용하여 제어 및 관리 작업을 담당하는 범용적인 IoC 엔진의 개념이라 생각하면 된다.

ApplicationContext 인터페이스를 구현해야 하며,
ApplicationContext 인터페이스BeanFactory 인터페이스를 상속한다.

설정정보 = 설정 메타정보 = configuration

애플리케이션 컨텍스트가 IoC를 적용하기 위해 사용하는 메타정보.
애플리케이션의 형상정보, 또는 애플리케이션의 전체 그림이 그려진 청사진으로 보면 된다.

컨테이너 = IoC 컨테이너

애플리케이션 컨텍스트 or 빈 팩토리를 부르는 또 다른 이름이라고 생각하면 될듯.
IoC 방식으로 빈을 관리한다는 의미에서 사용한다.
흔히 스프링에 빈을 등록한다고 말할 때의 스프링은 스프링 컨테이너를 의미한다.

스프링 프레임워크

IoC 컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말하는 용어.
그냥 스프링이라고 줄여서 말하기도 한다.

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

우선, DaoFactory를 스프링 빈 팩토리가 사용할 설정 정보로 만들어보자.

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

	@Bean
	public ConnectionMaker connectionMaker() {
		return new ConnectionMakerImple();
	}
} 

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

💡 어노테이션 짚고 가기

@Configuration

클래스에 붙는 어노테이션.
해당 애플리케이션 컨텍스트(또는 빈 팩토리)가 사용할 설정 정보라고 표시하는 의미.

@Bean

수동으로 스프링 컨테이너에 빈을 등록하는 어노테이션.

  • 어노테이션을 사용해 수동으로 스프링 컨테이너에 빈을 등록하는 경우, 메소드 이름으로 빈 이름이 결정된다.
  • 따라서, 중복된 빈 이름이 존재하지 않도록 주의해야 한다.

💡 수동으로 빈을 직접 등록해줘야 하는 상황은 다음과 같다.
1. 개발자가 직접 제어가 불가능한 라이브러리를 활용할 때
2. 애플리케이션 전범위적으로 사용되는 클래스를 등록할 때

위에서 우리는 DaoFactory를 설정 정보로 만드는 작업을 했다.
이 작업은 DaoFactory라는 설정 정보를 애플리케이션 컨텍스트 객체에 등록하는 과정이라고 볼 수 있다.

애플리케이션 컨텍스트는

  • DaoFactory 클래스를 설정 정보로 등록해두고
  • @Bean 이 붙은 메소드의 이름을 가져와 빈 목록을 만들어둔다.
  • 클라이언트가 애플리케이션 컨텍스트를 통해 getBean() 메소드를 호출하면 자신의 빈 목록에서 요청한 이름이 있는지 찾고,
  • 있다면 빈을 생성하는 메소드를 호출해 객체를 생성해서 클라이언트에 리턴한다.

애플리케이션 컨텍스트를 사용하는 이유

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없으므로. 일관된 방식으로 원하는 객체를 가져올 수 있게 된다.
  • 애플리케이션 컨텍스트가 제공하는 종합적인 IoC 서비스를 이용하기 위해서. 객체 생성 방식, 생성 시점과 전략을 다르게 가져갈 수도 있고, 이에 부가적으로 자동생성, 객체 후처리, 정보의 조합, 설정방식의 다변화, 인터셉팅 등의 다양한 활용 기능을 제공한다.
  • 빈을 검색하는 다양한 방법을 제공하므로.

📌 싱글톤 레지스트리, 오브젝트 스코프

동일성 vs 동등성

자바에서 두 개의 객체가 같다고 말할 때는 주의해야 한다.

  • 동일성(identity) 두 개의 객체가 완전히 같다 = 동일하다(identical) = == 연산자 를 사용한다
  • 동등성 두 개의 객체가 동일한 정보를 담고 있다 = 동등하다(equivalent) = equals() 메소드를 사용한다
  • 동일한 객체는 동등하지만, 동등한 객체가 언제나 동일한 것은 아니다.

⚠️ 자바 클래스를 만들 때 equals() 메소드를 따로 구현하지 않았다면,
최상위 클래스인 Object 클래스 내에 구현된 equals() 메소드가 사용된다.
이 때의 equals()는 두 객체의 동일성을 비교한다는 점에 유의하자.

스프링은 매번 동일한 빈 객체를 돌려준다

먼저, DaoFactory와, UserDao 객체 두 개를 콘솔에 출력해본다.

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

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

// 출력 결과
// springbook.dao.UserDao@118f375
// springbook.dao.UserDao@117a8bd

위 출력 결과를 보면, 두 개는 각기 다른 값을 가진 동일하지 않은 객체임을 알 수 있다.
즉, 서로 다른 객체 두 개가 생겼다는 사실을 알 수 있다.
userDao를 매번 호출하면 호출할 때마다 계속 새로운 객체가 만들어질 것이다.

이번에는 스프링 애플리케이션 컨텍스트에 DaoFactory를 설정 정보로 등록하고,
getBean() 메소드를 이용해 직접 userDao라는 이름으로 등록된 객체를 가져와본다.

ApplicatiolnContext 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

이번에는 다른 결과가 나왔다.
두 객체의 출력값이 같다는 것을 볼 수 있다.
즉, 우리가 만든 팩토리 객체에서와 스프링 애플리케이션 컨텍스트의 동작 방식이 다르다는 것을 짐작해볼 수 있다!
스프링은 여러 번에 걸쳐 빈을 요청하더라도, 매번 동일한 오브젝트를 돌려준다. 왜일까?

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

싱글톤 패턴

💡 GoF가 소개한 디자인 패턴 중 하나.
어떤 클래스를 애플리케이션 내에서 하나만 존재하도록 강제하는 패턴이다.
이를 통해 애플리케이션 내에서 전역적으로 접근하는 것이 가능해진다.
단일 오브젝트만 존재해야 하고, 이를 애플리케이션의 여러 곳에서 공유하는 경우에 주로 사용한다.

애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이다.
스프링은 싱글톤으로 빈을 만드는데,
그 이유는 스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이다.
실제 서버환경에서는 수많은 요청이 들어오는데, 매번 객체가 만들어진다면 서버에 부하가 걸린다.
따라서 엔터프라이즈 분야에서는 서비스 객체라는 개념을 일찍부터 사용해왔다.

일례로 Servlet 객체를 들 수 있겠다.
서블릿은 대부분 멀티스레드 환경에서 싱글톤으로 동작한다.
서블릿 클래스당 하나의 오브젝트만 싱글톤으로 만들어두고,
사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용한다는 의미이다.

싱글톤 패턴의 한계

자바에서 싱글톤을 구현하는 방법은 보통 다음과 같다.

  • 생성자를 private 로 제한 → 클래스 밖에서는 객체를 생성하지 못하도록.
  • 자신과 같은 type의 static 필드를 정의 → 생성된 싱글톤 객체를 저장하기 위해.
  • 스태틱 팩토리 메소드인 getInstance()를 만들고,
  • 이 메소드가 최초로 호출되는 시점에서 한 번만 객체가 만들어지게 한다.
  • 생성된 객체는 위에서 만든 static 필드에 저장된다.
  • 또는 static 필드의 초기값 객체를 미리 만들어둘 수도 있다.
  • 싱글톤 객체가 한번 만들어지고 난 후에는 getInstance() 메소드를 이용해 static 필드에 저장해둔 객체를 넘겨준다.

하지만 이와 같은 방식에는 다음과 같은 한계가 존재한다.

  • private 생성자를 이용하므로 상속이 제한된다.
  • 싱글톤은 테스트하기가 힘들다.
    생성 방식이 제한적이라 테스트용, mock 객체를 만들어 대체하기가 어렵다.
    테스트가 어렵다는 건 엔터프라이즈 개발에서 큰 문제점으로 작용한다.
  • 서버 환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
    여러 JVM에 분산되어 설치가 되는 경우나, 서버에서 클래스 로더를 어떻게 구성하고 있느냐에 따라
    싱글톤 클래스임에도 여러 개가 만들어질 수가 있다.
  • 싱글톤의 사용은 전역 상태를 만들 수 있다.
    싱글톤을 사용하기 위한 목적이 전역 호출이었던 걸 기억해보면,
    싱글톤은 사용하는 클라이언트가 정해져 있지 않다고 볼 수 있다.
    싱글톤의 static한 메소드를 이용해 애플리케이션 어디서든지 싱글톤에 쉽게 접근할 수 있어 전역 상태를 만들 수 있다.
    애플리케이션이 전역 상태를 갖는 것은 객체지향 프로그래밍에서 권장되지 않는다.

스프링의 싱글톤 레지스트리

스프링은 서버 환경에서 싱글톤이 만들어져서 서비스 객체 방식으로 사용되는 것을 지지한다.
하지만 고전적인 자바의 싱글톤 패턴을 구현하는 방식은 위와 같은 단점들이 존재하므로,
스프링에서 직접 싱글톤 형태의 객체를 만들고 관리하는 기능을 제공한다.
이것이 바로 싱글톤 레지스트리(Singleton Registry)다.

스프링 컨테이너는 싱글톤을 생성하고, 관리하고, 공급하는 싱글톤 관리 컨테이너기도 하다.
꼭 스태틱 메소드와 private 생성자를 사용하지 않아도, 평범한 자바 클래스를 싱글톤으로 활용할 수 있게 해준다.
덕분에 스프링을 사용하면

  • 싱글톤 방식으로 사용될 클래스도 public 생성자를 가질 수 있으며,
  • 싱글톤으로 사용되어야만 하는 환경이 아니라면 간단히 객체를 생성해 사용할 수 있다.
  • 테스트 환경에서 자유롭게 객체를 생성하고, mock 객체를 만들어 사용할 수도 있다.

하지만 싱글톤을 사용하기 위해 주의해야 할 점들이 있다.

❓ 싱글톤과 객체의 상태

💡 상태유지(Stateful) vs 무상태성(Stateless)
상태유지 : 인스턴스 필드의 값을 변경하고 유지하는 방식
무상태성 : 상태정보를 객체 내부에 가지고 있지 않음

싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근해 사용할 수 있다.
이때, 여러 스레드에서 싱글톤 객체의 인스턴스 변수를 동시에 수정하려는 상황은 매우 위험하다.
따라서 싱글톤 객체는 상태정보를 내부에 갖고 있지 않은 무상태 방식으로 만들어져야 한다.

스프링의 싱글톤 빈으로 사용되는 클래스를 만들 때는

  • 개별적으로 바뀌는 정보는 로컬 변수로 정의하거나,
  • 파라미터로 주고받으면서 사용하게 해야 한다.

❓ 스프링 빈의 Scope

스프링 빈이 생성되고, 존재하고, 적용되는 범위를 빈의 스코프라고 한다.

  • 스프링 빈의 기본 스코프는 싱글톤이다.
  • 즉, 해당 빈 객체는 컨테이너 내에 한 개만 만들어져서 제거하기 전까지 유지된다.
  • 하지만 경우에 따라 싱글톤 외의 스코프를 가질 수 있다.

💡 빈 스코프(Bean Scope)의 종류

[Spring] Bean Scope(빈 스코프)의 종류

  • 싱글톤
    • Spring 프레임워크에서 기본이 되는 스코프
    • 스프링 컨테이너의 시작과 종료까지 1개의 객체로 유지됨
  • 프로토타입
    • 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 스코프
    • 요청이 오면 항상 새로운 인스턴스를 생성하여 반환하고 이후에 관리하지 않음
    • 프로토타입을 받은 클라이언트가 객체를 관리해야 함
    • request: 각각의 요청이 들어오고 나갈때가지 유지되는 스코프
    • session: 세션이 생성되고 종료될 때 까지 유지되는 스코프
    • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

📌 의존관계 주입 (Dependenct Injection, DI)

❓ IoC와 의존관계 주입

IoC의 핵심을 짚어주는 또 다른 이름이 바로 의존관계 주입(이하 DI)이다.
DI는 스프링 프레임워크의 핵심이다.
따라서 스프링은 IoC 컨테이너보다 DI 컨테이너로 더 많이 불린다.

❓ 런타임 의존관계 설정

의존관계

A가 B에 의존하고 있는 상황을 보자.
이는 B가 변하면 B에 의존하고 있던 A에 영향을 미침을 의미한다.
하지만 의존 관계에는 방향성이 있으므로, A → B로 표기한다.
즉 달리 말해 B는 A에 의존하고 있지 않다는 것을 알 수 있다.

런타임 의존 관계

실제 런타임 시에 객체 사이에서 만들어지는 의존 관계를 런타임 의존 관계라고 말한다.
이는 설계나 코드 속에서 드러나지 않으며,
프로그램이 시작되고 런타임 시에 의존 관계를 맺게 된다.
이렇게 런타임 시에 의존 관계를 맺게 되는 대상, 즉 실제 사용대상 객체를 의존 객체라고 부른다.

의존관계 주입, DI

구체적인 의존 객체와, 이를 사용할 주체 (주로 클라이언트) 객체를 런타임 시에 연결해주는 작업을 의미한다.

❓ 의존관계 검색과 주입

스프링에서 제공하는 IoC 방법에는 DI만 있는 것이 아니다.

의존관계 검색(dependency lookup, DL)

  • 런타임 시 의존관계를 맺을 객체를 결정
  • 객체의 생성 작업은 외부 컨테이너에게 IoC 방식으로 맡기지만,
  • 객체를 가져올 때는 메소드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.
  • BeanFactory의 getBean() 메소드가 의존관계 검색에 사용된다!

구체적 클래스에 의존하지 않고, 런타임 시에 의존관계를 결정한다는 점에서 DI와 비슷하다.
하지만 의존 관계를 맺는 방법이 외부로부터의 주입이 아닌,
스스로 검색을 이용해 자신이 필요로 하는 의존 객체를 능동적으로 찾는다는 차이가 있다.

의존관계 주입을 사용하는 이유

의존관계 검색은 의존관계 주입의 거의 모든 장점을 가진다.

  • 하지만 의존관계 주입 쪽의 코드가 훨씬 단순하고 깔끔하다.
  • 또한 의존관계 검색 시 코드 안에 객체 팩토리 클래스나 스프링 API가 나타나게 된다.
  • 이는 애플리케이션 컴포넌트가 컨테이너처럼 성격이 다른 다른 객체의 의존하게 되는 것이므로, 바람직하지 않은 방식이다.

의존관계 검색 방식을 사용해야 하는 상황

의존관계 검색 방식에서 검색하는 객체는 자신이 스프링의 빈일 필요가 없다.

예를 들어 UserDao에 스프링의 getBean()을 사용한 의존관계 검색 방법을 적용했다고 가정해보자.
→ 이 경우 UserDao는 굳이 스프링이 만들고 관리하는 빈일 필요가 없다.

그러나 의존관계 주입에서는 UserDao와 ConnectionMaker 사이에 DI가 적용되려면,
UserDao도 반드시 컨테이너가 만드는 빈 객체여야 한다.
컨테이너가 UserDao에 ConnectionMaker 객체를 주입해주려면 UserDao에 대한 생성과 초기화 권한을 가져야 하기 때문이다.

즉, DI를 원하는 객체는 먼저 자기 자신이 스프링 빈이 되어야 한다는 사실을 잊지 말자.

❓ 의존관계 주입의 응용사례

  • 기능 구현의 교환
  • 부가기능 추가

개발 단계에서 로컬 DB를 사용하고 있다가, 서버에 배포해야 하는 상황에 직면했다고 가정하자.
개발 단계에서는 로컬 DB에 연결하는 LocalDBConnectionMaker 이라는 클래스를 만들고,
모든 DAO에서 이 클래스의 객체를 매번 생성해서 사용하게 했을 것이다.
하지만 서버에 배포할 때는 다시 서버가 제공하는 또 다른 DB 연결 클래스가 필요할 것이다.

DI 적용 전

모든 DAO에 new LocalDBConnectionMaker() 이라는 코드가 들어가 있을 것이다.

이를 서버에 배치하는 시점에서 서버 DB용 ProductionDBConnectionMaker 클래스로 변경해줘야 한다.

하지만 이렇게 되면 수십 수백개의 DAO에서 코드를 수정해야 한다.

DI 적용 후

모든 DAO는 생성 시점에서 ConnectionMaker 타입의 객체를 컨테이너로부터 제공받게 된다.
@Configuration이 붙은 DaoFactory를 사용한다고 하면,
개발자는 DaoFactory의 코드를 아래와 같이 딱 한 줄만 바꿔주면 된다!

// 개발용 ConnectionMaker 코드
@Bean
public ConnectionMaker connectionMaker() {
	return new LocalDBConnectionMaker();
}

// 배포용 ConnectionMaker 코드
@Bean
public ConnectionMaker connectionMaker() {
	return new ProductionDBConnectionMaker();
}

나머지 코드에 손대지 않고 개발과 운영 시에 각각 다른 런타임 객체에 의존관계를 갖게 해줌으로써 문제를 해결했다.
기능 구현 교환을 위해 DI가 적재적소에 쓰인 사례라고 볼 수 있다.

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

지금까지는 DI를 위해 생성자를 사용했다.
생성자에 파라미터를 만들어두고, 이를 통해 DI 컨테이너가 의존할 오브젝트 레퍼런스를 넘겨주는 방식이었다.
하지만 생성자가 아닌 일반 메소드를 사용할 수 있으며, 생성자를 사용하는 방법보다 더 자주 사용된다.

생성자가 아닌 일반 메소드를 이용해 의존 주입을 하는 방법은 크게 두 가지가 있다.

  1. 수정자=Setter 메소드를 이용한 주입
    • 외부에서 객체 내부의 어트리뷰트 값을 변경하려는 용도로 주로 사용된다.
    • 메소드는 항상 set으로 시작한다.
    • 주로 파라미터로 전달된 값을 내부의 인스턴스 변수에 저장하는 용도로 사용한다.
    • 부가적으로, 입력받은 값에 대한 검증이나 그 외의 작업을 수행할 수도 있다.
  2. 일반 메소드를 이용한 주입
    • Setter처럼 set으로 시작해야 하고, 한 번에 한 개의 파라미터만 가져야 한다는 제약이 싫은 경우 사용한다.
    • 여러 개의 파라미터를 받을 수 있다는 장점이 있다.
    • 하지만 비슷한 타입의 파라미터 여러 개를 받다 보면 실수하기 쉽다. (필자도 그랬다…)

📌 XML을 이용한 설정

스프링은 DaoFactory와 같은 자바 클래스 코드를 이용하는 것 외에도,
다양한 방법을 통해 DI 의존관계 설정정보를 만들 수 있다.
가장 대표적인 것이 바로 XML이다.

❓ XML 설정

  • 빈의 이름 : @Bean 메소드 이름이 빈의 이름이다.
  • 빈의 클래스 : 어떤 클래스를 이용해서 빈 객체를 만들 것인지 정의한다.
  • 빈의 의존 객체 : 빈의 생성자나 Setter 메소드를 이용해 의존 객체를 넣어준다. 의존 오브젝트도 하나의 빈이므로 이름이 있을 것이고, 그 이름에 해당하는 메소드를 호출해 의존 객체를 가져온다. 의존 오브젝트는 하나 이상일 수도 있다.
  • XML에서는 리턴 타입을 지정하지 않아도 된다.
  • 단, class 어트리뷰트에 넣을 클래스 이름은 패키지까지 모두! 포함해 적어야 한다. (IDE를 사용하면 자동완성을 시켜주므로 걱정하지 않아도 된다…)

예시로, connectionMaker() 메소드를 태그로 전환해보자.

// Java
@Bean
public ConnectionMaker connectionMaker() {
		return new ConnectionMakerImpl();
}

// XML
<bean id="connectionMaker" class="springbook.dao...ConnectionMakerImpl" />

이번에는 userDao 메소드를 XML로 변환해본다.
userDao에 들어있던 setter 메소드를 XML로 변환하게 되면 property가 된다.
userDao의 setConnectionMaker(connectionMaker()) 부분에서
property name은 메소드의 set을 제거한 부분, ref는 메소드 파라미터가 된다.
따라서 userDao를 XML로 변환하면 다음과 같다.

// JAVA
userDao.setConnectionMaker(connectionMaker());

// XML
<bean id="connectionMaker" class="springbook.dao...ConnectionMakerImpl">
	<property name="connectionMaker" ref="connectionMaker" />
</bean>
profile
블로그 이사했습니다 ✈ https://jennairlines.tistory.com

0개의 댓글