- POJO (Plain Old Java Object)
순수 Java 생성 객체 지향
- IoC / DI (Inversion of Control / Dependency Injection)
제어의 역전 / 의존성 주입
- AOP (Aspect Oriented Programming)
관심 지향 프로그래밍
- PSA (Portable Service Abstraction)
일관된 서비스 추상화
https://velog.io/@airoca/Spring-SpringSpring-Boot
IoC(Inversion of Control)란 제어의 역전이라는 개념으로, 객체의 생성과 그 객체 간의 의존성 관리를 개발자가 아닌 스프링 컨테이너가 대신하는 것을 의미한다.
DI(Dependency Injection)는 IoC의 한 형태로, 객체가 다른 객체를 필요로 할 때 그 의존성을 외부에서 주입하는 방법을 의미한다.
- 스프링 컨테이너란?
스프링 컨테이너는 스프링 프레임워크의 핵심 컴포넌트로, 자바 객체를 의미하는 Bean을 생성하고 관리하는 역할을 수행한다.
앞서 설명한 것과 같이, IoC란 객체의 생성과 의존성 관리를 스프링 컨테이너에 위임하는 것이다. 예시와 함께 살펴보자.
public class UserService {
private UserRepository userRepository;
public UserService() {
// UserService가 직접 UserRepository를 생성.
this.userRepository = new UserRepository();
}
public void saveUser(User user) {
userRepository.save(user);
}
}
IoC를 사용하지 않은 경우, 객체는 자신의 의존성을 스스로 생성한다. 이렇게 하면 코드의 결합도가 높아지고 테스트가 어려워진다.
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
// UserRepository를 외부에서 주입.
this.userRepository = userRepository;
}
public void saveUser(User user) {
userRepository.save(user);
}
}
IoC를 사용한 경우, 객체의 생성과 의존성 주입을 스프링 컨테이너가 관리하게 된다. 이를 통해 코드의 결합도가 낮아지고, 유지보수성과 테스트 용이성이 향상된다.
더 직관적으로 장점을 설명하면, 개발자가 객체의 생명 주기 관리 등의 복잡한 요소들을 신경 쓰지 않고, 비즈니스 로직에만 집중할 수 있게 된다.
얼핏 보면 비슷하다고 생각하는
나 같은사람들을 위한 추가 예시public class A { private B b = new B(); }
위와 같이 IoC를 사용하지 않을 경우, 만약 B 클래스가 변경되어 하나의 파라미터를 필요로 하게 된다면 new B() 부분은 에러를 일으키게 된다. 이를 해결하기 위해서는 A 클래스를 수정해야 한다.
public class A { private B b; public A(B b) { this.b = b; } }
위와 같이 IoC를 사용할 경우, B 클래스가 변경되더라도 의존 대상을 외부로부터 주입받기 때문에 A 클래스를 수정하지 않아도 된다. 예를 들어, Main 함수에서 B의 인스턴스를 생성할 경우, 아래와 같이 작성할 수 있다.
public class Main { public static void main(String[] args) { B bInstance = new B("Some parameter"); A aInstance = new A(bInstance); } }
IoC와 DI는 같이 설명되는 경우가 많기 때문에, 그 둘을 동일하게 간주하기 쉽지만 엄연히 다른 개념이다. IoC는 이전에 설명한 것처럼 프로그램의 흐름을 외부에서 제어한다는 개념이고, DI는 IoC를 구현하는 방법 중 하나인 의존성 주입을 의미한다. 즉, DI를 이용하지 않고도 IoC를 만족할 수 있다. (하지만 어렵다..)
이전 예시에서 사용한 방법이 정확히 DI를 통해 IoC를 구현한 것이다. 각 예시에서, 한 객체가 다른 객체를 필요로 할 때 의존성을 외부(스프링 컨테이너)에서 주입하고 있다. 의존성을 주입하는 방법은 아래와 같은 세 가지가 있다.
@Autowired
는 스프링 프레임워크에서 의존성을 자동으로 주입해주는 애노테이션으로, 아래의 예시에서는 ServiceA의 생성자에 ServiceB 객체를 자동으로 주입한다.
@RestController
public class ControllerA {
private final ServiceB serviceB;
@Autowired
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@RestController
public class ControllerA {
private ServiceB serviceB;
@Autowired
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@RestController
public class ControllerA {
@Autowired
private ServiceB serviceB;
}
스프링 공식 문서에 따르면, 생성자를 통해 의존성을 주입받는 방법을 권장한다.
불변성 보장: 생성자를 통해 의존성을 주입받으면 객체가 생성될 때 모든 의존성이 설정되므로, 이후에는 변경할 수 없고, 코드의 안정성이 높아진다.
필수 의존성 명시: 생성자를 통해 의존성을 주입받으면 객체 생성 시 필요한 모든 의존성을 명시적으로 선언해야 하므로, 필수적인 의존성이 누락되는 것을 방지할 수 있다.
테스트 용이성: 생성자 주입을 사용하면 의존성을 명시적으로 주입할 수 있어, DI 컨테이너 없이도 쉽게 객체를 생성하고 테스트할 수 있다.
순환 참조 방지: 생성자 주입은 순환 참조(A와 B가 서로를 참조)가 발생할 시 Exception을 일으켜 문제를 조기에 발견하도록 한다.
@RestController
@RequiredArgsConstructor
public class ControllerA {
private final ServiceB serviceB;
}
Lombok의 @RequiredArgsConstructor
애노테이션은 final 필드에 대한 생성자를 자동으로 생성한다. 결과적으로, Lombok을 사용하는 방법이 가장 유리한 의존성 주입 방법이라고 할 수 있다.
Lombok이란?
Lombok은 자바 애플리케이션 라이브러리로, 어노테이션을 사용하여 getter, setter, 생성자 등의 반복적인 코드를 자동으로 생성한다. 이를 통해 코드의 가독성과 유지보수성을 높일 수 있다.