[객체지향] 스프링 삼각형

JHJeong·2024년 5월 22일
0

객체지향 원리

목록 보기
5/5
post-custom-banner

스프링을 이해하는데는 POJO(Plain Old Java Object)를 기반으로 스프링 삼각형이라는 애칭을 가진 IoC/DI, AOP, PSA라고 하는 스프링의 3대 프로그래밍 모델에 대한 이해가 필수다.

1. IoC/DI - 제어의 역전/의존성 주입

(1) 프로그래밍에서 의존성이란?

  • 의존하는 객체(전체)와 의존되는 객체(부분) 사이에 집합관계(Aggregation)와 구성관계(Composition)으로 구분할 수 있음
  • 집합관계(Aggregation) : 전체와 부분이 독립적으로 존재할 수 있다. 예를 들어 축구팀(전체)와 선수(부분), 팀이 없어져도 선수는 다른 팀에 들어 갈 수 있음
  • 구성관계(Composition) : 전체가 없어지면 부분도 존재할 수 없다. 예를 들어, 집(전체)와 방(부분), 집이 없어지면 방도 없다.

(2) 스프링 없이 의존성 주입하기 1 - 생성자를 통한 의존성 주입

  • 생성자를 통한 의존성 주입은 객체를 만들 때 필요한 다른 객체를 생성자 매개변수로 전달하는 방식이다.
class Engine {
    public void start() {
        System.out.println("Engine starts.");
    }
}

class Car {
    private Engine engine;

    // 생성자를 통한 의존성 주입
    public Car(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("Car is driving.");
    }
}

public class Main {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine); // Engine 객체를 주입
        car.drive();
    }
}

(3) 스프링 없이 의존성 주입하기 2 - 속성을 통한 의존성 주입

  • 속성(Property)을 통한 의존성 주입은 객체를 생성한 후 필요한 다른 객체를 속성으로 설정하는 방식이다.
class Engine {
    public void start() {
        System.out.println("Engine starts.");
    }
}

class Car {
    private Engine engine;

    // 속성을 통한 의존성 주입
    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("Car is driving.");
    }
}

public class Main {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car();
        car.setEngine(engine); // Engine 객체를 주입
        car.drive();
    }
}

(4) 스프링을 통한 의존성 주입 - XML 파일 사용

  • XML파일을 사용한 의존성 주입은 설정 파일을 통해 객체 간의 관계를 정의하는 방식이다.
<!-- config.xml -->
<beans>
    <bean id="engine" class="Engine"/>
    <bean id="car" class="Car">
        <property name="engine" ref="engine"/>
    </bean>
</beans>
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

class Engine {
    public void start() {
        System.out.println("Engine starts.");
    }
}

class Car {
    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("Car is driving.");
    }
}

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("config.xml");
        Car car = (Car) context.getBean("car");
        car.drive();
    }
}

(5) 스프링을 통한 의존성 주입 - @Autowired를 통한 속성 주입

  • 스프링을 사용하면 어노테이션(Annotation)을 통해 쉽게 의존성을 주입할 수 있다. @Autowired는 자동으로 필요한 객체를 주입해준다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
class Engine {
    public void start() {
        System.out.println("Engine starts.");
    }
}

@Component
class Car {
    @Autowired
    private Engine engine;

    public void drive() {
        engine.start();
        System.out.println("Car is driving.");
    }
}

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
class AppConfig {}

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        Car car = context.getBean(Car.class);
        car.drive();
    }
}

(6) 스프링을 통한 의존성 주입 - @Resource를 통한 속성 주입

  • @Resource 어노테이션을 사용하면 이름을 통해 의존성을 주입할 수 있다.
import javax.annotation.Resource;
import org.springframework.stereotype.Component;

@Component
class Engine {
    public void start() {
        System.out.println("Engine starts.");
    }
}

@Component
class Car {
    @Resource(name = "engine")
    private Engine engine;

    public void drive() {
        engine.start();
        System.out.println("Car is driving.");
    }
}

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
class AppConfig {}

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        Car car = context.getBean(Car.class);
        car.drive();
    }
}

제어의 역전(IoC)

제어의 역전(IoC)은 프로그램의 제어 흐름을 개발자가 직접 관리하지 않고 프레임워크나 외부 환경에 맡기는 것을 의미한다. 객체의 생성, 초기화, 소멸 등을 프레임워크가 관리하게 된다. 이를 통해 개발자는 비즈니스 로직에 집중할 수 있다. 반복적이고 번거로운 작업이 줄어들어 코드가 간결해진다. 예를 들어, 스프링 프레임워크는 IoC 컨테이너를 통해 객체 간의 의존성을 관리하고 주입한다. 결과적으로 코드의 재사용성과 유지보수성이 향상된다.

의존성 주입(DI)

의존성 주입(DI)은 객체 간의 의존성을 외부에서 주입하는 방식을 말한다. 이를 통해 객체 간의 결합도가 낮아지고 코드가 유연해진다. DI는 생성자, 속성, 메서드를 통해 의존성을 설정할 수 있다. 스프링 프레임워크는 어노테이션이나 XML 설정 파일을 통해 DI를 쉽게 구현할 수 있다. DI를 사용하면 테스트가 쉬워지고, 코드의 가독성과 유지보수성이 높아진다. 결과적으로 객체 간의 관계를 명확하게 정의할 수 있다.

2. AOP

Aspect-Oriented Programming의 약자, 관점 지향 프로그래밍
스프링 DI가 의존성(new)에 대한 주입이라면, 스프링 AOP는 로직(code) 주입이라고 할 수 있다.

AOP는 관점 지향 프로그래밍(Aspect-Oriented Programming)의 약자로, 프로그램을 여러 관심사(Aspects)로 나누어 모듈화하는 프로그래밍 패러다임이다. 주로 핵심 관심사(Core Concern)과 횡단 관심사(Cross-Cutting Concern)를 분리하는 데 사용된다. 이를 통해 코드의 가독성과 유지보수성을 향상시키고, 단일 책임 원칙(Single Reponsibility Principle, SRP)을 준수할 수 있다.

핵심 관심사(Core Concern)

핵심 관심사는 애플리케이션의 본질적인 기능을 담당한다. 예를 들어, 은행 애플리케이션에서 계좌 이체 기능이 핵심 관심사에 해당한다. 핵심 관심사는 주로 비즈니스 로직을 포함하며, 단일 책임 원칙에 따라 하나의 책임을 가진다.

횡단 관심사(Cross-Cutting Concern)

횡단 관심사는 여러 모듈에 걸쳐 공통적으로 적용되는 기능이다. 예를 들어, 로깅, 보안, 트랜잭션 관리 등이 횡단 관심사에 해당한다. 이러한 기능은 모든 모듈에서 필요하지만, 핵심 로직과 분리하여 관리하는 것이 좋다. AOP를 사용하면 이러한 횡단 관심사를 별도의 모듈로 분리할 수 있다.

AOP의 주요 개념

  • Aspect: 횡단 관심사를 모듈화한 것. 예를 들어, 로깅 기능을 담당하는 모듈.
  • Join Point: Aspect가 적용될 수 있는 지점. 메서드 호출, 객체 생성 등.
  • Advice: Join Point에서 실행되는 코드. 언제 실행될지에 따라 Before, After, Around 등이 있다.
  • Pointcut: Advice가 적용될 Join Point를 선택하는 것.
  • Weaving: Aspect를 핵심 관심사에 적용하는 과정.

스프링 AOP의 특징

스프링 AOP는 인터페이스 기반, 프록시 기반, 런타임 기반으로 동작한다.

인터페이스 기반

스프링 AOP는 주로 인터페이스 기반으로 동작한다. 인터페이스를 통해 프록시 객체를 생성하고, 이 프록시 객체를 통해 AOP 기능을 적용한다. 이를 통해 인터페이스를 구현하는 클래스에 AOP를 적용할 수 있다.

프록시 기반

스프링 AOP는 프록시 패턴을 사용하여 동작한다. 프록시는 대상 객체를 감싸는 래퍼 객체로, 대상 객체에 접근하기 전에 필요한 작업(Advice)을 수행할 수 있다. 스프링은 JDK 동적 프록시와 CGLIB 프록시를 사용하여 프록시 객체를 생성한다. JDK 동적 프록시는 인터페이스를 기반으로 하고, CGLIB 프록시는 클래스 기반 프록시를 생성한다.

런타임 기반

스프링 AOP는 런타임 시에 AOP 기능을 적용한다. 즉, 컴파일 시나 로드 시가 아니라, 애플리케이션 실행 중에 프록시를 생성하고 Advice를 적용한다. 이를 통해 동적으로 AOP 기능을 적용할 수 있다.

예제 소스
다음은 AOP를 사용하여 횡단 관심사(로깅)를 핵심 관심사(계좌 이체 기능)와 분리하는 예제이다.

핵심 관심사: 계좌 이체 기능

// 핵심 관심사 클래스
public class BankService {
    public void transferMoney(String fromAccount, String toAccount, double amount) {
        // 계좌 이체 로직
        System.out.println("Transferring $" + amount + " from " + fromAccount + " to " + toAccount);
    }
}

횡단 관심사: 로깅 기능

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

// Aspect 클래스
@Aspect
public class LoggingAspect {
    @Before("execution(* BankService.transferMoney(..))")
    public void logBefore() {
        System.out.println("Logging before method execution");
    }
}

설정 파일: applicationContext.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">
    
    <!-- 핵심 관심사 빈 정의 -->
    <bean id="bankService" class="BankService"/>
    
    <!-- 횡단 관심사 빈 정의 -->
    <bean id="loggingAspect" class="LoggingAspect"/>
    
    <!-- AOP 설정 -->
    <aop:config>
        <aop:aspect ref="loggingAspect">
            <aop:pointcut id="logBeforeTransfer" expression="execution(* BankService.transferMoney(..))"/>
            <aop:before method="logBefore" pointcut-ref="logBeforeTransfer"/>
        </aop:aspect>
    </aop:config>
</beans>

메인 클래스

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        BankService bankService = (BankService) context.getBean("bankService");
        
        // 계좌 이체 호출
        bankService.transferMoney("123-456", "654-321", 1000.00);
    }
}

설명
1. 핵심 관심사: BankService 클래스는 계좌 이체 기능을 담당한다. 이 클래스는 계좌 이체 로직만을 포함하고 있다. 단일 책임 원칙에 따라 BankService 클래스는 오직 계좌 이체라는 하나의 책임만을 가진다.
2. 횡단 관심사: LoggingAspect 클래스는 로깅 기능을 담당한다. 이 클래스는 AOP의 Aspect로 정의되었고, @Before 어노테이션을 사용하여 BankService의 transferMoney 메서드가 호출되기 전에 로깅을 수행한다. 로깅 기능은 여러 클래스에 걸쳐 필요하지만, 이를 LoggingAspect로 분리함으로써 단일 책임 원칙을 준수할 수 있다.
3. 설정 파일: applicationContext.xml 파일에서는 스프링 빈과 AOP 설정을 정의한다. 여기서는 BankService와 LoggingAspect를 빈으로 등록하고, AOP 설정을 통해 LoggingAspect가 BankService의 transferMoney 메서드에 적용되도록 설정한다. 이를 통해 코드의 모듈화와 유지보수성을 높일 수 있다.
4. 메인 클래스: Main 클래스에서는 스프링 컨테이너를 초기화하고 BankService 빈을 가져와서 계좌 이체 기능을 호출한다. 이 때, AOP 설정에 의해 LoggingAspect의 로깅 기능이 실행된다. 이를 통해 핵심 로직과 횡단 관심사를 분리하여 코드의 가독성과 유지보수성을 향상시킬 수 있다.

로깅 기능은 계좌 이체 기능이 호출되기 전에 실행된다. 이는 횡단 관심사(로깅)가 핵심 관심사(계좌 이체)와 분리되어 AOP를 통해 적용되었음을 보여준다. 이렇게 하면 핵심 로직과 부가적인 기능이 분리되어 코드의 가독성과 유지보수성이 크게 향상된다.
이와 같이 AOP를 사용하면 여러 모듈에 걸쳐 공통적으로 적용되는 횡단 관심사를 별도로 관리할 수 있어, 코드의 복잡성을 줄이고 효율적인 개발을 할 수 있게 된다. 단일 책임 원칙을 통해 각 클래스가 하나의 책임만 가지도록 하여, 변경에 대한 영향을 최소화하고 코드의 품질을 높일 수 있다. 스프링 AOP는 인터페이스 기반, 프록시 기반, 런타임 기반으로 동작하여 이러한 기능을 효과적으로 구현할 수 있게 해준다.

3. PSA - 일관성 있는 서비스 추상화

PSA(Portable Service Abstraction, 일관성 있는 서비스 추상화)는 다양한 환경에서 일관된 API를 제공하여 서비스 사용의 일관성을 유지하는 것을 목표로 한다. PSA를 사용하면 특정 서비스의 구현에 의존하지 않고, 다양한 서비스 제공자에 대해 일관된 방식으로 접근할 수 있다. 이는 코드의 재사용성을 높이고 유지보수를 용이하게 한다.

PSA의 필요성

현대 애플리케이션은 다양한 서비스와 통합되어 동작한다. 예를 들어, 데이터베이스, 메시징 시스템, 클라우드 스토리지 등을 사용한다. 이러한 서비스들은 서로 다른 API와 설정 방식을 가지기 때문에, 서비스 제공자가 변경되면 애플리케이션 코드도 변경되어야 한다. PSA는 이러한 문제를 해결하기 위해 등장하였다. PSA를 사용하면 특정 서비스 제공자에 의존하지 않고, 일관된 API를 통해 서비스에 접근할 수 있다.

PSA의 주요 개념

  • 추상화(Abstraction): 구체적인 구현을 숨기고, 일관된 인터페이스를 제공하는 것.
  • 서비스 제공자(Service Provider): 특정 기능을 제공하는 구체적인 서비스 구현.
  • 서비스 인터페이스(Service Interface): 서비스 제공자들이 구현해야 하는 일관된 인터페이스.

PSA의 예

스프링 프레임워크는 PSA를 적극적으로 활용하여 다양한 서비스에 대한 일관된 접근 방법을 제공한다. 예를 들어, 스프링의 트랜잭션 관리, 메시징, 캐싱 등의 기능은 PSA를 통해 다양한 서비스 제공자에 대해 일관된 API를 제공한다.

예제: 트랜잭션 관리
스프링 트랜잭션 관리는 PSA를 사용하여 다양한 트랜잭션 관리자를 일관된 방식으로 사용할 수 있게 한다.

1. 트랜잭션 서비스 인터페이스

import org.springframework.transaction.annotation.Transactional;

public interface BankService {
    @Transactional
    void transferMoney(String fromAccount, String toAccount, double amount);
}

2. 서비스 제공자: JDBC 트랜잭션 매니저

import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

@Configuration
public class AppConfig {
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/bank");
        dataSource.setUsername("user");
        dataSource.setPassword("password");
        return dataSource;
    }

    @Bean
    public DataSourceTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}

3. 서비스 구현 클래스

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service
public class BankServiceImpl implements BankService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void transferMoney(String fromAccount, String toAccount, double amount) {
        jdbcTemplate.update("UPDATE account SET balance = balance - ? WHERE account_number = ?", amount, fromAccount);
        jdbcTemplate.update("UPDATE account SET balance = balance + ? WHERE account_number = ?", amount, toAccount);
    }
}

4. 설정 파일: applicationContext.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/tx
                           http://www.springframework.org/schema/tx/spring-tx.xsd">
    
    <!-- 트랜잭션 매니저 정의 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <!-- 트랜잭션 어드바이저 활성화 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    
    <!-- 데이터 소스 정의 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/bank"/>
        <property name="username" value="user"/>
        <property name="password" value="password"/>
    </bean>
</beans>

5. 메인클래스

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        BankService bankService = (BankService) context.getBean("bankService");
        
        // 계좌 이체 호출
        bankService.transferMoney("123-456", "654-321", 1000.00);
    }
}

설명
1. 추상화: BankService 인터페이스는 계좌 이체 기능을 추상화한다. 이 인터페이스는 트랜잭션 관리가 필요한 메서드를 정의한다.
2. 서비스 제공자: DataSourceTransactionManager는 JDBC 트랜잭션 매니저를 제공한다. 이는 데이터베이스 트랜잭션을 관리하는 구체적인 구현이다.
3. 서비스 인터페이스: BankService 인터페이스는 일관된 트랜잭션 관리 인터페이스를 제공한다. 이는 다양한 트랜잭션 매니저가 이 인터페이스를 구현하도록 한다.
4. 서비스 구현: BankServiceImpl 클래스는 BankService 인터페이스를 구현하고, JDBC를 사용하여 계좌 이체 기능을 제공한다. 스프링의 트랜잭션 관리 기능을 사용하여 트랜잭션을 처리한다.
5. 설정 파일: applicationContext.xml 파일에서는 트랜잭션 매니저와 데이터 소스를 정의하고, 트랜잭션 어드바이저를 활성화한다. 이를 통해 PSA를 통해 트랜잭션 관리가 일관되게 적용된다

위 예제처럼 PSA를 통해 다양한 트랜잭션 매니저를 사용할 수 있으며, 코드 변경 없이도 쉽게 다른 트랜잭션 매니저로 교체할 수 있다.

PSA는 다양한 서비스 제공자에 대해 일관된 인터페이스를 제공하여, 코드의 재사용성과 유지보수성을 높인다. 이를 통해 특정 서비스 제공자에 종속되지 않고, 다양한 환경에서 일관된 방식으로 서비스를 사용할 수 있게 된다. 스프링 프레임워크는 PSA를 적극적으로 활용하여 다양한 서비스에 대한 일관된 접근 방법을 제공한다.

profile
이것저것하고 싶은 개발자
post-custom-banner

0개의 댓글