날씨도 선선하니 기분좋은 월요일 시작이다.

주말에 공부하는게 습관이 되어지다보니
게임을하는 빈도수가 부쩍 줄었다.
하루도 빠짐없이하던 내가 주말에 아주 잠깐정도만 하는 쾌거를 이뤘다.

오늘 공부한 것도 정리하고 이외 공부도 해보자 !!


오늘은 트랜잭션의 마지막 시간이다.
트랜잭션의 개념자체는 어렵게 느껴지지 않는데
그 안에 있는 커넥션이 어떻게 연결되어지는지..
등등 DB의 접근 방법에 대해 구조를 명확히 파악하지 못했던 점이
가장 힘들었던 것 같다.

어제는 DB를 이용해 commit(); rollback(); 쿼리를 보내
실제로 세션마다 다르게 보이고 동작하는걸 확인해 볼 수 있었다.
오늘은 실제 코드에 적용해서 하는법을 배워보자

@Transactional

사용방법

해당 어노테이션으로 말할 것 같으면
AOP 기능까지 추가된 트랜잭션이라고 보면 될 것 같다.

Serivce 로직, 즉 비지니스 로직에서 트랜잭션을 적용해
하나의 그룹으로 묶어 최종적으로 완료하면 commit();
예외가 발생하면 rollback();을 시켜주어야한다.

하나의 비지니스 로직 메서드에는 여러종류의 쿼리가 들어갈 수도있다.
조회(findAll) 후 생성(save)을 할 수 도 있는 것이고
로직에 따라 DB에 접근 수가 늘어날 수도 있다.
이 모든걸 하나의 트랜잭션으로 묶어 모두성공=commit, 실패=rollback
이라고 생각하면 간단할 것 같다.

옛날에는 트랜잭션 매니저를 이용해 직접 작성했다고한다.

image

DB접근 방법에따라 트랜잭션매니저 구현체를 이용해
트랜잭션을 커밋하고 롤백하며 관리했었다고 한다.

예제)

public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    // 트랜잭션 시작
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        //비지니스 로직
        bizLogic(fromId, toId, money);
        transactionManager.commit(status);// 성공시 커밋
    } catch (Exception e) {
        transactionManager.rollback(status); // 실패시 롤백
        throw new IllegalStateException(e);
    }
}

하지만 이렇게될 경우 try, catch문이나
트랜잭션 매니저부분의 commit,rollback 등 반복적인 코드와
트랜잭션 매니저를 사용하기위한 객체를 얻어야해야해서
순수한 비지니스로직의 코드만 남지 못하게되어진다.

이러한 배경에서 Spring AOP를 적용을 할 수도 있지만
@Transactional 애노테이션을 붙여주면 AOP까지
적용되어 깔금한 코드로 변경할 수 있다.

예제)

@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    bizLogic(fromId, toId, money);
}

위와 같이 애노테이션을 붙여 트랜잭션을 적용하는 방법을
선언형 방식의 트랜잭션 적용이라고한다.

클래스 레벨에 적용하는 방법과 매서드 레벨에 적용할 수 있고
레벨에따른 적용범위가 결정되어진다.
해당 애노테이션이 붙은 메서드와 클래스는
자동적으로 트랜잭션 기능이 적용되어 서비스 로직 실행중
Exception이 발생하게되어진다면 rollback();을 실행하게되어진다.


예외에 따른 처리

하지만 Checked 예외와, Unchecked 예외가 서로 다르게 동작한다.

image

rollback();에 대한 차이가 있고
Checked 에외는 반드시 예외처리를 해주어야한다.


Transaction 전파

트랜잭션 전파는 트랜잭션 경계에서 진행중인
트랜잭션이 존재할때 또는 존재하지 않을 때
어떻게 동작할 것인지를 결정하는 방식을 의미한다.

ex) @Transactional(propagation = Propagation.REQUIRED)

  1. Propagation.REQUIRED
    -> 현재 진행 중인 트랜잭션이 존재하면 해당 트랜잭션을 사용하고
    존재하지 않으면 새 트랜잭션을 생성하도록 해준는 속성이다
    즉,서로 다른 서비스에서 다른 트랜잭션을 실행시키지 않게해주는 역할

  2. Propagation.REQUIRES_NEW
    -> 이미 진행중인 트랜잭션과 무관하게 새로운 트랜잭션이 시작된다.
    기존에 진행중이던 트랜잭션은 새로운 시작된 트랙잭션이 종료할 때까지 중지된다.

  3. Propagation.MANDATORY
    -> 진행중인 트랜잭션이 없으면 예외를 발생시킨다.

  4. Propagation.NOT_SUPPORTED
    -> 트랜잭션을 필요로 하지않음을 의미한다.
    -> 트랜잭션이 있으면 메서드 실행이 종료될때까지 진행중인 트랜잭션은 중지되며
    메서드 실행이 종료되면 트랜잭션을 계속 진행한다.

  5. Propagation.NEVER
    -> 트랜잭션을 필요로하지 않음을 의미한다.
    -> 진행 중인 트랜잭션이 존재할 경우에는 예외를 발생시킨다.

Transaction 격리 수준

  1. UNCOMMITED
    -> 커밋되지 않은 읽기
    -> 다른 트랜잭션에서 커밋하지 않은 데이터를 읽는 것을 허용

  2. READ COMMITTED
    -> 커밋된 읽기
    -> 다른 트랜잭션에 의해 커밋된 데이터를 읽는 것을 허용합니다.

  3. REPEATABLE READ
    -> 반복 가능한 읽기
    -> 트랜잭션 내에서 한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회되도록 합니다.

  4. SERIALIZABLE
    -> 직렬화 가능
    -> 동일한 데이터에 대해서 동시에 두 개 이상의 트랜잭션이 수행되지 못하도록 합니다.


Event

Event 란?

Spring 내부에는 Evnet라는 매커니즘을 가지고 있다고한다.
Bean과 Bean사이에 데이터를 전달하는 방법중 하나이다.

일반적으로는 스프링컨테이너의 빈을 주입할때 DI를 이용한다
Evnet를 이용해 객체를 넘겨줄 수도 있다는 말이다.


Evnet 사용방법

사용방법은 간단하다

  1. publisher(발행)로 이벤트를 발생시킨다
  2. listener로 이벤트를 받는다.

publisher로 발행시키는 방법은
ApplicationEventPublisher 클래스의 .publishEvent();
메서드를 통해서 발행시킬 수 있다.

트랜잭션과 연동시켜 한번 예제를 만들어보자

간단한 예제로 Member 클래스에는 여러가지 정보가 있다고 가정하고
회원가입을 눌러 POST매서드를 통해 Service로직이 호출된다 가정해보자

@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class MemberService {
    private final MemberRepository memberRepository;
    private final ApplicationEventPublisher publisher;

    public Member createMember(Member member) {
        Member savedMember = memberRepository.save(member);
        publisher.publishEvent(new MemberCreatedEvent(this, savedMember)); // 이벤트 발생시키기
        return savedMember;
    }
}

간단한 코드로 ApplicationEventPublisher 객체를 스프링컨테이너를 통해
주입받은 객체 publisher로 이벤트를 발생시키는 메서드를 실행시킨다.
해당 객체에는 Event 정보를 담은 객체가 들어있어야한다.

정보를 담은 객체를 따로 만들기위해 클래스를 만들었다.

@Getter
public class MemberCreatedEvent extends ApplicationEvent {
    private Member member;
    public MemberCreatedEvent(Object source, Member member) {
        super(source);
        this.member = member;
    }
}

MemberCreatedEvent라는 객체를 Event하여
우리는 이제 Listenr를 통해 객체를 받을 수 있다.

Listener 클래스를 만들어보자

@Component
@RequiredArgsConstructor
@Slf4j
public class MemberCreatedEventListener {
    private final EmailSender emailSender;
    private final MemberService memberService;
    
    @EventListener // EventListener로 Event 객체 가져옴
    public void listener(MemberCreatedEvent event) throws Exception {
        try {
            String message = "message test";
            emailSender.sendEmail(message);
        } catch (MailSendException e) {
            log.error("MailSendException, rollback");
            throw new RuntimeException(e);
        }
    }
}

@EventListener 어노테이션을 사용한
메서드는 Event로 발행한 객체를 가져올 수 있다.

Service 로직이 실행되어질때 Evnet 메서드를 만나는 순간
제어의 흐름이 Listener 클래스로 넘어오게 되어진다.

위에 예제는 간단하게 email을 보내는 예제로 작성하였고
메서드를 실행하면 5초뒤에 MailSendException이라는
Checked Exception을 발생하게 코드를 작성하였다.

실제 로직을 실행할 경우

  1. Service로직에서 Event 발행하여 Listener 호출
  2. Listener 클래스에서 Event 타입과 맞는객체를 찾아 객체를 주입
  3. .sendEmail(message)에서 Checked Exception 발생
  4. Checked Exception은 rollback이 안되기때문에 Unchecked
    Exception으로 다시 예외처리(RuntimeException)
  5. 최종적으로 rollback하여 기존에 처리되어진 작업 종료

위와 같이 동작하는 모습을 볼 수 있다.


@Async

스프링 프레임워크에서 지원하는 비동기 처리 방식이다.
해당 어노테이션을 이해하기 위해선 멀티쓰레드, 동기, 비동기에
이해가 필요하다.

정말 간단하게 정리하자면
동기 - 하나의 쓰레드에서 같은 제어흐름을 가짐
비동기 - 멀티쓰레드로 서로 다르게 제어흐름을가짐

이라고 생각해볼 수 있을 것 같다.

간단하게 위에서 보았던 코드 예제로 본다면
MemberService에서 하나의 쓰레드로
Event를 발행시키고 Listener 메서드로 호출되게 되어
최종적으로 Unchecked Exception이 발생하여
rollback(); 되어지는 모습을 확인할 수 있다.

만약 위에 코드에서 @Async를 적용하여
해당 메서드를 비동기로 가져갈 수가 있다.

@Component
@RequiredArgsConstructor
@Slf4j
@EnableAsync // 비동기 사용가능하도록 설정
public class MemberCreatedEventListener {
    private final EmailSender emailSender;
    private final MemberService memberService;

    @Async // 비동기로 이메일 전달하도록 변경
    @EventListener 
    public void listener(MemberCreatedEvent event) throws Exception {
        try {
            String message = "message test";
            emailSender.sendEmail(message);
        } catch (MailSendException e) {
            log.error("MailSendException, rollback");
            memberService.deleteMember(event.getMember().getMemberId());
            throw new RuntimeException(e);
        }
    }
}

@EnableAsync , @Async 어노테이션을 이용해
listenr라는 메서드는 비동기상태가 되었다.
즉, MemberSerivce 로직에서 호출하여도 제어의 흐름은
MemberSerivce는 본인 쓰레드대로 진행하고

listenr 메서드는 별도의 쓰레드가 생겨 진행하게된다.


오늘은 이렇게 Evnet와 Transaction에 대해
조금 더 알아보는 시간을 가졌다.

대략적인 내용과 이해는 어느정도 된 것 같다.
하지만 트랜잭션 어노테이션이 해주는 기능이 너무 풍부하기때문에
오히려 내지식의 한계가 보여지는 것 같다.

실제 내부에서 어떻게 동작하는지를 알아야
트래잭션에 대해 완벽히 이해했다고 할 수 있을 것 같다.
지금은 대략 적인 그림으로만 머릿속에서 그려지지만
후에 어드밴스한 공부를 통해 트랜잭션을 다시 정복해보자

오늘 공부는 여기서 끝!!


오늘의 커피량: ☕️ ☕️ ☕️
오늘의 점심: 카레, 김치찌개