
DI는 의존성 주입을 뜻하며, 개발자가 직접 new 키워드를 사용해 객체를 생성하는 대신, 스프링 프레임워크와 같은 외부 시스템에서 객체를 주입받는 방식이다. 이를 통해 의존성 제어가 역전(Inversion of Control, IoC)되어 객체의 생성을 관리하게 된다.
DI는 마틴파울러라는 사람에 의해 제안되었다고 한다.
new 연산자를 사용해 객체를 직접 생성하면 클래스 간 결합도가 높아진다. 결합도는 한 클래스가 다른 클래스에 얼마나 의존적인지를 나타내며, 결합도가 높아지면 유지보수 비용이 증가한다.
예시:
public class Car {
private Engine engine;
public Car() {
this.engine = new Engine(a, b, c); // 직접 Engine 객체를 생성
}
public void start() {
engine.start();
}
}
Car 클래스는 Engine 클래스에 직접적으로 의존한다. 만약 Engine 클래스의 동작이 바뀌거나 다른 엔진 타입을 사용하고 싶다면 Car 클래스의 코드를 직접 수정해야 한다.
강한 결합을 가진 객체는 유지보수가 어렵고, 이는 OOP의 목표인 유연성과 확장성에 부합하지 않는다. 이 문제를 해결하기 위해 new 연산자를 제거하고 객체 생성을 외부에서 관리하는 방식이 도입되었으며, 이 개념이 바로 DI다. DI는 결합도를 낮추어 유지보수와 테스트의 유연성을 제공한다.
예시 (DI 적용):
public class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine; // 외부에서 Engine을 주입받음
}
public void start() {
engine.start();
}
}
Car 클래스가 Engine의 구체적인 구현을 몰라도 되며, 어떤 타입의 Engine을 사용할지 외부에서 결정할 수 있다. 이를 통해 유연하게 엔진을 변경하거나 테스트 시 Mock 객체를 주입할 수 있다.
DI는 단순히 new 연산자를 제거하는 것 이상의 목적을 가지고 있다. OOP 원칙을 따르며, 유연하고 확장 가능한 설계를 지향하는 여러 이점을 제공한다.
컨테이너는 소프트웨어에서 여러 객체나 컴포넌트를 관리하고, 이들의 생명주기를 제어하며, 필요한 의존성 주입과 설정을 수행하는 역할을 한다. 스프링은 이러한 역할을 수행하기 때문에 스프링 컨테이너라고 부르며, 전체 애플리케이션의 구성 요소를 관리하는 역할을 담당한다. 스프링은 IoC 컨테이너의 구현체이고 스프링 컨테이너 안에 IoC가 내장되어있다.
// @Component: 스프링 컨테이너가 해당 클래스를 Bean으로 인식하고 관리하게 한다.
// @Component가 붙은 클래스는 자동으로 스프링 컨테이너에 등록된다.
@Component
public class Chef {
// Chef 클래스의 정의
}
@Component
public class Restaurant {
// @Autowired: 스프링이 자동으로 의존성을 주입하도록 한다.
// @Autowired가 붙은 필드나 생성자는 스프링 컨테이너에 의해 의존성이 자동으로 주입된다.
@Autowired // Chef 객체를 자동으로 주입
private Chef chef;
} @Component로 Chef와 Restaurant 클래스를 정의하면, 스프링이 자동으로 이들을 관리할 Bean으로 등록한다. @Autowired를 Restaurant 클래스의 chef 필드에 붙이면, 스프링이 Restaurant 객체를 생성할 때 Chef 객체를 자동으로 주입한다. 이와 같은 방식으로 의존성을 설정하면, 별도로 XML 설정 파일을 작성하지 않고도 스프링이 필요한 객체들을 자동으로 관리하고 주입하게 된다.참고로, 스프링에서 Bean은 기본적으로 싱글톤으로 생성되어 하나의 인스턴스만 존재한다. 즉, 스프링 컨테이너가 애플리케이션 실행 중 하나의 인스턴스만 생성하고, 그 인스턴스를 재사용한다.
@Configuration
public class AppConfig {
@Bean
public Chef chef() {
return new Chef();
}
@Bean
public Restaurant restaurant(Chef chef) {
Restaurant restaurant = new Restaurant();
restaurant.setChef(chef);
return restaurant;
}
} 이 예시에서는 @Configuration을 사용하여 설정 클래스를 정의하고, @Bean 메서드를 통해 Bean을 등록한다. restaurant 메서드는 Chef 객체를 파라미터로 받아 Restaurant 객체에 주입한다.AOP는 관점 지향 프로그래밍(Aspect-Oriented Programming) 방법론으로, 횡단 관심사(cross-cutting concerns)를 분리하여 코드 중복을 줄이고 모듈화를 개선하는 데 중점을 둔다.
횡단 관심사(또는 공통 관심사)는 여러 객체나 모듈에서 공통적으로 사용되는 기능(예: 로그 처리, 예외 처리, 트랜잭션 관리)을 의미하며, 비즈니스 로직과는 직접적인 관련이 없지만 시스템 전반에서 필요하다.
횡단 관심사는 여러 모듈에 걸쳐 나타나기 때문에 각각의 클래스에 중복적으로 포함되면 코드가 비효율적이고 유지보수가 어려워진다.
객체 지향 프로그래밍(OOP)에서는 비즈니스 로직과 횡단 관심사들이 여러 클래스에서 중복되어 작성되므로, 유지보수가 복잡해지고 코드의 재사용성이 떨어진다.
AOP는 이러한 횡단 관심사를 별도의 모듈(Aspect, 횡단 관심사를 구현한 단위 모듈)로 구현하고, 필요한 시점에 스프링이 해당 코드를 주입하도록 하여 코드 재사용성을 높이고 자원 관리를 효율적으로 할 수 있다.
Aspect는 Advice, Pointcut, Join Point 등의 요소로 구성되며, 횡단 관심사를 정의하고, 그 관심사를 어디에서 실행할지를 결정한다.
- Advice: 실제로 실행될 코드, 예를 들어 로그를 기록하거나 트랜잭션을 관리하는 기능.
- Pointcut: 어느 시점에 이 Advice를 실행할지를 정의하는 규칙.
- Join Point: 실제로 Advice가 실행될 수 있는 지점.
DI가 객체 간의 의존성을 주입하는 기술이라면, AOP는 특정 시점에 코드를 삽입하는 코드 주입 기술로, 이러한 횡단 관심사의 분리를 통해 모듈성을 증가시키는 것을 목적으로 하는 프로그래밍 패러다임이다.
1. 트랜잭션 관리
AOP를 사용하면 트랜잭션 관리를 공통 관심사로 정의해, 코드 중복 없이 트랜잭션을 관리할 수 있다.
예시 (AOP 없는 코드):
public void processOrder() {
try {
transactionManager.startTransaction();
// 비즈니스 로직
transactionManager.commit();
} catch (Exception e) {
transactionManager.rollback();
}
}
AOP를 통한 개선:
@Transaction
public void processOrder() {
// 비즈니스 로직만 남음
}
AOP는 @Transaction 어노테이션을 통해 트랜잭션 관리 코드를 자동으로 주입한다. 트랜잭션 관리 코드를 비즈니스 로직과 분리하여 트랜잭션 시작, 커밋, 롤백과 같은 자원 관리를 자동으로 처리할 수 있다.
2. 리소스 해제
파일, 네트워크 연결, 데이터베이스 연결 등 자원을 다룰 때, 사용 후에 적절히 해제하지 않으면 메모리 누수나 연결 과부하 등의 문제가 발생할 수 있다.
예시 (AOP 없는 코드):
public void readFile() {
FileReader fileReader = null;
try {
fileReader = new FileReader("example.txt");
// 파일 읽기 작업
} finally {
if (fileReader != null) {
fileReader.close(); // 파일 리소스 해제
}
}
}
AOP를 통한 개선:
@ManagedResource
public void readFile() {
// 파일 읽기 작업만 남음
}
파일 리소스의 열기, 닫기와 같은 자원 관리 코드가 어노테이션 설정 만으로 자동으로 주입되며, 개발자가 비즈니스 로직에만 집중할 수 있다.
이처럼 AOP는 자원 해제나 트랜잭션 관리 등 반복적인 자원 관리 로직을 일관성 있게 처리해, 효율적인 자원 관리가 가능하다.
PSA(Portable Service Abstraction)는 휴대 가능한 서비스 추상화라는 의미로, 애플리케이션이 특정 기술이나 플랫폼에 종속되지 않고, 다양한 환경에서 일관된 방식으로 서비스를 사용할 수 있도록 한다.
이 개념은 특히 스프링 프레임워크에서 중요한 역할을 하며, 개발자가 특정 기술이나 프레임워크에 종속되지 않게 도와준다. 예를 들어, 데이터베이스 접근, 메시징 시스템, 트랜잭션 관리 등의 서비스들이 PSA에 의해 추상화되면, 다양한 기술 스택을 사용하더라도 동일한 방식으로 애플리케이션을 개발할 수 있다.
1. 데이터베이스 추상화
스프링의 JdbcTemplate은 PSA의 대표적인 예시다. 이는 데이터베이스 접근을 추상화하여, 특정 데이터베이스 기술에 종속되지 않고 동일한 API를 통해 데이터베이스 작업을 처리할 수 있도록 해준다.
@Autowired
private JdbcTemplate jdbcTemplate;
public void saveData(String data) {
String sql = "INSERT INTO table_name (column_name) VALUES (?)";
jdbcTemplate.update(sql, data); // 다양한 데이터베이스에서 동일한 방식으로 사용 가능
}
트랜잭션 처리가 필요한 다양한 환경에서 PSA는 어노테이션 방식으로 트랜잭션을 관리하여, 특정 트랜잭션 시스템에 종속되지 않고 일관성 있는 트랜잭션 처리가 가능하다.
2. 트랜잭션 관리
PSA를 통해 트랜잭션 관리도 추상화되어 다양한 트랜잭션 관리 기법을 일관되게 사용할 수 있다. 스프링의 @Transactional 어노테이션을 사용하면, 다양한 트랜잭션 환경에서 동일한 트랜잭션 처리 방식이 적용된다.
@Service
public class OrderService {
@Transactional
public void processOrder(Order order) {
// 트랜잭션 관리 로직을 신경쓰지 않고 비즈니스 로직에만 집중
// PSA에 의해 트랜잭션 관리가 일관되게 처리됨
}
}
트랜잭션 처리가 필요한 다양한 환경에서 PSA는 같은 어노테이션 방식으로 트랜잭션을 관리하여, 특정 트랜잭션 시스템에 종속되지 않고 일관성 있는 트랜잭션 처리가 가능하다.
PSA는 애플리케이션이 특정 기술에 종속되지 않고, 다양한 기술 스택이나 플랫폼에서 일관된 방식으로 동작할 수 있도록 하는 중요한 개념이다. 이를 통해 개발자가 다양한 기술 스택이나 플랫폼에 종속되지 않고 자유롭게 기술을 선택하고 변경할 수 있다.
POJO는 Plain Old Java Object의 약자로, 특정 프레임워크나 라이브러리의 종속성을 갖지 않고 순수한 자바 객체를 의미한다. POJO는 Java EE나 Spring과 같은 복잡한 환경에서 벗어나, 단순하고 가독성 있는 객체 설계를 추구한다. 특정 프레임워크에 의존하지 않고, 순수한 자바 객체로 애플리케이션의 핵심 비즈니스 로직을 구현하는 방식은 테스트 용이성, 코드 가독성, 재사용성 등 여러 측면에서 유리하다.
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
위 User 클래스는 POJO의 전형적인 예로, 단순한 자바 객체로 구성되어 있고, 특별한 설정이나 어노테이션이 없다.
Spring은 POJO 기반의 설계를 지향한다. Spring 프레임워크는 애플리케이션의 핵심 로직을 POJO로 구현한 후, DI(의존성 주입)와 AOP(관점 지향 프로그래밍) 같은 기능을 통해 객체 간의 결합을 줄이고 기능을 확장하는 방식이다. 이를 통해 Spring 기반 애플리케이션은 프레임워크 종속성이 낮고, 유지보수 및 테스트가 쉬운 구조를 갖출 수 있다.
public class ProductService {
private ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product findProductById(Long id) {
return productRepository.findById(id);
}
}
위 ProductService 클래스는 POJO로 설계되어 있으며, ProductRepository와 같은 다른 객체에 의존성을 주입받아 사용할 수 있다. 이 방식은 Spring의 DI 기능을 통해 결합도를 낮추고, 더 유연한 설계를 가능하게 한다.
Spring은 경량 컨테이너로, 필요한 모듈만 선택적으로 사용할 수 있으며, 외부 모듈과의 통합이 용이하다. 이 경량성 덕분에 불필요한 복잡성을 줄일 수 있고, 개발자는 필요한 기능만을 추가하여 유연하고 확장 가능한 시스템을 구축할 수 있다.
MyBatis는 SQL 쿼리를 직접 관리하는 데이터 매핑 프레임워크로, 데이터베이스와의 연동을 효율적으로 처리할 수 있게 해준다. 특히, MyBatis-Spring 모듈은 Spring Framework와 MyBatis의 통합을 지원하여 데이터베이스 연결, 트랜잭션 관리, 세션 관리를 스프링의 빈(Bean) 관리와 결합시킨다. 이 통합을 통해 개발자는 데이터 접근 계층을 효율적으로 관리하면서도 스프링의 의존성 주입(Dependency Injection)과 트랜잭션 관리 기능을 그대로 활용할 수 있어, 시스템의 확장성과 유지보수성이 향상된다.
이러한 모듈들의 통합은 복잡한 시스템을 단순화하고, 반복 작업을 줄여 개발 생산성을 극대화할 수 있게 한다. 특히, Spring의 DI와 AOP 같은 핵심 기능들은 모듈 간의 결합도를 낮추어 독립적인 기능 개발을 가능하게 하고, 필요에 따라 손쉽게 모듈을 통합할 수 있게 한다.