sql 파일로 더미 데이터를 생성하던 중 이런 리뷰를 받았습니다.
production, test 모두 실제 데이터를 쿼리로 넣는 것은 좋은 방법은 아닐 것 같습니다!
이렇게 쿼리로 작성하면 어떤 문제가 있을까요?
쿼리로 데이터를 넣었던 이유는 쉽고 빠르기 때문이었습니다.
그러나, 왜 좋은 방법이 아닐까요?
'비즈니스 로직을 타지 않는다'는 점입니다. 직접 쿼리로 데이터를 넣으면 테이블에 걸려 있는 제약 조건 외에는 검증이 되지 않아, 데이터의 신뢰성이 떨어질 수 있습니다.
데이터 무결성과 정합성도 보장할 수 없습니다. varchar로 정의된 날짜 칼럼에 "abc" 같은 문자열이 들어갈 수도 있을 것이고, 외래키 제약이 걸려 있지 않다면 존재하지 않는 id를 참조하게 될 수도 있을 것입니다.
비즈니스 로직을 잘 알고 있는 사람이 아니라면 쿼리로 데이터를 넣을 수 없을 것입니다. 실수할 여지도 높습니다.
이를 방지하기 위해 코드로 데이터를 넣도록 리팩터링 해보겠습니다.
먼저, data.sql 파일로 더미 데이터를 넣는 코드를 살펴보겠습니다.
회원, 예약 시간, 테마, 예약 데이터를 생성하고 있습니다.
-- 회원
INSERT INTO member (name, email, password, role) VALUES ('조조', 'jojo@email.com', '1234', 'MEMBER');
-- 에약 시간
INSERT INTO reservation_time (start_at) VALUES ('10:00');
-- 테마
INSERT INTO theme (name, description, thumbnail)
VALUES ('공포', '무서워요', '<https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg>');
-- 예약
INSERT INTO reservation (member_id, date, time_id, theme_id, status) VALUES (1, CURRENT_DATE, 1, 1, 'SUCCESS');
그러나, 여기서 든 의문.
sql 파일은 스프링 부트가 실행될 때 자동으로 로드가 되는데, 코드는 어떻게 로드해야 할까요?
ApplicationRunner 인터페이스를 사용해서 로드할 수 있습니다.
ApplicationRunner로 데이터 생성
ApplicationRunner는 함수형 인터페이스입니다.
ApplicationRunner를 구현하여 스프링 애플리케이션이 시작된 후, 특정 코드를 실행하게 하게 할 수 있습니다.
더 명확하게는, SpringApplication.run()이 완료되기 직전에 호출됩니다.
동일한 애플리케이션 컨텍스트 내에서 여러 ApplicationRunner Bean을 정의할 수 있고, 순서를 명시해야 하는 경우 @Order 어노테이션으로 지정할 수 있습니다.
유사한 인터페이스로 CommandLineRunner가 있습니다. CommandLineRunner는 파라미터로 String 타입을 받는 반면, ApplicationRunner는 ApplicationArgumets를 받는 차이가 있습니다.
기존 sql 스크립트를 코드로 수정해 보겠습니다.
@Component
public class DataLoader implements ApplicationRunner {
private final MemberRepository memberRepository;
private final ThemeRepository themeRepository;
private final ReservationTimeRepository reservationTimeRepository;
private final ReservationRepository reservationRepository;
//... (생성자 생략)
@Override
public void run(ApplicationArguments args) throws Exception {
// 회원
Member jojo = memberRepository.save(new Member(null, Role.MEMBER, new MemberName("조조"), "jojo@email.com", "1234"));
// 예약 시간
ReservationTime time10_00 = reservationTimeRepository.save(new ReservationTime(LocalTime.parse("10:00")));
// 테마
Theme horror = themeRepository.save(new Theme(new ThemeName("공포"), new Description("무서워요"), "<https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg>"));
// 예약
reservationRepository.save(new Reservation(jojo, LocalDate.now(), horror, time10_00, Status.SUCCESS));
}
}
@Component 어노테이션을 붙여주고, ApplicationRunner의 run 메서드를 구현합니다.
Sql 없이 데이터가 잘 들어간 것을 확인할 수 있습니다.
Fixture를 사용한다면, 더 가독성 좋게 리팩터링 할 수 있습니다
@Override
public void run(ApplicationArguments args) throws Exception {
// 회원
Member jojo = memberRepository.save(MEMBER_JOJO);
// 예약 시간
ReservationTime time10_00 = reservationTimeRepository.save(TIME_10_00);
// 테마
Theme horror = themeRepository.save(THEME_HORROR);
// 예약
reservationRepository.save(new Reservation(jojo, LocalDate.now(), horror, time10_00, Status.SUCCESS));
}
public class DataFixture {
public static final Member MEMBER_JOJO = new Member(
null,
Role.MEMBER,
new MemberName("조조"),
"jojo@email.com",
"1234"
);
public static final ReservationTime TIME_10_00 = new ReservationTime(LocalTime.parse("10:00"));
public static final Theme THEME_HORROR = new Theme(
new ThemeName("공포"),
new Description("무서워요"),
"<https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg>"
);
private DataFixture() {
}
}
repository에서 넣어야 할까 vs service에서 넣어야 할까
그러나 위 코드처럼 repository에서 넣어준다면, 도메인 로직을 거치는 것 외에 sql 쿼리문으로 넣어주는 것과 큰 차이가 없지 않을까 하는 의문이 생겼습니다.
예약 데이터를 생성하기 위해 reservationRepository.save()를 호출할 수도 있고, reservationService.create()를 호출할 수도 있을 것입니다.
service를 거쳐서 데이터를 추가하면, 비즈니스 로직을 최대한 검증할 수 있어서 이상적이라고 생각합니다.
그러나, service에 지나간 날짜에 대한 예약은 추가할 수 없다 같은 조건이 있을 때, 이를 위반하는 데이터는 repository에서 추가할 수밖에 없습니다.
물론 도메인 검증을 할 순 있겠지만, 상위 레이어의 검증 로직은 타지 않기 때문에 이것 또한 데이터의 신뢰성을 보장할 수 없지 않을까요?
service에 핵심적인 로직이 없다면 repository로도 충분할 것 같아요. 정답을 찾기보다 트레이드오프를 잘 고려하면서 최선의 선택을 할 수 있는 연습을 하면 좋을 것 같아요!
리뷰어님은 트레이드오프라고 답을 주셨습니다.
이에 대해 가능한 핵심적인 로직을 거칠 수 있는 레이어에서 접근하고, 조건을 위반하는 데이터를 의도하여 넣을 때에는 하위 레이어에서 접근하는 방식을 사용할 것 같습니다.
더미 데이터 프로필 설정
프로덕션 코드에 추가한 더미 데이터를, 테스트에는 빼고 싶다면 어떻게 해야 할까요?
스프링의 @Profile과 @ActiveProfiles를 사용하여, 원하는 빈만 컨텍스트에 등록할 수 있습니다.
Profile 어노테이션을 사용하여 하나 이상의 지정된 프로필이 활성화되었을 때, 컴포넌트로 등록하게 해 줍니다.
ApplicationRunner 클래스가 test 이름의 프로필이 아닐 때 빈으로 등록되도록 어노테이션을 추가합니다.
value 문자열에는 !(NOT), &(AND), |(OR)과 같은 프로필 표현식이 사용될 수 있습니다.
@Profile("!test")
@Component
public class DataLoader implements ApplicationRunner {
ActiveProfiles는 애플리케이션 컨텍스트를 로드할 때, 활성화할 빈의 프로필을 선언하는 데 사용되는 어노테이션입니다. 프로필은 여러 개를 설정할 수 있습니다.
통합 테스트 클래스에 @ActiveProfiles을 붙이고, test 이름의 프로필을 활성화해줍니다.
@ActiveProfiles("test")
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class IntegrationTest {
테스트에서 더미 데이터가 추가되지 않은 것을 확인할 수 있습니다.
정리
- 데이터는 sql 파일 대신, 코드로 넣는 것이 좋다.
- ApplicationRunner를 구현하여 데이터 생성할 수 있다.
- @Profile과 @ActiveProfiles를 사용하여 테스트에서 데이터 생성을 제외시킬 수 있다.
참고
- https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/executing-sql.html
- https://docs.spring.io/spring-boot/api/java/org/springframework/boot/ApplicationRunner.html
- https://www.baeldung.com/spring-junit-prevent-runner-beans-testing-execution
- https://docs.spring.io/spring-framework/reference/core/beans/environment.html#beans-definition-profiles-java
- https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-activeprofiles.html#page-title
'Spring' 카테고리의 다른 글
👣 로그 수집과 모니터링 구축기 (2) | 2024.11.20 |
---|---|
Controller는 어떻게 테스트 할까?(feat. RestAssured vs MockMvc) (1) | 2024.05.20 |
SpringBootTest 격리 방법 : @DirtiestContext의 대체재 (0) | 2024.05.06 |
@Component와 @Repository는 무엇이 다른가요? (49) | 2024.04.22 |