@Scheduled에서 Thread관리하기

@Scheduled에서 외부 서비스에 데이터를 처리요청할때 데이터를 중복으로 처리하는 문제가 발생할수 있다.

Scheduled는 매번 실행할때마다 빠른 처리를 위해 fixedDelayString=”1000″ 으로 설정하였고, 한번에 실행되는 thread의 수는 5개로 제한하였다. 또한 Scheduled 한번실행에 처리되지 않은 데이터 1000건을 외부 API를통해 가져와 처리하도록 설계하였다.

이때 발생할수 있는 문제는 1000개의 데이터를 처리(Thread가 종료) 하기전에 Scheduled가 실행되면서 1000개의 데이터를 외부API에 다시 요청하게 되고 1000개의 데이터에는 첫번째 가져온 아직 처리되지 않은 데이터가 포함되어 중복 처리되면서 오류가 발생한다.
한번의 Scheduled의 실행으로 1000개의 데이터를 5개의 Thread가 모두 처리(종료)될때까지 대시 시켜 해당 문제를 해결할수 있는데 방법은 아래와 같다.

// Thread로 실행할 Class
public class Job implements Callable<Void> {
  
  @Override
  public Void call() throws Exception {
    // 외부 API 호출
    // 외부 API를 사용한 데이터 처리요청
    return null;
  }
}

// Scheduled Class
public class MyScheduler {
  
  @Scheduled(fixedDelay = 1000)
  public void executeJob() {
  

    // Thread 개수
    int threadSize = 5;
  
    // 외부에서 처리 데이터 가져오기
    int totalTasks = 1000;
  
    ExecutorService executor = Executors.newFixedThreadPool(threadSize);
    List<Callable<Void>> tasks = new ArrayList<>();
    
    // 모든 작업이 완료될때까지 대기
    for (int i=0; i < totalTasks; i++) {
      Callable<Void> task = new Job();
      tasks.add(task);
    }
    
    try {
      List<Future<Void>> results = executor.invokeAll(tasks);
      
      for (Future<Void> result : results) {
        // 작업 결과 처리
        result.get();
      }
    } catch (InterruptedException | ExcutionException e) {
      // 예외처리
    } finally {
      // ExecutorService 종료
      executor.shutdown();
    }
  }
}

위 코드처럼 ExecutorService를 사용하여 Callable에 작업요청하고 결과를 기다린다.
invokeAll을 사용하여 작업이 완료될때까지 대기하고, 작업결과를 얻어 처리할수 있다. 이렇게 하면 모든 스레드가 종료될때까지 대기한 다음 스케줄링을 실행할수 있다.

invokeAll(Collection> tasks)
여러개의 Callable 타입 작업을 동시에 스레드풀에서 실행하고, 모든 작업이 완료될때까지 기다린다.

invokeAny(Collection> tasks)
여러개의 Callable 타입 작업중 하나라도 완료되면 결과를 반환하고, 다른작업들은 취소한다.

shutdown()
스레드 풀을 종료하고 스레드 풀에 할당된 리소스를 해제한다. 작업이 완료되지 않은 경우에도 스레드 풀을 종료한다.

shutdownNow()
스레드 풀을 즉시 종료하고 아직 시작되지 않은 작업을 취소한다. 작업이 이미 실행중인경우에는 종료되지 않을수 있다.

Command failed with error 13 (Unauthorized)

com.mongodb.MongoCommandException: Command failed with error 13 (Unauthorized): ‘not authorized on hie to execute command

MongoDB에서 사용자 정의 function을 사용할때 발생하는 문제로 테스트시 사용된 MongoDB의 버전은 4.0.14이다.

function에서 처리하는 일은 A Collection의 특정 document를 B Collection으로 복사하는 일을 한다.

// function 생성
function copyDocument(documentId) {
  var doc = db['a_collection_name'].findOne(_id:documentId);
  if (doc) {
    db['b_collection_name'].insertOne(doc);
  }
}

db.system.js.save({
  _id: "copyDocument",
  value: copyDocument
});

// function 실행
// 방법1
db.runCommand({ eval: "copyDocument('documentId')" });
// 방법2
db.eval("copyDocument(ObjectId('documentId'))");

실행결과는 아래와 같다.

{
    "ok" : 0.0,
    "errmsg" : "not authorized on hie to execute command ...",
    "code" : 13,
    "codeName" : "Unauthorized"
}

function호출에 대한 사용자 권한이 없어 발생한 문제이다.

사용자에게 추가 권한 부여

현재 사용자에게는 readWrite권한이 부여되어 있는 상태로 모든 query를 실행하는데 문제가 없는 상태이지만 Admin권한이 누락되어 발생하는 오류일수 있으므로 dbAdmin, dbUser 권한을 추가로 부여한다.

db.grantRolesToUser('username', [{ role: "dbAdmin", db: "database" }]);
db.grantRolesToUser('username', [{ role: "dbUser", db: "database" }]);

사용자에게 추가 권한을 부여한 후에도 동일한 오류가 발생!

Mongod.conf 설정 변경

사용자에게 권한을 추가로 부여한 후에도 동일한 문제가 발생하고 있어 찾아본 결과 mongod.conf 설정파일의 설정을 변경해줘야 한다고 한다.

# 보안 설정 (mongod.conf)
security:
  authorization: enabled

하지만 설정파일을 열어보니 설정하는 방식이 좀 달랐고, 이미 설정되어 있는것으로 보인다.

# Turn on/off security.  Off is currently the default
#noauth=true
auth=true
keyFile=/home/ec2-user/mongodb-keyfile

mongDB에서 function사용에 대한 권한문제를 계속 찾아보던중 eval 사용에 대한 보안 이슈가 있으므로 주의를 요한다는 문구가 많이 보인다.

방향을 틀어 그럼 function을 사용하지 않고 function의 역할을 할수 있는 방법을 찾아보니 aggregate를 사용한 방법을 찾을수 있었다.

Aggregate를 사용하여 function 대체하기

// a_collection에서 documentId를 찾아 b_collection으로 복사
db.a_collection.aggregate([
  {
    $match: {
      _id: ObjectId(documentId)
    }
  },
  {
    $out: "b_collection"
  }
]);

위 방법을 통해 function을 사용하지 않고 원하는 처리를 진행할수 있었다.

하지만 function사용시 발생하는 권한문제는 아직 해결되지 않았으며 방법을 찾게 되면 commant예정이다.

Spring boot에서 Scheduler 사용하기

Scheduler는 어떤 조건을 갖고 일정 간격 또는 특정 시간에 주기적으로 실행됩니다.

Spring boot에서는 @Scheduled 어노테이션을 사용하여 위와 같은 작업을 진행할수 있습니다.

아래 설정은 스케줄러를 사용하기 위한 최소 설정 내용입니다.

먼저 @Scheduled 어노테이션을 사용하기 위해 스케줄러 활성화가 필요합니다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Application {
    public static void main(String []  args) {
        SpringApplication.run(Application.class, args);
    }
}

@EnableScheduling 어노테이션을 추가함으로서 스케줄러를 사용할 준비가 되었습니다.

다음은 스케줄러 클래스를 간단하게 작성하도록 합니다.

아래와 같이 코드를 작성하면 5초에 한번씩 console에 Scheduled executed. 문장을 출력할것입니다.

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class TimeScheduler {

    @Scheduled(initialDelay = 500, fixedDelay = 5000)
    public void execute() {
        System.out.println("Scheduled executed.");
    }
}

@Scheduled 어노테이션에서 사용할수 있는 옵션들을 아래 정리합니다.

자세한 내용은 다음을 차고하세요.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/annotation/Scheduled.html

// 5초마다 실행
@Schedulred(fixedRate = 5000)

// 이전 실행 완료 후 5초 대기 후 실행
@Schedulred(fixedDelay = 5000)

// 첫 실행시 5초 대기후 실행, 그 이후 5초마다 실행
@Schedulred(initialDelay = 5000, fixedRate = 5000)

// 매일 낮12시에 실행
@Schedulred(cron = “0 0 12 * * ?”