카카오 테크 캠퍼스 7주차

boseung·2023년 5월 30일
0

스프링에서 예외처리, 데이터 검증에 대해서 처음 다루게 되어서 부족한 부분이 많이 보인다. 이 부분에 대한 추가적인 공부가 필요해 보인다.

예외처리

스프링에서는 다양한 예외처리 방법을 제공한다.

try-catch

자바에서 지원하는 try-catch를 이용해서 예외를 처리하는 방식이다.

@RequestMapping("/ex")
	public String main(Model m) throws Exception {
		m.addAttribute("msg", "message from ExceptionController.main()");
		try{
			throw new Exception("예외가 발생했습니다.");
		} catch(Exception e){
			return "error";
		}
	}

하지만 이렇게 예외를 처리하면 공통적인 예외처리가 어려워서 코드가 지나치게 길어질 수 있다.

@ExceptionHandler

그래서 스프링에서 Controller 내부에서 @ExceptionHandler 어노테이션을 이용해서 메서드를 만들어서 공통 예외를 처리하는 방법을 지원하고 있다.

@Controller
public class ExceptionController {
	@ExceptionHandler(Exception.class) // Controller 내부에서 공통적인 예외처리
	public String catcher(Exception ex, Model m) {
		return "error";
	}
	
	@RequestMapping("/ex")
	public String main(Model m) throws Exception {
		throw new Exception("예외 발생");
	}

	@RequestMapping("/ex2")
	public String main2() throws Exception {
		throw new Exception("예외 발생");
	}
}

@ResponseStatus

@ResponseStatus를 활용하면 상태 코드를 지정할 수 있는데, 이것을 예외처리에도 활용할 수 있다.

스프링은 예외 발생시 기본적으로 상태코드 500을 반환하는데, @ResponseStatus를 통해 사용자가 직접 상태코드를 지정해줄 수 있다.

@Controller
public class ExceptionController {
	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 200 -> 500
	@ExceptionHandler(Exception.class) // Controller 내부에서 공통적인 예외처리
	public String catcher(Exception ex, Model m) {
		return "error";
	}
	
	@RequestMapping("/ex")
	public String main(Model m) throws Exception {
		throw new Exception("예외 발생");
	}

	@RequestMapping("/ex2")
	public String main2() throws Exception {
		throw new Exception("예외 발생");
	}
}

위의 예제에서 예외발생 시 error.jsp 페이지로 리다이렉트 시키기 때문에 상태 코드는 200이다.

하지만 실제로는 서버 내부에서 에러가 발생했기 때문에 @ResponseStatus를 통해 상태 코드 500으로 바꾸어 주었다.

하지만 이렇게 Controller 내부에서 에러를 처리하는 방식보다 Controller의 수가 많을때에는 공통적인 예외를 일괄적으로 처리할 클래스가 따로 존재하는 것이 유지보수나 설계 측면에서 더 유리하다.

@ControllerAdvice

스프링에서는 @ControllerAdvice 어노테이션을 통해 Controller들의 공통적인 예외를 처리하는 클래스를 따로 분리해서 더 깔끔하게 예외를 처리 할 수 있다.

@ControllerAdvice("com.fastcampus.ch2") // 패키지 명을 지정해줄 수도 있다.
	public class GlobalCatcher {
		@ExceptionHandler({NullPointerException.class, FileNotFoundException.class})
		public String catcher2(Exception ex, Model m) {
			m.addAttribute("ex", ex);
			return "error";
		}
		@ExceptionHandler(Exception.class)
		public String catcher(Exception ex, Model m) {
			m.addAttribute("ex", ex);
			return "error";
		}
	}

이때 Controller 내부에도 @ExceptionHandler있고 공통예외처리 클래스에도 @ExceptionHandler가 존재한다면 Controller 내부에 있는 @ExceptionHandler가 더 높은 우선순위를 가진다.

이외에도 web.xml을 통해 상태코드별로 view를 처리하는 방식이나 예외종류별로 view를 처리하는 방식 등 다양한 방식이 있다.

스프링 예외처리 전략

Controller에서 예외가 발생하면 DispatcherServlet에서 handlerExceptionResolvers에서 예외처리 전략을 살핀다.

handlerExceptionResolver의 예외처리 전략 우선순위

  1. ExceptionHandlerExceptionResolver
  2. ResponseStatusExceptionResolver
  3. DefaultHandlerExceptionResolver

가장 먼저 @ExceptionHandler로 예외처리가 가능한지 살피고, @ResponseStatus에 맞는 예외처리가 가능한지 살피고나서 가장 마지막으로 500 상태를 가진 DefaultHandlerExceptionResolver가 처리하게 된다.

데이터 변환, 검증

데이터 변환

WebDataBinder는 View에서 Controller로 넘어오는 요청 파라미터를 컨트롤러의 메서드 매개변수에 바인딩하거나, 컨트롤러에서 응답을 생성할 때 객체를 문자열로 변환하여 전송하는 등의 역할을 수행한다.

그리고 이렇게 데이터 바인딩 및 데이터 검증의 결과를 BindingResult에 저장되기 때문에 이를 통해 결과를 확인하고 에러 검증, 처리를 수행할 수 있다.

만약 BindingResult가 Controller에서 매개변수로 사용되지 않았다면 에러가 발생하면 View에서 에러 페이지를 보여준다.

하지만 BindingResult가 Controller에서 매개변수로 사용되었다면(이때 검증할 객체 뒤에서 매개변수로 사용) 에러 페이지를 띄우지 않고 Controller에게 에러 내용을 BindingResult에 담아서 에러 처리를 할 수 있도록 넘겨준다.

@RequestMapping("/day")
public String main(@ModelAttribute MyDate date, BindResult result){
																								// 검증 객체 뒤에서 BindResult 사용						
}

WebDataBinder는 @InitBinder와 함께 데이터 바인딩에 사용되는데 예제를 통해서 자세히 살펴보자.

public class User{
	private String id;
	private String pwd;
	private Date birth;
}

클라이언트가 View에서 User 객체의 데이터 값을 Controller에게 넘겨줬다고 생각해보자.

@RequestMapping("/register")
public class RegisterController{
		@PostMapping("/save")
		public String main(User user, BindResult result) {
}

그럼 위의 Controller가 View에서 제공한 데이터를 WebDataBinder로 데이터 바인딩을 통해 User 객체를 전달할 것이다.

이때 @InitBinder를 이용해서 데이터 변환을 좀더 구체적으로 정할 수 있다.

@RequestMapping("/register")
public class RegisterController{
		@InitBinder
		public void toDate(WebDataBinder binder){
				SimpleDateFormat df = new SimpleDateFormat("yyyy-dd-mm");
        binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
	}

		@PostMapping("/save")
		public String main(User user, BindResult result) {
	}
}

SimpleDateFormat 클래스에 데이터 변환 형식을 명시하고

WebDateBinder에 Date 클래스에서 CustomDateEditor 클래스로 변환 과정을 세팅하는 것이다.

이때 CustomDateEditor 클래스를 열어보면

public class CustomDateEditor extends PropertyEditorSupport {
	//...
}

PropertyEditorSupport클래스를 상속받고 있다는 것을 알 수 있다.

PropertyEditor

PropertyEditor는 속성 값을 문자열로 표현하고 문자열을 속성 값으로, 양방향 타입 변환을 지원한다(Object ↔ String만 지원한다)

PropertyEditors에서 PropertyEditor를 상속받은 다양한 클래스들을 확인할 수 있다.

PropertyEditor는 이름에서 알 수 있듯이 PropertyEditor 내부에서 인스턴스 변수(Property)를 사용한다.

따라서 Thread-safe 하지 않기 때문에 싱글톤 환경에서 사용할 수 없다는 단점을 가지고 있다.

예를 들면 아래와 같은 느낌일 것이다.(딱 봐도 인스턴스 변수 때문에 동시성 문제가 발생할 것 같다..)

public class CustomPropertyEditor extends PropertyEditorSupport {

    private int count;

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        count = Integer.parseInt(text);
    }

    @Override
    public String getAsText() {
        return String.valueOf(count);
    }

    // Getter and Setter for 'count'
}

그래서 등장한 게 Converter이다.

Converter

Convertor는 타입 A를 타입 B로 변환하는 것처럼 단방향 타입 변환을 지원한다.

PropertyEditor의 단점을 개선하기 위해 등장했기 때문에 Thread-safe하다.

public class CustomConverter implements Converter<String, String[]> {

    @Override
    public String[] convert(String source) {
        return source.split("#"); // String -> String[]
    }
}

Formatter

Formatter는 PropertyEditor처럼 양방향 타입 변환을 지원한다.

그리고 Covertor처럼 Thread-safe하다는 특징도 가지고 있다.

PropertyEditor보다 더 간단하게 바인딩할 필드에 어노테이션을 사용해서 데이터 변환을 할 수 있다.

예를 들어 위의 User 클래스의 필드에서

public class User{
	private String id;
	private String pwd;
	@DateTimeFormat(pattern="yyyy/MM/dd")
	private Date birth;
}

이렇게 어노테이션을 사용하면

@RequestMapping("/register")
public class RegisterController{
		@InitBinder
		public void toDate(WebDataBinder binder){
				//SimpleDateFormat df = new SimpleDateFormat("yyyy-dd-mm");
        //binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
	}

		@PostMapping("/save")
		public String main(User user, BindResult result) {
	}
}

위에 주석 처리한 두 줄과 같아지게 된다.

데이터 검증

데이터를 검증할 때, Validator를 이용해서 수동 / 자동으로 검증하는 방법이 있다.

수동 / 자동으로 검증하는 부분은 Controller에 구현하는 부분에서 달라진다.

먼저 Validator 인터페이스를 구현한다.

public interface Validator{
		// 검증 가능한 객체인지 확인하는 메서드
		boolean supports(Class<?> clazz);
		// 객체를 검증하는 메서드, target은 검증할 객체, error는 검증 후 에러 내용 저장소
		void validate(@Nullable Object target, Errors errors);
}

예를 들어 아래와 같이 구현해서 사용하면 된다.

public class UserValidator implements Validator {
		@Override
		public boolean supports(Class<?> clazz) {
			//return User.class.equals(clazz); // 검증하려는 객체가 User타입인지 확인
			return User.class.isAssignableFrom(clazz); // clazz가 User 또는 그 자손인지 확인
		}

		@Override
		public void validate(Object target, Errors errors) { 
			User user = (User)target;
			String id = user.getId();
			// 검증 결과 저장
			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id",  "required");
			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pwd", "required");
			if(id==null || id.length() <  5 || id.length() > 12) {
				errors.rejectValue("id", "invalidLength");
			}
		}
	}

그리고 Controller에 수동으로 검증하려면 객체를 생성해서 메서드를 사용하면 된다.

@RequestMapping("/register")
public class RegisterController{
		@PostMapping
    public String save(Model m, User user, BindingResult result){
        UserValidator userValidator = new UserValidator();
        userValidator.validate(user, result);
        if(result.hasErrors()){
            return "registerForm";
        }
}

반면에 자동으로 검증하려면 @InitBinder에 등록한 후 검증하려는 객체의 매개변수 앞에 @Valid를 사용하면 된다.

@RequestMapping("/register")
public class RegisterController{
		@InitBinder
		public void toDate(WebDataBinder binder){
				binder.setValidator(new UserValidator());
	}
		@PostMapping("/save")
		public String save(Model m, @Valid User user, BindResult result) {// @Valid로 검증
				if(result.hasErrors()){
	            return "registerForm";
	        }
	}
}

데이터 바인딩 추상화: Editor, Converter, Formatter

@InitBinder와 WebDataBinder의 쓰임

profile
Dev Log

0개의 댓글

관련 채용 정보