개요
소프트웨어 개발에서 '고결합과 저결합' 원칙은 유지보수성과 확장성을 극대화하는 설계 철학입니다. 스프링 프레임워크는 제어 역전(IoC)과 AOP(Aspect-Oriented Programming)를 통해 컴포넌트 간 의존성을 명확히 분리하는 기능을 제공합니다. 본문에서는 스프링이 어떻게 시스템 내부 요소를 분리하고, 실제 개발 환경에서 이를 적용하는 방법에 대해 다룹니다.
IoC/DI: 제어 역전과 의존성 주입
개념 정리
IoC는 전통적인 객체 생성 및 생명주기 관리를 개발자가 직접 수행하는 방식에서, 프레임워크 컨테이너가 이 권한을 위임받는 디자인 패턴입니다. 이는 시스템의 유연성과 재사용성을 높이는 데 기여합니다.
DI는 IoC의 구체적 구현 방식으로, 컨테이너가 객체가 필요한 의존 객체를 동적으로 주입하는 방식입니다. 일반적으로 생성자 또는 세터 메서드를 통해 이루어집니다.
전통적 방식 vs 스프링 IoC 방식
// 전통적 방식 - 강하게 결합된 예제
public class UserManagement {
private UserRepo userRepo = new UserRepoImpl();
}
// 스프링 IoC 방식 - 결합도 낮춤
public class UserManagement {
private UserRepo userRepo;
// 생성자 주입 방식
public UserManagement(UserRepo userRepo) {
this.userRepo = userRepo;
}
// 세터 주입 방식
public void setUserRepo(UserRepo userRepo) {
this.userRepo = userRepo;
}
}
Bean의 싱글톤 패턴
스프링 컨테이너 내 Bean은 기본적으로 싱글톤으로 관리됩니다. 컨테이너는 내부적으로 ConcurrentHashMap과 유사한 구조를 사용하여 Bean을 저장 및 관리합니다. 여기서의 싱글톤은 각각의 스프링 컨테이너 내에서 유효하며, JVM 전체 범위의 싱글톤이 아닙니다.
Bean 생성 시점
스프링은 두 가지 컨테이너를 제공하여 Bean 생성 전략을 조절합니다:
- ApplicationContext: 구성 파일 로딩 시 모든 싱글톤 Bean을 즉시 초기화
- BeanFactory: 요청이 발생할 때까지 Bean 인스턴스를 생성하지 않음
Bean 생성 방식
스프링은 다양한 Bean 생성 방법을 지원합니다:
<!-- 1. 생성자 주입 (가장 흔한 방식) -->
<bean id="department" class="com.example.model.Department"/>
<!-- 2. 일반 팩토리 패턴 -->
<bean id="factory" class="com.example.util.DepartmentFactory"/>
<bean id="department4" factory-bean="factory" factory-method="createInstance"/>
<!-- 3. 정적 팩토리 패턴 -->
<bean id="department5" class="com.example.util.DepartmentFactory" factory-method="createInstance"/>
속성 주입 방식
<!-- 1. 생성자 주입 -->
<bean id="department3" class="com.example.model.Department">
<constructor-arg index="0" value="10"/>
<constructor-arg name="name" value="운영팀"/>
<constructor-arg index="2" value="서울"/>
</bean>
<!-- 2. 세터 메서드 주입 -->
<bean id="department5" class="com.example.model.Department">
<property name="id" value="99"/>
<property name="name" value="디자인 부서"/>
<property name="location" value="상해"/>
</bean>
<!-- 3. 자동 주입 -->
<bean id="autoInject" class="com.example.repository.DepartmentRepository" autowire="byName"/>
AOP: Aspect-Oriented Programming
AOP는 로깅, 트랜잭션, 보안 등과 같은 공통 기능을 횡단적으로 추출하여 비즈니스 로직과 비비즈니스 로직을 분리하는 기법입니다.
AOP 구현 방식
- 정적 프록시: 컴파일 시간에 프록시 객체 결정, 프록시 클래스 수 증가
- 동적 프록시:
- JDK 동적 프록시: 인터페이스 기반
- CGLIB 동적 프록시: 상위 클래스 기반
- 스프링 AOP: 기본적으로 JDK 동적 프록시 사용, 설정 변경 가능
AOP 실무 예제
// 어스펙트 정의
@Aspect
@Component
public class AuditAspect {
@Before("execution(* com.example.service..*(..))")
public void auditBefore(JoinPoint joinPoint) {
System.out.println("메서드 실행 전: " + joinPoint.getSignature().getName());
}
@AfterReturning(pointcut = "execution(* com.example.service..*(..))", returning = "result")
public void auditAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("메서드 실행 후: " + result);
}
}
트랜잭션 관리: 전파 전략 활용
비즈니스 로직에서 여러 Mapper 메서드 호출 시, 이들을 하나의 트랜잭션으로 처리해야 합니다. 스프링의 트랜잭션 전파 전략은 이를 해결합니다.
트랜잭션 전파 전략 설정
<!-- MyBatis 세션 관리 트랜잭션 제어 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 어노테이션 기반 트랜잭션 활성화 -->
<tx:annotation-driven transaction-manager="txManager"/>
REQUIRED 전파 전략 적용
@Service
@Transactional(propagation = Propagation.REQUIRED)
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public int modifyUser(User user) {
int result = 0;
result += userMapper.deleteUserByUserId(7782);
result += userMapper.updateUser(user);
return result;
}
}
트랜잭션 테스트
@Test
public void testTransaction() {
ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService us = app.getBean("userServiceImpl", UserService.class);
User user = new User();
user.setSalary(0.0);
user.setUserId(7876);
try {
int result = us.modifyUser(user);
System.out.println("트랜잭션 성공");
} catch (Exception e) {
System.out.println("트랜잭션 롤백");
}
}
요약
스프링 프레임워크는 IoC/DI와 AOP라는 두 가지 핵심 기술을 통해 소프트웨어 모듈화에 효과적인 솔루션을 제공합니다:
| 개념 | 핵심 아이디어 | 구현 방식/목적 |
|---|---|---|
| IoC/DI | 컨테이너가 객체 생성 및 연결을 담당 | XML 또는 어노테이션을 통한 의존 관계 설정 |
| AOP | 공통 기능을 횡단적으로 추출 | 동적 프록시 기술을 활용한 비즈니스 로직 분리 |
| 트랜잭션 전파 | 데이터베이스 작업의 트랜잭션 경계 설정 | REQUIRED 전파 전략을 사용한 원자성 보장 |
이러한 기술을 적절히 활용하면 더 유연하고 유지보수가 쉬우며 확장성이 뛰어난 애플리케이션을 구축할 수 있습니다. 고결합과 저결합의 설계 목표를 효과적으로 달성할 수 있습니다.
최선의 실천 방법
- 어노테이션 기반 설정 우선:
@Autowired,@Component,@Service등을 사용하여 구성 파일을 간결하게 작성 - 적절한 주입 방식 선택: 필수 의존성에는 생성자 주입, 선택적 의존성에는 세터 주입을 활용
- 자동 주입 사용에 주의: 주입 대상 Bean 이름을 명시적으로 지정하여 예측 불가능한 동작을 방지
- AOP 사용 범위 제한: 횡단 관심사를 명확히 파악하여 과도한 사용을 피함
- 트랜잭션 경계 계획: 비즈니스 요구사항에 맞는 전파 전략을 선택하여 일관성 유지
스프링의 모듈화 기법은 단순한 기술이 아니라 설계 철학으로서, 지속 가능한 소프트웨어 시스템 구축을 지도합니다.