Spring MVC/Validation

Validation 검증 - 도메인 객체를 하나만 사용할 경우 문제점 해결

Hcode 2022. 12. 9.

 

이전 글로 Bean Validation의 글로벌 Validation 오류에 대해서 알아보았습니다.
 

Validation 검증 - Bean Validation의 글로벌 Validation 오류

이전 글로 Validation을 편하게 할 수 있는 Bean Validation에 대해 알아보았습니다. Validation 검증 - Bean Validation과 Spring MVC 이전 글로 Bean Validation에 대해 간단히 알아보았습니다. Validation 검증 - Bean Valida

hhhhicode.tistory.com

이번 글로 도메인 객체를 하나만 사용할 경우 문제점이 발생할 수 있는데, 그것을 해결하는 2가지 방법에 대해 알아보겠습니다.

 


 

도메인 객체를 하나만 사용할 경우 문제점 해결의 2가지 방법

  • @Validated의 groups 기능
  • 별도의 객체를 만들어서 사용

에 대해 다룹니다.

 


 

@Validated의 groups 기능

자바 애노테이션인 @Valid에는 groups 기능이 없습니다.

groups를 적용하려면
  스프링 애노테이션인 @Validated를 사용해야 합니다.

 


 

groups 사용

  • groups 생성
  • groups 적용
  • groups 사용

 


 

groups 생성

저장용 groups를 생성해 보겠습니다.

저장 페이지에서 사용할 Bean Validation에 적용할 groups입니다.

 

우선 저장용 태그로 사용될 Interface를 만들어 줍니다.

아... 벌서부터 왜 이러고 있나 생각이 듭니다.

그래도 일단 만들어 보겠습니다.

public interface SaveCheck {}

간단하게 그냥 이름만 의미 있도록 만드시면 됩니다.

해당 Interface만으로는 별다른 기능은 없습니다.

 

수정용 Interface도 만들어 보겠습니다.

public interface UpdateCheck {}

벌써부터 이 방법은 별로라는 생각이 드실 겁니다.

안 그래도 복잡한 프로젝트 목록에 태깅을 위한 Interface를 각 페이지마다 생성하라니요...

해당 Interface만으로는 별다른 기능이 없다는 것에 더욱 화가 치밀어 오릅니다.

 

groups 적용을 위한 Interface는 모두 만들었습니다.

 


 

groups 적용

groups를 적용해 보겠습니다.

사용할 객체의 Bean Validation 애노테이션에 groups 속성으로 적용시키면 됩니다.

동시에 적용시키기 위해 {}를 사용하는 배열이 지원됩니다.

@Data
public class MemberTest {

    @NotNull(groups = UpdateCheck.class)
    private Long id;

    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
    private String userId;

    @Range(min = 4, max = 10, groups = {SaveCheck.class, UpdateCheck.class})
    private String password;

    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
    private String userName;
    @Range(min = 0, max = 100, groups = SaveCheck.class)
    @Range(min = 0, max = 1000, groups = UpdateCheck.class)
    private Integer age;
}

 


 

groups 사용

적용한 groups를 사용해보겠습니다.

Handler의 파라미터에서 사용됩니다.

@Validated의 속성에 명시합니다.

명시한 groups에 해당하는 Bean Validation 애노테이션을 적용하라는 뜻입니다.

@PostMapping("/add")
public String addMember(@Validated(SaveCheck.class) @ModelAttribute Member member, 
                        BindingResult bindingResult) {
    ...
}

@PostMapping("/{userId}/update")
public String updateMember(@PathVariable String userId, 
                           @Validated(UpdateCheck.class) @ModelAttribute Member member, 
                           BindingResult bindingResult) {
    ...   
}

 


 

groups를 사용하는가?

groups 기능을 사용해서 Validator에 대한 profile을 만들 수 있습니다.

하지만 groups 기능을 사용하면

  class 복잡도가 올라가고 코드 복잡도도 올라갑니다.

 

그런 이유로 groups 기능은 잘 사용되지 않습니다.

주로 별도의 객체를 만들어서 각각의 Validator를 적용시킵니다.

각 기능별, 페이지별 요구하는 데이터가 다를 수도 있다는 문제도

  별도의 객체를 만들기 때문에 해결됩니다.

 

존재를 알아만 둡시다.

 


 

별도의 객체를 만들어서 사용

각각의 상황에서 별도의 Validator를 적용하기 위해

  groups는 잘 사용되지 않고

    별도의 객체를 만들어서 별도의 Validator를 적용시키는 방법을 많이 사용합니다.

 

등록 시 폼에서 전달하는 데이터가 해당 도메인 객체와 정확히 딱 들어맞지 않은 경우가 많습니다.

제가 진행 중인 개인 프로젝트에서 예를 들어보겠습니다.

 


 

객체를 나누어 사용하는 예시

Member의 본 객체입니다.

@Getter @Setter
@ToString
public class Member {

    private Long id;
    private String userId;
    private String password;
    private String userName;
    private String icon;
    private String emailAddress;
    private String displayPrograms;
    private String memo;
}

 

그리고 로그인을 위해 따로 MemberLoginDto 객체를 만들어 보겠습니다.

@Getter @Setter
public class MemberLoginDto {

    @Length(min = 4, max = 20)
    private String userId;
    @Length(min = 4, max = 20)
    private String password;
}

 

이렇듯이 필요한 데이터는 상황마다 다릅니다.

 

Controller에서 Dto 데이터를 전달받고

  이후 Controller에서 필요한 데이터를 사용하여 상황에 맞게 도메인 객체를 생성합니다.

Dto는 데이터 전달을 위한 객체일 뿐이기 때문입니다.

로직을 수행하려면 Dto 객체만으론 충분하지 않을 수 있습니다.

 


 

Dto To Domain Object

 

간단한 예를 들어보겠습니다.

@PostMapping("/login")
public String login(HttpServletRequest request,
                    @Validated @ModelAttribute MemberLoginDto memberLoginDto, BindingResult bindingResult) {

    Member loginMember = memberService.loginRequest(memberLoginDto);
    ...
}

MemberLoginDto 객체를 사용하여 Member 객체를 받았습니다.

변환된 Member 객체는 밑의 생략된 로직에서 사용될 것입니다.

이렇듯 Dto를 사용하면 객체를 변환하는 과정이 추가될 수 있습니다.

 

앞서 Dto를 계속 말하고 있는데
  Dto는 Data Transfer Object의 약자입니다.

정해져 있는 이름이 아니라 관례상 사용하는 하나의 의미가 있는 단어입니다.
그러므로 Dto가 붙지 않는다고 해서 틀린 것은 아니며
  해당 조직에서의 관례가 다를 뿐입니다.

 


 

객체를 나누어 사용해봅시다.

Validation은 데이터를 전달받을 때 주로 사용합니다.

그러므로 도메인 객체로 직접 데이터를 받는 것이 아니라면

  Bean Validation의 Validation 애노테이션을 지우셔도 됩니다.

 

위에서 예시를 드린 Member 객체를 보시면

  Bean Validation 애노테이션이 없습니다.

해당 객체로는 데이터를 받지 않기 때문입니다.

 


 

별도의 객체를 만들어서 Validation 적용

예를 들어 보여드리겠습니다.

우선 save를 위한 객체입니다.

@Getter @Setter
@ToString
public class MemberAddDto {

    @Length(min = 4, max = 20)
    private String userId;
    @Length(min = 4, max = 20)
    private String password;
    @Length(min = 1, max = 20)
    private String userName;
    @Email
    private String emailAddress;
    @Length(max = 1000)
    private String memo;
}

오직 save만을 위한 객체입니다.

회원가입 페이지에서 사용됩니다.

 

그리고 login을 위한 객체입니다.

@Getter @Setter
public class MemberLoginDto {

    @Length(min = 4, max = 20)
    private String userId;
    @Length(min = 4, max = 20)
    private String password;
}

도직 login만을 위한 객체입니다.

 

이렇게 각각의 상황에 필요한 객체를 따로 만들어서 Bean Validation 애노테이션 또한 각각 적용했습니다.

 

그럼 이제 Controller에서 나누어 만든 Dto 객체를 사용하는 것을 보여드리겠습니다.

 


 

Controller에서 Dto 객체 사용

MemberAddDto를 사용해보겠습니다.

이 객체는 @Validated를 통해 Bean Validator를 적용받습니다.

검증 오류가 있다면 BindingResult 객체에 담기게 됩니다.

 

바인딩 오류가 있다면, Bean Validator가 적용되기 전에 FieldError로 BindingResult에 담기게 됩니다.

또한 글로벌 오류는 Handler의 내부 로직에서 조건문을 통해 ObjectError로 BindingResult에 담기게 됩니다.

@GetMapping("/add")
public String addForm(@ModelAttribute("memberAddDto") MemberAddDto memberAddDto) {

    return "members/add";
}

@PostMapping("/add")
public String add(@Validated @ModelAttribute MemberAddDto memberAddDto, BindingResult bindingResult) {

    Boolean isUserIdDuplicate = memberService.isUseridDuplicate(memberAddDto.getUserId());
    ...
}

 

MemberLoginDto 객체 또한 사용해 보겠습니다.

@GetMapping("/login")
public String loginForm(Model model, HttpServletRequest request,
                        @ModelAttribute("memberLoginDto") MemberLoginDto memberLoginDto) {

    return "members/login";
}

@PostMapping("/login")
public String login(HttpServletRequest request,
                    @Validated @ModelAttribute MemberLoginDto memberLoginDto, BindingResult bindingResult) {

    Member loginMember = memberService.loginRequest(memberLoginDto);
    ...
}

해당 Handler에서는 MemberLoginDto가 Member 객체로 변환되고 있습니다.

이렇듯 Dto 객체는 Data Transfer Object이므로

  로직을 수행하기 위해 다른 객체로 변환되는 상황이 필요할 수 있습니다.

 

배가 고파서 잘 적혔나 모르겠습니다...
너무너무 배고픕니다.
새벽은 항상 배고픈 것 같습니다.
음... 뜨끈한 라면이 먹고 싶네요.

신라면에 짬뽕 다시 조미료 1/3 티스푼 넣어서 먹으면 맛있습니다.

 


 

다음 글로 Validation에 사용될 메시지에 대해 알아보겠습니다.
 

Validation 검증 - Message 메시지

이전 글로 도메인 객체를 하나만 사용할 때 발생할 수 있는 문제점과 그 해결을 알아보았습니다. Validation 검증 - 도메인 객체를 하나만 사용할 경우 문제점 해결 이전 글로 Bean Validation의 글로벌

hhhhicode.tistory.com

 

댓글