2. 스프링 입문

스프링 기반 코드 설계에 있어 기초적인 규칙 및 해당 개념들에 대해 상세히 알아보고 스프링 적용 연습은 제공되는 과제들로 진행하는 바쁘기 그지없어 죽을 것 같은 나날의 연속이다.

자바에서 설정 정보를 메소드 혹은 클래스, 필드 등에게 제공하는 어노테이션을 기반으로 스프링 프레임워크는 불필요한 코드 작성을 줄여주는 역할을 맡고 있는데, 느낌상 어노테이션만 잘 쓰면 날먹 프레임워크가 되는 거 아니냐...? 라는 안일한 생각에 빠질 것 같아서 걱정이다.

아무튼, 이번 공부 주제는 스프링에서의 제어 역전, 그리고 관리 대상으로 삼아지는 컨테이너에 대해 공부하고 이것이 내가 주로 다룰 스프링 부트에서는 어떻게 취급되는 지에 대해서 공부할 예정.

1) 설계 원칙의 적용

지난 포스팅에서 아주 중요한 개념들을 다뤘기지만, 그것이 스프링 프레임워크에서 어떤 의미를 가지고 어떻게 활용할 수 있는 지에 대해서는 결국 공부 및 코드 작성을 통해서 확인해야 할 부분. 그래서 어떤 중요한 개념이었는지 요약 정리로 간략히 보고 넘어가야겠다.

(1) SOLID

SRP : 단일 책임 원칙 (Single Responsibility Principle)

하나의 클래스는 하나의 책임만 가져야 한다.

OCP : 개방-폐쇄 원칙 (Open / Closed Principle)

소프트웨어 요소는 확장(구현체 추가)에는 열려 있으나 변경(코드 수정)에는 닫혀 있어야 한다.

LSP : 리스코프 치환 원칙 (Liskov Substitution Principle)

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀수 있어야 한다.

ISP : 인터페이스 분리 원칙 (Interface Segregation Principle)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

DIP : 의존 관계 역전 원칙 (Dependency Inversion Principle)

프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.

이들 중, 제어 역전과 관련해서 밀접하게 연관이 있었던 것은 OCPDIP였다.저 둘은 서로 다른 것을 가리키는 것 같지만 자세히 보면 겹치는 부분이 있다. 바로 기능의 확장성이다.

지난 포스팅에서 코드를 수정함에 있어, 클라이언트까지 수정해야 되는 코드는 좋은 설계가 아니라고 정리했다. 수정이 많아지는 코드는 결국 기능의 확장에 있어 소극적이게 된다.

객체 지향의 핵심은 다형성이다. 다형성을 통해 객체의 기능 확장을 꾀할 수 있지만, 결국에는 구현 객체에 의존하게 되는 현상이 눈에 띄게 자주 보였다. 구현이라는 것은 결국 생명주기의 제어를 개발자가 쥐고 있다는 것을 의미한다고 생각하기 때문에 이 부분을 근원적으로 대처할 수 있는 방법에 대해 공부해야 한다.

(2) 제어 역전 실현

앞서 언급한 객체의 생명주기 제어를 개발자가 쥐는 것은 그리 좋은 코드가 아니다. 그럼 개발자가 아닌(정확히는 개발자의 몫이 아닌) 다른 누군가 해당 객체를 관리해야 한다는 뜻인데(그러는 동안 개발자는 해당 객체의 로직과 동작 방식에만 집중) 그것을 스프링에서는 누가 어떻게 하는 것일까.

우선, 위의 내용이 곧 제어 역전 실현(IoC)임을 다시 되새겨보자. 그리고 그 제어 역전 실현의 방법들 중에서 대표적인 방법은 의존성 주입(DI)이었다. 의존성 주입을 통해 제어의 역전을 이끌어내고 이를 통해 의존 관계를 약화시킨다. 의존성 주입을 통해 유연성이 확장된 것은 알겠는데, 누가 그 객체의 생명주기를 쥐는 것일까? 그 역할을 하는 것이 바로 스프링 컨테이너다.

2) 스프링에서의 실현, 컨테이너와 빈

스프링에서의 제어 역전을 통한 객체의 생명주기 제어권은 스프링 컨테이너가 쥐게 된다. 또한, 객체의 의존성 관리 역시 컨테이너가 맡게 되는데, 이때 컨테이너에 등록된 객체를 스프링 빈이라고 한다.

작성에 앞서, 구별해야 할 것이 있는데 개념은 똑같이 적용되지만 코드의 실제 작성에 있어 스프링과 스프링 부트에서의 적용이 다르다. 스프링에서는 @Bean을 사용하는 클래스에 @Configuration 어노테이션을 부여하여 해당 클래스에서 빈을 등록하고자 함을 명시하고, 내부에 메소드 등을 작성해서 개발자가 직접 @Bean 어노테이션을 부여해서 빈의 생성을 제어할 수 있다.

여담으로 @Configuration 어노테이션은 스프링 컨테이너에게 해당 클래스가 하나 이상의 빈(Bean)을 정의하고 있음을 알려준다. @Configuration 어노테이션이 붙지 않은 클래스의 메소드에 @Bean 어노테이션을 붙여도 동작은 하나, 후술할 싱글톤을 보장받으려면 @Configuration를 잊지 말자.

// 스프링에서의 빈 등록 및 빈 생성

@Configuration
public class AppConfig {

    @Bean
    public UserService userService() {
        return new UserServiceImpl();
    }
    
    // ...
}

반면, 스프링 부트에서의 컨테이너와 빈은 대부분 자동적으로 설정된다. @SpringBootApplication 어노테이션을 통해 관련된 설정들이 자동적으로 구성되며, 톰캣 등의 내장된 서블릿 컨테이너가 존재하기 때문에 굳이 별도의 외부 컨테이너 클래스를 정의할 필요가 없다. 또한, 클래스 경로와 설정을 기반으로 자동으로 빈을 등록하고 필요한 구성을 처리한다.

// 스프링부트에서는 별도의 외부 컨테이너 작성 및 빈 등록이 불필요

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
    
    // ...
}

이 점을 명시하고 컨테이너와 빈 개념에 대해 좀 더 파고들기.

(1) 스프링 컨테이너

앞서 말했듯이 스프링에서 제어 역전을 실현하기 위해 사용 객체들의 생명주기 제어권을 위임받는 대상이 스프링 컨테이너다. 스프링 컨테이너의 역할은 크게 두 가지다.

First. 스프링 빈 등록 및 생명주기 관리

Second. 스프링 빈 의존성 주입

자바 스프링 프레임워크에서는 위의 역할을 수행할 수 있도록, 별도의 BeanFactory 클래스가 작성되어 있다. 정말 순수하게 IoC를 실현하는 기본 기능에 초점을 맞춘 컨테이너 클래스를 의미하는데, 이것에 더해서 소스 설정 및 프로퍼티 값 조회, 메시지 설정 파일의 국제화 등의 추가 기능이 덧붙여진 ApplicationContext 클래스로 생성할 수 있다.

// 스프링 컨테이너에 빈 등록 및 관리 시작
// AppConfig 클래스에 @Configuration 어노테이션 적용

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

기존에는 xml 파일로 직접 등록하고 관리한 것과는 다르게 ApplicaionContext 클래스는 스프링 컨테이너 클래스(AppConfig)에 있는 빈들을 자동 등록하고 관리를 시작하며, 빈 정보 조회 관련 메소드를 제공하게 된다.

(2) 스프링 빈

스프링 컨테이너가 관리 대상으로 삼도록 등록된 객체를 스프링 빈이라고 하며, 통상 컨트롤러, 서비스, 레포지토리 등의 핵심 로직들이 관리된다. 빈을 등록하는 방법은 직접 설정하는 방법컴포넌트 스캔 방법이 존재한다.

직접 설정(@Bean 어노테이션)

통상의 스프링 프레임워크에서는 컨테이너에게 빈 등록 의도를 선언한 AppConfig 등으로 명명된 클래스 내부에 작성된 메소드 등에게@Bean 어노테이션을 부여함으로써 빈 등록이 이루어진다.

// 스프링에서의 컨테이너 및 빈

@Configuration
public class AppConfig {

    @Bean
    public UserService userService() {
        return new UserServiceImpl();
    }
    
    // ...
}

앞서 봤던 위의 작성된 자바 코드가 스프링 컨테이너에서 빈을 직접 등록하는 방법에 해당한다.

컴포넌트 스캔(@Component 어노테이션)

스프링 부트는 앞서 말했듯이 별도의 스프링 컨테이너 클래스 작성이 불필요하다고 했었다. 스프링 빈이라 함은 프로그램의 핵심 로직들이 등록되는 것인데, 스프링 컨테이너 작성이 내부에서 자동적으로 이뤄지는 스프링 부트의 빈 등록은 핵심 로직 클래스들에게 @Component 어노테이션이 부여되면서 빈으로 등록된다.

@Service
public class MyService {

	private final MyRepository myRepository;

	public MyService(MyRepository myRepository) {
		this.myRepository = myRepository;
	}
    
    // ...
}


@Controller
public class MyController {

	private final MyService myService;

	public MyService(MyService myService) {
		this.myService = myService;
	}
    
    // ...
}

위의 예시에서는 @Component가 없고, @Service@Controller 어노테이션이 붙여져 있는데, 여기에 등장하지 않은 @Repository 어노테이션까지 더불어서 데이터 엑세스 계층에서 쓰이는 사용되는 빈을 등록할 때 쓰이는 어노테이션이며, 내부적으로 @Component 어노테이션 기능을 갖추고 있다.

이렇게 어노테이션을 파고들면 @Component 어노테이션이 부여되어 있음을 확인할 수 있다.

(3) 싱글톤 레지스트리

스프링 컨테이너는 빈의 생명주기를 관리하고 의존성을 주입해주는 역할을 맡았는데, 생각나는 의문점이 있다.

  1. 만약 레포지토리를 의존성 주입으로 서비스에서 받아 컨트롤러에서 CRUD 작업을 수행함에 있어, 요청이 3번 이뤄지면 3번의 레포지토리 인스턴스가 생성돼서 상태 공유가 이뤄지지 않을 텐데?

  1. 앞서 언급한 다수의 요청 횟수만큼 인스턴스 개수가 생성되는데 만약 동일한 요청이 여러 번 반복되면, 그것에 대한 자원 소모와 낭비가 심할 텐데?

스프링 프레임워크는 위의 예상되는 우려 사항에 대한 대비책으로 모든 빈을 싱글톤으로 관리한다. 싱글톤은 간단하게 말해서 클래스가 단 하나의 인스턴스만을 갖도록 하는 것이다. 즉, 다수의 요청에 있어 오로지 하나의 인스턴스가 처리하고 그 상태에 대하여 요청 단위마다 공유, 확인할 수 있는 것이다.

순수 자바 코드로는 싱글톤을 다음과 같이 작성할 수 있다.

public class Singleton {
    // private 정적 변수로 싱글톤 인스턴스 저장
    private static Singleton instance;

    // private으로 생성자를 선언해서 외부의 인스턴스 생성 방지
    private Singleton() {
    }

    // 싱글톤 인스턴스 반환용 정적 메소드
    public static Singleton getInstance() {
        // 인스턴스 생성 여부 확인 후, 싱글톤 인스턴스 반환
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }

    // ...
}

핵심은, 무분별한 생성자를 통해 싱글톤 인스턴스를 다시 생성할 수 없고 오로지 딱 한 번 생성돼서 필드에 저장된 인스턴스를 반환하는 정적 메소드를 통해서만 싱글톤 인스턴스를 조회할 수 있는 것이다. 정적 변수에 싱글톤 인스턴스를 저장하는 이유는 클래스 단위의 단일 생성 및 접근성을 제공하기 위해서다.

다만, 위의 싱글톤 패턴은 단점이 매우 명확하다.

  1. 상속이 불가능하므로 다형성 구현이 불가능하다.
  2. 전역 상태를 가지므로 객체지향 프로그래밍에 부합하지 않는다.
  3. 서버의 클래스 로더 및 JVM의 세팅에 따라 싱글톤 클래스도 복수의 인스턴스를 지닐 수 있다.
  4. 테스트 코드 작성이 매우 난해하다.

스프링 프레임워크는 이런 싱글톤 패턴의 단점을 극복하기 위해서 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공하는 싱글톤 레지스트리를 채택한다.

기존의 싱글톤 패턴은 정적 메소드와 private 생성자를 사용해야만 하는 비유연적이고 비정상적인 클래스가 아닌, 단순한 자바 클래스를 싱글톤으로 활용하게 해줌으로써 public 생성자를 구현할 수 있어서 생성자 기반 의존성 주입이 가능하고, 테스트 목적의 모의 객체도 생성이 가능해진다.

이를 통해서 무분별한 인스턴스 생성으로 불필요한 메모리 자원 낭비를 막고, 객체의 상태 공유를 이끌어내서 저장된 데이터의 업데이트 반영을 이뤄냄과 함께, 싱글톤 패턴과 달리 스프링이 목적으로 삼는 객체지향 프로그래밍을 추구할 수 있으며, 싱글톤 패턴 외의 다른 디자인 패턴 등을 적용하는 것에 있어 아무런 제약이 없어지게 된다.

profile
scientia est potentia / 벨로그 이사 예정...

0개의 댓글