스프링 프레임워크 핵심 기술: 모듈화와 유연성의 실현

개요

소프트웨어 개발에서 '고결합과 저결합' 원칙은 유지보수성과 확장성을 극대화하는 설계 철학입니다. 스프링 프레임워크는 제어 역전(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 생성 전략을 조절합니다:

  1. ApplicationContext: 구성 파일 로딩 시 모든 싱글톤 Bean을 즉시 초기화
  2. 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 구현 방식

  1. 정적 프록시: 컴파일 시간에 프록시 객체 결정, 프록시 클래스 수 증가
  2. 동적 프록시:
  • JDK 동적 프록시: 인터페이스 기반
  • CGLIB 동적 프록시: 상위 클래스 기반
  1. 스프링 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 전파 전략을 사용한 원자성 보장

이러한 기술을 적절히 활용하면 더 유연하고 유지보수가 쉬우며 확장성이 뛰어난 애플리케이션을 구축할 수 있습니다. 고결합과 저결합의 설계 목표를 효과적으로 달성할 수 있습니다.

최선의 실천 방법

  1. 어노테이션 기반 설정 우선: @Autowired, @Component, @Service 등을 사용하여 구성 파일을 간결하게 작성
  2. 적절한 주입 방식 선택: 필수 의존성에는 생성자 주입, 선택적 의존성에는 세터 주입을 활용
  3. 자동 주입 사용에 주의: 주입 대상 Bean 이름을 명시적으로 지정하여 예측 불가능한 동작을 방지
  4. AOP 사용 범위 제한: 횡단 관심사를 명확히 파악하여 과도한 사용을 피함
  5. 트랜잭션 경계 계획: 비즈니스 요구사항에 맞는 전파 전략을 선택하여 일관성 유지

스프링의 모듈화 기법은 단순한 기술이 아니라 설계 철학으로서, 지속 가능한 소프트웨어 시스템 구축을 지도합니다.

태그: Spring Framework IOC AOP Transaction Management Dependency Injection

6월 28일 18:45에 게시됨