본문 바로가기
BackEnd/Project

[PT Manager] Ch03. Batch 이용권 일괄 지급

by 개발 Blog 2024. 8. 27.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

이번 시간에는 이용권 일괄 지급을 Tasklet 기반의 Step으로 구현해 본다. 이 기능을 위해 기존 구조에서 bulk_pass 테이블과 user_group_mapping 테이블을 추가하고, 이를 통해 다수의 사용자에게 일괄적으로 이용권을 지급하는 작업을 수행한다.

1. 테이블 생성 및 데이터 삽입

먼저, bulk_pass 테이블과 user_group_mapping 테이블을 추가한다.

CREATE TABLE `bulk_pass` (
    `bulk_pass_seq`   int         NOT NULL AUTO_INCREMENT COMMENT '대량 이용권 순번',
    `package_seq`     int         NOT NULL COMMENT '패키지 순번',
    `user_group_id`   varchar(20) NOT NULL COMMENT '사용자 그룹 ID',
    `status`          varchar(10) NOT NULL COMMENT '상태',
    `count`           int                  DEFAULT NULL COMMENT '이용권 수, NULL인 경우 무제한',
    `started_at`      timestamp   NOT NULL COMMENT '시작 일시',
    `ended_at`        timestamp            DEFAULT NULL COMMENT '종료 일시, NULL인 경우 무제한',
    `created_at`      timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시',
    `modified_at`     timestamp            DEFAULT NULL COMMENT '수정 일시',
    PRIMARY KEY (`bulk_pass_seq`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='대량 이용권, 다수의 이용자에게 이용권을 지급하기 위함';

CREATE TABLE `user_group_mapping` (
    `user_group_id`   varchar(20) NOT NULL COMMENT '사용자 그룹 ID',
    `user_id`         varchar(20) NOT NULL COMMENT '사용자 ID',
    `user_group_name` varchar(50) NOT NULL COMMENT '사용자 그룹 이름',
    `description`     varchar(50) NOT NULL COMMENT '설명',
    `created_at`      timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시',
    `modified_at`     timestamp            DEFAULT NULL COMMENT '수정 일시',
    PRIMARY KEY (`user_group_id`, `user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='사용자 그룹 매핑';


-----------------------------------------------------------

INSERT INTO `user_group_mapping` (user_group_id, user_id, user_group_name, description, created_at)
VALUES ('HANBADA', 'A1000000', '한바다', '한바다 임직원 그룹', '2022-08-01 00:00:00'),
       ('HANBADA', 'A1000001', '한바다', '한바다 임직원 그룹', '2022-08-01 00:00:00'),
       ('HANBADA', 'A1000002', '한바다', '한바다 임직원 그룹', '2022-08-01 00:00:00'),
       ('HANBADA', 'B1000010', '한바다', '한바다 임직원 그룹', '2022-08-01 00:00:00'),
       ('HANBADA', 'B2000000', '한바다', '한바다 임직원 그룹', '2022-08-01 00:00:00'),
       ('TAESAN', 'B2000001', '태산', '태산 임직원 그룹', '2022-08-01 00:00:00');
  • bulk_pass 테이블과 user_group_mapping 테이블을 통해 특정 사용자 그룹에 일괄적으로 이용권을 지급할 수 있다.
  • bulk_pass 테이블에서 user_group_id를 가지고 user_group_mapping 테이블을 조회하여 해당 그룹에 속한 사용자들의 user_id를 가져온 후, 이를 기반으로 각 사용자에게 pass를 생성해 줄 수 있다.

2. Tasklet 기반 처리 구현

Tasklet 기반 처리는 Spring Batch에서 반복적으로 실행해야 하는 작업을 정의하는 데 유용하다. 아래 이미지에서 나타난 것처럼 Tasklet은 단일 작업을 정의하기 위해 사용되며, 주로 배치 처리에서 단순한 작업을 수행할 때 적합하다.

Tasklet 인터페이스는 execute 메서드를 제공하며, 이 메서드는 RepeatStatus를 반환한다. RepeatStatus에는 두 가지 값이 있다.

  • CONTINUABLE: 작업이 계속될 수 있는 상태를 의미하며, Spring Batch에게 이 Tasklet을 다시 실행하도록 지시한다.
  • FINISHED: 작업이 완료되었음을 의미하며, 다음 처리 단계로 넘어가게 된다.

Tasklet의 주요 사용 사례로는 데이터를 초기화하거나 간단한 작업을 수행하는 데 유용하다. 예를 들어, 단순히 로그를 남기거나 특정 조건에 따라 작업을 종료하는 등의 작업을 처리할 수 있다. Tasklet 기반 Step은 단일 작업을 처리하며, 처리의 성공 여부와 관계없이 다음 Step으로 이동하게 된다.

 

AddPassesJobConfig

AddPassesJobConfig는 Tasklet을 기반으로 Job과 Step을 정의한다.

package com.example.pass.job.pass;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AddPassesJobConfig {
    // @EnableBatchProcessing로 인해 Bean으로 제공된 JobBuilderFactory, StepBuilderFactory
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final AddPassesTasklet addPassesTasklet;

    public AddPassesJobConfig(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, AddPassesTasklet addPassesTasklet) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.addPassesTasklet = addPassesTasklet;
    }

    @Bean
    public Job addPassesJob() {
        return this.jobBuilderFactory.get("addPassesJob")
                .start(addPassesStep())
                .build();
    }

    @Bean
    public Step addPassesStep() {
        return this.stepBuilderFactory.get("addPassesStep")
                .tasklet(addPassesTasklet)
                .build();
    }

}
  • AddPassesJobConfig
    • Spring Batch의 Job과 Step을 설정하는 역할을 한다.
  • addPassesJob()
    • Job을 정의하며, 이 Job은 단일 Step(addPassesStep)으로 구성된다.
  • addPassesStep()
    • Tasklet을 설정하여, 해당 Tasklet이 반복적인 작업을 처리하도록 한다. 이 때 실제로 실행되는 로직은 AddPassesTasklet에 구현된다.

AddPassesTasklet

AddPassesTasklet은 실제로 이용권을 생성하는 로직을 포함한다.

package com.example.pass.job.pass;

import com.example.pass.repository.pass.*;
import com.example.pass.repository.user.UserGroupMappingEntity;
import com.example.pass.repository.user.UserGroupMappingRepository;

import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Component
public class AddPassesTasklet implements Tasklet {
    private final PassRepository passRepository;
    private final BulkPassRepository bulkPassRepository;
    private final UserGroupMappingRepository userGroupMappingRepository;

    public AddPassesTasklet(PassRepository passRepository, BulkPassRepository bulkPassRepository, UserGroupMappingRepository userGroupMappingRepository) {
        this.passRepository = passRepository;
        this.bulkPassRepository = bulkPassRepository;
        this.userGroupMappingRepository = userGroupMappingRepository;
    }

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
        // 이용권 시작 일시 1일 전 user group 내 각 사용자에게 이용권을 추가해줍니다.
        final LocalDateTime startedAt = LocalDateTime.now().minusDays(1);
        final List<BulkPassEntity> bulkPassEntities = bulkPassRepository.findByStatusAndStartedAtGreaterThan(BulkPassStatus.READY, startedAt);

        int count = 0;
        for (BulkPassEntity bulkPassEntity : bulkPassEntities) {
            // user group에 속한 userId들을 조회합니다.
            final List<String> userIds = userGroupMappingRepository.findByUserGroupId(bulkPassEntity.getUserGroupId())
                    .stream().map(UserGroupMappingEntity::getUserId).toList();

            // 각 userId로 이용권을 추가합니다.
            count += addPasses(bulkPassEntity, userIds);
            // pass 추가 이후 상태를 COMPLETED로 업데이트합니다.
            bulkPassEntity.setStatus(BulkPassStatus.COMPLETED);

        }
        log.info("AddPassesTasklet - execute: 이용권 {}건 추가 완료, startedAt={}", count, startedAt);
        return RepeatStatus.FINISHED;

    }

    // bulkPass의 정보로 pass 데이터를 생성합니다.
    private int addPasses(BulkPassEntity bulkPassEntity, List<String> userIds) {
        List<PassEntity> passEntities = new ArrayList<>();
        for (String userId : userIds) {
            PassEntity passEntity = PassModelMapper.INSTANCE.toPassEntity(bulkPassEntity, userId);
            passEntities.add(passEntity);

        }
        return passRepository.saveAll(passEntities).size();

    }

}
  • AddPassesTasklet 클래스는 실제로 이용권을 일괄 지급하는 로직이 구현된 Tasklet이다.
  • execute() 메서드에서 이용권 지급 조건에 맞는 BulkPassEntity 목록을 조회한 뒤, 사용자 그룹에 속한 사용자들에게 이용권을 추가하는 작업을 수행한다.
  • 각 BulkPassEntity에 대한 이용권이 사용자들에게 지급되면, 해당 상태를 COMPLETED로 변경한다.
  • addPasses() 메서드는 사용자의 ID를 기반으로 PassEntity를 생성하고 저장한다.

이 Tasklet은 Spring Batch에서 RepeatStatus.FINISHED를 반환하여 작업이 완료되었음을 알린다.

 

주요 포인트

  • 이 구조에서는 BulkPassEntity와 UserGroupMappingEntity를 기반으로 하여 어떤 사용자에게 어떤 이용권이 지급되어야 하는지를 결정한다.
  • PassModelMapper를 사용해 BulkPassEntity에서 PassEntity로 데이터를 매핑하여 변환한다.
  • 상태 관리가 중요한 부분이며, 각 작업 후에는 상태를 업데이트하여 중복 처리를 방지한다.

AddPassesTaskletTest

AddPassesTaskletTest는 Spring Batch의 Tasklet을 테스트하는 단위 테스트 코드다. 이 테스트는 Mockito를 활용하여 의존성을 모킹(mocking)하고 AddPassesTasklet의 동작을 검증한다.

package com.example.pass.job.pass;

import com.example.pass.repository.pass.*;
import com.example.pass.repository.user.UserGroupMappingEntity;
import com.example.pass.repository.user.UserGroupMappingRepository;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.repeat.RepeatStatus;

import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;


@Slf4j
@ExtendWith(MockitoExtension.class) // JUnit5
public class AddPassesTaskletTest {
    @Mock
    private StepContribution stepContribution;

    @Mock
    private ChunkContext chunkContext;

    @Mock
    private PassRepository passRepository;

    @Mock
    private BulkPassRepository bulkPassRepository;

    @Mock
    private UserGroupMappingRepository userGroupMappingRepository;

    // @InjectMocks 클래스의 인스턴스를 생성하고 @Mock으로 생성된 객체를 주입합니다.
    @InjectMocks
    private AddPassesTasklet addPassesTasklet;

    @Test
    public void test_execute() {
        // given
        final String userGroupId = "GROUP";
        final String userId = "A1000000";
        final Integer packageSeq = 1;
        final Integer count = 10;

        final LocalDateTime now = LocalDateTime.now();

        final BulkPassEntity bulkPassEntity = new BulkPassEntity();
        bulkPassEntity.setPackageSeq(packageSeq);
        bulkPassEntity.setUserGroupId(userGroupId);
        bulkPassEntity.setStatus(BulkPassStatus.READY);
        bulkPassEntity.setCount(count);
        bulkPassEntity.setStartedAt(now);
        bulkPassEntity.setEndedAt(now.plusDays(60));

        final UserGroupMappingEntity userGroupMappingEntity = new UserGroupMappingEntity();
        userGroupMappingEntity.setUserGroupId(userGroupId);
        userGroupMappingEntity.setUserId(userId);

        // when
        when(bulkPassRepository.findByStatusAndStartedAtGreaterThan(eq(BulkPassStatus.READY), any())).thenReturn(List.of(bulkPassEntity));
        when(userGroupMappingRepository.findByUserGroupId(eq("GROUP"))).thenReturn(List.of(userGroupMappingEntity));

        RepeatStatus repeatStatus = addPassesTasklet.execute(stepContribution, chunkContext);

        // then
        // execute의 return 값인 RepeatStatus 값을 확인합니다.
        assertEquals(RepeatStatus.FINISHED, repeatStatus);

        // 추가된 PassEntity 값을 확인합니다.
        ArgumentCaptor<List> passEntitiesCaptor = ArgumentCaptor.forClass(List.class);
        verify(passRepository, times(1)).saveAll(passEntitiesCaptor.capture());
        final List<PassEntity> passEntities = passEntitiesCaptor.getValue();

        assertEquals(1, passEntities.size());
        final PassEntity passEntity = passEntities.get(0);
        assertEquals(packageSeq, passEntity.getPackageSeq());
        assertEquals(userId, passEntity.getUserId());
        assertEquals(PassStatus.READY, passEntity.getStatus());
        assertEquals(count, passEntity.getRemainingCount());

    }

}

Mock 객체 생성

  • @Mock 애너테이션을 사용하여 StepContribution, ChunkContext, PassRepository, BulkPassRepository, UserGroupMappingRepository를 모킹 한다.
  • @InjectMocks 애너테이션을 사용하여 AddPassesTasklet 인스턴스를 생성하고, 모킹 된 객체들을 주입한다.

테스트 시나리오

  • 주어진 조건(given)으로 bulkPassRepository와 userGroupMappingRepository에서 리턴되는 값을 설정한다. 이를 통해 실제 데이터베이스를 사용하지 않고도 테스트를 수행할 수 있다.
  • 테스트 실행(when)에서는 addPassesTasklet.execute(stepContribution, chunkContext)를 호출하여 Tasklet의 핵심 로직을 실행한다.

검증(then)

  • 첫 번째로 RepeatStatus.FINISHED가 반환되는지 확인한다. 이는 Tasklet이 정상적으로 실행되어 종료되었음을 의미한다.
  • 두 번째로 PassRepository.saveAll() 메서드가 호출되어 PassEntity가 저장되었는지 검증한다. ArgumentCaptor를 사용하여 실제로 저장된 엔티티를 캡처하고, 이를 통해 올바른 데이터가 저장되었는지 확인한다.

중요한 테스트 포인트

  • Tasklet의 execute() 메서드가 올바르게 동작하여, bulkPassEntity의 정보를 바탕으로 유저별 PassEntity가 잘 생성되고 저장되는지 검증한다.
  • Mockito를 활용하여 외부 의존성을 모킹함으로써, 독립적으로 비즈니스 로직을 검증할 수 있다.

이 테스트를 통해 Tasklet이 여러 의존성을 잘 연동하여 복잡한 로직을 수행하는지, 그리고 올바르게 PassEntity를 생성하는지 확인할 수 있다.

3. BulkPassEntity와 관련된 테이블 및 Repository

이용권 일괄 지급을 처리하기 위해 bulk_pass 테이블과 관련된 Entity 및 Repository를 정의한다.

 

BulkPassEntity

BulkPassEntity는 대량 이용권 정보를 저장하는 JPA 엔티티 클래스이다. 여러 사용자 그룹에게 동일한 이용권을 일괄 지급하기 위해 사용된다. 이 엔티티는 이용권 패키지, 사용자 그룹 ID, 상태, 이용권 수, 시작 및 종료 일시 등을 저장한다.

 

package com.example.pass.repository.pass;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;
import java.time.LocalDateTime;

@Getter
@Setter
@ToString
@Entity
@Table(name = "bulk_pass")
public class BulkPassEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키 생성을 DB에 위임합니다. (AUTO_INCREMENT)
    private Integer bulkPassSeq;
    private Integer packageSeq;
    private String userGroupId;

    @Enumerated(EnumType.STRING)
    private BulkPassStatus status;
    private Integer count;

    private LocalDateTime startedAt;
    private LocalDateTime endedAt;

}
  • bulkPassSeq: 대량 이용권 순번. Primary Key로, 자동 생성된다.
  • packageSeq: 패키지 순번.
  • userGroupId: 사용자 그룹 ID. 이 ID를 통해 특정 그룹에 소속된 사용자들에게 이용권을 지급한다.
  • status: 대량 이용권의 상태. 이 필드는 BulkPassStatus Enum을 사용해 관리된다.
  • count: 이용권의 수. null인 경우 무제한.
  • startedAt, endedAt: 이용권의 시작 및 종료 일시.

BulkPassRepository

BulkPassRepository는 BulkPassEntity를 관리하는 JPA 리포지토리 인터페이스이다. 주로 데이터베이스에서 대량 이용권 데이터를 조회하고 저장하는 작업을 수행한다.

package com.example.pass.repository.pass;

import org.springframework.data.jpa.repository.JpaRepository;

import java.time.LocalDateTime;
import java.util.List;

public interface BulkPassRepository extends JpaRepository<BulkPassEntity, Integer> {
    // WHERE status = :status AND startedAt > :startedAt
    List<BulkPassEntity> findByStatusAndStartedAtGreaterThan(BulkPassStatus status, LocalDateTime startedAt);

}
  • findByStatusAndStartedAtGreaterThan
    • 특정 상태와 시작 일시를 기준으로 대량 이용권 목록을 조회하는 메서드이다. 이 메서드는 이용권 발급 시점이 다가온 대량 이용권만을 조회해 처리할 수 있도록 한다.

BulkPassStatus Enum

BulkPassStatus는 대량 이용권의 상태를 관리하는 Enum 클래스이다. 주로 이용권의 발급 상태를 나타낸다.

package com.example.pass.repository.pass;

public enum BulkPassStatus {
    READY, COMPLETED
}

 

  • READY: 발급 준비 상태.
  • COMPLETED: 발급이 완료된 상태.

4. MapStruct를 이용한 데이터 매핑

이 단계에서는 BulkPassEntity를 PassEntity로 변환하기 위해 MapStruct를 사용하여 매핑을 처리한다.

 

PassModelMapper

PassModelMapper는 MapStruct를 사용하여 BulkPassEntity를 PassEntity로 변환하는 매핑 인터페이스이다. Spring Batch 작업 중 대량 이용권 데이터를 개별 이용권 데이터로 변환할 때 사용된다.

package com.example.pass.repository.pass;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.ReportingPolicy;
import org.mapstruct.factory.Mappers;

// ReportingPolicy.IGNORE: 일치하지 않은 필드를 무시합니다.
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface PassModelMapper {
    PassModelMapper INSTANCE = Mappers.getMapper(PassModelMapper.class);

    // 필드명이 같지 않거나 custom하게 매핑해주기 위해서는 @Mapping을 추가해주면 됩니다.
    @Mapping(target = "status", qualifiedByName = "defaultStatus")
    @Mapping(target = "remainingCount", source = "bulkPassEntity.count") //expression = "java(bulkPassEntity.getCount())"
    PassEntity toPassEntity(BulkPassEntity bulkPassEntity, String userId);

    // BulkPassStatus와 관계 없이 PassStatus값을 설정합니다.
    @Named("defaultStatus")
    default PassStatus status(BulkPassStatus status) {
        return PassStatus.READY;
    }

}
  • toPassEntity 메서드는 BulkPassEntity와 사용자 ID(userId)를 받아 PassEntity 객체를 반환한다.
  • 상태 필드(status)는 항상 READY 상태로 설정된다.
  • remainingCount 필드는 bulkPassEntity.count 값으로 매핑된다.

매핑 과정

  • @Mapper: MapStruct 인터페이스로 선언하며, 매핑 규칙을 정의한다.
  • @Mapping: 특정 필드에 대해 매핑 규칙을 정의한다. 예를 들어, remainingCount는 bulkPassEntity.count에서 값을 가져온다.
  • @Named: 특정 필드에 대해 기본값을 설정하는 메서드를 정의한다. status 필드는 항상 READY로 설정된다.

PassModelMapperTest

PassModelMapperTest는 PassModelMapper의 매핑 로직이 올바르게 동작하는지 검증하기 위한 JUnit 테스트 클래스이다.

package com.example.pass.repository.pass;

import org.junit.jupiter.api.Test;

import java.time.LocalDateTime;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class PassModelMapperTest {

    @Test
    public void test_toPassEntity() {
        // given
        final LocalDateTime now = LocalDateTime.now();
        final String userId = "A1000000";

        BulkPassEntity bulkPassEntity = new BulkPassEntity();
        bulkPassEntity.setPackageSeq(1);
        bulkPassEntity.setUserGroupId("GROUP");
        bulkPassEntity.setStatus(BulkPassStatus.COMPLETED);
        bulkPassEntity.setCount(10);
        bulkPassEntity.setStartedAt(now.minusDays(60));
        bulkPassEntity.setEndedAt(now);

        // when
        final PassEntity passEntity = PassModelMapper.INSTANCE.toPassEntity(bulkPassEntity, userId);

        // then
        assertEquals(1, passEntity.getPackageSeq());
        assertEquals(PassStatus.READY, passEntity.getStatus());
        assertEquals(10, passEntity.getRemainingCount());
        assertEquals(now.minusDays(60), passEntity.getStartedAt());
        assertEquals(now, passEntity.getEndedAt());
        assertEquals(userId, passEntity.getUserId());
    }
}

주요 테스트 목적

  • BulkPassEntity의 필드 값이 정확하게 PassEntity로 매핑되는지 확인한다.
  • 각 필드가 예상한 대로 매핑되었는지 검증한다.

테스트 과정

  • BulkPassEntity를 생성하고, 다양한 필드 값을 설정한 후, PassModelMapper.INSTANCE.toPassEntity 메서드를 호출하여 PassEntity 객체를 생성한다.
  • 생성된 PassEntity의 필드 값이 기대한 값과 일치하는지 검증한다.

테스트 검증

  • packageSeq, status, remainingCount, startedAt, endedAt, userId 등의 필드가 BulkPassEntity에서 기대한 값으로 매핑되었는지 확인한다.
  • status 필드는 항상 READY로 설정되는지 검증한다.

5. 사용자 그룹 매핑 테이블 및 리포지토리 정의

이 단계에서는 UserGroupMappingEntity와 이를 관리할 UserGroupMappingRepository를 정의하여, 사용자 그룹과 사용자 간의 매핑을 관리한다.

 

UserGroupMappingEntity

UserGroupMappingEntity는 사용자 그룹과 사용자 간의 매핑 정보를 저장하는 JPA 엔티티 클래스이다. 이 엔티티는 특정 그룹에 속한 사용자들을 관리하기 위해 사용된다.

package com.example.pass.repository.user;

import com.example.pass.repository.BaseEntity;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;

@Getter
@Setter
@ToString
@Entity
@Table(name = "user_group_mapping")
@IdClass(UserGroupMappingId.class)
public class UserGroupMappingEntity extends BaseEntity {
    @Id
    private String userGroupId;
    @Id
    private String userId;

    private String userGroupName;
    private String description;

}
  • 이 엔티티는 userGroupId와 userId를 복합 키로 사용한다. 이는 한 그룹에 여러 사용자가 속할 수 있도록 하며, 각각의 사용자 그룹에 대한 정보를 관리할 수 있게 한다.
  • @IdClass를 사용해 복합 키를 정의하고 있으며, 이를 통해 두 필드를 기본 키로 설정한다.

UserGroupMappingId

UserGroupMappingId는 UserGroupMappingEntity의 복합 키를 정의하기 위한 클래스이다. 이 클래스는 Serializable을 구현해야 한다.

package com.example.pass.repository.user;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.io.Serializable;

@Getter
@Setter
@ToString
public class UserGroupMappingId implements Serializable {
    private String userGroupId;
    private String userId;
}

 

  • 이 클래스는 userGroupId와 userId를 복합 키로 사용하며, 이를 통해 UserGroupMappingEntity가 올바르게 동작하도록 한다.
  • Serializable을 구현하여 JPA에서 복합 키를 처리할 수 있게 한다.

UserGroupMappingRepository

UserGroupMappingRepository는 UserGroupMappingEntity를 관리하기 위한 JPA 리포지토리 인터페이스이다.

package com.example.pass.repository.user;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface UserGroupMappingRepository extends JpaRepository<UserGroupMappingEntity, Integer> {
    List<UserGroupMappingEntity> findByUserGroupId(String userGroupId);
}
  • 이 리포지토리는 JPA를 사용해 UserGroupMappingEntity에 대한 기본적인 CRUD 작업을 처리한다.
  • findByUserGroupId 메서드를 통해 특정 그룹에 속한 사용자들을 쉽게 조회할 수 있다. 이 메서드는 대량 이용권 발급 시 해당 그룹에 속한 사용자들을 조회할 때 사용된다.

이번 시간에는 Tasklet 기반의 Batch Job을 통해 대량 이용권을 사용자 그룹에 일괄 지급하는 과정을 구현해 보았다. 이를 위해 BulkPassEntity와 UserGroupMappingEntity를 설계하고, MapStruct를 활용한 엔티티 간 매핑을 통해 효율적으로 데이터를 처리하는 방법을 알아보았다. 마지막으로, 각 Tasklet과 관련된 테스트 코드를 작성하여 정확한 동작을 검증하였다.