'java'에 해당되는 글 148건

web/Spring

Custom Validation 만들어서 추가하기

Spring에서 @NotBlank, @Email등 여러 템플릿에 맞게 Validation을 넣을 수 있다.

하지만 추가적으로 패스워드 규칙과 같이 별도 체크할 validator가 필요할 때 만들어서 사용해야 하는데 만들어서 지정해보는 작업을 해보자.


1. Controller

요청을 받을 DTO앞에 @Valid 어노테이션을 추가해야한다.

1
2
3
4
5
6
7
8
9
10
11
/**
 * 회원가입
 *
 * @param reqDto
 * @return
 * @throws Exception
 */
@RequestMapping("/join")
public ResponseEntity<?> join(@Valid UserDto reqDto) throws Exception {
    return ResponseEntity.ok(userService.insertUser(reqDto));
}
cs


2. Annotation 추가

Validation 사용을 위해서 필드에 @NotNull, @NotBlank와 같이 어노테이션을 붙혀줘야한다. 그래서 Custom Validation을 만들고 필드에 붙히기 위해서 어노테이션을 만들어줘야한다. 기본적으로 표현되는 메시지는 설정을 진행한 messageSource에서 가져오는데 가져오지 못하면 default로 설정한 메시지를 출력하게 할 수있다. 그리고 기존에 어노테이션 만들던 방법과 조금 다른 부분이 있는데 바로 @Constraint 필드이다. 여기서 지정하는 설정은 어떤 검증 클래스를 사용해서 필드의 값을 검증할 건지 지정해준다. 이 방법을 위해서는 ConstraintValidator 인터페이스를 구현한 클래스를 지정해줘야한다. 아래에서 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.wedul.common.annotation;
 
import com.wedul.common.validation.PasswordValidator;
 
import javax.validation.Constraint;
import java.lang.annotation.*;
 
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {PasswordValidator.class})
public @interface PasswordCheck {
 
  String message() default "";
Class<?>[] groups() default {};
 Class<? extends Payload>[] payload() default {};
cs
}
 
cs


3. Validator 추가

ConstraintValidator 인터페이스를 구현해주면서 지정해주는 제네릭 값 첫 번째에는 2번에서 만든 애노테이션 객체가 들어가고 두 번째 값에는 이 어노테이션 값이 붙어서 Constraint 작업을 진행할 필드의 데이터 유형을 넣는다. (패스워드라면 String) 만약 특정하기 어려운 어노테이션인경우 Object를 붙여서 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.wedul.common.validation;
 
import com.wedul.common.annotation.PasswordCheck;
import org.apache.commons.lang.StringUtils;
 
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
 
/**
 * 패스워드 validation
 *
 * @author wedul
 * @since 2018-12-23
 **/
public class PasswordValidator implements ConstraintValidator<PasswordCheck, String> {
 
  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    // 6자리이상 대문자 포함
    return StringUtils.isNotBlank(value) && value.length() >= 6 && value.chars().boxed().filter(data -> Character.isUpperCase(data)).findAny().isPresent();
  }
}
 
cs


4. DTO 적용

적용하고자하는 곳에 추가한 필드에 넣어보자!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.wedul.wedulpos.user.dto;
 
import com.wedul.common.annotation.PasswordCheck;
import com.wedul.common.dto.CommonDto;
import com.wedul.common.util.HashUtil;
import lombok.*;
import org.apache.ibatis.type.Alias;
 
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
 
/**
 * User정보 Dto 
 * 
 * @author wedul
 * @date 2017. 11. 4.
 * @name UserDto
 */
@Alias("UserDto")
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper=false)
@Entity
@Table(name = "user")
public class UserDto extends CommonDto implements Serializable {
 
    @Id
    @GeneratedValue
    private int userId;
 
    @Column(nullable = false)
    private String nickname;
 
    @Column(nullable = false)
    @NotBlank(message = "user.login.message.mustemail")
    @Email(message = "user.login.message.validation_email")
    private String email;
 
    @Column(nullable = true)
    @PasswordCheck(message = "user.join.message.password")
    private String password = "";
 
    @Column(nullable = true)
    private String snsId;
 
    @Column(nullable = false)
    private boolean isAdmin = false;
    
    public UserDto(String email) {
        this.email = email;
    }
    
    public UserDto(String email, String password) {
        this.email = email;
        this.password = password;
    }
    
    public UserDto(String email, String password, boolean isAdmin) {
        this.email = email;
        this.password = password;
        this.isAdmin = isAdmin;
    }
    
    public UserDto(String email, String password, String nickname, boolean isAdmin) {
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.isAdmin = isAdmin;
    }
    
    public String getEcPassword() {
        return HashUtil.sha256(this.password);
    }
 
}
 
cs


validation 오류 발생시 상황에 맞는 문구가 나올 수 있도록 별도의 설정을 해줘야 하는데 그 부분은 다음시간에 정리해서 올려보자.


우선 Custom Validation을 만든부분만 정리하자.


web/JPA

연관관계 매핑 (다대일 - 양방향)

바로 앞에서 다대일 관계에서 단반향으로써 학생이 반을 접근하는 방식으로 진행했으나 이번에는 반에서 학생들을 접근하는 방식을 사용해보자.


그렇게 되면 학생 -> 반에서 반 -> 학생이 추가되어 결국 반 <-> 학생 이런 양방향 연관관계가 형성된다.

 

하나의 반에는 여러 학생이 포함되어 있다. 그렇기 때문에 반 클래스에 List<Student> 객체를 추가한다.


1

2

  @OneToMany(mappedBy = "classes")

  private List<Student> students;

cs

 


@OneToMany(mappedBy = "classes")

- 일대다 매핑을 정보를 추가하고 학생쪽에서 사용되는 반 필드명을 mappedBy에 값으로 추가해준다.


조회

반에 포함되어 있는 학생들을 조회한다.

1

2

3

4

5

6

7

8

9

10

@Override

@Transactional

public void selectClasses() {

  Classes classes1_1 = entityManager.find(Classes.class"1-1");

  List<Student> students = classes1_1.getStudents();

 

  for (Student student : students) {

    print(student);

  }

}

Colored by Color Scripter

cs

 


연관관계 주인 지정


테이블은 외래키 하나로 테이블의 연관관계를 관리 있다. 예를 들면 이름이 외래키라고 했을 학생 테이블에서 외래키 이름을 추가할 수도 있고, 테이블에서 반 이름을 관리할 있다. 하지만 엔티티에서는 외래키를 관리(추가, 수정, 삭제) 있는 것은 개의 엔티티의 연관관계의 주인이 되는 엔티티만이 가능하다. 나머지 다른 엔티티는 조회만 가능하다.

 

예를 들어 저번 시간에 공부 했었던 Student 엔티티 클래스는 Classes 외래키의 주인으로써 외래키를 추가, 수정, 삭제 있다. Classes 엔티티 클래스는 외래키의 주인이 아니므로 조회만 가능하다.

 

@ManyToOne 설정이 있는 곳이 무조건 주인이다. 그리고 양방향 설정된 엔티티에서 조회가 가능하도록 하기 위해서 다른 엔티티에 @OneToMany(mappedby ="classes") 지정해 주면 주인 설정이 끝난다.

 

그럼 진짜 주인이 아닌 엔티티 Classes에서는 외래키 관리가 안되는지 확인해보자.

 

1

2

3

4

5

6

7

@Override

@Transactional

public void saveClasses() {

  Student wedul = entityManager.find(Student.class"1-1-01");

  Classes classes2_1 = new Classes("2-2""2학년2", Arrays.asList(wedul));

  entityManager.persist(classes2_1);

}

Colored by Color Scripter

cs

-> 처음 생각대로라면 id : "2-2", name : "2학년2" 반이 classes테이블에 추가되고 학생 테이블에 wedul 학생의 반이 2-2 같지만 그렇지 않다. 왜냐하면 Classes 엔티티는 주인이 아니기 때문이다. 그래서 테이블에 값만 추가된다.



주의사항

 만약 단방향 그러니까 Student 엔티티에만 @ManyToOne 해줄 경우 Classes 엔티티를 통해 반에 등록된 학생을 조회 하려 값을 받게 된다. 왜냐하면 연관관계가 맺어지지 않았기 때문이다. 그래서 무조건 이럴 경우 양방향 연관관계를 맺어 주는것이 좋다.(@OneToMany)

 

아래 코드를 보면 wedul학생에 classes2_1 반을 추가해줬지만 classes2_1에서 학생을 조회하면 wedul 학생이 없다.

1

2

3

4

5

6

Student wedul = entityManager.find(Student.class, "1-1-01");

Classes classes2_1 = new Classes("2-2", "2학년2", Collections.emptyList());

wedul.setClasses(classes2_1);

    

// 반영 되어 있지 않아서 wedul 출력되지 않음

classes2_1.getStudents();

Colored by Color Scripter


그래서 이런 문제로 버그가 발생할 있기 때문에 좋은 방법으로 setClasses() 메소드를 다음과 같이 변경해주면 좋다.

1

2

3

4

public void setClasses(Classes classes) {

  this.classes = classes;

  classes.getStudents().add(this);

}

Colored by Color Scripter

그렇지만 이렇게만 해주고 나면 또다른 버그가 발생할 있다. 다음과 같은 상황을 가정해보자.

1

2

3

4

5

6

7

8

9

Student wedul = entityManager.find(Student.class, "1-1-01");

Classes classes2_1 = new Classes("2-2", "2학년2", Collections.emptyList());

wedul.setClasses(classes2_1);

 

classes2_1.getStudents();

 

// 학생의 반을 다른 반으로 변경할 경우 기존의 반에 들어있는 getStudents List안에서 학생을 지워줘야한다.

Classes classes3_1 = new Classes("3-1", "3학년1", Collections.emptyList());

wedul.setClasses(classes3_1);

Colored by Color Scripter

상황에서는 위에 변경해주었던 방식대로 진행하면 기존에 반이었던 classes2_1에도 wedul이 있고 classes3_1에도 wedul 있는 문제가 발생한다. 그래서 다음과 같이 바꿔주면 해결된다. 

1

2

3

4

5

6

7

8

  public void setClasses(Classes classes) {

    // 먼저 지워준다.

    classes.getStudents().remove(this);

    

    // 그리고 반을 바꾸고 학생추가

    this.classes = classes;

    classes.getStudents().add(this);

  }

Colored by Color Scripter


단방향 매핑만으로도 테이블과 객체의 연관관계 매핑이 되었지만 양방향 매핑을 통해서 더욱 편리하게 객체의 탐색이 가능하게 있다. 하지만 위에 보았듯이 양방향 매핑에서는 주의해서 관리 해줘야 포인트가 많다.


web/JPA

JPA 매핑 어노테이션 - DDL

JPA 매핑에 사용되는 어노테이션은 크게 유형에 따라 4가지로 나누어진다.

유형
어노테이션
객체와 테이블 매핑
@Entity, @Table
기본 키 매핑
@Id
필드와 컬럼 매핑
@Column
연관관계 매핑
@ManyToOne, @JoinColumn


@Entity

JPA를 사용해서 테이블과 매핑할 클래스에는 무조건 붙혀야하는 어노테이션이다. 속성으로 name을 지정할 수 있다. 안할 시 기본 클래스 이름으로 한다.

1
@Entity(name = "Member")
cs


- 기본 생성자가 필수로 있어야 한다. 
- final 클래스와 private 생성자는 할 수 없다.


@Table

엔티티와 매핑할 테이블을 지정한다. 생략하면 매핑하는 엔티티이름으로 대신한다.
-name, catalog, schema, uniqueConstraints(DDL 선언시 유니크 제약조건) 등을 같이 설정하여 만들 수 있다.

이렇게 설정된 클래스를 가지고 자동으로 테이블을 생성할 수 있다.

1
spring.jpa.hibernate.ddl-auto=update
cs

설정하는 값에 따라서 테이블 생성이 달라지는데 create는 기존 테이블을 삭제하고 새로 생성하고, create-drop는 애플리케이션이 종료될때 제거되고 update는 변경된 사항만 수정하고 validate는 매핑정보가 달라졌을때 경고를 주면서 애플리케이션 실행을 하지 않고 none는 아무런 동작을 하지 않는다.


DDL 생성 시 설정

@Colummn 매핑에는 nullable 속성과 length등의 속성을 지정할 수 있다

1
2
3
4
@Column(name = "NAME", length = 10, nullable = false)
private String userName;
 
// NAME varchar(10) not null 과 매핑
cs


@Table 어노테이션은 UniqueConstraint를 지정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * springboottest
 *
 * @author wedul
 * @since 03/10/2018
 **/
@Entity(name = "Member")
@Data
@Table(name = "MEMBER", uniqueConstraints = {@UniqueConstraint(
        name = "NAME_AGE_UNIQUE", columnNames = {"NAME""AGE"}
)})
public class Member {
 
  @Id
  @Column(name = "ID")
  private String id;
 
  @Column(name = "NAME", length = 10, nullable = true)
  private String userName;
 
  // 매핑 정보가 없는 필드
  private int age;
 
}
 
// ALTER TABLE MEMBER ADD CONSTRAINT NAME_AGE_UNIQUE UNIQUE(NAME, AGE); 로 매핑
cs


기본키는 @Id를 사용하여 지정할 수 있고 AUTO_INCREMENT와 같은 기능을 @GeneratedValue를 사용하여 지정할 수 있다.

1
2
3
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long orderId;
cs


추가적인 속성으로 IDENTITY(기본키 생성을 데이터베이스가 함), SEQUENCE(데이터베이스 시퀀스를 사용해서 기본 키 할당), TABLE (키 생성 테이블 생성)이 있다. 각 속성이 모든 데이터베이스 업체에 적용되는건 아니고 적용되는 벤더만 사용된다. 만약 이 모든 설정을 하는게 번거롭다면 그냥 @GeneratedValue 어노테이션만 사용하면 된다. 왜냐하면 기본 설정이 GenerationType.AUTO인데 이는 벤더에 맞게 전략이 선택되어 사용된다.


레퍼런스 : 자바 ORM 표준 JPA 프로그래밍

web/JPA

JPA 기본 어노테이션 설명

JPA에서 사용되는 기본적인 어노테이션 몇개를 정리해보자.


@Entity

- 클래스와 테이블과 매핑한다고 JPA에게 알려준다. 이렇게 @Entity가 사용될 클래스를 엔티티 클래스라고 한다.


@Table

- 엔티티 클래스에 매핑할 테이블 정보를 알려준다. (이 어노테이션을 생략하면 클래스 이름을 테이블정보로 매핑한다.)


@Id

- 엔티티 클래스의 필드를 테이블에 기본키로 매핑한다. (데이터베이스는 엔티티를 구별할때 이 키값으로 구분한다.)


@Column

- 필드를 컬럼에 매핑한다.


매핑 정보가 없는 필드

- @Column을 생략하면 필드명을 사용해서 컬럼명과 매핑하게 된다. 만약 대소문자를 데이터베이스가 구분할 경우에는 꼭 위에 @Column어노테이션을 사용해서 진행해야한다.


#Dialect(방언)

- 데이터베이스마다 서로 다른 특징(Mysql의 limit, Oracle의 Rownum....)들을 JPA에서 방언(Dialect)라고 한다. 이렇게 데이터베이스 벤더사마다 서로 다른 기능을 사용해서 JPA를 사용하면 나중에 데이터베이스를 바꿀때 문제가 발생하기 때문에 문제가 없는 방언 클래스를 사용해야한다. 


위의 어노테이션이 적용된 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.wedul.springboottest.member.dto;
 
import lombok.Data;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
 
/**
 * springboottest
 *
 * @author wedul
 * @since 03/10/2018
 **/
@Entity
@Data
@Table(name = "MEMBER")
public class Member {
 
  @Id
  @Column(name = "ID")
  private String id;
 
  @Column(name = "NAME")
  private String userName;
 
  // 매핑 정보가 없는 필드
  private int age;
 
}
 
cs


web/JPA

JPA(Java Persistence Api)의 장점과 단점 및 사용해야 하는 이유.

국내에서는 Mybatis를 사용하지만 대기업과 여러 스타트업에서는 JPA를 사용하고 있는 추세이다. 그리고 세계적으로 JPA 특히 ORM 프레임워크인 하이버네이트 사용률이 더 높다.

그래서 더 자세한 부분을 이해하기 위해서 공부를 시작했다. 공부 시작하기전에 왜 JPA가 좋은지 정리해봤다.


생산성

- 개발자가 일일히 CRUD용 쿼리를 작성해줘야하던 Mybatis와 같은 Mapper방식은 컬럼이 추가되거나하면 수정해주어야하는 부분이 상당히 많았다. 이로 인해서 자바를 사용하지만 객체중심 개발이 아니라 데이터베이스 흐름으로 개발을 하게되는 문제가 있다. JPA를 사용하게 되면 쿼리를 직적 생성하는 것이 아니고 만들어진 객체로 데이터베이스를 다루기 때문에 객체 중심으로 개발을 진행할 수 있다. 


유지보수

- SQL을 직접적으로 작성하지 않고 엔티티 필드가 되는 객체를 다뤄서 데이터베이스를 동작시키기 때문에 유지보수가 더욱 간결하다. 왜냐하면 쿼리가 수정되면 그에 따라서 그를 담을 DTO 필드도 모두 변경이 되야 하지만 JPA를 사용하게 되면 단순히 엔티티 클래스 정보만 변경하면 쉽게 관리가 가능하기 때문이다.


성능

- 일반적인 Spring의 encache 기능처럼 동일한 쿼리에 대한 캐시 기능을 사용하기 때문에 더욱 높은 성능적 효율성을 경험할 수있다. 


RDBMS 종류와 무관한 코딩

- 객체 중심으로 동작하기 때문에 Oracle, Mysql, Mssql과 같이 서로 다른 벤더사 데이터베이스를 사용하려고 할 때 문법을 바꿔줘야하는 수고를 줄일 수 있다. 


제약사항 및 단점

- JPA는 통계처리와 같이 복잡한 쿼리보다는 실시간 처리용 쿼리에 더 최적화되어 있다. 물론 JPA에서 제공하는 Native query기능을 사용할 수 있지만 통계처럼 복잡하고 미세하게 쿼리 작업이 필요하다면 Mybatis와 같은 Mapper 방식을 사용하는 것이 더 효율적일 수 있다. Spring에서 JPA와 Mybatis를 혼용해서 사용할 수 있기 때문에 필요에 따라 적절한 방식으로 선택해가면서 사용하면 될 것 같다.



  1. 이사작전 편집답글

    좋은 포스팅 감사합니다.

푸터바

알림

이 블로그는 구글에서 제공한 크롬에 최적화 되어있고, 네이버에서 제공한 나눔글꼴이 적용되어 있습니다.

카운터

  • Today : 36
  • Yesterday : 651
  • Total : 55,511