JPA에서 Bean 유효성 검증

데이터 유효성 검증은 지속성을 포함하여 모든 애플리케이션 계층에서 발생하는 공통 태스크입니다. JPA (Java™ Persistence API) 는 런타임 시 데이터 유효성 검증을 수행할 수 있도록 Bean 유효성 검증 API에 대한 지원을 제공합니다. 이 주제에는 샘플 디지털 이미지 갤러리 애플리케이션의 JPA 환경에서 Bean 유효성 검증이 사용되는 사용법 시나리오가 포함되어 있습니다.

Bean 유효성 검증 API는 Java Enterprise Edition (Java EE ) 의 기술 간에 완벽한 유효성 검증을 제공합니다. 및 JSE (Java Platform, Standard Edition ) 환경에서 사용할 수 있습니다. JPA외에도 이러한 기술에는 JavaServer Faces) 및 JCA ( Java EE Connector Architecture) 가 포함됩니다. Bean 유효성 검증 API 주제에서 Bean 유효성 검증에 대한 자세한 정보를 읽을 수 있습니다.

Bean 유효성 검증에는 세 개의 핵심 개념인 제한조건, 제한조건 위반 처리, 유효성 검증기가 있습니다. JPA와 같은 통합 환경에서 애플리케이션을 실행 중인 경우 유효성 검증기와 직접 인터페이스할 필요가 없습니다.

유효성 검증 제한조건은 JavaBeans 컴포넌트의 클래스, 필드 또는 메소드에 추가되는 어노테이션 또는 XML 코드입니다. 제한조건은 빌드되거나 사용자 정의될 수 있습니다. 이는 정규 제한조건 정의 정의 및 제한조건 작성에 사용됩니다. 기본 제공 제한조건은 Bean 유효성 검증 스펙에 의해 정의되고 모든 유효성 검증 제공자에 사용 가능합니다. 기본 제공 제한조건의 목록은 'Bean 유효성 검증 기본 제공 제한조건' 주제를 참조하십시오. 기본 제공 제한조건과 다른 제한조건이 필요하면, 사용자 자신의 사용자 정의 제한조건을 빌드할 수 있습니다.

제한조건 및 JPA

다음의 사용법 시나리오는 기본 제공 제한조건이 샘플 디지털 이미지 갤러리 애플리케이션의 JPA 아키텍처에서 어떻게 사용되는지 설명합니다.

첫 번째 코드 예제에서는 image라는 JPA 모델의 단순 엔티티에 기본 제공 제한조건을 추가합니다. 이미지에는 ID, 이미지 유형, 파일 이름, 이미지 데이터가 포함됩니다. 이미지 유형은 지정해야 하며 이미지 파일 이름에는 올바른 JPEG 또는 GIF 확장자가 포함되어야 합니다. 이 코드에서는 일부 기본 제공 Bean 유효성 검증 제한조건이 적용된 어노테이션이 있는 이미지 엔티티를 보여줍니다.
package org.apache.openjpa.example.gallery.model;

import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

@Entity
public class Image {

    private long id;
    private ImageType type;
    private String fileName;
    private byte[] data;

    @Id
    @GeneratedValue
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    @NotNull(message="Image type must be specified.")
    @Enumerated(EnumType.STRING)
    public ImageType getType() {
        return type;
    }

    public void setType(ImageType type) {
        this.type = type;
    }

    @Pattern(regexp = ".*\\.jpg|.*\\.jpeg|.*\\.gif",
        message="Only images of type JPEG or GIF are supported.")
    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }
}

Image 클래스에서 두 개의 기본 제공 제한조건(@NotNull 및 @Pattern)을 사용합니다. @NotNull 제한조건에서는 ImageType 요소를 지정하고, @Pattern 제한조건에서는 지원되는 이미지 형식이 이미지 파일 이름의 접미부로 지정될 수 있도록 정규식 패턴 일치를 사용합니다. 각 제한조건에는 실행 시 이미지 엔티티의 유효성을 검증할 때 시작되는 각각의 유효성 검증 로직이 있습니다. 제한조건이 충족되지 않으면 JPA 제공자는 정의된 메시지로 ConstraintViolationException을 발생시킵니다. JSR-303 스펙은 또한 메시지 속성에서의 변수 사용에 대해 프로비저닝하도록 합니다. 변수는 자원 번들에서 키 입력 메시지를 참조합니다. 자원 번들에서는 환경별 메시지와 다국어 지원, 번역 및 다문화 메시지 지원을 제공합니다.

자체적으로 사용자 정의 유효성 검증기 및 제한조건을 작성할 수 있습니다. 이전 예제에서는 이미지 엔티티는 이미지의 파일 이름을 유효성 검증하기 위해 @Pattern 제한조건을 사용했습니다. 그러나, 실제 이미지 데이터 자체에 대해 제한조건을 확인하지 않았습니다. 패턴 기반 제한조건을 사용할 수 있지만, 이 경우 데이터의 검사 제한조건용으로만 제한조건을 작성한 경우와 같은 유연성은 없습니다. 이 경우 사용자 정의 메소드 레벨 제한조건 어노테이션을 빌드할 수 있습니다. 다음은 ImageContent라는 맞춤형 또는 사용자 정의 제한조건입니다.
package org.apache.openjpa.example.gallery.constraint;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import javax.validation.Constraint;
import javax.validation.Payload;

import org.apache.openjpa.example.gallery.model.ImageType;

@Documented
@Constraint(validatedBy = ImageContentValidator.class)
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface ImageContent {
    	String message() default "Image data is not a supported format.";
    	Class<?>[] groups() default {};
		Class<? extends Payload>[] payload() default {};
		ImageType[] value() default { ImageType.GIF, ImageType.JPEG };
}
Next, you must create the validator class, ImageContentValidator. The logic within this validator gets implemented by the validation provider when the constraint is validated. The validator class is bound to the constraint annotation through the validatedBy attribute on the @Constraint annotation as shown in the following code:
package org.apache.openjpa.example.gallery.constraint;
import java.util.Arrays;
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.openjpa.example.gallery.model.ImageType;
/**
 * Simple check that file format is of a supported type
 */
public class ImageContentValidator implements ConstraintValidator<ImageContent, byte[]> {
    private List<ImageType> allowedTypes = null;
    /**      
     * Configure the constraint validator based on the image      
     * types it should support.      
     * @param constraint the constraint definition      
     */     
    public void initialize(ImageContent constraint) {         
        allowedTypes = Arrays.asList(constraint.value());     
    }      
    /**      
     *Validate a specified value.      
     */     
    public boolean isValid(byte[] value, ConstraintValidatorContext context) {
        if (value == null) {             
            return false;
        }
        // Verify the GIF header is either GIF87 or GIF89
        if (allowedTypes.contains(ImageType.GIF)) {
            String gifHeader = new String(value, 0, 6);
            if (value.length >= 6 &&
                (gifHeader.equalsIgnoreCase("GIF87a") ||
                 gifHeader.equalsIgnoreCase("GIF89a"))) {
                return true;
            }
        }
        // Verify the JPEG begins with SOI and ends with EOI
        if (allowedTypes.contains(ImageType.JPEG)) {
            if (value.length >= 4 &&
                value[0] == 0xff && value[1] == 0xd8 &&
                value[value.length - 2] == 0xff && value[value.length -1] == 0xd9) {
                return true;
            }
        }
        // Unknown file format
        return false;     
    }
}
이미지 클래스의 getData() 메소드에 이 새 제한조건을 적용하십시오. 예를 들면 다음과 같습니다.
@ImageContent
    public byte[] getData() {
        return data;
    }
데이터 속성의 유효성 검증을 수행할 때 ImageContentValidator의 isValid() 메소드가 시작됩니다. 이 메소드에는 2진 이미지 데이터 형식에 대해 간단한 유효성 검증을 수행하는 로직이 포함되어 있습니다. ImageContentValidator에서 잠재적으로 간과된 기능은 특정 이미지 유형에 대해서도 유효성 검증을 수행할 수 있다는 것입니다. 정의에 따라, JPEG 또는 GIF 형식을 승인하지만 특정 형식에 대해 유효성을 검증할 수도 있습니다. 예를 들어, 어노테이션을 다음 코드 예제로 변경하면 올바른 JPEG 컨텐츠가 있는 이미지 데이터만 허용하도록 유효성 검증기에 지시됩니다.
@ImageContent(ImageType.JPEG)
    public byte[] getData() {
        return data;
    }
유형 레벨 제한조건 역시 고려사항입니다. 엔티티의 속성에 대한 조합을 유효성 검증해야 할 수도 있기 때문입니다. 이전 예에서는 개별 속성에서 유효성 검증 제한조건이 사용되었습니다. 유형 레벨 제한조건을 사용하여 공통 유효성 검증을 제공할 수 있습니다. 예를 들어, 이미지 엔티티에 적용된 제한조건은 이미지 유형이 설정되었으며(널이 아님) 이미지 파일 이름의 확장자가 지원되는 유형이고 데이터 형식이 표시된 유형에 올바른지 유효성을 검증합니다. 그러나, 예를 들어 img0.gif 파일이 GIF 유형이고 데이터 형식이 올바른 GIF 파일 이미지인지 공통적으로 검증하지 않습니다. 유형 레벨 제한조건에 대한 자세한 정보는 OpenJPA Bean Validation Primer 문서 및 "유형 레벨 제한조건" 절을 참조하십시오.

유효성 검증 그룹

Bean 유효성 검증은 유효성 검증 그룹을 사용하여 유효성 검증 유형과 유효성 검증 발생 시기를 판별합니다.

유효성 검증 그룹을 작성하기 위해 적용해야 하는 어노테이션이나 구현할 특수 인터페이스가 없습니다. 유효성 검증 그룹은 클래스 정의로 표시됩니다.
우수 사례: 그룹을 사용할 때 단순 인터페이스를 사용하십시오. 단순 인터페이스를 사용하면 유효성 검증 그룹이 여러 환경에서 더 사용 가능하게 됩니다. 반면, 클래스 또는 엔티티 정의가 유효성 검증 그룹으로 사용되면, 애플리케이션에 대해 맞지 않는 도메인 클래스 및 논리를 가져와서 다른 애플리케이션의 오브젝트 모델을 오염시킬 수 있습니다. 개별 제한조건에 유효성 검증 그룹 또는 여러 그룹이 지정되지 않은 경우 기본적으로 javax.validation.groups.Default 그룹을 사용하여 유효성을 검증합니다. 사용자 정의 그룹을 작성하려면 새 인터페이스 정의를 작성하기만 하면 됩니다.

OpenJPA를 포함하는 유효성 검증 그룹에 대한 자세한 정보는 OpenJPA Bean Validation Primer 문서 및 "유효성 검증 그룹" 절을 참조하십시오.

JPA 도메인 모델

이미지 엔티티 외에 앨범, 작성자 및 위치 지속적 유형이 있습니다. 앨범 엔티티에는 이미지 엔티티의 콜렉션에 대한 참조가 있습니다. 작성자 엔티티에는 이미지 작성자가 기여하고 이미지 엔티티에 대한 참조가 작성된 앨범 엔티티에 대한 참조가 포함됩니다. 따라서 도메인의 각 엔티티 사이를 이동하는 전체 기능이 제공됩니다. 이미지에 위치 정보를 저장하는 기능을 지원하도록 이미지에 임베드 가능한 위치도 추가되었습니다.

Album과 Creator 엔티티에는 표준 기본 제공 제한조건이 있습니다. 임베드 가능한 위치는 @Valid 어노테이션을 사용하여 임베드된 오브젝트의 유효성 검증을 설명한다는 점에서 더욱 고유합니다. 위치를 이미지로 임베드하기 위해, 새 필드 및 해당되는 지속적 특성이 이미지 클래스에 추가됩니다. 예를 들면 다음과 같습니다.
private Location location;

    @Valid
    @Embedded
    public Location getLocation() {
        return location;
    }

    public void setLocation(Location location) {
        this.location = location;
    }
@Valid 어노테이션에서는 JPA 환경에서 임베드 가능한 오브젝트의 연쇄적인 유효성 검증을 제공합니다. 따라서, 이미지의 유효성을 검증할 때 이미지가 참조하는 위치에 대한 제한조건 역시 유효성이 검증됩니다. @Valid가 지정되지 않으면, 위치는 유효성이 검증되지 않습니다. JPA 환경에서 @Valid를 통한 연쇄적 유효성 검증은 임베드 가능한 오브젝트에만 사용할 수 있습니다. 참조된 엔티티와 엔티티의 콜렉션은 순환 유효성 검증을 금지하기 위해 별도로 유효성이 검증됩니다.

Bean 유효성 검증 및 JPA 환경

JPA 스펙은 Bean 유효성 검증 API와의 통합을 단순하게 해줍니다. JSE 환경에서, Bean 유효성 검증은 런타임 클래스 경로에서 Bean 유효성 검증 API와 Bean 유효성 검증 제공자를 제공할 때 기본적으로 사용 가능하게 됩니다. Java EE 환경에서 애플리케이션 서버에는 Bean 유효성 검증 제공자가 포함되므로 애플리케이션과 번들로 제공할 필요가 없습니다. 두 환경 모두에서 버전 2.0 이상의 persistence.xml 파일을 사용해야 합니다.

버전 1.0 persistence.xml은 Bean 유효성 검증을 구성하기 위한 수단을 제공하지 않습니다. 버전 2.0 이상의 persistence.xml 사용을 통해 순수한 JPA 1.0 애플리케이션이 유효성 검증 시작 및 런타임 비용을 발생시키는 것을 방지합니다. 1.0 기반 애플리케이션이 유효성 검증을 사용하지 않도록 설정하기 위한 표준 수단이 없는 경우 이러한 사항은 중요합니다. Java EE 환경에서 persistence.xml 파일의 루트 요소를 수정하여 기존 1.0 애플리케이션에서 유효성 검증을 사용으로 설정하십시오. 다음 예제는 persistence.xml 파일을 나타냅니다.
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence        
				http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0" >
...
</persistence>
Bean 유효성 검증에서는 JAP 환경의 세 가지 조작 모드를 제공합니다.
  • 자동

    클래스 경로에서 유효성 검증 제공자를 사용할 수 있으면 Bean 유효성 검증을 사용합니다. Auto가 기본값입니다.

  • 콜백

    콜백 모드가 지정될 때, Bean 유효성 검증 제공자는 JPA 제공자에서 사용 가능해야 합니다. 그렇지 않다면, JPA 제공자는 새 JPA 엔티티 관리자 팩토리의 인스턴스화에 예외를 처리합니다.

  • 없음

    특정 지속성 단위에 대해 Bean 유효성 검증을 사용하지 않습니다.

자동 모드를 사용하면 배치가 간단해 지지만, 구성 문제점으로 인해 유효성 검증을 수행할 수 없는 경우 문제가 발생할 수 있습니다.
우수 사례: 일관된 동작을 위해 명시적으로 없음 또는 콜백 모드를 사용하십시오.
또한, none을 지정하는 경우 JPA는 시작 시 최적화하며 예기치 않은 유효성 검증을 수행하려고 하지 않습니다. 유효성 검증을 명시적으로 사용 안함으로 설정하는 것은 컨테이너가 유효성 검증 제공자를 제공해야 하는 Java EE 환경에서 특히 중요합니다. 따라서, 지정되는 경우를 제외하고 컨테이너에서 시작되는 JPA 2.0 이상 애플리케이션은 유효성 검증이 사용됩니다. 이 프로세스에서는 라이프사이클 이벤트 중에 처리를 추가합니다.
JPA에서는 두 가지 방식으로 유효성 검증 모드를 구성합니다. 가장 간단한 방법은 다음 예에 표시된 대로 원하는 유효성 검증 모드를 사용하여 persistence.xml에 유효성 검증-모드 요소를 추가하는 것입니다.
 <persistence-unit name="auto-validation">
		        	... 
				<!-- Validation modes: AUTO, CALLBACK, NONE -->
				<validation-mode>AUTO</validation-mode>
				...
		</persistence-unit> 
다른 방법으로는 다음 예에 표시된 대로 새 JPA 엔티티 관리자 팩토리를 작성할 때 javax.persistence.validation.mode 특성에 자동, 콜백 또는 없음 값을 지정하여 프로그래밍 방식으로 유효성 검증 모드를 구성할 수 있습니다.
Map<String, String> props = new HashMap<String, String>();
		props.put("javax.persistence.validation.mode", "callback");
        EntityManagerFactory emf = 
            Persistence.createEntityManagerFactory("validation", props);
JPA 내의 Bean 유효성 검증은 JPA 라이프사이클 이벤트 처리 동안 발생합니다. 사용 가능한 경우, 유효성 검증은 PrePersist, PreUpdate 및 PreRemove 라이프사이클 이벤트의 최종 단계에서 발생합니다. 해당 이벤트의 일부는 유효성 검증 중인 엔티티를 수정할 수 있으므로, 유효성 검증은 모든 사용자 정의 라이프사이클 이벤트 후에만 발생합니다. 기본적으로 JPA에서는 PrePersist 및 PreUpdate 라이프사이클 이벤트의 기본 유효성 검증 그룹에 대해서만 유효성 검증을 사용합니다. 다른 유효성 검증 그룹의 유효성을 검증하거나 PreRemove 이벤트에 대해 유효성 검증을 사용하도록 설정하려면, 다음 예제에 표시된 것처럼 persistence.xml에서 각 라이프사이클 이벤트의 유효성을 검증하도록 유효성 검증 그룹을 지정할 수 있습니다.
<persistence-unit name="non-default-validation-groups">
		<class>my.Entity</class>
		<validation-mode>CALLBACK</validation-mode>
		<properties>
			<property name="javax.persistence.validation.group.pre-persist"
       value="org.apache.openjpa.example.gallery.constraint.SequencedImageGroup"/>
			<property name="javax.persistence.validation.group.pre-update"
       value="org.apache.openjpa.example.gallery.constraint.SequencedImageGroup"/>
			<property name="javax.persistence.validation.group.pre-remove"
       value="javax.validation.groups.Default"/>
		</property>
	</persistence-unit>
다음 예에서는 여러 JPA 라이프사이클 단계(지속, 업데이트 및 제거 등)를 보여줍니다.
EntityManagerFactory emf = 
            Persistence.createEntityManagerFactory("BeanValidation");
        EntityManager em = emf.createEntityManager();

        Location loc = new Location();
        loc.setCity("Rochester");
        loc.setState("MN");
        loc.setZipCode("55901");
        loc.setCountry("USA");

        // Create an Image with non-matching type and file extension
        Image img = new Image();
        img.setType(ImageType.JPEG);
        img.setFileName("Winter_01.gif");
        loadImage(img);
        img.setLocation(loc);
        
        // *** PERSIST ***
        try {
            em.getTransaction().begin();
            // Persist the entity with non-matching extension and type
            em.persist(img);
        } catch (ConstraintViolationException cve) {
            // Transaction was marked for rollback, roll it back and
            // start a new one
            em.getTransaction().rollback();
            em.getTransaction().begin();
            // Fix the file type and re-try the persist.
            img.setType(ImageType.GIF);
            em.persist(img);
            em.getTransaction().commit();
        }

        // *** UPDATE ***
        try {
            em.getTransaction().begin();
            // Modify the file name to a non-matching file name 
            // and commit to trigger an update
            img.setFileName("Winter_01.jpg");
            em.getTransaction().commit();
        }  catch (ConstraintViolationException cve) {
            // Handle the exception.  The commit failed so the transaction
            // was already rolled back.
            handleConstraintViolation(cve);
        }
        // The update failure caused img to be detached. It must be merged back 
        // into the persistence context.
        img = em.merge(img);

        // *** REMOVE ***
        em.getTransaction().begin();
        try {
            // Remove the type and commit to trigger removal
            img.setType(ImageType.GIF);
            em.remove(img);
        }  catch (ConstraintViolationException cve) {
            // Rollback the active transaction and handle the exception
            em.getTransaction().rollback();
            handleConstraintViolation(cve);
        }
        em.close();
        emf.close();

Exceptions

유효성 검증 오류는 JPA 라이프사이클의 일부에서 발생할 수 있습니다.

라이프사이클 이벤트 중에 하나 이상의 제한조건이 유효성 검증에 실패하면 JPA 제공자에서 ConstraintViolationException이 발생합니다. JPA 제공자에서 발생한 ConstraintViolationException에는 발생한 ConstraintViolations 세트가 포함됩니다. 개별적인 제한조건 위반에는 메시지, 루트 Bean 또는 JPA 엔티티, JPA 임베드 가능한 오브젝트 유효성 검증 시 유용한 리프 Bean, 유효성 검증에 실패한 속성, 그리고 실패를 야기한 값을 비롯하여, 제한조건에 관한 정보가 포함됩니다. 다음은 샘플 예외 처리 루틴입니다.
private void handleConstraintViolation(ConstraintViolationException cve) {
      Set<ConstraintViolation<?>> cvs = cve.getConstraintViolations();
			for (ConstraintViolation<?> cv : cvs) {
					System.out.println("------------------------------------------------");
          	System.out.println("Violation: " + cv.getMessage());
          	System.out.println("Entity: " + cv.getRootBeanClass().getSimpleName());
          	// The violation occurred on a leaf bean (embeddable)
          	if (cv.getLeafBean() != null && cv.getRootBean() != cv.getLeafBean()) {
              System.out.println("Embeddable: " + 
cv.getLeafBean().getClass().getSimpleName());
          }
          System.out.println("Attribute: " + cv.getPropertyPath());
          System.out.println("Invalid value: " + cv.getInvalidValue());
      }
    }
제한조건 위반 처리는 속성 레벨 제한조건을 사용할 때 일반적으로 단순합니다. 유형-레벨 제한조건과 함께 유형-레벨 유효성 검증기를 사용하는 경우, 유효성 검증에 실패한 속성 또는 속성 조합을 결정하기가 훨씬 어려워질 수 있습니다. 또한 전체 오브젝트가 개별 속성이 아니라 유효하지 않은 값으로 리턴됩니다. 구체적인 실패 정보가 필요한 경우 속성-레벨 제한조건을 사용하십시오. 그렇지 않으면 Bean 유효성 검증 스펙에 설명된 대로 사용자 정의 제한조건 위반이 제공될 수 있습니다.

샘플

이 주제에서 제공하는 JPA 모델 및 이미지 갤러리 애플리케이션 사용 시나리오는 OpenJPA Bean Validation Primer 문서에서 제공하는 샘플을 통해 구현할 수 있습니다.