1 ~ 10000까지의 숫자중에 셀프 넘버가 아닌 데이터를 noSelfNumber에 집어넣고 loop를 순회하면서 selfNumber 여부를 체크하면 된다. 간단한 문제이다.

https://www.acmicpc.net/problem/4673

 

4673번: 셀프 넘버

문제 셀프 넘버는 1949년 인도 수학자 D.R. Kaprekar가 이름 붙였다. 양의 정수 n에 대해서 d(n)을 n과 n의 각 자리수를 더하는 함수라고 정의하자. 예를 들어, d(75) = 75+7+5 = 87이다. 양의 정수 n이 주어졌을 때, 이 수를 시작해서 n, d(n), d(d(n)), d(d(d(n))), ...과 같은 무한 수열을 만들 수 있다.  예를 들어, 33으로 시작한다면 다음 수는 33 + 3 + 3 = 39이고, 그 다음 수는

www.acmicpc.net

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        getNonSelfNumber();
    }

    public static void getNonSelfNumber() {
        List<Integer> nonSelfNumber = new ArrayList<>();

        for (int i = 1; i < 10000; i++) {
            if (!nonSelfNumber.contains(i)) {
                System.out.println(i);
            }

            nonSelfNumber.add(func(i));
        }

    }

    public static int func(int num) {
        int result = num;

        while (num != 0) {
            result += num % 10;
            num /= 10;
        }

        return result;
    }

}

애그리거트는 관련된 객체를 하나의 군으로 묶어주는 것 으로 상위수준에서 모델을 조망하는 방법 중 하나이다.

애그리거트는 비슷한 속성을 가진 객체를 묶어놓은 것을 의미한다.
예를 들어 주문 시스템에 주문 관련 애그리거트는 Order, Receiver, OrderLine.. 등이 있고 회원정보에는 Member, MemberInfo등으로 나눌 수 있다. 각 애그리거트에 연관된 객체를 담고 있으며 유사하고 동일한 라이프 사이클을 보유하고 있다.

애거리거트 루트


애그리거트에서 가장 핵심이 되는 주체 즉, 애그리거트 전체를 관리하고 책임지는 주체를 애그리거트의 루트 엔티티라고 한다. 애그리거트내에 존재하는 모든 엔티티는 루트 엔티티와 직간접적으로 연결되어있다.

애거리거트 루트의 핵심 역할은 애거리거트의 일관성을 유지하는 것이다. 그렇기 때문에 모든 애거리거트의 주요 기능은 애거리거트 루트 엔티티에 구현되어야 한다. 다시말하면 애거리거트 루트가 아닌 다른 객체가 애그리거트에 속한 객체를 직접 변경하면 안된다. 

예를 들어 상품에 대한 애그리거트가 있고 루트 엔티티로 Product가 있을 때 상품에 대한 정보를 보유한 ProductInfo의 price를 할인율 반영없이 단순히 변경하면 모델에 일관성을 깨트릴 수 있다.

ProductInfo productInfo = product.getProductInfo();
productInfo.setPrice(price);

또한 이렇게 변경하면서 중간에 할인율을 검사하도록 할 수 있지만 매번 중복된 코드가 만들어 질 수 있다.

ProductInfo productInfo = product.getProductInfo();
price = priceCalculator(price);
productInfo.setPrice(price);

 

트랜잭션의 범위


트랜잭션의 크기는 작을 수록 좋다. 하나의 트랜잭션에서 두 개 이상의 애거리거트(주문, 사용자정보등등)를 수정하게 되면 충돌이 발생할 가능성이 크다.

예를 들어 주문을 하면서 입력한 배송지를 사용자의 기본 배송지로 설정하는 경우에는 주문정보와 사용자 정보를 동시에 수정하는 경우가 발생 할 수 있다.

이런 경우에는 Order라는 애거리거트 루트에서 값을 두개 모두 수정하지 말고 OrderService라는 곳에서 값을 수정하는것이 옳다. 간단하게 보면 다음과 같이 OrderService에서 수정하는 것이 옳다.

public class OrderService {
  public void order(ShipInfo ship, isNewShippingAddr) {
    if(isnewShippingAddr) custermer.setAddr(ship.getAddr());
  }
}

 

애거리거트 필드 참조


애거리거트에서 다른 애거리거트를 필드로써 참조할 수 있다. 아래 예처럼 학교라는 School 클래스에 Student라는 애거리거트를 필드로써 참조 할 수있다.

public class School {
    private Student student;
}

이럴경우 ORM을 통해서 데이터를 함께 가져올 수 있는데 이럴 경우 단점이 있다.

1. 편한 탐색 오용
- 편하게 필드로써 애그리거트에서 다른 애그리거트를 접근할 수 있으므로 다른 애그리거트내에서 다른 애그리거트의 값을 수정할 수 있는 문제를 야기할 수 있다.
2. 성능에대한 문제
- 이는 사용에 따라서 lazy와 eager 둘 중의 하나로 선택해서 진행할 수 있다.
3. 확장이 어려움
- 만약 확장해서 student를 다른 도메인으로 빼고 싶을경우에 연관성이 깊어지다보면 분리하기 어려워서 확장이 여려워진다.

이에 대한 해결방법으로 School 내부에 Student라는 애그리거트를 포함하지 말고 student의 id인 studentId만을 포함하고 있는 것이다. 이럴경우 School에서 Student를 수정할 문제도 방지할 수 있고 lazy, eager를 고민할 필요도 dbms 확장도 자유롭게 할 수 있다.

 

애거리거트에 연관을 ID로 했을 시 상황


만약 School 내부에 Student가 List형태로 있다고 가정했을 때, 모든 학생정보를 가지고 오기 위해서 학생들 정보를 하나씩 다 가져와야한다.

이는 결국 School과 내부에 있는 학생들 수인 N개를 합쳐서 N + 1번 조회하게 된다고 해서 N+1문제라고 한다. 이를 속도가 엄청 느려지기 때문에 해결하기 위해서 조인을 사용해야 한다. 이런문제는 JPA에서 쿼리를 직접적으로 실행할 수 있는 기능등을 사용해서 해결하거나 이부분만 mybatis를 사용하는 등의 방법으로 해결할 수 있다.

 

출처 : DDD Start 도메인 주도 설계 구현과 핵심 개념 익히기 (출판 지앤선, 저자 최범균)

'DDD' 카테고리의 다른 글

DDD. 응용서비스 코드 규칙과 트랜잭션 관리  (0) 2019.05.21
DDD. 애거리루트 정리  (0) 2019.05.16
DDD. DIP 의존 역전 원칙  (0) 2019.05.06
DDD. 도메인 주도 개발 시작  (0) 2019.05.03

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을 만든부분만 정리하자.


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


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

 

하나의 반에는 여러 학생이 포함되어 있다. 그렇기 때문에 반 클래스에 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


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


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 프로그래밍

+ Recent posts