BackEnd/쿠링

[쿠링] Spring에서 Custom Annotation을 사용하여 객체를 Map에 등록시키기

샤아이인 2023. 3. 21.

해당 글은 개인 프로젝트를 개선해 나가면서 내용을 정리하는 글입니다.

1. 현 상황 (개선하기 전의 코드)

우선 쿠링에서는 DepartmentName이라는 enum값과, 해당 학과의 정보를 저장하고 있는 DeptInfo객체를 저장하고 있다.

이를 통해 map을 필요한 곳에서 전달받아 enum을 key로 사용하여 해당 학과의 정보를 사용하는 코드이다.

 

우선 코드는 대략 다음과 같다.

하나하나 다 받고 이던 생성자

이 정도만 보면 몇 개 안돼서 수동 등록할 수도 있을 것 같지만...

무려 학과가 75개.... 이걸 수동으로 다 등록하는 건 진짜 무리다... (물론 지금 코드는 그렇게 구현된 있긴 한데...)

 

나는 Custom Annotation을 만들어서 Reflection을 통해 configuration에서 등록하여 bean객체로 만들어줄 생각이다.

 

2. 개선 후의 코드

2 - 1) Custom Annotation 만들기

▶ RegisterDepartmentMap

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RegisterDepartmentMap {
    DepartmentName key();
}

key로 등록시킬 때 사용할 Enum을 가지고 있게 된다.

 

▶ RegisterDepartmentMap을 사용하기

@RegisterDepartmentMap(key = DepartmentName.COMPUTER)
public class ComputerScienceDept extends EngineeringCollege {

    public ComputerScienceDept() {
        super();
        List<String> professorForumIds = List.of("12351719");
        List<String> forumIds = Collections.emptyList();
        List<String> boardSeqs = List.of("882");
        List<String> menuSeqs = List.of("6097");

        this.staffScrapInfo = new StaffScrapInfo(professorForumIds);
        this.noticeScrapInfo = new NoticeScrapInfo(forumIds, "CSE", boardSeqs, menuSeqs);
        this.code = "127428";
        this.deptName = "컴퓨터공학부";
    }
}

학과정보를 가지고 있는 ComputerScienceDept라는 class위에 우리의 애너테이션을 추가해 주자!

이때 key로는 mapping 될 학과의 enum값을 지정해 준다.

 

2 - 2) 등록하기

이제 Reflection을 활용하여 등록하는 코드는 다음과 같다.

@Configuration
public class DepartmentNameDeptInfoMappedBeanConfig {

    @Bean
    public Map<DepartmentName, DeptInfo> departmentNameDeptInfoMap() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Map<DepartmentName, DeptInfo> map = new HashMap<>();

        Reflections reflector = new Reflections("com.kustacks.kuring.worker.scrap.deptinfo");
        Set<Class<?>> list = reflector.getTypesAnnotatedWith(RegisterDepartmentMap.class);

        for (Class<?> clazz : list) {
            DepartmentName departmentName = clazz.getAnnotation(RegisterDepartmentMap.class).key();
            DeptInfo deptInfo = (DeptInfo) clazz.getDeclaredConstructor().newInstance();
            map.put(departmentName, deptInfo);
        }

        return map;
    }
}

 

생성자는 다음과 같이 prefix를 사용하여, 특정 package 하부의 class들만 등록하도록 만들었다.

 

이렇게 key와 value를 동적으로 mapping 하여 Bean객체를 만들어 등록하게 된다.

해당 Bean은 singleton으로 등록될 것이며, 해당 값들은 읽기 전용으로 사용될 것 이기 때문에 동시성에도 문제가 발생하지 않게 된다.

 

2 - 3) 테스트 코드

class MappedBeanTest {

    @Test
    public void department_name_mapping_to_dept_info_success() {
        // given
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(DepartmentNameDeptInfoMappedBeanConfig.class);

        // when
        Map deptInfoMap = applicationContext.getBean("departmentNameDeptInfoMap", Map.class);

        // then
        assertThat(deptInfoMap.size()).isEqualTo(63);
    }
}

 

3. Spring Bean으로 등록하면서 코드 변경

원래 나는 DeptInfo가 Bean으로 등록될 필요는 없다 생각하여, Reflection을 사용해서 Map에 등록해주고 있었다.

하지만 다형성을 적용하다 보니 DeptInfo에서 자신에게 필요한 Client를 들고 있게 되었다.

따라서 Clietn를 DI(의존성 주입)를 받기 위해서는 DeptInfo도 Bean으로 등록될 필요가 있었다.

 

▶ 변경된 RegisterDepartmentMap

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RegisterDepartmentMap {
    DepartmentName key();
}

위와 같이 @Component를 추가해 주자. 이러면 Bean으로 등록할 수 있다.

사실 Java의 애노테이션이 상속을 지원하는 것이 아니다, Spring이 이를 지원해 주는 것이다.

 

▶ 변경된 등록 코드

@Configuration
@RequiredArgsConstructor
public class DepartmentNameDeptInfoMappedBeanConfig {

    private final ApplicationContext applicationContext;

    @Bean
    public Map<DepartmentName, DeptInfo> departmentNameDeptInfoMap() {
        Map<DepartmentName, DeptInfo> map = new HashMap<>();

        Map<String, DeptInfo> beansOfType = applicationContext.getBeansOfType(DeptInfo.class);
        for (DeptInfo deptInfo : beansOfType.values()) {
            DepartmentName departmentName = deptInfo.getClass().getDeclaredAnnotation(RegisterDepartmentMap.class).key();
            map.put(departmentName, deptInfo);
        }

        return map;
    }
}

Reflection을 사용하는 것 이 아니라, Application을 통해 등록된 Bean을 모두 찾아온 후, class()를 활용하여 Annotation 정보를 가져와서 DepartmentName을 찾아온다.

 

이렇게 Map에 등록하게 된다.

 

 

▶ 변경된 테스트 코드

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MappedBeanTest {

    @Autowired
    ApplicationContext applicationContext;

    @Test
    public void department_name_mapping_to_dept_info_success() {
        // when
        Map deptInfoMap = applicationContext.getBean("departmentNameDeptInfoMap", Map.class);

        // then
        assertThat(deptInfoMap.size()).isEqualTo(63);
    }
}

 

댓글