스프링 삼각형
스프링을 이해하는 데는 POJO(Plain Old Java Object)를 기반으로 스프링 삼각형이라고 불리는 IoC/DI, AOP, PSA의 이해가 필수이다.
스프링 프레임워크는 스프링 삼각형의 조합으로 이해할 수 있다.
IoC/DI
IoC (Inversion Of Control : 제어의 역전) 이라고도 하는 DI(Dependency Injection : 의존성 주입)을 알아보자.
우선 의존성이란 무엇일까?
의존성
의존성은 new이다.
의사 코드 : 운전자가 자동차를 생산한다.
코드 : new Car();
의사 코드 : 자동차는 내부적으로 타이어를 생산한다.
코드 : Car 객체 생성자에서 new Tire();
Car는 Tire에 의존한다.
즉 전체가 부분에 의존한다.
의존관계
집합 관계 (Aggregation) : 부분이 전체와 다른 생명주기를 가진다.
집 - 냉장고
구성 관계 (Composition) : 부분은 전체와 같은 생명주기를 가진다.
사람 - 심장
사실 의존관계는 new 뿐만 아니라 변수에 값을 할당하는(=) 모든 곳에 생긴다.
의존 대상을 구현하고 배치할때 응집도는 높이고 결합도는 낮추는 기본 원칙에 충실해야한다.
순수 Java Code - 의존 관계 표현
클래스 다이어그램 & 시퀀스 다이어그램
Java Code
interface Tire {
String getBrand();
}
public class KoreaTire implements Tire {
public String getBrand(){
return "코리아 타이어";
}
}
public class AmericaTire implements Tire{
public String getBrand(){
return "미국 타이어";
}
}
public class Car {
Tire tire;
public class Car(){
tire = new KoreaTire();
}
public String getTireBrand(){
return "장착된 타이어 : " + tire.getBrand();
}
}
자동차는 타이어에 의존한다.
public class Driver {
public static void main(String[] args){
Car car = new Car();
System,out.println(car.getTireBrand());
}
}
운전자는 자동차를 의존한다(= 사용한다)
public class CarTest{
@Test
public void CarTireBrandTest(){
Car car = new Car();
assertThat(car).isEqualTo("장착된 타이어 : "+ car.getTireBrand());
}
}
스프링 없이 의존성 주입 - 1. 생성자 주입
의사 코드 : 운전자가 타이어를 생산한다.
코드 : Tire tire = new KoreaTire();
의사 코드 : 운전자가 자동차를 생산하면서 타이어를 장착한다.
코드 : Car car = new Car(tire);
의존성 주입 : 외부에서 생산된 타이어를 자동차에 장착한다.
생성자를 통해 의존성을 주입할 수 있다. (setter, 필드 주입도 있다.)
바뀐 코드
public class Car {
Tire tire;
public Car(Tire tire){
this.tire = tire;
}
public String getTireBrand(){
return "장착된 타이어: " + tire.getBrand();
}
}
생성자에 인자가 추가되었다.
public class Driver {
public static void main(String[] args){
Tire tire = new KoreaTire();
Car car = new Car(tire);
System,out.println(car.getTireBrand());
}
}
기존 코드에서는 Car가 구체적으로 KoreaTire을 사용할지, AmericaTire을 사용해야할지 결정해야한다.
Car은 KoreaTire과 AmericaTire 각각에 대해 알고있어야한다.
현재 코드는 생성자를 통해 의존성을 주입한다.
Car는 어떤 타이어를 사용할지 결정할 필요가 없다.
Car는 주입받은 타이어를 표준화된 규약(=인터페이스)에 따라 사용하기만 하면 된다.
운전자에게 어떤 타이어를 사용할지에 대한 결정을 넘긴다.
=> 어떤 Tire을 사용하더라도 Tire 인터페이스를 구현하기만 한다면 Car 코드 변경없이 확장할 수 있다.
적용된 원칙 및 DP(디자인 패턴)
OCP : 소프트웨어 개체는 확장에 대해 열려있어야하고, 수정에 대해서는 닫혀있어야한다.
Tire 인터페이스를 사용함으로써 Tire의 구현체가 변경되거나 새로운 Tire 구현체가 추가되더라도 Car 코드를 변경할 필요가 없다.
전략 패턴
- 전략 : Tire
- 컨텍스트 (전략을 사용) : Car.getBrand()
- 클라이언트 : Driver의 main()
스프링 없이 의존성 주입 - 2. setter 주입
의사 코드 : 운전자가 타이어를 생산한다.
코드 : Tire tire = new KoreaTire();
의사 코드 : 운전자가 자동차를 생산한다.
코드 : Car car = new Car();
의사 코드 : 운전자가 자동차에 타이어를 장착한다.
코드 : car.setTire(tire);
setter 주입을 이용하면 운전자가 원할때 타이어를 교체할 수 있다.
현재는 setter 주입보다는 생성자 주입을 선호한다.
프로그램에서 한번 주입된 의존성은 바뀌지 않고 거의 계속 사용하기 때문이다.
클래스 다이어그램 & 시퀀스 다이어그램
바뀐 코드
public class Car {
Tire tire;
public Car(Tire tire){
this.tire = tire;
}
public Tire getTire(){
return tire;
}
public Tire setTire(Tire tire){
this.tire = tire;
}
public String getTireBrand(){
return "장착된 타이어: " + tire.getBrand();
}
}
getter, setter 만들기
public class Driver {
public static void main(String[] args){
Tire tire = new KoreaTire();
Car car = new Car();
car.setTire(tire);
System,out.println(car.getTireBrand());
}
}
스프링을 통해 의존성 주입하기
책에서는 xml 파일을 사용하고 setter로 의존성을 주입했다.
@Configuration은 xml 설정 파일 없이도 빈을 등록하고 설정할 수 있다.
그래서 여기서는 @Configuration과 생성자 주입을 이용해서 정리했다.
@Component, @Service 등의 어노테이션을 사용할 수도 있다.
설정 클래스 - 빈 등록
@Configuration
public class AppConfig {
@Bean
public Tire tire() {
return new KoreaTire(); // 다른 Tire 구현체로 교체 가능
}
@Bean
public Car car() {
return new Car(tire()); // 생성자 주입
}
}
빈 이름은 메서드 이름이다. (name으로 따로 지정도 가능하다.)
애플리케이션
public class Driver {
public static void main(String[] args){
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
Car car = context.getBean("car", Car.class);
System.out.println(car.getTireBrand());
}
}
getBean()
으로 스프링 IoC 컨테이너에서 car라는 이름으로 등록된 Car 타입의 빈을 가져온다.
스프링 IoC 컨테이너 : 객체의 생성과 관리, 의존성 주입을 담당
스프링 프레임워크를 이용해서 의존성을 주입하면 Car과 Tire 앞에 종합 쇼핑몰(Spring Framework)가 들어온 것 외에 달라진게 없다.
하지만 스프링을 이용하여 의존성을 주입하면 타이어가 달라지더라도 설정 파일(@Configuraiton)에서 빈의 구현체만 변경해주면 된다.
@Autowired 란?
Spring에게 의존성 주입이 필요한 생성자를 알려준다.
xml에서 굳이 property 설정을 하지 않아도 자동으로 함께 묶어준다.
기존 xml
<bean id="car" class="com.example.Car">
<constructor-arg ref="tire" />
</bean>
<bean id="car" class="com.example.Car">
<property name="tire" ref="tire" />
</bean>
property tag를 이용해 의존성 주입을 한다.
@Autowired 사용 후
<bean id="car" class="com.example.Car">
<constructor-arg ref="tire" />
</bean>
<bean id="car" class="com.example.Car"> </bean>
property 부분이 사라졌다.
public class Car {
@Autowired
Tire tire;
public String getTireBrand(){
return "장착된 타이어: " + tire.getBrand();
}
}
@Autowired를 통해 car의 property를 자동으로 엮어줄 수 있다.
@Autowired는 생성자 주입과 setter 주입, 필드 주입에서 사용된다.
@Autowired - 생성자 주입
- 생성자가 하나만 있다면 @Autowired를 생략할 수 있다.
@Component
public class Car {
private final Tire tire;
private final Engine engine;
@Autowired
public Car(Tire tire) {
this.tire = tire;
this.engine = null;
}
public Car(Engine engine) {
this.tire = null;
this.engine = engine;
}
}
- 두개 이상의 생성자가 존재할 경우, 의존성을 주입하고 싶은 생성자에만 @Autowired를 사용한다.
@Autowired - setter 주입
@Component
public class Car {
private Tire tire;
// @Autowired 필요
@Autowired
public void setTire(Tire tire) {
this.tire = tire;
}
}
- setter 주입에서 @Autowired를 사용하지 않는 경우 의존성이 주입되지 않는다.
@Autowired - 필드 주입
@Component
public class Car {
@Autowired
private Tire tire;
}
필드 주입을 하기 위해서는 무조건 @Autowired를 붙여야한다.
참고 : @Resource
@Resource는 자바가 제공하는 의존성 주입 어노테이션이다.
@Autowired과 같은 역할을 하지만 @Autowired는 타입을 우선으로 매칭하지만 @Resource는 이름(id)를 우선으로 매칭한다.
Reference
https://m.yes24.com/Goods/Detail/17350624
+ 책 내용 기반으로 추가적으로 내용을 작성했는데 오개념이 있다면 지적 부탁드립니다~!!
+ 추가 정리
스프링은 왜 IoC/DI를 제공하는 것일까?
자바 프로그래밍으로서 객체지향의 한계
자바 프로그래밍의 다형성만으로는 OCP, DIP를 지킬 수 없다.
MemoryRepository memoryRepository = new JDBCMemoryRepository();
MemoryRepository라는 인터페이스를 사용했지만 실제로는 JDBCMemoryRepository 구현 객체에 의존하고 있다 (알고있다).
구현 객체 변경시에 기존 코드도 변경해야한다. 이는 OCP, DIP를 위반한다.
스프링은 객체 지향 프레임워크이다.
스프링은 객체 지향 프레임워크의 다형성을 극대화하고 OCP, DIP를 지키기 위해 IoC/DI를 지원한다.
IoC는 객체의 생성과 관리를 개발자가 직접 하는 것이 아니라 스프링 컨테이너에게 위임하는 것을 말한다.
DI는 객체 간의 의존 관계를 개발자가 직접 설정하는 것이 아니라 스프링 컨테이너가 runtime에 의존 객체를 주입해주는 것을 말한다.
POJO는 가장 단순한 객체지향적인 개발 모델이다.
자바의 기술이 발전해가면서, 객체지향언어의 특성을 점차 잃어버렸다.
스프링은 POJO를 지향하기 위해 등장했다.
아래에서 더 자세히 알아보자.
다형성
다형성은 하나의 인터페이스나 추상 클래스를 통해 다양한 구현 클래스를 사용할 수 있는 특성을 말한다.
IoC (Inversion Of Control)
제어의 역전을 의미하며 객체의 생성과 관리를 개발자가 직접 하는 것이 아니라 스프링 컨테이너에 위임하는 것을 말한다.
스프링 컨테이너는 객체의 생명주기를 관리하고 객체 간의 의존관계를 설정한다.
IoC를 통해 개발자는 객체 생성과 관리에 대한 부담을 줄이고, 비즈니스 로직에 집중할 수 있다.
DI (Dependency Injection)
의존성 주입으로 IoC의 구현 방식 중 하나이다.
객체 간의 의존관계를 스프링 컨테이너가 자동으로 연결해주는 것이다.
스프링 컨테이너는 설정 정보(XML, 어노테이션, Java Config)를 바탕으로 객체 간의 의존 관계를 주입한다.
생성자 주입, 수정자(setter)주입, 필드 주입이 있다.
DI는 객체 간의 의존 관계를 주입하는 방식을 결정하는 것이다.
코드로 보는 IoC/DI
public interface UserService {
void registerUser(User user);
}
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void registerUser(User user) {
userRepository.save(user);
}
}
IoC (제어의 역전)
UserServiceImpl은 UserRepository에 의존한다.
하지만 UserServiceImpl은 UserRepository를 직접 생성하지 않고, 생성자를 통해 외부에서 주입받고 있다.
객체의 생성(new)과 관리를 UserServiceImpl이 직접 하지 않고, 외부(스프링 컨테이너)에게 위임하는 것이 IoC이다.
스프링 컨테이너는 UserServiceImpl 인스턴스 생성시, 필요한 UserRepository 인스턴스도 함께 생성하고 관리한다.
DI (의존성 주입)
@Autowired 생성자는 스프링 컨테이너가 UserServiceImpl의 인스턴스 생성시에 자동으로 UserRepository의 인스턴스를 주입해준다.
이렇게 객체 간의 의존관계를 스프링 컨테이너가 자동으로 연결해주는 것을 DI라고 한다.
생성자 주입 외에도 수정자 주입, 필드 주입등의 방법이 있다.
의존성 주입은 제어의 역전 구현 방식 중 하나이다.
IoC와 DI는 객체지향의 다형성을 극대화한다
IoC와 DI를 이용하면 인터페이스를 기반으로 프로그래밍할 수 있다.
public interface UserRepository {
void save(User user);
}
public class MySQLUserRepository implements UserRepository {
@Override
public void save(User user) {
System.out.println("MySQL에 사용자 저장: " + user.getName());
}
}
public class MongoDBUserRepository implements UserRepository {
@Override
public void save(User user) {
System.out.println("MongoDB에 사용자 저장: " + user.getName());
}
}
UserRepository 인터페이스(역할)을 구현하는 여러 구현체가 있다.
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void registerUser(User user) {
userRepository.save(user);
}
}
UserServiceImpl은 UserRepository 역할을 하는 어떠한 구현체든 입력받을 수 있다.
@Configuration
public class AppConfig {
@Bean
public UserRepository mySQLUserRepository() {
return new MySQLUserRepository();
}
@Bean
public UserRepository mongoDBUserRepository() {
return new MongoDBUserRepository();
}
@Bean
public UserService userService() {
// UserRepository 구현 클래스 선택
UserRepository userRepository = mySQLUserRepository();
// UserRepository userRepository = mongoDBUserRepository();
return new UserServiceImpl(userRepository);
}
}
스프링에 각 구현체를 빈으로 등록하고 userService에서 원하는 userRepository 구현 클래스를 선택하여 생성자로 주입한다.
클라이언트(컨트롤러)
@Controller
public class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/register")
public String registerUser(@RequestBody User user) {
userService.registerUser(user);
return "User registered successfully";
}
}
클라이언트인 UserController는 UserService 인터페이스에만 의존하고, 실제로 어떤 UserRepository 구현 클래스가 사용되는지는 스프링 설정에 따라 결정된다.
다형성을 활용하여 코드의 유연성과 확장성을 높인다.
'Spring > 객체지향' 카테고리의 다른 글
스프링 삼각형 : AOP(Aspect-Oriented Programming), PSA(Portable Service Abstraction) (2) | 2024.04.18 |
---|---|
코드에 SRP 원칙 적용 후 Mock 테스트 작성하기 (0) | 2024.04.06 |
객체 지향과 디자인 패턴 (0) | 2024.04.02 |
객체 지향 설계 5원칙: SOLID (0) | 2024.04.01 |
Java가 확장한 객체 지향 (abstract, 생성자, static, final, this, super) (0) | 2024.03.26 |