스프링의 이해

스프링으로 어떻게 개발할 것인가?


스프링 애플리케이션의 핵심

  1. 애플리케이션 코드를 설계하고 스프링 빈 선정
  2. 구성정보 메타데이터 작성
  3. 스프링 컨테이너 준비

객체와 의존관계 - 스프링에서 가장 중요한 용어

클래스와 오브젝트 - 왜 클래스 지향이 아니고 객체 지향일까?

Q. 자바에서 오브젝트란 무엇일까? A. 자바에서 오브젝트는 클래스의 인스턴스와 배열이다.

의존관계

A --- > B

관심사의 분리

변경의 관점으로 설명할 수 있으며, 행위에 대한 방법이 변경 기준에 따라 분리한다.

IoC

제어권 이전을 통한 제어관계 이전 - 프레임워크의 기본 동작 원리

비즈니스 로직의 흐름에서 직접 사용해야 하는 객체의 생성을 제어 하다가, 외부 클래스에게 제어권을 이전 시켜 비즈니스 로직은 객체의 생성을 더이상 관여하지 않고 반환 받는 인스턴스를 사용하는 것이다.

스프링 컨테이너와 의존 관계 주입

스프링 컨테이너의 특징

스프링의 핵심은 컨테이너이고, 컨테이너는 객체를 미리 생성 하고 보관하는 용도만 하는 것이 아니라 객체간 의존 관계를 자동으로 설정 해주는 컨테이너이다. 한 번 사용하고 객체의 생성과 종료가 아닌, 계속 재사용할 수 있는 싱글턴으로 관리한다.

Configuration 어노테이션을 사용하면 프록시를 이용하여 빈 객체를 싱글턴으로 구성한다.

의존성 역전 원칙

  1. 상위 수준의 모듈이 하위 수준의 모듈을 의존하면 안된다.
  2. 추상화는 구체적인 사항의 의존하면 안된다.

인터페이스 소유권의 역전이 필요하다

  • Seperated Interface pattern

인터페이스의 위치는, 자신을 구현하고 있는 모듈이 아닌 자신을 사용하고 있는 클라이언트 모듈에 위치해야한다.

테스트

JUnit 테스트 프레임워크를 통해 테스트 하게 되면, 매번 테스트 클래스를 새롭게 인스턴스화 하여 테스트 메서드를 실행한다.

콜백

콜백은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트 파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행 시키는 것이 목적이다.

하나의 메소드를 가진 인터페이스 타입의 오브젝트 또는 람다 오브젝트를 활용한다.

예외

Checked Exception

적절한 추상화 레벨에서 RuntimeException 으로 전환 하여 코드를 호출 하는 클라이언트에서 더이상 예외 처리를 강제 하지 않도록 전환한다.

JPA

EntityManager

스프링에 대한 오해

서비스란?

서비스는 클라이언트에게 서비스를 제공해주는 오브젝트나 모듈

서비스는 일반적으로 상태를 갖지 않음

서비스의 종류

트랜잭션

트랜잭션이 필요한 이유는 여러가지 행위가 하나의 흐름으로 묶여야 하는 경우에 필요하다. 주로 계좌 이체 시 출금과 입금에 대한 흐름이 하나의 이체라는 행위로 묶이는 것을 트랜잭션의 예시로 볼 수 있다.

기술에 따라 트랜잭션을 적용하는 방법이 모두 다르다.

JPA Transaction


EntityTransaction transaction = entityManger.getTransaction();

transaction.begin();
... // 흐름
transaction.commit();

JDBC Transaction


Connection con = ...
con.setAutocommit(false);
... // 흐름
con.commit();

트랜잭션의 추상화

📝

추상화 구현의 복잡함과 디테일을 감추고 가장 핵심적이고 중요한 것만 남기는 기법

추상화를 적용하면 트랜잭션을 적용하는 데이터 접근 기술이 변경 되더라도 기존 서비스 코드는 영향을 받지 않는다.

TransactionManager 는 데이터 접근 기술 마다 각각 다르게 템플릿에 인자로 활용해야 하지만, 위와 같은 구성도를 띄고 있다면 인터페이스를 참조하는 DIP 를 적용하여 추상화를 적용할 수 있다.

@Service  
public class OrderService {  
  
    private final OrderRepository orderRepository;  
    private final PlatformTransactionManager transactionManager;

    public List<Order> createOrders(List<OrderReq> reqs) {  
        return new TransactionTemplate(transactionManager).execute(status -> reqs.stream()  
                .map(req -> createOrder(req.no(), req.total()))  
                .toList());  
}

하지만, 트랜잭션을 모두 적용하기 위해 비즈니스 로직과 관계가 먼 불필요한 람다식을 적용한다면 코드를 읽는 입장에서 굉장히 큰 방해를 받을 것이다.

이로 인하여 트랜잭션 프록시 라는 기술을 적용하여 비즈니스 로직과 거리가 멀지만 공통적인 로직을 적용하는 기술이 도입 되었다.

트랜잭션을 테스트 하는 방법

트랜잭션이 적용 되었는지 어떻게 테스트 해야할까?

정확히 트랜잭션이 적용 되었는지 테스트 하기 까다롭다.

JDBC 처럼 자동 커밋이 되거나 JPA 의 기본 리포지토리 구현에서 트랜잭션을 알아서 적용해주는 기술을 사용할 때, 트랜잭션이 적용되지 않은 것을 놓치기 쉽다.

즉, 사용자가 정의하지 않은 환경에서 마법 같이 일어나는 일들 때문에 사용자의 의도에 맞게 수행 되었는지 테스트 하기 어렵다.

트랜잭션을 테스트 하고 싶다면 트랜잭션 흐름의 중간에 실패하는 케이스를 만들 수 있다면 롤백 여부로 검증할 수 있다.

트랜잭션 프록시


📝

데코레이터 패턴 오브젝트의 코드를 변경하지 않고 새로운 기능을 런타임에 부여하는 디자인 패턴

📝

프록시 패턴 타깃을 대신해서 존재하며 접근을 제거하거나 보안, 지연, 원격 접속 등의 기능을 제공

트랜잭션의 부가 기능을 제공하기 위해 OrderServiceTransactionProxy 프록시를 구성한다.

public class OrderServiceTxProxy implements OrderService {  
  
    private final OrderService target;  
    private final PlatformTransactionManager transactionManager;  
  
    public OrderServiceTxProxy(OrderService target, PlatformTransactionManager transactionManager) {  
        this.target = target;  
        this.transactionManager = transactionManager;  
    }  
  
    @Override  
    public Order createOrder(String no, BigDecimal total) {  
        return target.createOrder(no, total);  
    }  
  
    @Override  
    public List<Order> createOrders(List<OrderReq> reqs) {  
        return new TransactionTemplate(transactionManager)  
                .execute(status -> target.createOrders(reqs));  
    }  
}

이렇게 프록시 객체를 구현하면 실제 Target 이 책임 지고 있는 비즈니스 로직에만 관심을 갖고, 부가적인 트랜잭션은 관심을 갖지 않아도 된다.

나중에 구현체를 사용할 때 프록시 객체를 사용하면 트랜잭션이 적용된 비즈니스 로직을 실행할 수 있게 되는 것이다.

스프링이 만들어주는 트랜잭션 프록시

@Transactional 애노테이션이 붙은 클래스의 메소드가 트랜잭션 안에서 실행되도록 프록시를 만들어줌.

스프링의 프록시 AOP