Spring Mvc 메시지, 커맨드객체

김정훈·2024년 7월 12일

Spring

목록 보기
10/24

1. 메시지

1) MessageSource

  • Bean으로 등록
  • 반드시 빈의 이름이 messageSource로 지정

2) ResourceBundleMessageSource

3) 다국어 지원 위한 메시지 파일

  • properties파일

MessageConfig

@Configuration
public class MessageConfig {

    @Bean
    public MessageSource messageSource(){
        ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
        ms.setBasenames("messages.commons");
        ms.setDefaultEncoding("UTF-8");
        ms.setUseCodeAsDefaultMessage(true); //메세지 코드가 없는 경우 코드로 메세지 대체

        return ms;
    }
}

commons.properties

LOGIN_MSG={0}({1})님 로그인
EMAIL=이메일

commons_en.properties

LOGIN_MSG={0}({1}) logged in
EMAIL=email
@SpringJUnitWebConfig
@ContextConfiguration(classes = MvcConfig.class)
public class MessageSourceTest {

    @Autowired
    private MessageSource messageSource;

    @Test
    void test1(){
        String message = messageSource.getMessage("LOGIN_MSG", new Object[] {"사용자01","user01"}, Locale.KOREAN);
        System.out.println(message);
    }

    @Test
    void test2(){
        String message = messageSource.getMessage("EMAIL", null, Locale.KOREAN);
        System.out.println(message);
    }
    
    @Test
    void test3(){
        String message = messageSource.getMessage("LOGIN_MSG", new Object[] {"사용자01","user01"}, Locale.ENGLISH);
        System.out.println(message);
    }
}

요청 헤더 Accept-Language통한 언어설정

MemberController.java

@Slf4j
@Controller
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberController {

    private final MessageSource messageSource;
    private final HttpServletRequest request;

    @GetMapping("/join")
    public String join(@ModelAttribute RequestJoin form) {
        Locale locale = request.getLocale(); //요청 헤더 Accept-Language
        String message = messageSource.getMessage("EMAIL", null, locale);
        log.info(message);

        return "member/join";
    }
}

Spring message code

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<spring:message code="???" />

join.jsp

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<c:url var="actionUrl" value="/member/join" />
${commonValue}

<spring:message code="LOGIN_MSG" arguments="사용자01,user01" />
<spring:message code="LOGIN_MSG">
    <spring:argument value="사용자01"/>
    <spring:argument value="user01"/>
</spring:message>

<h1>
    <spring:message code="회원가입" />
</h1>
<form:form method="POST" action="${actionUrl}" autocomplete="off" modelAttribute="requestJoin">
    <dl>
        <dt>
            <spring:message code="이메일" />
        </dt>
        <dd>
            <form:input path="email" cssClass="input-txt" cssStyle="border-color: red;"/>
        </dd>
    </dl>
    <dl>
        <dt>
            <spring:message code="비밀번호"/>
        </dt>
        <dd>
            <form:password path="password" />
        </dd>
    </dl>
    <dl>
        <dt>
            <spring:message code="비밀번호_확인"/>
        </dt>
        <dd>
            <form:password path="confirmPassword" />
        </dd>
    </dl>
    <dl>
        <dt>
            <spring:message code="회원명"/>
        </dt>
        <dd>
            <form:input path="userName" />
        </dd>
    </dl>
    <dl>
        <dt>
            <spring:message code="약관동의" />
        </dt>
        <dd>
            <form:checkbox path="agree" value="true" label="회원가입 약관에 동의합니다." />
        </dd>
    </dl>
    <button type="submit"><spring:message code="가입하기" /></button>
</form:form>

2. 커맨드 객체 검증

1) Validator 인터페이스

  • supports(...) : 검증하는 커맨드 객체 한정 설정
  • validate(Object target, Erros errors) : 커맨드 객체 검증
    Object target : 커맨드 객체 👉 형변환
    Errors errors : 검증 실패시 전달할 메세지 등록...

JoinValidator

@Component
@RequiredArgsConstructor
public class JoinValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) { //RequestJoin 커맨드 객체만 검증하도록 제한
        return clazz.isAssignableFrom(RequestJoin.class);
    }

    @Override
    public void validate(Object target, Errors errors) {

    }
}

2) Errors

커맨드 객체 자체 오류에 대한 처리

reject("에러코드");
reject("에러코드", "기본 메세지")

커맨드 객체의 특정 필드 오류에 대한 처리

rejectValue("필드명", "에러코드");
rejectValue("필드명", "에러코드", "기본메세지");
hasErrors() : 한개라도 reject 또는 rejectValue가 호출되면 true

MemberController

@Controller
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberController {

    @Override
    public boolean supports(Class<?> clazz) { //RequestJoin 커맨드 객체만 검증하도록 제한
        return clazz.isAssignableFrom(RequestJoin.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        //1. 필수 항목 검증(email, password, confirmPassword, userName, agree)
        //2. 이메일 중복 여부 검증(회원이 가입되어 있는지 체크)
        //3. 비밀번호 자리수 체크 ( 8자리 이상 )
        //4. 비밀번호 + 비밀번호확인 일치 여부

        RequestJoin form = (RequestJoin) target;
        String email = form.getEmail();
        String password = form.getPassword();
        String confirmPassword = form.getConfirmPassword();
        String userNAme = form.getUserName();
        boolean agree = form.isAgree();

        // 필수 항목 검증
        if(!StringUtils.hasText(email)) { //null값, isBlank() 대신해줌.
            errors.rejectValue("email", "Required", "이메일을 입력하세요.");
        }

        if(!StringUtils.hasText(password)) {
            errors.rejectValue("password", "Required", "비밀번호를 입력하세요");
        }

    }
}

타임리프

fields.errors("필드명") : -> errors 객체 담긴 메세지를 필드명으로 조회 -> 배열

<form:erros path="팰드명" />

  • 기본 에러 출력 태그 span, 여러 에러 메세지가 있는 경우 <br>구분 (delimiter)
  • element="태그명"
  • delimiter="에러와 에러 메세지 사이에 추가될 태그"

join.jsp

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<c:url var="actionUrl" value="/member/join" />
<form:form method="POST" action="${actionUrl}" autocomplete="off" modelAttribute="requestJoin">
    <dl>
        <dt>
            <spring:message code="이메일" />
        </dt>
        <dd>
            <form:input path="email"/>
            <form:errors path="email" element="div" delimiter=""/>
        </dd>
    </dl>
    <dl>
        <dt>
            <spring:message code="비밀번호"/>
        </dt>
        <dd>
            <form:password path="password" />
            <form:errors path="password"/>
        </dd>
    </dl>
    <dl>
        <dt>
            <spring:message code="비밀번호_확인"/>
        </dt>
        <dd>
            <form:password path="confirmPassword" />
        </dd>
    </dl>
    <dl>
        <dt>
            <spring:message code="회원명"/>
        </dt>
        <dd>
            <form:input path="userName" />
        </dd>
    </dl>

    <dl>
        <dt>
            <spring:message code="약관동의" />
        </dt>
        <dd>
            <form:checkbox path="agree" value="true" label="회원가입 약관에 동의합니다." />
        </dd>
    </dl>
    <button type="submit"><spring:message code="가입하기" /></button>
</form:form>

3) ValidationUtils

필수 항목 검증에 편의 메서드

  • rejectIfEmpty(...) : null 또는 ""
  • rejectIfEmptyOrWhitespace(...) : 공백 포함 체크

JoinValidator

@Component
@RequiredArgsConstructor
public class JoinValidator implements Validator {
    
    private final MemberMapper mapper;
    
    @Override
    public boolean supports(Class<?> clazz) { //RequestJoin 커맨드 객체만 검증하도록 제한
        return clazz.isAssignableFrom(RequestJoin.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        //1. 필수 항목 검증(email, password, confirmPassword, userName, agree)
        //2. 이메일 중복 여부 검증(회원이 가입되어 있는지 체크)
        //3. 비밀번호 자리수 체크 ( 8자리 이상 )
        //4. 비밀번호 + 비밀번호확인 일치 여부

        RequestJoin form = (RequestJoin) target;
        String email = form.getEmail();
        String password = form.getPassword();
        String confirmPassword = form.getConfirmPassword();
        String userNAme = form.getUserName();
        boolean agree = form.isAgree();

        //1. 필수 항목 검증(email, password, confirmPassword, userName, agree)
        ValidationUtils.rejectIfEmptyOrWhitespace(errors,"email","Required", "이메일을 입력하세요");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors,"password","Required", "비밀번호를 입력하세요");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors,"confirmPassword","Required", "비밀번호를 확인하세요");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors,"userName","Required", "회원명을 입력하세요");
        if(!agree){
            errors.rejectValue("agree", "Requered", "회원가입 약관에 동의하세요");
        }

        //2. 이메일 중복 여부 검증(회원이 가입되어 있는지 체크)
        if(StringUtils.hasText(email) && mapper.exists(email) != 0L){
            errors.rejectValue("email", "Duplicated");
        }

        //3. 비밀번호 자리수 체크 ( 8자리 이상 )
        if(StringUtils.hasText(password) && password.length() < 8){
            errors.rejectValue("password", "Length");
        }
        //4. 비밀번호 + 비밀번호확인 일치 여부
        if(StringUtils.hasText(password) && StringUtils.hasText(confirmPassword) && !password.equals(confirmPassword)){
            errors.rejectValue("confirmPassword", "Mismatch");
        }
    }
}

join.jsp

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<c:url var="actionUrl" value="/member/join" />
<form:form method="POST" action="${actionUrl}" autocomplete="off" modelAttribute="requestJoin">
    <dl>
        <dt>
            <spring:message code="이메일" />
        </dt>
        <dd>
            <form:input path="email"/>
            <form:errors path="email"/>
        </dd>
    </dl>
    <dl>
        <dt>
            <spring:message code="비밀번호"/>
        </dt>
        <dd>
            <form:password path="password" />
            <form:errors path="password"/>
        </dd>
    </dl>
    <dl>
        <dt>
            <spring:message code="비밀번호_확인"/>
        </dt>
        <dd>
            <form:password path="confirmPassword" />
            <form:errors path="cofirmPassword"/>
        </dd>
    </dl>
    <dl>
        <dt>
            <spring:message code="회원명"/>
        </dt>
        <dd>
            <form:input path="userName" />
            <form:errors path="userName"/>
        </dd>
    </dl>

    <dl>
        <dt>
            <spring:message code="약관동의" />
        </dt>
        <dd>
            <form:checkbox path="agree" value="true" label="회원가입 약관에 동의합니다." />
            <form:errors path="agree" element="div" delimiter=""/>
        </dd>
    </dl>
    <button type="submit"><spring:message code="가입하기" /></button>
</form:form>

3. 에러 코드에 해당하는 메시지 코드를 찾는 규칙

  • 에러코드 + "." + 커맨드객체이름 + "." + 필드명
  • 에러코드 + "." + 필드명
  • 에러코드 + "." + 필드타입(자료형)
  • 에러코드

MessageConfig

@Configuration
public class MessageConfig {

    @Bean
    public MessageSource messageSource(){
        ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
        ms.setBasenames("messages.commons", "messages.validations", "messages.errors");
        ms.setDefaultEncoding("UTF-8");
        ms.setUseCodeAsDefaultMessage(true); //메세지 코드가 없는 경우 코드로 메세지 대체

        return ms;
    }
}

validations.properties

Required=필수 입력항목 입니다.

Required.email =이메일을 입력하세요.
Required.password=비밀번호를 입력하세요.
Required.confirmPassword=비밀번호를 확인하세요.
Required.userName=회원명을 입력하세요.

#name명이 중복될수있기 때문에 범위를 좀더 좁혀서 커맨드객체로 좁힘 = 회원 가입시 검증 메세지로 한정
Required.requestJoin.agree=회원가입 약관에 동의하세요.
Duplicated.requestJoin.email=이미 가입된 회원입니다.
Length.requestJoin.password=비밀 번호는 8자리 이상 입력하세요.
Mismatch.requestJoin.confirmPassword=비밀번호가 일치하지 않습니다.

4. 프로퍼티 타입이 List나 목록인 경우 메시지코드 생성방법

에러코드 + "." + 커맨드객체이름 + "." + 필드명[인덱스].중첩필드명
에러코드 + "." + 커맨드객체이름 + "." + 필드명.중첩필드명
에러코드 + "." + 필드명[인덱스].중첩필드명
에러코드 + "." + 필드명.중첩필드명
에러코드 + "." + 중첩필드명
에러코드 + "." + 필드타입
에러코드

5. 글로벌 범위 Validator와 컨트롤러 범위 Validator

1) 글로벌 범위 Validator 설정과 @Valid 애노테이션

  • WebMvcConfigurer의 getValidalor()
  • 모든 컨트롤러의 공통적인 검증이 필요한 경우

MvcConfig

@Configuration
@EnableWebMvc
@ComponentScan("org.choongang")
@Import({DBConfig.class, MessageConfig.class})
@RequiredArgsConstructor
public class MvcConfig implements WebMvcConfigurer {

    private final JoinValidator joinValidator;

    @Override
    public Validator getValidator() {
        return joinValidator;
    }
}

2) @InitBinder 애노테이션을 이용한 컨트롤러 범위 Validator

@InitBinder
protected void InitBinder(WebDataBinder binder) {
	binder.setValidator(new RegisterRequestValidator());
}
  • 특정 컨트롤러에서 사용할 공통적인 Validator

3) 컨트롤러 범위 Validator 👉 글로벌 범위 Validator

6. Bean Validation

Bean Validation API
hibernate Validator : 구현체

1) 커맨드 객체 검증 과정

1) Bean Validation Api - 에노테이션으로 기본 검증처리
2) 기본 애노테이션으로 안되는 검정 Validator를 정의 후 검증처리

2) 의존성

implementation 'jakarta.validation:jakarta.validation-api:3.0.2' //Jakarta Validation API » 3.0.2
implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' //Hibernate Validator Engine » 8.0.1.Final

3) Bean Validation의 주요 애노테이션

애노테이션주요 속성설명지원 타입
@AssertTrue
@AssertFalse
값이 true인지 또는 false인지 검사한다.
null은 유효하다고 판단한다.
boolean
Boolean
@DecimalMax
@DecimalMin
String value
- 최대값 또는 최소값
boolean inclusive
- 지정값 포함 여부
- 기본값 : true
지정한 값보다 작거나 같은지 또는 크기가 같은지 검사한다.
inclusive가 false이면 value로 지정한 값은 포함하지 않는다.
null은 유효하다고 판단한다.
BigDecimal
BigInteger
CharSequence
정수타입
@Max
@Min
long value지정한 값보다 작거나 같은지 또는 크거나 같은지 검사한다.
null은 유효하다고 판단한다.
BigDecimal
BigInteger
정수타입
@Digitsint integer
- 최대 정수 자릿수
int fraction
- 최대 소수점 자릿수
자릿수가 지정한 크기를 넘지않는지 검사한다.
null은 유효하다고 판단한다.
BigDecimal
BigInteger
CharSequence
정수타입
@Sizeint min
- 최소 크기
- 기본값 : 0
int max
- 최대 크기
- 기본값 : 정수 최대값
길이나 크기가 지정한 값 범위에 있는지 검사한다.
null은 유효하다고 판단한다.
CharSequence
Collection
Map
배열
@Null
@NonNull
값이 null인지 또는 null이 아닌지 검사한다.
@PatternString regexp
- 정규표현식
값이 정규표현식에 일치하는지 검사한다.
null은 유효하다고 판단한다.
CharSequence

애노테이션설명지원타입
@NotEmpty문자열이나 배열의 경우 null이 아니고 길이가 0이 아닌지 검사한다.
콜렉션의 경우 null이 아니고 크기가 0이 아닌지 검사한다.
CharSequence
Collection
Map
배열
@NotBlanknull이 아니고 최소한 한 개 이상의 공백아닌 문자를 포함하는지 검사한다.CharSequence
@Positive
@PositiveOrZero
양수인지 검사한다.
OrZero가 붙은 것은 0 또는 양수인지 검사한다.
null은 유효하다고 판단한다.
BigDecimal
BigInteger
정수타입
@Email이메일 주소가 유효한지 검사한다.
null은 유효하다고 판단한다.
CharSequence
@Future
@FutureOrPresent
해당 시간이 미래 시간인지 검사한다.
OrPresent가 붙은 것은 현재 또는 미래 시간인지 검사한다.
null은 유효하다고 판단한다.
시간 관련 타입
@Past
@PastOrPresent
해당 시간이 과거 시간인지 검사한다.
OrPresent가 붙은 것은 현재 또는 과거 시간인지 검사한다.
null은 유효하다고 판단한다.
시간 관련 타입

RequestJoin

@Data
public class RequestJoin {
    @NotBlank(message = "이메일을 입력하세요")
    @Email(message="이메일 형식이 아닙니다.")
    private String email;

    @NotBlank
    @Size(min=8)
    private String password;

    @NotBlank
    private String confirmPassword;

    @NotBlank
    private String userName;

    @AssertTrue
    private boolean agree;
}

validations.properties

Duplicated.requestJoin.email=이미 가입된 회원입니다.
Mismatch.requestJoin.confirmPassword=비밀번호가 일치하지 않습니다.

Email=이메일 형식이 아닙니다. 
NotBlank=필수 입력항목 입니다.
NotBlank.email=이메일을 입력하세요.
NotBlank.password=비밀번호를 입력하세요.
NotBlank.confirmPassword=비밀번호를 확인하세요.
NotBlank.userName=회원명을 입력하세요.
AssertTrue.requestJoin.agree=회원가입 약관에 동의합니다.
Size.requestJoin.password=비밀번호는 8자리 이상 입력하세요.

MemberController
@Valid 애노테이션 추가

@Controller
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberController {

    private final JoinValidator joinValidator;

    @GetMapping("/join")
    public String join(@ModelAttribute RequestJoin form){

        return "member/join";
    }

    @PostMapping("/join")
    public String joinPs(@Valid RequestJoin form, Errors errors){
        //회원 가입 데이터 검증(유효성 검사)
        joinValidator.validate(form, errors);

        if(errors.hasErrors()){ //reject, rejectValue가 한번이라도 호출되면 treu = 에러가 한개라도 존재할경우
            return "member/join";
        }


        return "redirect:/member/login";
    }
}

profile
안녕하세요!

0개의 댓글