728x90

도커를 공부하다보니 궁금한 점이 생겼다. 원하는 어플리케이션을 도커에 띄울 때 dockerFile을 사용하기도 하고 docker-compose.yml을 사용하기도 한 다. 왜 둘이 분리해놓은 것일까? 다른 점은 무엇일까?

구글링한 결과 다음과 같은 글을 볼 수 있었다.
Difference between Docker Compose Vs Dockerfile | dockerlabs

해석하면 다음과 같다.

Dockerfile은 사용자가 이미지를 어셈블하기 위해 호출할 수 있는 명령이 포함된 간단한 텍스트 파일인 반면, Docker Compose는 다중 컨테이너 Docker 애플리케이션을 정의하고 실행하기 위한 도구입니다.

Docker Compose는 앱을 구성하는 서비스를 docker-compose.yml에 정의하여 격리된 환경에서 함께 실행할 수 있습니다. docker-compose up을 실행하여 하나의 명령으로 앱을 실행합니다.
프로젝트의 docker-compose.yml에 빌드 명령을 추가하면 Docker compose는 Dockerfile을 사용합니다. Docker 워크플로는 생성하려는 각 이미지에 적합한 Dockerfile을 빌드한 다음 compose를 사용하여 build 명령을 사용하여 이미지를 조합하는 것이어야 합니다.

 

  • Dockerfile
    • 목적
      • 이미지를 어셈블하기 위해 호출할 수 있는 명령이 포함된 간단한 텍스트 파일
      • 단일 Docker 이미지를 빌드하기 위한 명세서
      • 이 파일에는 베이스 이미지 선택, 추가 파일 복사, 환경 변수 설정, 필요한 소프트웨어 설치, 컨테이너 실행 시 실행할 명령어 등 이미지를 생성하기 위한 모든 명령어가 포함
    • 기능
      • Docker 이미지를 생성
      • 이 이미지는 애플리케이션과 그 애플리케이션을 실행하는 데 필요한 모든 종속성을 포함
      • 이미지는 컨테이너를 생성하는 데 사용
    • 사용 예시
      • 보통 애플리케이션 개발 과정에서 특정 서비스나 애플리케이션의 빌드 방식을 정의할 때 사용
      • Node.js 애플리케이션을 위한 Dockerfile은 Node.js 환경을 설정하고 애플리케이션 코드를 이미지 내부로 복사하는 명령어를 포함할 수 있다.
  • Docker Compose
    • 목적
      • 앱을 구성하는 서비스를 docker-compose.yml에 정의하여 docker-compose up을 실행하여 하나의 명령으로 앱을 실행
      • 즉, 앱이 실행되는 동안 컨테이너를 관리하는 역할
      • 앱이 시작되면 컨테이너를 띄우고 앱이 실행되는 중에 컨테이너가 종료되면 다시 띄워줌
      • 여러 Docker 컨테이너를 정의하고 실행하기 위한 도구인 Docker Compose의 설정 파일
      • 이 파일에서는 애플리케이션을 구성하는 여러 서비스(예: 데이터베이스, 백엔드 애플리케이션, 프론트엔드 애플리케이션 등)를 정의하고, 각 서비스에 대한 이미지, 포트 매핑, 볼륨 마운트, 네트워크 설정 등을 지정
    • 기능
      • 여러 컨테이너의 배포 및 관리를 단순화
      • docker-compose 명령어를 사용하여 모든 서비스를 한 번에 시작, 중지, 재구축할 수 있다.
    • 사용 예시
      • docker-compose.yml은 마이크로서비스 아키텍처 또는 여러 종속성을 가진 복잡한 애플리케이션을 로컬 개발 환경이나 테스트 환경에서 실행할 때 주로 사용
      • 예를 들어, 웹 애플리케이션, 관련 데이터베이스, 그리고 그것들을 연결하는 네트워크를 동시에 정의하고 실행할 수 있다.
  • docker-compose.yml 파일에서 Dockerfile의 경로를 통해 Dockerfile에 정의된 이미지를 관리할 수도 있다.
  • 그래서 docker-compose.yml에서 빌드에 대한 내용이 어떻게 나타나 있는지 찾아보게 되었고 docker 공식 홈페이지에서 이런 내용을 찾아 볼 수 있었다.

Legacy versions | Docker Docs

As with docker run, options specified in the Dockerfile, such as CMD, EXPOSE, VOLUME, ENV, are respected by default - you don’t need to specify them again in docker-compose.yml.


Dockerfile에서 나타난 CMD, EXPOSE, VOLUME, ENV에 관한 내용에 따라서 빌드가 되기 때문에 이에 관련된 내용은 docker-compose.yml에서 따로 나타낼 필요는 없다.

 

  • docker-compose.yml에서 Dockerfile의 경로 지정
version: "3.9"
services:
  webapp:
    build: ./dir

 

  • Dockerfile의 args아래에 지정된 경로가 있는 경우
version: "3.9"
services:
  webapp:
    build:
      context: ./dir
      dockerfile: Dockerfile-alternate
      args:
        buildno: 1

 

 


한 줄 정리


Dockerfile : 이미지 빌드
docker-compose.yml : 앱이 실행되는 동안 컨테이너 관리

728x90
728x90

Docker로 개발환경을 빌드하려고 공부중에 Docker, docker-compose의 차이에 대해 알아 볼려고 한다.

 

1. Docker

  • Single Container를 관리하는것
  • 커맨드 라인에서 명령어를 실행할 수 있다.
  • Go언어로 작성된 리눅스 컨테이너를 기반으로하는 오픈소스 가상화 플랫폼
  • VM처럼 가상화기술을 사용하여 독립된 환경에서 애플리케이션을 실행할 수 있는 컨테이너를 생성
  • Docker file과 Docker Image를 활용해 컨테이너를 생성할 수 있음

 

2. Docker-compose

  • 여러 Docker 컨테이너를 정의하고 실행하기 위한 도구
  • yaml file 기반으로 multi container 관리할 수 있는 client이고 yaml파일에 명령어를 적어서 컨테이너를 정의하고 관리한다.
  • 여러 컨테이너가 서로 어떻게 상호 작용하는지, 네트워크와 볼륨 설정은 어떻게 되어야 하는지 등을 정의
  • 관리나 가독성 측면에서 docker-compose를 사용해 주는게 더 좋다.
  • Window에서 docker를 사용하기 위해 docker desktop을 설치하면 자동으로 docker compose가 설치 된다.
  • cmd창에서 아래 명령어로 해당 버전을 확인 할 수 있다. 
docker-compose -v 
docker -v

 

3. Docker-compose 작성법

version: '3'

services: // 생성하고자 하는 컨테이너들을 컨테이너이름, 설정 순으로 나열
    nginx:
        build:
            context: ./requierments/nginx // 빌드명령을 실행할 경로
            dockerfile: ./Dockerfile // 빌드를 실행할 파일
        volumes: // 컨테이너에 마운트할 볼륨 경로
            - wp:/var/www/html
        ports: // 호스트에서:같은네트워크
            - "443:443"
        networks:
            - intra
        depends_on: // 종속성 순서대로 실행될 수 있도록 설정
            - wordpress
        env_file: // 환경변수가 저장되있는 파일
            - .env 
        restart: always

    mariadb:
        build:
            context: ./requierments/mariadb
            dockerfile: ./Dockerfile
        networks:
            - intra
        volumes:
            - db:/var/lib/mysql // volumes의 db를 /var/lib/mysql와 연결하는것
        env_file:
            - .env
        restart: always

    wordpress:
        build:
            context: ./requierments/wordpress
            dockerfile: ./Dockerfile
        depends_on:
            - mariadb
        volumes:
            - wp:/var/www/html
        networks:
            - intra
        env_file:
            - .env
        restart: always

volumes:
    wp:
        driver: local // 볼륨에 사용할 볼륨 드라이버
        driver_opts: // 볼륨 드라이버에 전달할 옵션 목록
            type: none // 사용할 볼륨 드라이버 유형
            o: bind // 마운트 옵션
            device: 'home/$user_name/data/wordpress' // 호스트 파일 시스템에서 사용할 경로

    db:
        driver: local
        driver_opts:
            type: none
            o: bind
            device: 'home/$user_name/data/mariadb'

networks:
    intra:
        driver: bridge

 

4. Docker-compose 명령어

  • docker-compose up
    • docker-compose.yml을 바탕으로 컨테이너들을 생성하기 시작
    • -d 옵션을 사용하면 컨테이너를 백그라운드에서 실행시킴
    • --build 옵션을 사용하면 컨테이너 시작시 dockerfile을 빌드함
  • docker-compose down
    • docker-compose.yml을 바탕으로 생성한 컨테이너나 이미지들을 정지시키고 일괄적으로 삭제해줌
    • --rmi all 옵션을 사용하면 모든 이미지를 삭제함
    • -v 옵션을 사용하면 데이터 볼륨을 삭제함
  • docker-compose ps
    • 현재 작동중인 컨테이너의 목록들을 보여줌
    • -q옵션을 사용하면 컨테이너 ID만 출력
  • docker-compose stop
    • 현재 작동중인 컨테이너들을 일괄적으로 정지함
  • docker-compose start
    • 현재 정지된 컨테이너들을 일괄적으로 시작함
  • docker-compose restart
    • 현재 작동중인 컨테이너들을 일괄적으로 재시작함
  • Docker를 사용하다보면 이미지 생성중 에러가 발생시 생기는 <none> 이미지, 컨테이너가 쌓이게 됨
    • docker rm $(docker ps --filter status=exited -q)
    • docker rmi $(docker images -f "dangling=true" -q)
    • 두 명령어를 사용하면 <none> 이미지와 컨테이너를 삭제할 수 있음
728x90
728x90

동시성 이슈 (a.k.a. 따닥 이슈)

개념 및 문제점

동시성 이슈, 흔히 말하는 따닥 이슈라고 합니다.
유저가 어떤 버튼을 한 순간에 여러 번 클릭하여 API 호출이 중복으로 일어나게 되면 따닥 이슈가 발생했다고 합니다.
이런 경우 비즈니스 로직에서 예외 처리를 해주어도, 여러 요청이 동시에 비즈니스 로직을 타게 되어 예외가 발생하지 않고 통과하게 됩니다.

따닥 이슈는 아래와 같은 문제를 발생합니다.
예시를 들어서, 개발 중인 혜택 서비스의 경우 유저가 어떠한 액션을 수행했을 때, 이 액션에 대한 보상으로 페이포인트 리워드를 제공합니다. 페이포인트는 실제 현금은 아닌데 그렇지만 현금처럼 사용할 수 있는 디지털 화폐이고, 회사 예산을 사용하기 때문에 중복으로 제공하면 안 됩니다. 하지만 따닥 이슈가 발생하면, 유저에게 페이포인트가 중복으로 지급되는 문제가 발생할 수 있습니다.

해결 방안 및 선택 이유

혜택 서비스를 배포하기 전 해당 이슈를 방지할 필요가 있었습니다.
1차적으로는 FE에서 디바운스를 통해 동시성 이슈를 막고 있지만, 100% 막을 수는 없어 서버에서도 이를 인지하고 방어해야 하는데요. 서버에서 해결할 수 있는 방법에는 애플리케이션 단 분산 락 구현, DB 단 베타 락(쓰기 락) 사용 등이 있습니다.

 

1. 분산 락 구현

여러 서버가 공유 자원을 동시에 사용하는 경우, Redis 같은 외부 시스템을 이용해 락을 걸어 동시성 문제를 방지할 수 있습니다.

Redis: SETNX 명령어를 통해 락을 걸고 해제하는 분산 락 구현이 가능합니다. Redisson 라이브러리를 사용하면 쉽게 Redis 기반의 락을 적용할 수 있습니다. 이미 기존에 키가 존재하는 경우에는 작업 수행에 실패하여 false를 반환하도록 할 수 있습니다. 

그리고 레디스는 단일 스레드로 작동되는 키-값 저장소이기 때문에 모든 명령은 순차적으로 처리됩니다. 이러한 특성으로 인해 분산락을 구현하면 다음 그림과 같습니다.

 

 

2. 데이터베이스 수준에서의 잠금

  • 행 잠금 (Row Locking): 특정 데이터베이스의 레코드를 잠그는 방식입니다. 특정 트랜잭션이 끝날 때까지 다른 트랜잭션은 잠긴 행에 접근할 수 없습니다. 주로 금융 등 정합성이 중요한 경우 사용됩니다.
  • 비관적 잠금 (Pessimistic Locking) == 베타 락 : 트랜잭션이 데이터를 수정하려 할 때 데이터를 잠가 다른 트랜잭션이 접근하지 못하게 합니다. 주로 SELECT FOR UPDATE 구문을 사용하여 잠금을 걸 수 있습니다.
  • 낙관적 잠금 (Optimistic Locking): 데이터를 갱신하기 전 데이터의 버전을 확인하여, 갱신하려는 동안 다른 트랜잭션이 데이터를 변경하지 않았는지 확인하는 방식입니다. version 컬럼을 추가하여 버전을 검사하는 방식이 자주 사용됩니다.
728x90
728x90

RedissonClient는 데이터베이스, 캐시, 메시지 브로커 등 일반적으로 사용되는 오픈 소스 인메모리 데이터 구조인 Redis에 사용할 수 있는 자바 클라이언트 라이브러리입니다. RedisClient는 Redis와 함께 사용하는데 도움을 주는 고급 인터페이스를 제공하는데, 분산 잠금, 스케줄링 등 기능을 제공합니다.

저는 RedisClient의 분산락 기능을 활용하여 해당 문제를 해결하였습니다.

 

먼저, Spring에서 RedisClient를 사용하기 위해서는 해당 라이브러리 의존성을 주입하고 config 설정을 해야 합니다.

 

implementation 'org.redisson:redisson-spring-boot-starter:3.17.7'
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6381");
        return Redisson.create();
    }
}

 

 

RedissonClient에서 분산락 설정을 위해 사용한 기능은 아래와 같습니다.

 

RLock: RLock은 여러 스레드 또는 프로세스 간에 공유 리소스에 대한 액세스를 동기화하는 방법을 제공하는 Redisson 분산락 기능입니다. 

 

lock.tryLock(): RLock의 tryLock() 메서드는 즉시 잠금을 획득하려고 시도하여 잠금을 획득한 경우, true를 반환하고, 잠금을 획득하지 않은 경우 false를 반환합니다.

 

lock.unlock(): 해당 메소드는 이전에 획득한 잠금을 해제합니다. 보통 try-catch-finally의 finally에 작성하여 해당 락이 종료되도록 하여 데드락이 발생하는 것을 방지할 수 있습니다.

 

lock.isHoldByCurrentThread(): 잠금이 현재 스레드에 의해 유지되고 있는지 확인하여, 맞다면 true, 아니라면 false를 반환합니다.

 

// 좋아요 버튼을 눌렀을 때
@PostMapping("/posts/{postId}/like")
public ResponseEntity<?> like(@PathVariable("postId") Long postId, Authentication authentication) {

    // Redis 분산 락을 postId에 기반하여 생성
    RLock lock = redissonClient.getLock("post-like-lock:" + postId);

    try {
        // 락을 5초 동안 시도하고, 락을 얻으면 3초 후 자동 해제
        boolean isLocked = lock.tryLock(5, 3, TimeUnit.SECONDS);
        if (isLocked) {
            if (authentication != null && authentication.isAuthenticated()) {
                String username = authentication.getName();
                Optional<User> userOptional = userService.findByUserName(username);

                if (userOptional.isPresent()) {
                    User user = userOptional.get();
                    Optional<Post> postOptional = postService.getPostById(postId);

                    if (postOptional.isPresent()) {
                        Post post = postOptional.get();
                        boolean liked = likeService.like(post, user);
                        Long likeCount = likeService.countByPostId(postId);

                        String postOwnerUsername = postService.getPostOwnerUsername(postId);
                        if (!username.equals(postOwnerUsername)) {
                            notificationService.createNotification(postOwnerUsername, username + "님이 게시글에 좋아요를 눌렀습니다.");
                        }
                        return ResponseEntity.ok().body(new LikeResponse(liked, likeCount));
                    }
                }
            }
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("사용자를 찾을 수 없습니다.");
        } else {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("잠금 획득에 실패했습니다. 잠시 후 다시 시도해 주세요.");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("잠금 중 오류가 발생했습니다.");
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }

 

 

 

 

 

 

728x90

'Spring' 카테고리의 다른 글

Docker 란?  (0) 2024.12.09
동시성 이슈  (1) 2024.12.01
날씨 API 불러오기 - OpenWeather API 사용  (2) 2024.08.22
날씨 API 불러오기 - Weather API 사용  (0) 2024.08.22
Spring : @Controller와 @RestController의 차이  (0) 2024.08.02
728x90

개요

스터디 중에 AOP에 대한 압박 질문을 받았는데 하나도 대답을 못해서 정리할려고 한다...ㅎㅅㅎ

 


 

1. 사용 목적

애플리케이션 로직은 크게 핵심 기능과 부가 기능으로 나눌 수 있다. 

  • 핵심 기능은 해당 객체가 제공하는 고유의 기능
  • 부가 기능은 핵심 기능을 보조하기 위해 제공되는 기능이다.(로그 추적 기능, 트랜잭션 기능 등...)

보통 기존 프로젝트에 부가 기능을 추가하게 되면 하나의 클래스가 아닌 여러 클래스에 부가 기능을 추가하게 된다.

 

예를 들어서 프로젝트의 모든 클래스에 로그 기능을 추가한다면 하나의 부가 기능(로그 추적)을 여러 곳에 동일하게 사용하게 된다.

 

이러한 부가 기능횡단 관심사라고 한다.

 

1.1 단점

하지만 이러한 기존 프로젝트에 부가 기능을 추가하게 된다면 여러 문제점이 있다. 만약 부가 기능을 적용해야 할 클래스가 100개 라면 100개에 모두 똑같은  부가 기능 코드를 추가해야 하고 거기에 더해 단순 호출이 아닌 try- catch-finally 구문이 필요하다면 더욱 복잡해질 것이다. 

이렇게 많은 클래스에 부가 기능을 힘들게 추가했다고 가정한 후 만약 수정이 필요하다면,,,? 또 100개의 클래스를 하나씩 뜯어 고쳐야 한다는 노가다 과정을 겪어야 한다.

 

 

2. AOP란?

이러한 부가 기능을 적용할 때 위와 같은 문제점이 발생한다. 그래서 많은 개발자들은 오랜 시간 고민해왔다. 고민 끝에 AOP라는 용어가 나오게 되었다.

AOP는 관점 지향 프로그래밍의 약어로 부가 기능을 핵심 기능에서 분리해 한 곳으로 관심하도로 하고 이 부가 기능을 어디에 적용할지 선택하는 기능을 합한 하나의 모듈이다. 

 

 

 

3. AOP 용어 정리

AOP 프록시

AOP 기능을 구현하기 위해 만든 프록시 객체, 스프링에서 AOP 프록시는 JDK 동적 프록시 또는 CGLIB 프록시이다.

 

조인 포인트 ( Join Point )

어드바이스가 적용될 수 있는 위치로, AOP를 적용할 수 있는 모든 지점이라 생각하면 된다.

스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메소드 실행 지점으로 제한된다.

 

포인트컷 ( Pointcut )

조인 포인트 중에서 어드바이스(부가 기능)를 어디에 적용할 지, 적용하지 않을 지 위치를 판단하는 필터링하는 기능 ( 주로 AspectJ 표현식을 사용해서 지정 )

프록시를 사용하는 스프링 AOP는 메서드 실행 지점을 포인트 컷으로 필터링 한다.

 

타겟 ( Target )

어드바이스를 받는 객체, 포인트컷으로 결정

 

어드바이스 ( Advice )

부가 기능

특정 조인 포인트에서 Aspect에 의해 취해지는 조치

Around(주변), Before(전), After(후)와 같은 다양한 종류의 어드바이스가 있음

 

애스펙트( Aspect )

어드바이스 + 포인트컷을 모듈화 한 것

하나의 어드바이스만이 아닌 여러 어드바이스와 포인트 컷이 함께 존재할 수 있다.

 

어드바이저 ( Advisor )

하나의 어드바이스와 하나의 포인트 컷으로 구성

즉, 어드바이스 + 포인트 컷 = 어드바이저

 

 

 

4. AOP 적용 방식

AOP의 적용 방식은 크게 3가지가 있다.

  • 컴파일 시점
  • 클래스 로딩 시점
  • 런타임 시점 ( 프록시 사용 )

컴파일 시점과 클래스 로딩 시점 적용 방식은 AspectJ 프레임워크를 직접 사용해야 하고, 이 AspectJ를 학습하기 위해선 엄청난 분량과 설정의 번거로움이 있다. 그래서 주로 런타임 시점 적용 방식을 사용하는 스프링 AOP를 사용 한다.

 

 

 

 

5. AOP 적용 가능 위치

주로 사용하는 런타임 시점 적용 방식의 스프링 AOP 관점에서의 방식이다. 

  • 스프링 AOP는 메서드 실행 지점에만 AOP를 적용할 수 있다.
  • 프록시 방식을 사용하는 스프링 AOP는 스프링 컨테이너에 해당 @Aspect을 빈 등록을 해야 AOP를 적용할 수 있다.

 

 

6. 스프링에서 AOP 적용하기 (@Aspect)

스프링 AOP를 적용하기 위해서는 아래의 라이브러리를 build.gradle에 의존성을 추가 해줘야 한다.

implementation 'org.springframework.boot:spring-boot-starter-aop' // 스프링 aop 추가

위의 라이브러리를 추가하면 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록해준다.

이러한 스프링 부트의 자동 설정은 "AnnotationAwareAspectJAutoProxyCreator"라는 빈 후처리기가 스프링 빈에 자동으로 등록해주는데, 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기이다.

 

6.1 AnnotationAwareAspectJAutoProxyCreator란? 

1. @Aspect 어노테이션이 붙은 클래스를 Advisor(어드바이저)로 변환하여 저장

2. Advisor(어드바이저)를 자동으로 찾아와 프록시를 생성하고 Pointcut(프록시 적용 대상 필터링)을 보고 프록시가 필요한 곳에 Advice(부가 기능)을 적용

 

 

6.2 코드 - @Around 

  • 스프링은 @Aspect 애노테이션으로 매우 편리하게 포인트컷과 어드바이스로 구성되어 있는 어드바이저 생성 기능을 지원한다.
  • 어드바이저로 사용할 클래스에 @Aspect 어노테이션을 붙여줌으로써 스프링 AOP를 적용할 수 있다.
@Aspect
public class LogTraceAspect {
 
    @Around("execution(* hello.proxy.app..*(..))")  // 포인트컷 (AspectJ 표현식)
    public Object execute(ProceedingJoinPoint joinPoint) { // 어드바이스
        // 어드바이스 로직
    }
}

@Around 어드바이스를 사용할 경우 메서드의 파리미터로 "ProceedingJoinPoint"를 꼭 넣어줘야 한다.

ProceedingJoinPoint의 proceed()는 다음 어드바이스나 타켓을 호출하는 것으로, 어드바이스를 사용하기 위해서는 꼭 proceed() 메서드를 호출해줘야 한다.

 

이 외에도  호출되는 대상 객체에 대한 정보, 실행되는 메서드에 대한 정보 등이 필요할 때가 있는데 이 경우에는

ProceedingJoinPoint 인터페이스가 제공하는 아래의 메서드를 사용할 수 있다. (이 외에도 더 있지만 일부만 설명)

메서드 설 명
Signature getSignature()  호출되는 메서드에 대한 정보를 반환
Object getTarget()  대상 객체를 반환
String getName  메서드의 이름을 반환
String toLongString()  메서드를 완전하게 표현한 문장을 반환 (메서드의 리턴 타입, 파라미터 타입 모두 표시)

 

이렇게  ProceedingJoinPoint 호출되는 객체에 대한 정보나, 실행되는 메서드의 정보 알 수 있는 이유

스프링 부트 자동 설정으로 "AnnotationAwareAspectJAutoProxyCreator" 이라는 자동 프록시 생성기가 빈 등록되어 있는데,

이 자동 프록시 생성기가 @Aspect가 붙은 클래스를 보고 Advisor(어드바이저)로 변환해 저장해준다.

그리고 이 Advisor(어드바이저)를 보고 포인트컷의 대상이 되는 것들을 "ProxyFactory"에 인자로 넘겨 자동으로 프록시를 생성하고 적용해준다.

여기서 생성된 프록시 객체가 메서드를 호출할 때, ProceedingJoinPoint 객체를 생성하고 이를 advice에 전달한다.

즉, ProceedingJoinPoint는 프록시가 메서드를 호출하는 시점의 정보를 가져 어드바이스가 적용되는 대상을 이미 알고 있다.

 

 

6.3 @Pointcut

@Around 에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리해 재사용할 수 도 있다.

@Slf4j
@Aspect
@Component
public class AspectExample {
 
    // hello.aop.test 패키지와 하위 패키지에 적용
    @Pointcut("execution(* hello.aop.test..*(..))")
    private void allTestLog() {} //  포인트컷 시그니쳐 
 
    @Around("allTestLog()") // 포인트컷을 메서드로 만들어 사용
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 실제 타깃 호출
    }
}

 

 

 

7. 어드바이스 종류

  • @Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능 (사실 이거 하나만 사용해도 무방)
  • @Before : 조인 포인트 실행 이전에 실행
  • @AfterReturning : 조인 포인트가 정상 완료후 실행
  • @AfterThrowing : 메서드가 예외를 던지는 경우 실행
  • @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

@Around를 제외한 나머지 어드바이스들은 JoinPoint를 첫 번째 파라미터에 사용한다. (생략도 가능)

( JoinPoint는 ProceedingJoinPoint의 부모 타입)

 

728x90
728x90

개요

스프링 배치를 사용해서 알림 재전송 기능을 구현했을 때 다양한 방식들의 트리거링 방식에 대해서 정리할려고 한다.

 

 

배치 트리거링(Batch Triggering)

 

  • 배치 트리거링(Batch Triggering)은 특정 조건이나 이벤트가 발생할 때 자동으로 배치 작업을 시작하는 메커니즘을 말한다.
  • 배치 작업은 대량의 데이터를 처리하거나 주기적으로 실행되는 작업을 자동화하는 데 사용되는데 이때 트리거링을 통해 작업 시작을 제어한다.

 

 

배치 트리거링 방식

  • 시간 기반 트리거(Time-based Trigger)
    • 특정 시간이나 주기로 배치 작업이 실행되도록 설정하는 방식이다.
    • 매일 자정에 데이터 백업 작업을 실행하는 경우
  • 이벤트 기반 트리거(Event-based Trigger)
    • 특정 이벤트가 발생할 때 배치 작업을 실행하는 방식이다.
    • 사용자가 주문을 완료하면 해당 주문 정보를 처리하는 배치 작업이 실행되는 경우
  • 조건 기반 트리거(Condition-based Trigger)
    • 특정 조건이 충족될 때 배치 작업을 실행하는 방식이다.
    • 데이터베이스에 일정한 양의 데이터가 쌓이면 이를 처리하는 배치 작업이 실행되는 경우

 

배치 트리거링은 시스템 효율성을 높이고 사용자가 수동으로 작업을 실행하지 않아도 자동으로 필요한 처리가 이뤄지도록 돕는 중요한 개념이다.!!!!

728x90
728x90

개요

1편에서 배치에 대해 살펴보았고 이제 내가 하는 프로젝트에 배치를 적용해볼 예정이다. 

 

일단 나는 SSE 알림 기능을 구현했는데, 일정 기간동안 읽지 않은 알림이 있다면 사용자에게 "읽지 않은 알림이 있습니다." 라는 문구를 띄어줄려고 한다. 주기적인 스케줄을 가지고 필요한 알림을 처리하기 위한 기술이 필요했다. 그래서 주기적인 schedule 에 따라 Spring Batch를 통해서 필요 데이터를 reading, 알람을 보내는 기능을 구현하기로 했고 알람은 SSE를 사용하기로 했다.

 

사용자가 접근하지 않는 시간대인 매일 자정에 매번 실행해서 7일동안 읽지않은 알림이 존재할 경우 알림을 보내는 역할을 할 것이다.

 

 


설정

 

build.gradle

// spring batch
implementation 'org.springframework.boot:spring-boot-starter-batch'

batch 의존성 추가

 

 

application.yml

spring:
  application:
    name: {name}

  batch:
    jdbc:
      initialize-schema: always #batch 스키마 자동 생성
    job:
      enabled: false #시작과 동시에 실해되는건 방지

 

 

 

Cofing.java

@Configuration
@EnableBatchProcessing
@Slf4j
@RequiredArgsConstructor
public class NotificationBatchConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager platformTransactionManager;
    private final NotificationRepository notificationRepository;

    // 배치 작업의 전체 실행
    @Bean
    public Job deleteNotificationsJob(Step deleteUnreadNotificationsStep) {
        log.info(">>> Creating deleteNotificationsJob");
        return new JobBuilder("deleteNotificationsJob", jobRepository)
                .start(deleteUnreadNotificationsStep)
                .build();
    }

    // 배치 작업 내 실행되는 개별 처리
    @Bean
    public Step deleteUnreadNotificationsStep() {
        log.info(">>> Creating deleteUnreadNotificationsStep");
        return new StepBuilder("deleteUnreadNotificationsStep", jobRepository)
                .tasklet(deleteUnreadNotificationsTasklet(), platformTransactionManager)
                .build();
    }

    // 10분 이상 읽지 않은 알림 삭제
    @Bean
    public Tasklet deleteUnreadNotificationsTasklet() {
        return (contribution, chunkContext) -> {
            LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(10);
            List<Notification> notificationsToDelete = notificationRepository.findByIsReadAndCreatedAtBefore(false, cutoffTime);
            notificationRepository.deleteAll(notificationsToDelete);
            log.info("Deleted unread notifications older than 10 minutes");
            return RepeatStatus.FINISHED;
        };
    }
}

 

 

Tasklet

  • 일반적인 Component와 비슷한 개념, 개발자가 지정한 커스텀한 기능을 위한 단위
  • 개발자가 이 STEP에서 하고 싶은 내용을 자유롭게 만들 수 있다.

 

 

BatchScheduler.java

@Component
@Slf4j
@RequiredArgsConstructor
public class NotificationBatchScheduler {

    private final JobLauncher jobLauncher; // 정의된 job 을 주기적 실행
    private final Job deleteNotificationsJob;

    @Scheduled(cron="0 0 12 * * *", zone = "Asia/Seoul") // 매일 자정에 실행
    public void runDeleteUnreadNotificationsJob() {
        try {
            jobLauncher.run(deleteNotificationsJob, new JobParametersBuilder()
                    .addLong("timestamp", System.currentTimeMillis())
                    .toJobParameters());
            log.info("Notification deletion job executed successfully.");
        } catch (Exception e) {
            log.error("Failed to execute notification deletion job", e);
        }
    }
}

 

jobLauncher.run()메서드는 첫번째 파라미터로 Job, 두번째 파라미터로 Job Parameter를 받고있다.
Job Parameter의 역할은 반복해서 실행되는 Job의 유일한 ID이다.

Launcher 가 어떤 배치 작업(Job)을 실행할 지에 대해 작성한다. 

Job은 어떤 데이터들을 통해서 어떤 작업을 실행할 지, 실행 순서 등에 대한 내용(Step)을 담고 있어야 한다.

 

 

Scheduler

  • batch를 수행하기 위한 Scheduler를 파일에 구성(매일 자정에 실행)
Spring Batch는 Batch Job을 관리하지만 `Job`을 구동하거나 실행시키는 기능은 지원하고 있지않다.
Spring에서 Batch Job을 실행시키기 위해서는 `Quartz, Scheduler`와 같은 전용 Scheduler를 사용해야한다.
728x90
728x90

 

 

 

배치란? 스프링 배치란? 

  • 배치 는 일정 시간마다 또는 특정 조건에 따라 대규모 데이터를 일괄 처리하는 방식을 의미한다.
  • 스프링 배치(Spring Batch)는 이러한 배치 작업을 보다 쉽게 구현하고 관리할 수 있도록 돕는 프레임워크이다.
  • 스프링 배치는 스프링 생태계의 일환으로, 스프링의 강력한 기능을 활용하여 배치 작업을 안정적이고 효율적으로 수행할 수 있도록 지원한다.
  • 이를 통해 개발자는 복잡한 배치 처리 로직을 간결하게 구현하고 모니터링 기능을 통해 배치 작업을 효과적으로 관리할 수 있다.

 

스프링 배치와 스프링 스케줄러 차이

  • 많은 개발자들이 이 두 가지를 혼동하곤 하지만, 스프링 배치(Spring Batch)와 스프링 스케줄러(Spring Scheduler)는 서로 다른 목적과 기능을 가지고 있다.

 

스프링 배치

  • 대량의 데이터를 효율적으로 처리하기 위해 설계된 프레임워크
  • 복잡한 배치 작업을 구성하고 실행하기 위한 다양한 도구와 기능을 제공
    • 배치 프로세스를 주기적으로 커밋: 막강한 커밋 전략을 제공
    • 동시 다발적인 job 의 배치 처리, 대용량 병렬 처리
    • 실패 후 수동 또는 스케줄링에 의한 재시작: 오류에 대한 대처 옵션을 제공
    • 반복, 재시도, skip 처리 : 실패한 배치 작업을 중단된 지점부터 다시 시작하거나 skip 할 수 있다.
    • 모니터링: 배치 작업의 진행 상태와 소요 시간 등의 정보를 제공
    • 트랜잭션 관리: 트랜잭션 관리를 지원하여 데이터의 일관성 유지

 

스프링 배치는 주로 금융, 전자상거래, 건강관리 등 대량의 데이터 처리가 필요한 도메인에서 널리 사용된다.

배치(batch)는 데이터를 실시간으로 처리하는게 아니라, 일괄적으로 모아서 한번에 처리하는 작업을 의미한다.

 

 

스프링 스케줄러

  • 스프링 스케줄러는 특정 시간에 작업을 실행하기 위해 설계된 도구이다.
  • 반복적인 작업을 자동화하고, 시간 기반의 작업을 쉽게 설정할 수 있다.
    • 시간 기반 작업 실행: 특정 시간 또는 주기적으로 작업을 실행할 수 있다.
    • 다양한 스케줄링 방식: 크론 표현식(Cron Expression) 또는 Fixed Delay 를 통해 작업 스케줄을 설정할 수 있다.
    • 비동기 처리: 스케줄된 작업을 비동기적으로 실행하여 애플리케이션의 성능을 유지할 수 있다.
    • 유연한 구성: XML 설정 또는 애노테이션을 통해 쉽게 설정할 수 있다.

 

스프링 스케줄러는 주로 정기적인 백업, 이메일 알림, 데이터 동기화 등의 작업을 자동화하는 데 사용된다.

 

 

특징

                                              스프링 배치 (Spring Batch)                                         스프링 스케줄러 (Spring Scheduler)

목적 대량 데이터 일괄 처리 시간 기반 작업 실행
주요 기능 트랜잭션 관리, 재시작 및 중단, 데이터 검증 및 오류 처리 크론 표현식, 고정된 지연, 비동기 처리
사용 사례 금융 데이터 처리, 대량 파일 처리, 시스템 통합 정기 백업, 이메일 알림, 데이터 동기화
구성 방식 주로 XML 설정 또는 Java Config 애노테이션 또는 XML 설정

 

 

 


 

스프링 배치 구조

 

스프링배치 주요 흐름 이미지 출처 (https://terasoluna-batch.github.io/)

 

  1. 스프링 배치 (Spring Batch):
    • 스프링 배치는 대규모 데이터 처리 작업을 자바 기반의 배치 처리 어플리케이션을 만들기 위한 프레임워크
    • 대용량 데이터를 처리하는 경우나 주기적인 업무 일괄 처리 등에 사용
    • 스프링 배치는 배치 작업을 트랜잭션 처리와 함께 관리하고, 실패한 작업을 복구하고 재시작할 수 있는 기능을 제공
    • 잡(Job), 스텝(Step), 리더(Reader), 프로세서(Processor), 라이터(Writer) 등의 개념을 이용하여 배치 처리를 설계하고 실행한다.
    • job은 여러개의 step 으로 구성되고 step은 taskelt(기능) 으로 구성된다.
    • 나는 tasklet으로 구현하였다.!!
  2. 쿼츠 (Quartz):
    • 쿼츠는 자바 기반의 오픈 소스 스케줄링 라이브러리
    • 스케줄링 작업을 관리하고 실행할 수 있는 기능을 제공
    • 주기적으로 실행되어야 하는 작업이나 예약된 작업을 관리하고 실행하는 데 사용
    • 쿼츠는 크론 표현식을 사용하여 실행 스케줄을 설정하고, 다양한 트리거(trigger)를 통해 작업을 실행할 수 있음
    • 분산 환경에서도 사용할 수 있는 확장 가능한 아키텍처를 가지고 있어, 대규모 시스템에서도 유연하게 활용됨
요약하자면, 스프링 배치는 대규모 데이터 처리를 위한 일괄 처리 프레임워크이고, 쿼츠는 예약된 작업을 스케줄링하고 실행하는 데 사용되는 라이브러리 
-> 함께 사용되어 시스템에서 정기적인 작업 처리와 스케줄링을 효과적으로 관리할 수 있음

 

 

 

Job : 전체 배치 프로세스를 캡슐화한 엔티티, 배치 처리 과정을 하나의 단위로 만들어 놓은 객체

 

JobInstance : Job 실행의 단위(job이 각각 실행할 때마다 따로 추적할 수 있도록 매 실행마다 생성되며 특정 job과 식별 가능한 JobParameters에 상응하는 JobInstance는 단 한 개뿐이다.)

 

JobParameters : JobInstance에 전달되는 매개변수 역할, JobInstance를 구분하는 식별자 역할

 

JobExecution : Job에 대한 실행 시도 정보를 담은 객체.

 

Step : 배치 Job의 배치처리를 정의하고 순차적인 단계를 캡슐화 한 도메인 객체. Job은 하나 이상의 Step을 가져야 한다.

 

StepExecution : Step에 대한 실행 시도 정보를 담은 객체.

 

ExecutionContext : 프레임워크에서 유지/관리하는 key-value의 컬렉션. StepExecution객체 또는 JobExecution객체에 속하는 상태를 저장

 

JobExecutionContext : Commit 시점에 저장

 

StepExecutionContext : 실행 사이에 저장

 

JobRepository : 위에 업근된 모든 저장(Persistence)매커니즘을 담당.(JobLauncher, Job, Step 구현체에 CRUD 기능을 제공)

 

JobLauncher : 주어진 JobParameters로 Job을 실행하는 간단한 인터페이스.

 

- Step에서 아이템을 한 번에 하나씩 읽어오는 작업을 추상화한 개념

Tasklet 

   or

Item Reader 

Item Writer : Step에서 배치나 청크 단위로 아이템을 출력하는 작업을 추상화한다.

Item Processor : 아이템을 처리하는 비즈니스 로직을 나타내는 추상화 개념

 


 

https://github.com/spring-projects/spring-batch/wiki/Spring-Batch-5.0-Migration-Guide

 

Spring Batch 5.0 Migration Guide

Spring Batch is a framework for writing batch applications using Java and Spring - spring-projects/spring-batch

github.com

스프링배치5부터는 매인 애플리케이션에 @EnableBatchProcessing 어노테이션을 사용할 필요가 없다.

자세한 내용은 마이그레이션 가이드부터 읽어 보길 권장한다!!

 

 

728x90

+ Recent posts