저번에는 Spring Boot 기초에 대해서 알아봤고, 이번 유닛에서는 Spring Container에 대해서 학습을 했다. 그 중에서도 DI(Dependency Injection)이라는 의존성 주입이라는 개념에 집중해서 학습했다. 학습했던 것들의 정리와 느낀점들을 다시 풀어보겠다.
SRP : 단일 책임 원칙 (Single responsibility principle)
한 클래스는 하나의 책임만 가져야 한다.
OCP : 개방-폐쇄 원칙 (Open/closed principle)
소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀 있어야 한다.
LSP : 리스코프 치환 원칙 (Liskov substitution principle)
프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
ISP : 인터페이스 분리 원칙 (Interface segregation principle)
특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
DIP : 의존관계 역전 원칙 (Dependency inversion principle)
프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안 된다.
스프링의 파일 구조
1) Controller
Controller는 API 요청을 매핑하는 클래스다. @Controller, @GetMapping 같은 어노테이션을 사용하는 클래스다. Controller 클래스의 단일 책임은 API 요청에 매핑하고 결과를 리턴하는 것이다. 이 과정에서 필요한 로직은 Service 클래스를 사용한다.
2) Service
Service는 비즈니스 기능을 수행하는 클래니다. 만약 회원가입 기능을 구현한다면 Controller 클래스를 통해 전달된 데이터의 유효성을 검증하고 중복회원 체크 등의 기능을 수행할 수 있다. 아래 Repository 클래스를 사용하여 데이터를 전달받고 로직 수행 후 결과를 Controller 클래스에 전달한다.
3) Repository
Repository는 DB에 원하는 데이터를 가지고 오고 저장하는 기능을 수행하는 클래스다. JPA 코드가 Repository에 작성이 되고 주로 SQL 관련 코드가 작성된다. Service에서 메소드를 호출하여 사용된다.
4) Domain
Domain은 앞서 배운 DTO, VO, Entity 코드를 모아두는 패키지이다.
5) Configuration
Configuration은 추후 배울 의존성 주입을 위한 스프링 어노테이션인 @Configuration을 사용하는 클래스다. 해당 클래스에서 스프링 컨테이너에 클래스를 등록한다.
Controller는 Service를 참조하고 Service는 Repository를 참조한다. 이 같은 참조 관계는 스프링 개발에서 매우 많이 등장하는 패턴이다. 그래서 위의 예시와 같이 스프링에서 기본으로 권장하는 패턴으로 만들고 이 참조 관계를 자동으로 정의해주기 위해서 @Service, @Repository를 만들었다.
new 키워드를 통한 참조가 일어난다면, API 요청이 올때마다 인스턴스를 새로 생성하게 된다. 객체지향의 특성상 객체를 쓰려면 생성을 해야 하기 때문에 요청마다 메모리에 새로운 인스턴스가 생성된다. 이를 해결하기 위해서는 인스턴스를 재사용한다. 그래서 new 키워드로 사용하는 것이 아닌 우리가 접근 가능한 곳에 미리 클래스를 인스턴스화 시켜두고 해당 인스턴스를 사용한다. 이것이 싱글톤 패턴이다.
서버 자원을 안정적으로 사용하기 위해 싱글 톤을 적용하였지만, 싱글 톤 패턴의 남발은 여러 문제가 발생한다.
싱글 톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 때, 다른 클래스의 인스턴스 간의 결합도가 높아져 "개방-폐쇄 원칙"을 위배하게 된다.
멀티스레드 환경에서 동기화 처리를 하지 않으면 인스턴스가 두 개가 생성되는 이슈가 발생할 수 있다.
스프링에서는 스프링 컨테이너를 만들어 모든 클래스를 스프링 컨테이너에서 관리하게 했으며, 스프링 컨테이너를 사용하면 스프링 프레임워크가 싱글 톤도 적용해주고 객체에 인스턴스를 전달해준다.
의존 관계
자바 애플리케이션에서 행동 방식을 제공하기 위해 객체가 다른 객체와 상호작용하는 경우, 예를 들어 A class가 B class의 메소드를 사용한다면, A class는 B class에 의존 관계를 맺는다고 할 수 있다. 이는 생성자를 통한 참조 관계를 포함하는 개념이다.
우리는 스프링 컨테이너를 사용해 의존 관계를 설정할 수 있다. 이를 통해서 개발의 생산성과 안정성을 보장받을 수 있다.
Bean
빈은 스프링 컨테이너에서 생성하고 관리하는 애플리케이션 객체를 뜻한다. @Configuration
클래스 안에 구현한 메소드에서 @Bean
어노테이션을 붙여 특정 객체를 스프링 컨테이너에 등록할 수 있다.
IoC (Inversion of Control : 제어의 역전)
제어 권한이 누구에게 있는지를 나타내는 용어다.
기존의 객체의 관리(생성과 의존 관계 설정)는 개발자가 직접 제어했으나, 스프링을 사용하면 객체 제어 권한을 스프링 컨테이너가 가지게 되어 어노테이션에 따라 자동으로 객체를 관리해주기 때문에 제어 주체가 역전됐다고 표현한다.
DI (Dependency Injection : 의존성 주입)
스프링 컨테이너를 통해 의존 관계를 설정하는 방식을 뜻한다. 기존의 new 키워드나 직접 싱글톤 패턴을 사용하지 않고 컨테이너를 통해 Bean 객체를 주입 받아 사용한다. 생성자나 세터를 통해 의존 관계를 주입한다.
우리가 만든 클래스를 Bean으로 만들어 주기 위해서는 스프링 컨테이너에서 어떤 클래스를 Bean으로 관리해야 하는지 알려줘야 한다. Bean 객체로 설정하는 방법은 2가지가 존재한다.
@Configuration
클래스의 @Bean
메소드를 통해 인스턴스를 등록하는 방법
//Controller class @RestController class UserController{ private UserService userSerivce; @Autowired public UserContorller(UserService userService){ this.userService = userService; } } //Service class public class UserService{ private UserRepository userRepository; public UserService(UserRepository userRepository){ this.userRepository = userRepository; } } //Service class public class UserService{ private UserRepository userRepository; public UserService(UserRepository userRepository){ this.userRepository = userRepository; } } //Repository class public class UserRepository{ ... } //Configuration @Configuration public class SpringConfig { @Bean public UserService userService(){ return new UserService(userRepository()); } @Bean public UserRepository userRepository(){ return new UserRepository(); } }
@Component
를 클래스에 직접 명시해서 설정하는 방법이 있다. (컴포넌트 스캔을 이용한다.) @Autowired
를 사용해서 주입받는다.
//Controller class @RestController class UserController{ private UserService userSerivce; @Autowired public UserContorller(UserService userService){ this.userService = userService(); } } //Service class @Service public class UserService{ private UserRepository userRepository; @Autowired public UserService(UserRepository userRepository){ this.userRepository = userRepository; } } //Repository class @Repository public class UserRepository{ //... }
@RestController
또한 스프링 빈에 등록해주고 있다. 내부 구현을 보면 @Component
가 작성되어 있어 @RestController
를 사용하면 스프링 컨테이너의 등록과 더불어 API 요청을 위한 부가 기능을 손쉽게 사용할 수 있다.
@Service
와 @Repository
또한 마찬가지로 @Component
를 포함하고 있어서 스프링 빈으로 등록할 수 있고 각 기능에 맞는 부가 기능을 쉽게 얻을 수 있다.
컴포넌트 스캔은 @Component
가 선언된 클래스 파일을 전부 찾아 스프링 빈으로 설정해주는 기능이다.
생성자 주입
생성자 인자를 통해 Bean 객체를 받아오는 방식이다. 보통 private final
을 이용해서 사용하는데 이 이유는 전역 변수로 사용하기 위해 이와 같이 선언한다.
생성자 주입의 경우 @Autowired
를 생략해도 되지만 생성자가 2개 이상일 때는 명시해주어야 한다.
final
키워드는 생성자 주입에서만 사용할 수 있다. 생성자 주입은 스프링에서 권장하는 DI 방법이며, 생성자 주입을 권장 하는 이유는 다음과 같다.
세터 주입
세터 주입은 전역 변수에 setter 메소드의 인자로 Bean 객체를 받아오는 방식이다. 생성자 주입과 다르게 주입받는 객체가 변경될 가능성이 있는 경우에 사용한다.
필드 주입
변수에 바로 @Autowired
키워드를 사용해 Bean 객체를 받아오는 방식이다. 테스트 코드를 작성하는 경우에만 제한적으로 사용하는 것을 권장한다.
컴포넌트 스캔 범위
Application 클래스가 있는 폴더부터 그 하위 폴더까지 컴포넌트 스캔을 진행한다.
저번 유닛에서는 API 요청에 대한 매핑에 대한 구현만을 했다면 지금은 객체지향적으로 더 생각을 해서 의존성도 주입하는 방법과 함께 실습도 진행했었다. 실습을 하면서 기능과 목적에 맞게 클래스를 나누고 그에 따른 스프링 어노테이션을 같이 사용하는 것을 실습을 했는데 생각보다 구현에 있어서는 어려움이 없었으며 오히려 객체 지향적으로 생각을 해서 기획을 하고 어떻게 나누어야 하는지에 대한 생각을 할 수 있었던 유닛이었다. 앞으로 스프링에 대해 더 많이 배울텐데 이 점들을 잘 참고하여 나중에 잘 기억해서 써먹을 수 있도록 하자. 습관화를 하는 것이 좋을 것 같다.