Spring Core

디우·2022년 6월 23일
0

우아한테크코스 레벨2 기간에 학습한 Spring Core 강의 내용을 정리한다.


Spring Core

객체는 "의존 관계"를 가진다. (의존 대상 B가 변하면 그것이 A에 영향을 미친다.) 스프링은 이러한 관계를 관리하는데 모든 관계를 관리하는 것은 아니다. 즉, 스프링은 개발자를 대신해서 "객체들을 생성하고 관리"하는 일을 해주지만 모든 객체를 관리해주는 것은 아니다.

세 가지 질문, 그리고 답

  1. 스프링이 관리할 객체를 "어떻게(how)" 지정할까?
    -> 컨테이너에 빈을 등록함으로써 관리할 수 있고, 그 방법에는 XML, 어노테이션, 자바코드 3가지 방법이 존재한다.
  2. "어떤 객체(hwat)"를 스프링이 관리하게 해야 할까?
    -> 스프링이 가이드해주는 것은 없다. 즉, 규정이 없다. 따라서 개발자들이 본인의 애플리케이션에 맞게 규정해야한다.
    여기서 우아한테크코스 BE 4기 크루들이 "어떤 객체를 스프링이 관리하게 해야 할까?" 라는 주제로 토론한 내용을 담은 Discussion이 있어 첨부한다.
    우아한테크코스 BE 4기 크루들의 토론 내용
  • 본인이 생각하는 스프링 빈으로 관리해야하는 객체에 대한 기준은 다음과 같다.
    • 스프링 빈으로 만들 객체에 대한 기준
      -> 상태를 가지는 객체는 빈으로 등록하지 않는다.
      -> 왜? 빈으로 등록하면 싱글톤으로 관리되는데 여러 클라이언트에서 상태를 공유하게 된다.
    • 어플리케이션에서 사용할 인터페이스의 구현체가 빈으로 등록되어야 한다.
    • 스프링은 빈을 싱글톤으로 관리하므로 상태가 없는 객체를 빈으로 등록한다. (스코프를 싱글톤으로 하는 경우)
    • 어플리케이션 실행 시점에 결정되는 것은 빈으로 등록하고, 실행시간(특정 시점)에 객체가 생성되고 소멸되는 경우는 빈으로 등록하지 않는다.
  1. "왜(why)" 객체를 스프링이 관리해야 할까?
    -> 빈으로 등록한 객체는 우리가 직접 관리를 하지 않고 Spring IoC Container가 관리하게 된다. 즉, 객체의 생성을 위임할 수 있고, 의존 관계를 관리(의존성 주입등)하는 책임을 위임할 수 있다. 또한 객체의 생명주기를 컨트롤하는 책임을 위임할 수 있다.

어떤 객체를 스프링이 관리해야 할까?

  • 상태를 지니지 않는 객체는 스프링 빈으로 관리한다.
    -> 만약 상태를 가지고 있는데 빈으로 등록해야하는 상황이라면?
    : 싱글톤이 안되게 bean scope를 변경해보거나 불필요한 상태를 제거해볼 수 있겠다.

  • 도메인을 빈으로 등록하면 안되나?
    대부분 도메인을 스프링 빈으로 등록해서 사용하지 안흔ㄴ데 그 이유는?
    -> 도메인은 상태를 가지고 있기 때문에
    -> 프레임워크에 종속적이지 않게 하기 위해서
    -> 도메인 객체는 우리가 생명주기를 컨트롤 해야하기 때문에

  • 인스턴스가 단 하나만 존재해야 하는 객체는 스프링 빈으로 등록하면 좋다.
    그러면 스프링 빈은 모두 하나만 생성되어서 관리되나?
    -> 빈 스코프 라는 개념이 존재한다.
    -> signleton, prototype, request, session, global session

    • singleton : 하나의 Bean 정의에 대해서 Spring Ioc Container 내에 단 하나의 객체만 존재한다.
    • prototype : 하나의 Bean 정의에 대해서 다수의 객체가 존재할 수 있다.
    • request : 하나의 Bean 정의에 대해서 하나의 HTTP request의 생명주기 안에 단 하나의 객체만 존재한다. 즉, 각각의 요청에는 자신만의 하나의 객체를 가진다.
    • session : 하나의 Bean 정의에 대해서 하나의 HTTP Session의 생명주기 안에 단 하나의 객체만 존재한다.
    • global session : 하나의 Bean 정의에 대해서 하나의 global HTTP Session의 생명 주기 안에 단 하나의 객체만 존재한다.

Service에서 Dao에 의존하기, 어떻게?

  • 직접 의존 추가하기
    Service가 Dao가 많아지면 중복코드가 발생하게 된다.
    (변경 발생 -> 모든 코드 수정)

    @Service
    public class ChessService {
    	private ChessDao chessDao;
        
        public ChessService() {
        	DataSource dataSource = new DataSource() {...};
            JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
            this.chessDao = new ChessDao(jdbcTemplate);
        }
        
        public Stirng get() {
        	return chessDao.select();
        }
    }
  • 컨테이너?
    -> 컨테이너에는 스프링 빈이 담긴다.
    -> 스프링 컨테이너는 스프링 빈을 담는 그릇과 같다.

즉, 우리는 컨테이너에 빈을 등록함으로써 의존성 주입을 Spring에게 책임을 넘길 수 있다.

@Repository
public class ChessDao {
	private JdbcTemplate jdbcTemplate;
    
    public ChessDao(JdbcTemplate jdbcTemplate) {
    	this.jdbcTemplate = jdbcTemplate;
    }
    
    public String select() {
    	return jdbcTemplate.queryForObject(...);
    }
}
@Repository
public class ChessBoardDao {
	private JdbcTemplate jdbcTemplate;
    
    public ChessBoardDao(JdbcTemplate jdbcTemplate) {
    	this.jdbcTemplate = jdbcTemplate;
    }
    
    public String select() {
    	return jdbcTemplate.queryForObject(...);
    }
}

Configuration Metadata

Configuration Metadata라는 설정을 통해서 우리가 작성한 자바 객체들 중에 컨테이너가 관리할 빈들을 생성해서 Application이 동작할 수 있는 준비를 할 수 있다.
즉, 객체 관리 대상을 알려주고, 의존성을 정의하면 이게 바로 Metadata이고 이를 기반으로 을 생성할 수 있는 것이다.

이런 COnfiguration Metadata는 어떻게 설정할까? (컨테이너 설정은 어떻게?)
앞서 언급한 것과 같이 3가지 방법이 존재한다.
1. Annotation-based configuration (어노테이션 기반의 설정)
2. XML
3. Java-based configuration

컨테이너 설정 방법, 즉 빈을 등록하는 방법에는 여러 방법들 중에서 어노테이션 기반의 설정인 @ComponentScan@Component를 이용하여 빈을 등록한다.
흔히 사용하는 @Controller, @Service, @Repository는 Component 어노테이션의 한 부분으로 해당 어노테이션의 코드를 보면 @Component가 포함되어 있음을 알 수 있다.

@ComponentScan 어노테이션은 @Component 어노테이션이 부여된 class들을 자동으로 scan하여 빈으로 등록해주는 역할을 하는 어노테이션이다. @ComponentScan 어노테이션을 통해 등록할 빈을 스캔할 classpath를 지정할 수 있다.

보통 프로젝트의 제일 상단에 존재하는 main함수를 포함한 클래스에 @SpringBootApplication 어노테이션을 달아주는데, 이 때 @SpringBootApplication 내부에는 @ComponentScan 어노테이션이 포함되어 있어 해당 클래스를 포함하여 하위의 패키지들 모두를 탐색하여 @Component 어노테이션이 붙어있는 클래스를 빈으로 등록한다. 따라서 @SpringBootApplication 어노테이션이 붙은 클래스의 위치는 중요하다. (상위 패키지에 존재하는 @Component가 붙은 클래스는 빈으로 등록하지 못한다.)


스프링에서 의존성 주입 방법

스프링에서 의존성을 주입하는 방법으로는 크게 3가지가 존재한다.

첫번째 방법은 생성자 기반의 의존성 주입방법으로 다음과 같다.

public class TestClass {
	private final TestDao testDao;
    
    public TestClass(TestDao testDao) {
    	this.testDao = testDao;
    }
}

(Spring 4.3 부터 생성자가 하나인 경우 @Autowired 어노테이션을 생략할 수 있다.)

두번째 방법은 Setter기반의 의존성 주입이다.

public class TestClass {
	private final TestDao testDao;
    
    @Autowired
    public void setTestDao(TestDao testDao) {
    	this.testDao = testDao;
    }
}

세번째 방법은 필드 주입 방법인데, 스프링에서는 필드 주입을 비추천하고 있다.

Spring Team recommends: “Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies”.

public class TestClass {
	private final TestDao testDao;
    
    @Autowired
    private TestCatalog testCatalog;
    
    @Autowired
    public TestClass(TestDao testDao) {
    	this.testDao = testDao;
    }
}

필드 주입의 경우 매우 간단하게 사용할 수 있으므로 편리할 것 같은데 왜 비추천할까?
이에 대해서는 생성자 주입을 @Autowired를 사용하는 필드 주입보다 권장하는 이유을 참고하였다.

생성자 주입 방법을 권장하는 이유

1. 순환 참조를 방지할 수 있다.
여러 컴포넌트 간에 의존성이 생길 수 있다. 그 중에서도 A가 B를 참조하고, B가 다시 A를 참조하는 순환참조도 발생할 수 있는데, 생성자 주입을 사용하는 경우에는 BeanCurrentlyInCreationException이 발생하여 애플리케이션 구동을 미리 방지한다.
필드 주입의 경우에는 Setter 주입과 동일하게 먼저 빈을 생성한 후에 어노테이션이 붙은 필드에 해당하는 빈을 찾아 주입하는 방식이다. 하지만 생성자 주입의 경우에는 생성자로 객체를 생성하는 시점에 필요한 빈을 주입한다. 즉, 먼저 생성자의 인자에 사용되는 빈을 찾은 후에 주입하려는 빈의 생성자를 호출한다.
그렇기 대문에 순환 참조는 생성자 주입에서만 문제가 된다. 객체 생성 시점에 빈을 주입하기 때문에 서로 참조하느 객체가 생성되지 않은 상태에서 그 빈을 참조하기 때문이다.

우테코 4기 크루 연로그 글을 보다가 필드 주입도 순환참조가 방지된다는 글을 보았다. Spring Boot 2.6에서 패치되어 필드 주입에 대해서도 순환 참조가 일어나면 서버 구동에 실패한다.

2. 테스트가 용이하다.
독립적으로 인스턴스화가 가능한 POJO이므로 DI 컨테이너를 사용하지 않고서도 단위 테스트에서 인스턴스화 할 수 있다.

3. 코드의 가독성
생성자 주입의 경우 의존성이 명시적으로 드러나는 장점이 있고, 필드 주입과 비교해서 무분별한 @Autowired 어노테이션의 사용을 막을 수 있다.

4. 불변성
필드 주입과 수정자 주입은 주입되는 필드에 대해서 final을 선언할 수 없지만, 생성자 주입의 경우에는 해당 필드에 final을 선언할 수 있다.

5. 오류 방지
필드 주입의 경우에는 final이 아니므로 값을 변경할 수 있다. 즉, null을 할당할 수도 있다. 생성자 주입은 이러한 오류를 사전에 방지할 수 있다.


크루들 질문

Q. 스프링에서 어노테이션을 많이 사용하는데 어노테이션이 동작하는 내부 구현 자바 코드를 확인해보고 싶을 때는 어떻게 찾아야 하나요?
A. 코드를 통해서만 그 원리를 이해하는 것은 미로찾기와 같다. 특히 스프링 같이 추상화된 프레임워크인 경우 더더욱 그렇다.

Spring MVC의 동작원리를 이해하는 것은 중요하다.
-> Dispatcher Servlet, Handler Mapping, Handler Adapter, View Resolver 등 각 키워드를 학습하여 Spring MVC의 전체적인 동작원리를 이해해야 한다. (해당 내용은 향후 포스팅 하는 글에서 다루도록 하겠습니다😃)

Q. 생성자에 Autowired를 안 붙이는 이유는?
A. 명시적으로 하고 싶을 때는 붙여도 되지만 생략가능하기 때문이다.

Q. 빈이랑 컴포넌트가 같은 건가요?
A. 빈은 추상화, 컴포넌트는 구현체

Q. 생성자 주입할 때 @Autowired를 좀 더 명시적인 목적으로 사용하기 보다 무조건 생략하는 것을 추천하는 건가요?
A. @Autowired를 사용하지 않고 의존성 주입 받는 것임을 알 수 있는 어노테이션이 있을까요? @Autowired를 사용하지 않으면 어떤 장점이 있을까요? DI는 스프링 프레임워크에서만 되나요?


주의사항

스프링 빈을 관리할 때 주의해야 할 사항이 있다. 여러 클라이언트가 "동시에" 요청을 보낼 수 있으므로 빈의 설정을 잘못하게 되면 여러 요청이 들어왔을 때 요청 간에 간섭이 발생할 수 있다.

즉, 상태를 가지며 공유하면 안되는 객체는 의존하지 않아야한다.

profile
꾸준함에서 의미를 찾자!

0개의 댓글