기존에 테스트 데이터를 초기화하기 위해 @PostConstruct를 사용했다. 아래와 같이 순서대로 admin 유저에 대한 데이터를 만들고, 스포츠 시설에 대한 테스트 데이터를 초기화하는 것을 의도한다.
//...
@Component
@RequiredArgsConstructor
public class TestInit {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
private final FacilityRepository facilityRepository;
@Order(value = 1)
@PostConstruct
void initAdminUser() {
User user = User.builder()
.id(AuthConstant.ADMIN_USER)
.password(passwordEncoder.encode(AuthConstant.ADMIN_PWD))
.email("[email protected]")
.name("master")
.phone("01012345678")
.meta(new HashMap<>())
.build();
user.getMeta().put(UserMetaType.ROLE, UserRoleType.ROLE_ADMIN.name());
userService.createUser(user);
}
@Order(value = 2)
@PostConstruct
void initTestFacilities() {
String[] title = new String[]{"영휘트니스", "짐박스피트니스 신림역점", "자마이카 피트니스 신림역점", "스포애니 보라매점", "파운드짐 신림역점", "익스트림에스 신림점", "짐인더하우 2호점"};
double[][] latLng = new double[][]{
{37.4846553, 126.9272265},
{37.4832519, 126.9287583},
{37.484787, 126.9300366},
{37.489879, 126.9271118},
{37.4854721, 126.9299512},
{37.484847, 126.9328386},
{37.4905415, 126.9270999}
};
String[][] address = new String[][]{
{"서울특별시 관악구 신림로 206", "서울특별시 관악구 신림동 96-3", "1162010200"},
{"서울특별시 관악구 신림로59길 14", "서울특별시 관악구 신림동 1640-31", "1162010200"},
{"서울특별시 관악구 신림로 340", "서울특별시 관악구 신림동 1422-5", "1162010200"},
{"서울특별시 관악구 봉천로 227", "서울특별시 관악구 봉천동 972-5", "1162010100"},
{"서울특별시 관악구 신림로 350", "서울특별시 관악구 신림동 1424-28", "1162010200"},
{"서울특별시 관악구 남부순환로 1641", "서울특별시 관악구 신림동 1412-3", "1162010200"},
{"서울특별시 관악구 보라매로 13", "서울특별시 관악구 봉천동 702-49", "1162010100"}
};
User adminUser = userService.getUser("admin");
IntStream.range(0, title.length)
.forEach(i -> {
FacilityDto facilityDto = FacilityDto.builder()
.address(Address.builder()
.jibunAddress(address[i][0])
.roadAddress(address[i][1])
.regionCode(address[i][2])
.latitude(latLng[i][0])
.longitude(latLng[i][1])
.build())
.category(FacilityType.HEALTH)
.title(title[i])
.build();
facilityRepository.save(Facility.create(adminUser, facilityDto));
});
}
}
그러나 이 코드는 항상 원하는 의도대로 동작하지는 않는다. 왜냐하면 @Order는 메서드 레벨에서 적용되지 않기 때문이다. 때문에 테스트가 정상 통과할 때도 있지만, 순서가 반대로 실행되는 경우 아래의 오류를 만나게 된다. 유저가 존재하지 않는다는 메시지가 보이는데, 이는 스포츠 시설을 초기화 할 때(order=2) admin 유저가 존재하지 않았기 때문이다. 즉, 스포츠 시설 초기화가 admin 유저 초기화보다 먼저 실행되었음 알 수 있다.

위 링크를 참고하면 @Order는 component나 bean의 순서를 결정할 때 사용되므로 클래스 레벨에 annotation을 달아줘야 정상적으로 동작할 것이다. 그래서 TestInit 클래스 안에 inner class로서 UserInit과 FacilityInit 등을 만들어서 component scan의 대상이 되도록 하였다. 그리고 @Order를 붙여 줌으로써 원하는 순서대로 @PostConstruct의 기능이 동작할 것이라 기대하였다.
하지만 문제는 다른 곳에서 발생하였다. inner class는 outer class가 bean으로 등록되지 않은 이상 bean으로 등록되지 않기 때문이다. 아래 영상에서 이에 대해 매우 자세하게 알려준다.
자바의 내부 클래스는 스프링 빈이 될 수 있을까?
그래서 TestInit 클래스 안에 inner class가 아닌 static class를 만들어서 @Order를 설정하고, @PostConstruct를 붙여주면 원하는 순서대로 초기화가 진행되어야 할 것이다. 이에 따라 아래 코드와 같이 만들었다.
//...
@RequiredArgsConstructor
public class TestInit {
//...
@Component
@Order(1)
@RequiredArgsConstructor
static class UserInit {
//...
}
@Component
@Order(2)
@RequiredArgsConstructor
static class FacilityInit {
//...
}
@Component
@Order(3)
@RequiredArgsConstructor
static class ScheduleInit {
//...
}
}
그러나 오류가 발생하였고, 원인을 분석해 본 결과 @Order(2)를 가지고 있는 FacilityInit이 UserInit보다 먼저 실행되었다. 이에 대해 구글링을 하다가 원하는 답을 얻지 못해서 Chat GPT에서 답을 얻었다.
However, the order of bean creation and initialization is not solely determined by the
@Orderannotation. It also depends on the dependencies between the beans and the actual resolution of those dependencies.
다시 말해 @Order가 항상 원하는 순서를 보장해 주지는 못한다. 그래서 보다 확실하게 초기화 메서드 순서를 결정하기 위해 애플리케이션이 완전히 로딩된 후에 초기화를 실행하기로 했다. 이를 구현하기 위해 사용한 방법은 아래와 같다.
//...
@Component
@RequiredArgsConstructor
public class TestInit {
//...
private final ObjectProvider<OrderedPostConstruct> initializers;
@EventListener(ApplicationReadyEvent.class)
public void initEntryPoint() {
initializers.orderedStream().forEach(OrderedPostConstruct::init);
}
@Component
@Order(1)
@RequiredArgsConstructor
static class UserInit implements OrderedPostConstruct {
//...
@Override
public void init() {
//...
}
//...
}
@Component
@Order(2)
@RequiredArgsConstructor
static class FacilityInit implements OrderedPostConstruct {
//...
@Override
public void init() {
//...
}
}
@Component
@Order(3)
@RequiredArgsConstructor
static class ScheduleInit implements OrderedPostConstruct {
//...
@Override
public void init() {
//...
}
}
}
OrderedPostConstruct라는 인터페이스를 만들고, 이를 구현하는 초기화 클래스들은 init() 메서드를 구현해야 한다. 즉, init() 메서드 안에 초기화 로직을 작성하면 된다. 그리고 TestInit 클래스를 bean으로 등록하고, ObjectProvider를 주입받는다. 애플리케이션 로딩이 완료된다면(ApplicationReadyEvent) initEntryPoint() 메서드가 실행될텐데, 이것은 OrderedPostConstruct 구현체들 중에서 @Order의 값에 따라 init() 메서드를 각각 실행하는 로직이다. 이로써 정확한 순서를 보장 받으면서 테스트 데이터를 올바르게 초기화 할 수 있다.