-
[SpringBoot] @TestInstance / @WithUser & @WithAccount / 중복된 코드에 대한 메서드 분리Back-end/TIL 2022. 3. 21. 09:00
특정 API의 POST와 UPDATE 시 예외처리 코드를 추가하는 업무를 진행했다.
업무를 진행하며 배운 내용에 대해 잊지 않기 위해 작성하였다.
📌 SpringBoot의 테스트 코드 작성시 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
- 해당 어노테이션은 테스트 인스턴스의 라이프 사이클을 설정할 때 사용한다.
- PER_METHOD: 테스트 함수 당 1개의 인스턴스가 생성된다.
- PER_CLASS: 테스트 클래스 당 1개의 인스턴스가 생성된다.
- 라이프 사이클을 클래스 단위로 설정하면, @BeforeAll, @AfterAll 어노테이션을 static method가 아닌 곳에서도 사용할 수 있다.
- 장점
- 매 테스트 메서드마다 클래스의 인스턴스를 생성하는데 필요한 자원이 많을 때 유용하다. (ex. 데이터베이스 연결, 큰 용량의 파일 로딩 등)
- 상태 공유가 가능하다. 단위 테스트에서는 상태 공유 패턴이 지양해야하는 형태이지만, 통합 테스트에서는 유용하다. (이번에 회사에서 작성한 테스트 코드는 통합 테스트였으므로 사용했다)
- @BeforeEach나 @AfterEach를 사용해 공유되는 변수의 상태를 초기화시키면 된다.
참고) https://www.baeldung.com/junit-testinstance-annotation
여기서부터는 작성한 코드(POST와 UPDATE시 예외처리, 해당 API의 통합테스트 작성)에 대한 팀장님의 피드백을 받고 정리한 내용이다.
📌 @WithUser, @WithAccount
- 위에서 작성했듯이, @TestInstance(TestInstance.Lifecycle.PER_CLASS) 어노테이션을 사용한 이유는 위와 같은 장점도 있었지만, 로그인을 위해 기존에 인스턴스 변수로 선언되어 있던(회사 코드에서 이렇게 설정이 되어 있었음) mockMvc를 사용하기 위함도 있었다. 이를 위해 해당 어노테이션을 사용하고, @BeforeAll 어노테이션이 사용된 메서드는 static이 아닌 인스턴스로 메서드로 정의해 사용하였다.
- 하지만, static 메서드의 빠른 속도를 사용하기 위해 @BeforAll의 static 메서드를 유지해서 사용하려면, @WithUser, @WithAccount 사용해 사용자를 생성할 수도 있다.
- 다만, 꼭 위와같은 방법이 정답은 아니고, 상황에 따라 적절하게 사용하면 된다.
통합 테스트 작성 후 궁금했던 부분은 '값 변경 후 or 값 생성 후 데이터베이스에서 불러와 확인을 해봐야 하는지' 였다.
팀장님께 여쭤보니 이것도 정답이 있는 것은 아니지만, 팀장님의 경우 API 호출 결과 200 OK가 나오면 데이터베이스에 저장이 되었다고 생각하신다고 하셨다.
📌 중복된 코드에 대한 메서드 분리 ❗
상황
Post와 Put에 대한 Service 클래스에서 취하된 약품인지 확인하는 코드가 중복되어 작성되었다.
개선
해당 코드 내용이 2 곳 이상에서 사용되면 따로 메서드로 분리하는 것이 좋다. (다른 곳에서 추가로 사용될 수도 있으니까)
추가 고민 부분: 메서드의 형태
메서드로 분리하고자 하였는데, 생각해보니 수정 가능한 메서드의 형태도 2가지가 나왔다.
//기존 코드 if(medicine.getIsDrop()){ throw new CommonBadRequestException("userMedicineIsDropCreate"); } if(medicine.getIsDrop()){ throw new CommonBadRequestException("userMedicineIsDropUpdate"); } //수정1 private boolean isDropMedicine(Medicine medicine) { //분리된 메서드 return medicine.getIsDrop(); } if(isDropMedicine(medicine)) { throw new CommonBadRequestException("userMedicineIsDropCreate"); } if(isDropMedicine(medicine)) { throw new CommonBadRequestException("userMedicineIsDropUpdate"); } //수정2 private void isDropMedicine(Medicine medicine, String exceptionMsg) { //분리된 메서드 if(medicine.getIsDrop()) { throw new CommonBadRequestException(exceptionMsg); } } isDropMedicine(medicine, "userMedicineIsDropCreate"); isDropMedicine(medicine, "userMedicineIsDropUpdate");
- 수정1의 경우에는 boolean 값만 반환하는 메소드이므로, 기존의 코드와 크게 달라지는 부분이 없어 이렇게 메소드를 구성하는 것은 맞지 않다.
- 수정2의 경우처럼 따로 분리한 메소드 내부에서 예외처리를 하는 것도 괜찮다. 어차피 예외 발생되면 intelliJ가 자세히 알려주기 때문이다.
- 지금은 2개의 param으로 공통 로직을 뽑아낼 수 있지만, 만약 여러 개의 param이 전달되어야 하는 상황에서는 DTO로 만들어 전달하는 방법도 있다. (팀장님께서는 3개 이상이면 DTO로 생성해 전달한다고 한다)
▶️ 고민해야할 부분1 - public vs. private
A) Service 클래스 내부에 public 메서드로 정의해서 Controller 부분에서도 호출 가능하도록 함
public postController() { //1.취하된 약품인지 확인 //2.약품 등록 }
- 장점
- 만약 데이터베이스에 취하된 약품이 더 많을 경우 1번에서 처리한 후 2번은 수행이 안되므로 불필요한 자원 낭비 및 DB 처리가 필요하지 않게 된다.
- 단점
- 만약 데이터베이스에 취하된 약품이 많지 않을 경우 1번과 2번에서 모두 DB 조회를 하게 된다.
B) Service 클래스 내부에 private 메서드로 정의해서 Service 클래스 내부에서만 호출 가능하도록 함
- 단점은 딱히 없다.
▶️ 고민해야할 부분2 - 접근 지정자(public vs. private vs. protected)
- 만약 해당 메서드가 Service 외에 다른 모듈에서도 사용된다면, 접근 지정자를 private 대신 protected까지는 설정해도 좋다. (public으로 하면 오남용될 수 있으므로 지양한다)
▶️ 결정사항
현재 우리 데이터베이스에는 취하된 약품이 많지 않고, 아직 특정 Service 클래스에서만 사용하므로, 해당 Service 클래스 내에 private 메소드로 분리하였다.
📌 느낀점
단순한 코드 작성이어도 생각해야 될 부분이 많다는 것을 느꼈다. 앞으로 2번 생각해보는 개발자가 되야겠다.
'Back-end > TIL' 카테고리의 다른 글
[SpringBoot] Converter를 사용한 사용자 정보 암호화 / Cascade 옵션 설정 / 모르는 개념 공부 방법 (0) 2022.03.30 [SpringBoot] 테스트 코드 작성에 대하여 / 멀티 모듈의 장점 / API 설계 및 배포 / 코드 작성 관련(다형성, lombok, import문) (0) 2022.03.24 [SpringBoot] ManyToOne으로 설정된 엔티티 조회 (0) 2022.03.17 [SpringBoot] Repository에서 nativeQuery 사용시 / private final로 변수 선언하는 이유 / Page vs. Slice (0) 2022.03.16 [SpringBoot] JPA를 통해 엔티티 반환시 DtoVo 사용하기 (0) 2022.03.16