Custom Validation 만들어서 추가하기

web/Spring|2018. 12. 24. 00:11

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


댓글()

node.js에서 sharp를 사용해서 이미지 크기 변경하기

web/node.js|2018. 10. 22. 09:27

node.js에서 이미지 크기를 변경하기 위해서 임시 파일에 데이터를 다운로드 받고 파일 크기를 변경하는 작업을 보통 진행한다.


하지만 이번에는 html stream으로 임시로 내려받은 버퍼를 사용해서 사이즈를 변경하고 바로 s3에 전송하거나 파일로 내보내는 작업을 진행해보려한다.


이미지의 사이즈를 변경하기 위해서 필요한 라이브러리는 sharp이다. 

1
npm install sharp
cs

https://www.npmjs.com/package/sharp


먼저 request 요청으로 내려받은 image데이터를 sharp로 이미지를 변경하고 파일로 내보내보자.

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
'use strict';
 
const request = require('request-promise');
const sharp = require('sharp');
const fs = require('fs');
const Stream = require('stream').Transform;
 
class ImageResize {
 
  async imageResize() {
 
    const transformer = sharp().resize(250211);
    const writeStream = fs.createWriteStream('test.jpg');
 
    // fs.writeaStream으로 파일로 쓰기
    await request('https://image.toast.com/aaaaab/ticketlink/TKL_3/ion_main08061242.jpg').pipe(transformer).pipe(writeStream);
 
    // stream 처리를 통해 파일로 직접 쓰기
    this.streamToFile(await request('https://image.toast.com/aaaaab/ticketlink/TKL_3/ion_main08061242.jpg').pipe(transformer));
  }
 
  streamToFile (stream) {
    const chunks = new Stream();
    return new Promise((resolve, reject) => {
      stream.on('data', chunk => chunks.push(chunk));
      stream.on('error', reject);
      stream.on('end', () => fs.writeFileSync('tt.jpg', chunks.read()));
    });
  }
};
 
module.exports = new ImageResize();
cs

코드를 보면 알겠지만 생각보다 간단하다. request 요청으로 받은 html stream을 sharp로 넘겨서 처리하고 그리고 나온 스트림을 writeStream으로 넘겨서 처리하거나 별도의 스트림 처리를 설정해서 파일로 저장할 수 있다.


추가적으로 파일로 내보내는 작업이 아닌 s3로 이미지 파일을 내보내기 위해 이미지 크기가 변경된 stream을 buffer로 바꾸는 작업을 해보자. 

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
'use strict';
 
const request = require('request-promise');
const sharp = require('sharp');
const fs = require('fs');
const Stream = require('stream').Transform;
 
class ImageResize {
 
  async imageResize() {
 
    const transformer = sharp().resize(250211);
    const writeStream = fs.createWriteStream('test.jpg');
 
    // s3로 전송 (String)
    let dd1 = await this.streamToString(await request('https://image.toast.com/aaaaab/ticketlink/TKL_3/ion_main08061242.jpg').pipe(transformer));
 
    s3.upload({
        Body : ddl 
    });
  }
 
  streamToString (stream) {
    const chunks = [];
    return new Promise((resolve, reject) => {
      stream.on('data', chunk => chunks.push(chunk));
      stream.on('error', reject);
      stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
    });
  }
 
};
 
module.exports = new ImageResize();
cs


node.js stream을 이해하면 생각보다 어렵지 않은 작업으로 이미지를 임시로 저장하지 않고 바로 크기를 변경할 수 있다.


소스 git repository

https://github.com/weduls/nodejs_study


참고

https://stackoverflow.com/questions/12740659/downloading-images-with-node-js

https://stackoverflow.com/questions/10623798/writing-node-js-stream-into-a-string-variable

https://stackoverflow.com/questions/13979558/saving-an-image-stored-on-s3-using-node-js


댓글()

node.js express에서 request 사용자 아이피 찾기

web/node.js|2018. 10. 15. 22:26

요청한 사용자의 Ip를 찾아서 로그를 남기거나 Ip 지역정보를 활용해서 geoIp를 찾아내거나 할 때 request를 요청한 사용자의 IP 주소가 필요하다.


Spring에서는 간단하게 HttpServletRequest에서 getHeader의 X-FORWARDED-FOR에 있는 정보를 가져오거나 getRemoteAddr()을 통해 가져올 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * 주문요청.
 *
 * @param req the req
 * @return the response entity
*/
@PostMapping(value = "/order")
public ResponseEntity<?> order(@Valid @RequestBody OrderRequestDto req, HttpServletRequest request) {
    String ip = request.getHeader("X-FORWARDED-FOR");
    if (ip == null)
        ip = request.getRemoteAddr();
 
    return ResponseEntity.ok(orderService.order(req));
}
cs


우선 헤더에서 X-FORWARDED-FOR에서 왜 아이피 정보를 찾는건가? 대부분의 사용자 요청이 오면 사용자 요청은 Load Balance등을 통해서 오게되기 때문에 원천 아이피를 찾을 수 없기 때무에 X-FORWARDED-FOR에 원천 아이피를 넣어서 전송한다.

결론은 XFF는 HTTP Header 중 하나로 HTPP Server에 요청한 Client의 IP를 식별하기 위한 표준이다.

참고 : http://blog.plura.io/?p=6597


그럼 node.js에서는 어떻게 가져와야 하나? 아주 간단하다.

1
const ip = req.headers['x-forwarded-for'||  req.connection.remoteAddress;
cs


그리고 만약 로컬에서 ::1로 사용자 IP가 출력되는 경우가 있다. ::1은 IpV6에서 로컬 호스트를 의미한다. 그래서 express 서버를 생성할 때 IPV4를 사용하도록 설정하면 된다.


댓글()

elasticsearch session timeout 이슈

node.js에서 엘라스틱서치 클라이언트 사용시에 반복되는 요청이나 오랜 시간이 필요한 요청이 있을때 session이 끊어져 버리고 socket hang up 이슈가 발생한다.

※해결방법
session timeout과 keepalive 옵션을 지정해주면 된다. 아래 내용을 보고 참고하여 설정하시길.




댓글()

request-promise를 통해 가져온 euc-kr 문자열 인코딩 문제 해결 (iconv)

web/node.js|2018. 10. 6. 01:31

크롤러를 만들기위해 필요로하는 페이지를 가지고 오기위해 request-promise를 사용하였다. 

요새 대부분의 홈페이지는 utf-8을 사용하기 때문에 큰 문제가 없으나 euc-kr를 사용하는 옛날 사이트들이 있다.

그런 사이트들의 정보를 그냥 request해서 가지고 오게되면 한글이 다 깨져버린다. 그것을 해결해보자.

우선 request-promise를 사용하여 데이터를 가지고 와보자.

1
2
3
4
5
6
7
8
const request = require('request-promise');
 
class Crawler {
 
  async crawler() {
    let doc = await reqest('http://url');
  }
}
cs


역시 euc-kr를 사용하는 것을 확인하고 있고 깨져있다.


iconv-lite를 사용한 디코딩

문자열의 인코딩, 디코딩을 제공해주는 라이브러리 iconv-lite 라이브러리를 다운 받는다.

1
npm install iconv-lite --save
cs


그리고 다음과 euc-kr로 인코딩 되어있는 문장을 euc-kr로 디코딩해주면 된다. 

주의※
만약 request-promise로 데이터를  데이터를 가지고 오는것이라면 encoding: null 속성을 null로 해주어야 정상적으로 디코딩이 되니 꼭 참고해야한다.

1
    let doc = iconv.decode(new Buffer(await request({url: 'http://', encoding: null})), 'EUC-KR').toString();
cs


decoding이 된 문자열

댓글()
  1. 노드암 5기 2018.12.05 07:58 댓글주소  수정/삭제  댓글쓰기

    이 글을보고 암이 나았습니다.

  2. Favicon of http://github.com/krsteve BlogIcon 메모지 2019.01.22 12:32 댓글주소  수정/삭제  댓글쓰기

    이 글을 보고 암이 나았습니다. (2)
    감사합니다.

  3. Favicon of https://blog.ggaman.com BlogIcon 2019.05.08 18:24 신고 댓글주소  수정/삭제  댓글쓰기

    이 글을 보고 암이 나았습니다. (2)
    감사합니다.

    encoding: null 이 문제였네요.

Git 저장소 Fork 후 Pull Request 및 최신 내용 갱신 방법

IT 지식/Git|2018. 10. 4. 00:34

이전직장에서도 SVN을 사용했다. 물론 GtitLab을 1년반정도 사용했으나 아쉽게도 협업하여 진행하지를 않아서 모두 master에 직접 커밋을 하였다.

그래서 이번에는 협업한다고 생각하고 시나리오를 만들어서 테스트를 진행해보자.

Github 저장소에서 상대방 Repository를 Fork하고 Pull Request를 요청해보고 변경된 최신내용을 Local에 반영하는 작업을 진행해보자.

1. 우선 Repository를 Fork 한다.


fork를 진행할 계정을 선택하고 확인을 누르면 내 Repository에 Fork된 저장소가 보이는 것을 확인할 수 있다.


2. SourceTree에 추가

Git을 Command를 사용해서 멋지게 사용하면 좋지만 실력이 부족하기 때문에 SourceTree를 사용해서 진행해보자.
SourceTree에 방금 Fork 했던 저장소를 추가해보자.


3. 소스코드 변경

Clone을 받은 코드를 내가 원했던 내용대로 수정하고 커밋과 push를 진행한다.


작업을 완료한 후 내 저장소에 가보면 정상적으로 커밋과 푸시가 완료된것을 확인 할 수 있다.


4. 변경사항 Pull Request
지금까지 진행한 내용은 내 개인 Repository에만 반영되어있다. 실제로 원래 저장소에 가보면 원래 상태 그대로인것을 확인할 수 있다.

그럼 위에 그림에서 New pull request 버튼을 누르고 비교할 브랜치를 선택 해준다.

그러면 아래와 같이 변경사항과 함께 Pull request를 만들 수 있는 화면이 나온다

그럼 코멘트를 작성하고 Pull Request를 만들어보자.

요청이 완료되었고  이제 저장소의 원래 소유자가 요청을 받아주면 작업을 완료 할 수 있습니다.


5. Pull Request 수락.

원래 Repository의 주인인 dauns 계정으로 들어가 보면 Pull Request가 하나 있는것을 확인할 수 있다.
여기서 이 요청을 거절할 수도 있고, merge 할수도 있고 rebase 할수도 있다.

테스트이기 때문에 Merge 해보자.

Merge를 하고 나면 저장소에 내용이 변경되었고 Pull Request도 사라진것을 확인할 수 있다.

6. 원격 저장소 변경사항 Local에 반영

내가 fork를 진행한 원격 저장소 내용이 변경되어서 내가 fork를 진행한 저장소와 달라졌다고 가정해보자. 

위에 그림에 보면 tt.java라는 파일이 추가되었고 내 저장소에는 저 내용이 반영되어 있지 않다.

이를 반영하기 위해서는 먼저 원래 저장소를 upstream의 이름으로 원격저장소로 추가해줍니다.


그리고 추가된 upstream에서 우측클릭을 한 후  "upstream에서 가져와 병합하기"를 선택합니다.

그러면 원격저장소의 내용이 내 Local Repository에 반영이 되었습니다. 


이 내용을 commit & push 하여 내 원격 저장소에도 반영을 해주면 완료됩니다.

확실히 GIT이 더 어렵다. 

지금이야 간단하지 꼬이면 참 귀찮아질 것 같다. 사례를 많이 만들어봐서 연습해봐야겠다.

댓글()

Spring에서 get으로 한글 데이터를 requestparam으로 받을 때 깨지는 현상

web/Spring|2018. 6. 7. 09:39

업무 진행 시 Spring에서 페이지 이동 시 같이 전송한 parameter 값을 

controller에서 받을 때 깨지는 현상이 발생했다. 




그래서 이를 해결하기위해 전송받은 데이터를 UTF-8로 인코딩을 진행하였더니 정상적으로 한글을 받아서 처리할 수 있었다.


1
new String(bizName.getBytes("8859_1"), "UTF-8")
cs


댓글()

Spring에서 url 요청하는 RestTemplate 설명

web/Spring|2018. 5. 27. 19:38

Spring 기반 프로젝트 진행 시 URL을 요청할 때가 있다. 

이를 Apache의 HttpClient 라이브로리를 사용하여 할 수 있지만, 스프링 프로젝트에서는 SpringTemplate를 사용하여 쉽게 요청할 수 있다. (httpClient와 RestTemplate의 차이는 하단의 링크참조)

사용법은 아주 간단하다. 각 요청 Method 마다 하단의 메소드를 이용하여 호출하기만 하면된다.

1. Get


1
2
3
4
5
RestTemplate restTemplate = new RestTemplate();
String fooResourceUrl
  = "http://localhost:8080/spring-rest/foos";
ResponseEntity<String> response
  = restTemplate.getForEntity(fooResourceUrl + "/1", String.class);
cs




2. Post


1
2
3
4
5
6
ClientHttpRequestFactory requestFactory = getClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(requestFactory);
 
HttpEntity<Foo> request = new HttpEntity<>(new Foo("bar"));
Foo foo = restTemplate.postForObject(fooResourceUrl, request, Foo.class);
 
cs



PUT과 DELETE는 하단 참조페이지에서 확인가능하다.

또한 이런 RestTemplate 기능에서 비동기로 결과값을 받을 수 있는 라이브러리가 추가되었다.
Spring 4에 추가된 AsyncRestTemplate를 이용하면 url을 요청하고 비동기로 결과를 받을 수 있다. 

사용법은 다음과 같다.



1
ListenableFuture<ResponseEntity<Map>> entity = asyncRestTemplate.getForEntity("https://httpbin.org/get", Map.class); 
cs




각 사례별 자세한 설명 (httpClient와 비교) 
https://vnthf.github.io/blog/Java-RestTemplate%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC/ 

Spring API 문서 
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html 

자세한 사용법 
http://www.baeldung.com/rest-template



댓글()