Java

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

jwKim96 2022. 3. 11. 01:46

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

관심사의 분리

관심사 추출

세상에는 변하는것(변할 가능성이 있는것)변하지 않는것이 있는것처럼, 코드도 마찬가지 입니다.
DB에 회원 DAO에는 다음과 같은 로직이 필요합니다.

  • DB 연결
  • 회원 관련 쿼리 요청

이 로직들이 하나의 메소드에 있다면, 무조건 개선해야할 메소드이죠.
하나의 메소드는 하나의 관심사만 가져야 하지만, 이 메소드에서는 2가지의 관심사가 모두 있습니다.
그래서 변하는것변하지 않는것을 잘 파악해서 을 따로 추출해 내는것이 중요한데요.
변하는것을 따로 추출하면 요구사항의 변경이 있을 경우, 변경점이 하나이기 때문에 유지보수가 수월해집니다.

이렇게 관심사를 추출하는 방법에는 메소드로 추출, 상속을 통한 추출 등이 있습니다.

확장성

클래스 분리

위에서 메소드 혹은 상속으로 관심사를 분리하는 방법을 알게되었는데요.
하지만 좀 더 엄격하게 본다면, DAO는 여전히 관심사가 분리되지 않은 상태라고 할 수 있습니다.

DAO에서 필요한 핵심 로직은 회원 관련 쿼리 요청인데, DB 연결이 포함되어있습니다.
그래서 변하는것일 가능성이 높은 DB 연결 부분을 따로 클래스로 분리하는게 좋습니다.

image

이렇게 DB 연결 부분을 SimpleConnectionMaker 로 분리하고, UserDao에서 참조하게 한다면,
각 클래스들은 한가지의 관심사만 갖게 됩니다.

인터페이스 도입

위에서 살펴본 UserDao에도 사실 개선해야할 점이 있습니다.

SimpleConnectionMaker 클래스를 확장하여 새로운 기능을 추가한 클래스를 쓰게 된다면,
SimpleConnectionMaker를 사용하는 모든 Dao들의 코드를 수정해야합니다.

이러한 문제가 발생하는 이유는 UserDao가 SimpleConnectionMaker를 직접적으로 알고있기 때문입니다.
이를 두 클래의 결합도가 높다(강결합) 고 합니다.
강결합상태는 변경이나 확장이 필요한 경우에 아주 취약한 상태인데요.
그래서 이를 약결합상태로 만들어주어야 합니다.

이때 유용한 것이 인터페이스입니다.

image

UserDao는 인터페이스를 참조하게 하고, 필요에 따라 이를 구현한 클래스를 주입해주면 됩니다.
하지만, 아직 문제가 있는데요.

이렇게 인터페이스를 도입해도, 코드레벨에서는 여전히 구현체를 알아야 합니다.

// 강결합 예제
public class UserDao {
    private ConnectionMaker connectionMaker;
    public UserDao() {
        connectionMaker = new NConnectionMaker();
    }
}

코드레벨에서 결국 특정 구현체를 선택해줘야하기 때문에, 사실 아직까지 강결합 상태라고 할 수 있습니다.

관계 설정 책임의 분리

이러한 문제를 해결하기 위해서는, 구현체를 선택하는 책임을 분리해야 합니다.

  • DB 연결
  • 회원 관련 쿼리 요청

이때까지는 위의 두가지의 관심사에 대해서만 분리를 했는데요.
분리하다보니 UserDao에서 ConnectionMaker의 구현체 중 어떤 클래스를 사용할지 정하는 책임이 추가되었습니다.
이 또한 분리하면 좋을텐데, 어떻게 분리하면 좋을까요?

추천하는 방법은 UserDao를 호출하는 곳에서, 생성자를 통해 구현체를 선택해서 넘겨주는것 입니다.

// 약결합 예제
public class UserDao {
    private ConnectionMaker connectionMaker;
    public UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}

class OtherClass {
    public void doSomething() {
        UserDao dao = new UserDao(new NConnectionMaker());
    }
}

그렇게 하면 UserDao는 ConnectionMaker라는 인터페이스만 알면 되고, 기능이 확장 또는 변경된 새로운 클래스가 생기면
호출하는 쪽에서 ConnectionMaker의 다른 구현체를 넘겨주면 됩니다.

이렇게 되면 객체지향 5원칙(SOLID) 중에서 OCP를 만족하는 상태가 됩니다.

  • OCP : 변경에는 닫혀있고(변경이 있어도 파급효과가 없고), 확장에는 열려있다(확장은 가능한 구조이다)

이렇게 되면, 두 클래스 사이에는 낮은 결합도가 형성되고, 각 클래스는 높은 응집도를 갖게 됩니다.
그리고 이렇게 외부에서 일부 로직(전략)을 주입해주는 방식은 전략패턴이라고 합니다.

Ioc - 제어의 역전

오브젝트 팩토리

위에서 관계 설정 책임의 분리가 바로 IoC 입니다.

// 일반코드 예제
public class Main {
    public static void main(String[] args) {
        Codesquad codesquad = new BackendCodesquad();
    }
}

사실 일반적인 코드에서는 클래스가 필요한 곳에서 바로 생성해서 사용합니다.
하지만 위의 약결합 예제를 보면, 호출하는 곳에서 구현체를 생성해서 주입해줍니다.
즉, 클래스를 선택하고 생성하는것을 제어하는 주체가 실제로 사용하는 클래스가 아닌 호출하는 클래스가 됩니다.
이것이 제어의 역전이라고 할 수 있습니다.

제어권 이전을 통한 제어관계 역전

하지만 조금 더 엄격하게 보면, 호출하는 곳에서도 결국 UserDao와 NConnectionMaker를 모두 알아야 합니다.
그래서 두 클래스를 조합하는 책임오브젝트 팩토리로 넘겨보겠습니다.

// 오브젝트 팩토리 예제
public class DaoFactory {
    public UserDao userDao() {
        return new UserDao(new DConnectionMaker());
    }
}

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

class OtherClass {
    public void doSomething() {
        UserDao dao = new DaoFactory().userDao();
    }
}

오브젝트 팩토리를 활용하면, 연관있는 클래스를 조합하는 책임을 한 곳에서 관리할 수 있게 됩니다.
그리고 제어권을 온전히 오브젝트 팩토리로 넘김으로서 제어 관계가 역전되었다고 할 수 있습니다.

스프링의 IoC

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

사실 위에서 만든 오브젝트 팩토리는 스프링에서 자주 사용되는 Configuration class의 형태와 유사합니다.
그래서 이를 스프링에서 사용할 수 있는 형태로 변경하면 아래와 같습니다.

// 스프링 오브젝트 팩토리 예제
@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao() {
        return new UserDao(new DConnectionMaker());
    }
}
  • @Configuration : 스프링의 빈 팩토리가 참조할 클래스라는 설정 표시
  • @Bean : 오브젝트 생성을 담당하는 IoC용 메소드라는 표시

ApplicationContext

하지만 사실 우리가 스프링을 사용할 때, 빈 팩토리를 직접적으로 사용하지는 않습니다.
스프링에서는 이를 ApplicationContext 라는 인터페이스로 정의하고, 이를 구현한 클래스들을 사용하게 됩니다.
대표적으로는 많이 사용하는 구현체 중 하나인 AnnotationConfigApplicationContext 등이 있습니다.

ApplicationContext 구현체에 아까 만든 Configuration class 를 주입해주면, 내부에서 빈 팩토리가 이를 참조하여 사용합니다.
그리고 내부에서 생성된 객체는 ApplicationContext 에서 Bean으로 관리합니다.
그래서 getBean이라는 메소드로 원하는 객체를 가져올 수 있습니다.

// ApplicationContext 예제
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao = context.getBean(UserDao.class);

ApplicationContext를 사용하면 아래와 같은 장점이 있습니다.

  • 호출하는 곳에서 구체적인 팩토리 클래스를 알 필요가 없다.
  • ApplicationContext는 종합 IoC 서비스를 제공한다.
    • 생성, 관계설정 부터 객체의 생성방식, 시점, 전략을 다르게 할 수도 있습니다.
  • 을 검색하는 다양한 방법을 제공한다.
    • 위 예제처럼 클래스 타입으로 검색하는 방법도 있지만, 지정된 이름으로 검색하는 방법도 있습니다.

스프링 IoC 관련 용어정리

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

싱글톤 레지스트리로서의 ApplicationContext

스프링은 모든 싱글톤으로 관리합니다.
서버 애플리케이션의 예로 들면, 요청 처리를 위해 사용하는 클래스가 Controller, Dto, Service, Repository 등이 있다고 가정하면
만약 요청시마다 클래스를 생성한다면, 4개의 객체가 만들어지는데요.
만약 1000번의 요청이 온다면 4000개의 객체, 100만번의 요청이 온다면 400만개의 객체가 생성될 것입니다.

그래서 스프링은 기본적으로 을 싱글톤으로 관리합니다.
하지만 반대로 말하면, 요청시마다 생성해야하는 클래스는 으로 등록하지 않아도 된다는것 입니다.
그래서 위에서 예시로 든 클래스 중에서 Dto는 요청시마다 새로 생성되어야합니다.

여기서 나머지 클래스와 Dto의 차이는 무엇일까요?
바로 상태입니다.
Dto는 요청에 따라 항상 다른 값을 갖게 되는데, 이게 상태를 갖게 되는 것 입니다.
그러나 Controller, Service, Repository는 미리 정의해놓은 로직을 수행하는것 뿐이고, 상태를 갖지 않아도 됩니다.
(이들도 물론 상태를 가질 수 있습니다. 만약 상태를 가진다면 동시성 문제를 신중히 고려해야 합니다.)
따라서 이 클래스들은 으로 등록하여 싱글톤으로 관리하는것이 유리합니다.

그래서 정리하면, ApplicationContext는 이 싱글톤 빈들을 저장하는 싱클톤 레지스트리의 역할을 하게 되는 것 입니다.

싱글톤의 한계

하지만, 싱글톤은 주의해서 사용해야 합니다.
위에서도 잠시 언급한것 처럼, 하나의 객체를 여러 스레드가 공유하기 때문에 Race Condition이 발생할 가능성이 있습니다.
이 이외에도 싱글톤은 단점이 있는데요.

  • private 생성자 때문에 상속이 불가능함
  • 생성 방식이 제한되어, 테스트하기 어려움
  • 서버 환경에서는 싱글톤이 하나라는 보장을 하기 어려움
    • 가령, 여러개의 JVM을 설치하여 서버를 돌리는 경우 JVM 마다 싱글톤 객체가 생김

스프링 빈의 스코프

스프링 빈은 일반적으로 애플리케이션의 구동과 함께 생성되고, 종료가 되며 사라지는 싱글톤 스코프를 가집니다.
하지만, 설정을 통해 다양한 스코프를 가질 수 있습니다.

  • 프로토타입
  • 요청 스코프
  • 세션 스코프

등등

DI - 의존관계 주입

런타임 의존관계 설정

IoC 컨테이너가 해주는 작업 중 가장 중요한 작업이 의존성을 주입해주는 작업입니다.
이 작업은 DI 라고 하는데요.

위의 예제들에서도 DaoFactory나 ApplicationContext를 통해서 DI를 해주었지만, 스프링이 해주는 DI는 차이점이 있습니다.
DaoFactory나 ApplicationContext를 통해 주입을 받으면, 결국 코드레벨에 주입받는 코드를 작성해야합니다.

하지만, 스프링은 이를 Runtime에 대신 해주게 됩니다.
예로 @Autowired 어노테이션이 붙은 필드는, 실행시간에 맞는 타입의 빈을 주입받을 수 있습니다.

DL

여기서 DL에 대해서도 잠깐 짚고 넘어가면, 우리가 ApplicationContext를 통해 원하는 빈을 찾아서 가져왔었는데
이를 Dependency Lookup 이라고 합니다.

DIDL은 호출하는쪽이 인지의 여부가 다른데요.
DI는 스프링이 의존관계를 주입해주며, 호출하는쪽의 생성주기에도 관여를 해야하기 때문에 이어야 하는데,
DL은 호출하는 쪽이 이 아니어도 된다는 것입니다.