JAVA 테스트케이스 (1)

승환·2025년 1월 6일

순서가 좀 꼬이긴 했는데 당일 발견하고 해결한 문제를 다뤄야 할 것 같아서
순서가 다소 꼬이더라도 하루마다 작성하려고 한다.


📌 세팅

public class SignupVO {
    @Valid
    private Account account;
    // 기타 필드와 메서드
}

이런 식으로 생긴 JAVA코드를 @Valid를 이용해서 유효검증을 하려고 한다.

@NotNull(message ="null password")
    private String pw;

@NotNull 어노테이션을 사용해서 Null 필드를 사용하지 않겠다고 하였고

@Override
    public Account signUp(@Valid SignupVO signupVO, String mobile)

이런 식으로 메서드를 재정의 해서 @Valid를 적용하여 SignupVO를 검증하게 하였다.


📌 문제

1. @Valid가 적용되지 않는다.

분명 @Valid를 적용하면 유효성 검증을 할 수 있다고 하였다. 하지만 이 코드의 경우 그냥 넣었을 때는 유효성 검증이 불가능 하였다.
MethodArgumentNotValidException 이라는 예외가 등장하여야 하는데
다른 예외가 터지는것으로 보아 검증이 제대로 이뤄지지 않는 것이었다.

1. @Valid가 적용되지 않는다 (해결)

문제의 해결은 간단했다.

@Validate
public class TestCase{

	public void case1(@Valid signUpVO signupvo){
    ...
    }
    
}

이런 식으로 작성해야 했다. public class위에 Validate를 적용해줘야 아래 @Valid가 정상적으로 검증을 수행하였다.


2. jakarta.validation.ConstraintDeclarationException: HV000151 오류 발생

public class AuthServiceImpl implements AuthService {
    @Override
    public Account signUp(@Valid SignupVO signupVO, String mobile) {
        // 메서드 구현
    }
}

이런 식으로 작성된 코드를 실행하였더니 오류가 발생하였다.
찾아보니 호완성 오류라는데 해결은 간단했다.

2. jakarta.validation.ConstraintDeclarationException: HV000151 오류 발생(해결)

public interface AuthService {
    Account signUp(SignupVO signupVO, String mobile);
}

문제의 인터페이스이다. AuthService를 인터페이스로 작성하였는데, 그 안에있는 메서드인 signUp이 호완이 되지 않아 발생하는 문제이다.

public interface AuthService {
    Account signUp(@Valid SignupVO signupVO, String mobile);
}

이렇게 바꿔주면 정상적으로 작동하게 된다. @Valid또한 문법인지라, 인터페이스에 맞게 작성해줘야 한다.


3. jakarta.validation.ConstraintViolationException을 CommonException으로 변환 불가

테스트케이스의 핵심은 CommonException으로 다국어화 한 Exception을 발생시키는 것이었다.
DB에서 그것들을 불러와서 Exception Enum으로 정의된 것을 통해 획일화된 Exception을 발생시켜야 하는데, 이것을 하기 위해서 GlobalExceptionHandler을 사용하였다.

public class GlobalExceptionHandler {

    // ConstraintViolationException을 처리
    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException(ConstraintViolationException ex) {
        // 오류 메시지 추출
        List<String> errorMessages = ex.getConstraintViolations().stream()
            ...
        // CommonException 던지기
        throw new CommonException(ApiCodeEnum.ERROR_938, errorMessages);
    }

이렇게 작성해서 ConstraintViolationException을 잡으려고 하였다. 하지만

org.opentest4j.AssertionFailedError: Unexpected exception type thrown, 
Expected :class com.duegosystem.core.config.exception.CommonException
Actual   :class jakarta.validation.ConstraintViolationException

이런 오류문구(테스트결과)를 얻게 되었다.
해석하면, 예외를 기대한 것은 commonException이었는데 ConstraintCiolationException이 터졌다는 말이었다.

즉, 핸들러가 작동하지 않아서 원래 오류인 ConstraintCiolationException이 터졌다는 말이다.


3. jakarta.validation.ConstraintViolationException을 CommonException으로 변환 불가(해결)

많은 서칭과 GPT를 통해 해결책을 얻었다.

@RestControllerAdvice
@Validated
public class GlobalExceptionHandler {

    // ConstraintViolationException을 처리
    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException(ConstraintViolationException ex) {
        // 오류 메시지 추출
        List<String> errorMessages = ex.getConstraintViolations().stream()
            ...

        // CommonException 던지기
        throw new CommonException(ApiCodeEnum.ERROR_938, errorMessages);
    }

먼저 @RestContollerAdvice를 추가하였다. 여기서의 역할은 아래의 public class를 전역적인 class로 만들어서 어디서 예외가 발생하던 잡을 수 있도록 만드는 것이라는데, 이건 Controller 계층에서 해당하는 말이고, 지금 작업하는 계층은 Service계층이기 때문에 별로 의미있는 어노테이션은 아니다.

문제점은 테스트에 있었다. SpringBoot와 Junit으로 테스트 케이스를 작성하였는데, 두개의 특성상
GlobalExceptionHandler에 예외를 던지기 전에 예외를 반환하고 테스트 케이스를 종료한다.

그랬기 때문에 아무리 메인 코드를 바꿔도 처리가 되지 않았던 것. 이걸 하기 위해 방법이 두가지가 있었다.
1. Mock를 사용하는 것(실패)
예전에도 한번 시도해 봤었다.

@Autowired
private MockMvc mockMvc;

이런 식으로 MockMvc로 가상의 객체를 만들어서 API를 전달하는 방법인데, 어째선지 이유를 찾지는 못했는데 NPE(NullPointException)이 발생해서 빈번히 실패했었다.
이유는 Mock객체가 만들어지지 않았다고 하는데...
이건 나중에 다시 찾아보는 것으로
2. Intellij Http 사용(성공)
Intellij에는 http라는 기능이 있다. 처음 써봤을 때 매우 신기했는데,
API를 직접 만들어서 서버단에서 전송할 수 있는 기능이다.

GET https://localhost:8080/auth/service

이런 방법으로 .http파일에 작성하면 옆에 실행 버튼이 뜨게 된다.
그 버튼을 누르면 해당 주소에서 API를 불러오게 된다.

POST https://localhost:8080/auth/service
Content-Type: application/json

{
  "groupName": "그룹명",
  "members": [
    "회원1",
    "회원2",
    "회원3"
  ],
  "date": {
    "year": 2018,
    "month": 1,
    "day": 24
  }
}

이런 식으로 POST도 할 수 있다. 이렇게 하면 내가 원하는 데이터를 API형식으로 웹에 전달하게 된다.
물론 서버는 켜져 있어야 정상적으로 작동한다.

아무튼 이런 방식으로 내가 원하는 데이터를 만들어서 전송시켰더니
CommonException이 정상적으로 나오는 것을 볼 수 있었다.


📌 추가적으로 생각해 본 것

1. @Valid의 Exception은 MethodArgumentNotValidException을 발생시켜야 하는데 왜 ConstraintViolationException이 터질까?

이유는 @Valid의 사용 위치에 따라 달랐다

@RestController
public class AuthController {

    @PostMapping("/signUp")
    public ResponseEntity<String> signUp(@Valid @RequestBody SignupVO signupVO) {
        return ResponseEntity.ok("Signup successful");
    }
}

이렇게 Controller메서드에서 @Valid가 사용될 때는
MethodArgumentNotValidException을 발생시킨다.

@Service
@Validated
public class AuthService {

    public void signUp(@Valid SignupVO signupVO) {
        // 메서드 로직
    }
}

그리고 이렇게 Service메서드에서 @Valid를 호출하면
ConstraintViolationException가 발생하게 된다.

@Controller
public class FormController {

    @PostMapping("/submitForm")
    public String submitForm(@Valid FormData formData, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "error";
        }
        return "success";
    }
}

또 이런 경우가 있다는데 한번도 보진 못했다 Controller메서드에서 발생하는 것 같다.
BindException을 발생시킨다. form에 매핑이 제대로 되지 않으면 발생하는 것 같다.

이런 3가지 경우로 발생시킬 수 있다.

profile
왕초보 학부생

0개의 댓글