본문 바로가기
Web/Spring

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

by 희조당 2022. 12. 21.
728x90

1.2 dao의 분리

객체지향의 꽃은 결국 관심사를 분리하는 것 (Seperating of Concerns)
왜? 자유로운 변경, 발전, 확장을 위해서
어떻게? 관심이 다른 것을 가능한 분리하여 서로 영향을 주지 않게

 

템플릿 메소드 패턴 :
슈퍼 클래스에 기본적인 기능을 구현하고 기능의 일부를 서브 클래스에 필요에 맞게 구현할 수 있도록 하는 패턴.
(protect로 선택 가능하게, 추상 메서드로는 꼭 구현하게)


여기서 템플릿 메소드란? 기본 알고리즘 골격을 담은 메소드, 애플리케이션의 run() 같은게 템플릿 메서드이다.
훅 메서드 : protect로 구현된 선택적으로 오버라이드가 가능한 메서드

 

팩토리 메서드 패턴 : 서브 클래스에서 구체적인 오브젝트 생성 방법과 클래스를 결정하게 하는 패턴. 슈퍼클래스의 기본 코드에서 독립시키는 방법

 

두 패턴의 공통점 : 상속을 통해 기능을 확장하게 해주는 패턴.

 

하지만 java에서는 상속은 한계가 많다. 다중상속x.. 상하위 클래스 관계가 밀접. (하위 클래스에서 상위 클래스의 기능을 사용 할 수 있음)
또한 다른 dao 클래스에서 적용할 수 없다.. 낮은 확장성

 

1.3 dao 확장

인터페이스를 활용한다는 것은 object와 object의 사이를 추상적으로 다리를 놓아주는 것.
클래스 사이 관계를 만드는 것과 다르다. 클래스 사이 관계는 인터페이스 없이 직접적으로 사용함을 의미한다.


따라서 오브젝트 사이의 관계는 런타임 시에 한쪽이 다른 오브젝트의 레퍼런스를 갖고 있는 방식.
그렇다면 클라이언트(오브젝트 사용자)가 해야할 것은 런타임 오브젝트 관계를 만들어주는 것. (의존관계를 생성)

 

전략 패턴 : 자신의 기능 맥락에서 필요에 따라 변경이 필요한 기능을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 기능 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴.


예를 들어, UserDao는 컨텍스트로 기능을 수행하는데 필요한 핵심 기능을 ConnectionMaker 인터페이스로 분리해서 확장을 가능케 했다.


이 인터페이스를 구현한 클래스 NConnectionMaker로 필요한 전력에 맞춰서 기능을 구현할 수 있게 되었다.

 

1.4 제어의 역전

팩토리 : 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것
팩토리를 통해서 생성하는 쪽과 사용하는 쪽을 구분한다. 즉, 컴포넌트 역할을 하는 오브젝트와 애플리케이션의 구조를 결정하는 오브젝트를 분리했다는 말이다.


제어의 역전이라는건 프로그램의 제어 흐름 구조가 뒤바뀌는 것. 대표적인 예시 서블릿, 프레임워크.
ioc가 적용되어있지 않으면 프레임워크라고 할 수 없음.


ioc의 이해 예시
오브젝트를 만드는 제어권과 ConnectionMaker의 구현 클래스를 결정하는 권한은 UserDao에게 있었다.
UserDao가 어떤 ConnectionMaker 구현 클래스를 만들고 사용할 지에 대한 권한을 Daofactory에게 넘겨 UserDao는 수동적인 존재로 변했다.
UserDao 자신도 팩토리에 의해 수동적으로 만들어지고 사용할 오브젝트도 Daofactory가 공급해주는 것을 수동적으로 사용해야 할 입장이 되었다.
또한 DaofactoryUserDaoConnectionMaker의 구현체를 생성하는 책임도 가지고 있다.

 

IoC를 사용하면 깔끔하게 설계할 수 있고 유연성과 확장성이 좋아진다.

프레임워크와 컨테이너와 같이 컴퍼넌트의 생성과 관계설정, 사용, 생명주기 관리 등을 관장하는 존재가 필요하다.

 

1.5 스프링의 IoC

1.5.1 오브젝트 팩토리를 이용한 IoC

스프링 빈 : 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트. 다시 말하면 제어의 역전이 적용된 오브젝트.

빈 팩토리가 빈의 생성과 관계설정 같은 제어를 담당 보통 확장된 애플리케이션 컨텍스트를 사용한다.

 

애플리케이션 컨텍스트 : 별도의 정보(설정 메타 정보)를 참고해서 빈의 생성, 관계설정 등의 제어 작업을 총괄한다.

즉 별도의 설정정보를 가져와 이를 활용하는 범용적인 IoC 엔진.

 

스프링의 빈 팩토리가 사용할 수 있는 만드는 것 -> config 클래스 만들기

@Configuration은 설정을 담당하는 클래스라고 인식할 수 있게,

@Bean은 오브젝트를 만들어주는 메소드를 의미한다.

 

UserDao의 경우 오브젝트를 생성하고 초기화해서 돌려주니 @Bean을 붙이는게 맞다.

ApplicationContext 구현은 여러 가지 방법이 존재.

@Configuration이 붙은 자바 코드를 설정정보로 사용하려면 AnnotationConfigApplicationContext을 사용한다.

이후 getBean()으로 UserDao의 오브젝트를 가져올 수 있다.

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

@Bean 어노테이션을 붙인 메소드의 이름이 빈의 이름이 된다.

따라서 getBean()으로 빈을 가져올 때 빈의 이름을 사용한다.

이름으로 빈을 불러오는 이유는 같은 타입이라도 생성 방식과 구성 방식을 다른 메소드를 구현할 수 있기 때문이다.

 

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

Application Context는 BeanFactory 인터페이스를 상속한 구현체. 즉 Application Context는 BeanFactory.

Application Context는 IoC를 적용해서 관리하는 모든 오브젝트의 생성과 관계설정을 담당.

@Configuration이 붙은 클래스를 설정 정보로 등록하고 @Bean이 붙은 메소드의 이름을 가져와 빈 목록을 만든다.

오브젝트 팩토리를 대신에 애플리케이션 컨텍스트를 사용했을 때 얻을 수 있는 장점은 다음과 같다.

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다. 오브젝트가 아무리 많아져도 생성하고 사용하기 쉽다.
  • 종합 IoC 서비스를 제공해준다. 생성과 관계설정 외에 전략, 연동 등 다양한 기능을 제공한다.
  • 빈을 검색하는 다양한 방법을 제공한다. 

1.5.3 스프링 IoC 용어 정리

  • 빈 : 스프링 IoC로 관리하는 오브젝트. 직접 생성과 제어를 담당하는 오브젝트만을 의미.
  • 빈 팩토리 : 스프링의 IoC를 담당하는 핵심 컨테이너. 보통 생성과 제어 수준의 IoC 컨테이너를 말한다.
  • 애플리케이션 컨텍스트 : 빈 팩토리를 확장한 IoC 컨테이너. 보통 스프링이 제공하는 애플리케이션 지원 기능을 모두 포함한 수준의 IoC 컨테이너를 말한다.
  • 설정정보/설정 메타정보 : IoC 컨테이너가 IoC를 적용하기 위해 사용하는 메타정보이다.
  • 컨테이너 또는 IoC 컨테이너 : 컨테이너는 보다 추상적인 표현이고 IoC의 개념을 담고 있는 말이다.

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

오브젝트 팩토리에서 가져온 오브젝트는 매번 다른 오브젝트이지만 

애플리케이션 컨텍스트에서 가져온 오브젝트는 매번 같은 오브젝트이다. 왜 그럴까?

 

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

애플리케이션 컨텍스트는 IoC 컨테이너이자 싱글톤을 저장하는 싱글톤 레지스트리이다.

 

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

스프링은 대규모 엔터프라이즈 서버환경을 감당하기 위해서 만들어졌다.

매 클라이언트의 요청마다 오브젝트를 만들면 GC의 성능이 아무리 좋아져도 서버가 감당할 수 없다.

따라서 서블릿 클래스마다 하나의 오브젝트만 만들어두고, 이 오브젝트를 공유해서 사용한다.

싱글톤 패턴의 한계

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(new DConnectionMaker());
        }
        return INSTANCE;
    }
    ...
}
  • private 생성자를 갖고 있어서 상속할 수 없다.
  • 싱글톤은 테스트하기 힘들다.
  • 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
  • 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.

싱글톤 레지스트리

자바에서 싱글톤 패턴 구현은 여러 단점이 존재한다.

스프링에서는 싱글톤 레지스트리을 제공해 이러한 단점들을 해결한다.

평범한 자바 클래스를 싱글톤으로 만들어 관리해 public 생성자를 지원하고, 테스트를 위한 오브젝트 생성이 자유롭고 목 오브젝트로 대체하는 것도 간단하게 만들어 준다.

나아가 스프링이 지지하는 객체지향 설계 방식과 원칙, 디자인 패턴 등을 사용하는데 문제가 없어진다.

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

멀티스레드 환경에서 여러 스레드가 동시에 접근할 수 있기 때문에 서비스 오브젝트는 상태를 가지고 있으면 안된다.

상태가 없을 때 각 요청에 대한 정보나 리소스로부터 생성한 정보는 파라미터, 로컬 변수, 리턴 값으로 이용하면 된다.

읽기전용의 정보일 때는 인스턴스 변수를 사용해도 무방하다.

1.6.3 스프링 빈의 스코프

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

빈은 기본적으로 싱글톤이기 때문에 강제로 제거하지 않는 한 스프링 컨테이너가 존재하는 동안 유지된다.

프로토타입은 싱글톤과 달리 컨테이너에 빈을 요청할 때마다 매번 새로운 오브젝트를 만들어준다.

그 외에 HTTP 요청이 생길때마다 생성되는 요청(request) 스코프가 있고, 웹의 세션과 스코프가 유사한 세션(Session) 스코프도 있다.

1.7 의존관계 주입(DI)

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

IoC라는 용어는 너무 폭넓은 의미를 가진다.

의존관계 주입이라는 용어를 통해 스프링이 제공하는 기능이 다른 프레임워크와의 차이점을 잘 드러낸다.

1.7.2 런타임 의존관계 설정

의존관계

의존관계란, A가 B에 의존할 때 B가 변하면 A에 영향을 미친다는 것을 말한다.

의존관계에는 방향성이 존재한다. A가 B를 의존할 때 B는 A에 영향을 받지 않는 뜻이다.

UserDao의 의존관계

인터페이스를 통해 설계 시점에 느슨한 의존관계를 갖는 경우에는 UserDao의 오브젝트가 런타임 시에 사용할 오브젝트가 어떤 클래스로 만든 것인지 미리 알 수 없다.

개발자나 운영자는 사전에 미리 알 수 있지만 설계와 코드 속에서는 드러나지 않는다는 말이다.

의존관계 주입(DI)는 의존 오브젝트를 런타임 시에 연결해주는 작업을 말한다.

UserDao의 의존관계 주입

의존관계 주입을 위해서는 DaoFactory와 같은 제 3의 존재가 필요하다. 

주입이라는 것은 외부에서 내부로 무언가를 넘겨주는 것을 말한다.

자바에서 주입은 메소드로 파라미터로 오브젝트의 래퍼런스를 전달해주는 방법뿐이고, 가장 쉬운 방법은 생성자이다.

private ConnectionMaker connectionMaker;

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

DI 컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있게 래퍼런스를 전달받는 과정을 DI 라고 한다.

사용할 오브젝트에 대한 선택과 생성권을 외부에 넘기고, 자신은 수동적으로 주입받기에 IoC를 잘 따른다.

1.7.3 의존관계 검색과 주입

외부로부터 주입되는 방식을 사용하지 않고 스스로 검색해서 사용하는 의존관계 검색이라는 것도 존재한다.

메소드나 생성자를 통하지 않고 스스로 컨테이너에게 요청하는 방법이다.

// 스스로 컨테이너에게 요청
public UserDao() {
    DaoFactory daoFactory = new DaoFactory();
    this.connectionMaker = daoFactory.connectionMaker();
}

DaoFactory가 넘겨주는 오브젝트를 런타임 의존관계를 맺어 IoC를 잘 따르고 있다.

하지만 일반화한 애플리케이션 컨텍스트라면 정해놓은 이름을 전달해서 해당 오브젝트를 찾는다.

따라서 일종의 검색이라고 부르는 것이고 애플리케이션 컨텍스트에선 getBean() 메소드를 제공한다.

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

다른 오브젝트에 의존한다는 점에서 의존관계 주입 방식을 사용하는게 낫다.

경우에 따라서 의존관계 검색을 사용하는 경우가 있다. (오브젝트를 한 번은 가져와야 하는 경우)

DI를 사용하려면 컨테이너가 관리하는 빈이 돼야 한다.

DI에서 말하는 주입은 다이내믹하게 구현 클래스를 결정해서 제공받을 수 있도록 인터페이스 타입의 파라미터를 통해 이루어지는 것.

1.7.4 의존관계 주입의 응용

DI의 장점은 다음과 같다.

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

기능 구현의 교환

기능 구현 중에 DB를 변경할 일이 있으면 단순하게 DaoFactory를 고쳐주기만 하면 된다.

@Bean
public ConnectionMaker connectionMaker() {
	return new LocalDBConnectionMaker();
    // or return new AnotherDBConnectionMaker();
}

부가기능 추가

DAO가 DB를 얼마나 많이 사용하는지 파악하고 싶을 때 단순하게 카운팅하는 방법을 사용할 수도 있다.

새로운 메소드를 넣어 분석하고, 분석이 끝나면 지우는 이런 작업들은 피하려고 했던 작업이다.

또한 DB 연결횟수를 세는 일은 DAO의 관심사항이 아니고 분리돼야 할 책임이다.

 

DI 컨테이너에선 DAO와 DB 커넥션을 만드는 오브젝트 사이에 오브젝트를 하나 더 추가하면 된다.

DI를 통해 기존 코드 수정 없이 설정정보만 수정해서 런타임 의존관계만 새롭게 정의해주면 된다.

 

CountingConnectionMaker라는 ConnectionMaker의 구현체에서 새 관심사항을 담당해준다.

public class CountingConnectionMaker implements ConnectionMaker {
    int counter = 0;
    private ConnectionMaker realConnectionMaker;

    public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
        this.realConnectionMaker = realConnectionMaker;
    }

    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        this.counter++;
        return realConnectionMaker.makeConnection();
    }

    public int getCounter() { return this.counter; }
}

UserDao에서 DB 커넥션을 가져오려할 때마다 CountingConnectionMaker을 통해서 카운팅을 하고

이후 DconnectionMaker을 호출해서 실질적인 DB 커넥션을 제공해주면 된다.

 

기존에 UserDao → DconnectionMaker 의존관계에서 UserDao → CountingConnectionMaker → DconnectionMaker의 의존관계로 변경된다.

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

생성자가 아닌 일반 메소드를 이용해 의존 오브젝트와의 관계를 주입해주는 방법은 두 가지가 존재한다.

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

수정자는 파라미터로 전달된 값으로 내부 인스턴스 변수에 저장한다. 

외부로부터 제공받은 오브젝트 레퍼런스를 저장해뒀다가 내부의 메소드에서 사용하게 하는 방식으로 활용할 수 있다.

일반 메소드를 이용한 주입

수정자처럼 set으로 시작해야 하고 한 번에 하나의 파라미터만 가지는 것이 싫을 때 사용할 수 있는 방법이다.

여러 개의 파라미터를 받을 수 있다는 점이 장점이자 단점이다.

 

전통적으로 스프링은 수정자 메소드 방식을 가장 많이 사용해왔다. 적용한 코드는 다음과 같다.

// setter 방식의 DI
public void setConnectionMaker(ConnectionMaker connectionMaker) {
    this.connectionMaker = connectionMaker;
}

// CountingDaoFactory 패키지
@Bean
public UserDao userDao() {
    UserDao userDao = new UserDao();
    userDao.setConnectionMaker(connectionMaker());
    return userDao;
}

 

'Web > Spring' 카테고리의 다른 글

[Spring] @Valid, @Validated과 Custom Annotation (1)  (0) 2023.03.19
[토비의 스프링] 테스트 (2장)  (0) 2023.01.16
[Spring] AOP (feat. Proxy Pattern)  (0) 2022.09.21
[Spring] Servlet, 서블릿  (0) 2022.09.15
[Spring] Spring의 MVC  (0) 2022.09.14

댓글