Web/Spring 2019. 6. 13. 22:30

 

오늘 포스팅할 내용은 Spring의 RestTemplate입니다. 우선 RestTemplate란 Spring 3.0부터 지원하는 Back End 단에서 Http 통신에 유용하게 쓰이는 템플릿 객체이며, 복잡한 HttpClient 사용을 한번 추상화하여 Http 통신사용을 단순화한 객체입니다. 즉, HttpClient의 사용에 있어 기계적이고 반복적인 코드들을 한번 랩핑해서 손쉽게 사용할 수 있게 해줍니다. 또한 json,xml 포멧의 데이터를 RestTemplate이 직접 객체에 컨버팅해주기도 합니다.

 

이렇게 사용하기 편한 RestTemplate에서도 하나 짚고 넘어가야할 점이 있습니다. RestTemplate 같은 경우에는 Connection Pooling을 직접적으로 지원하지 않기 때문에 매번 RestTemplate를 호출할때마다, 로컬에서 임시 TCP 소켓을 개방하여 사용합니다. 또한 이렇게 사용된 TCP 소켓은 TIME_WAIT 상태가 되는데, 요청량이 엄청 나게 많아진다면 이러한 상태의 소켓들은 재사용 될 수 없기 때문에 응답이 지연이 될것입니다. 하지만 이러한 RestTemplate도 Connection Pooling을 이용할 수 있는데 이것은 바로 RestTemplate 내부 구성에 의해 가능합니다. 바로 내부적으로 사용되는 HttpClient를 이용하는 것입니다. 바로 예제 코드로 들어가겠습니다.

 

우선 Connection pool을 적용하기 위한 HttpClientBuilder를 사용하기 위해서는 dependency 라이브러리가 필요하다.

 

compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.6'

 

각자 필요한 버전을 명시해서 의존성을 추가해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    /*
     * Connection Pooling을 적용한 RestTemplate
     */
    @Bean(name="restTemplateClient")
    public RestTemplate restClient() {
        HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        /*
         * 타임아웃 설정
         */
        //httpRequestFactory.setConnectTimeout(timeout);
        //httpRequestFactory.setReadTimeout(timeout);
        //httpRequestFactory.setConnectionRequestTimeout(connectionRequestTimeout);
        HttpClient httpClient = HttpClientBuilder.create()
                                                 .setMaxConnTotal(150)
                                                 .setMaxConnPerRoute(50)
                                                 .build();
        httpRequestFactory.setHttpClient(httpClient);
        
        return new RestTemplate(httpRequestFactory);
    }
cs

 

위에 코드를 보면 최대 커넥션 수(MaxConnTotal)를 제한하고 IP,포트 1쌍 당 동시 수행할 연결 수(MaxConnPerRoute)를 제한하는 설정이 포함되어있습니다. 이런식으로 최대 커넥션 수를 150개로 제한하여 150개의 자원내에서 모든 일을 수행하게 되는 것입니다. 마치 DB의 Connection Pool 과 비슷한 역할이라고 보시면 됩니다.(물론 같지는 않음.) 그리고 RestTemplate은 Multi Thread 환경에서 safety 하기 때문에 빈으로 등록하여 가져다 쓰도록 하였습니다. 

 

마지막으로 구글링을 하던 도중에 Keep-alive 활성화가 되야지만 HttpClient의 Connection Pooling 지원이 가능하다고 나와있습니다. 기본적으로 HTTP1.1은 Keep-alive가 활성화되어 있지만 이부분은 더 깊게 알아봐야할 점인것 같습니다. 만약 해당 부분에 대해 아시는 분은 꼭 댓글에 코멘트 부탁드리겠습니다.

posted by 여성게
:
Web/Spring 2019. 4. 10. 21:29

컨트롤러에서 요청을 엔티티객체로 받는 경우가 있다. 이럴경우 받은 엔티티 객체로 DB까지 로직들이 순차적으로 수행이 될것이다. 그런데 만약 엔티티를 조회하거나, 리스트를 조회하는 경우가 있다고 가정해보자. 그렇다면 요청을 받고 엔티티객체를 조회한 후에 컨트롤러에서 응답값으로 ResponseEntity body에 엔티티객체를 실어 보낼 수 있다. 하지만 여기에서 만약 엔티티객체에서 내가 보내기 싫은 데이터가 포함되어있다면? 그것이 만약 유저정보에 대한 것이고, 그 객체에 패스워드까지 존재한다면? 상상하기 싫은 상황일 것이다. 여기서 해결할 수 있는 방법은 몇가지 있다. 예를 들어 @JsonIgnore,@JsonProperty로 응답을 JSON으로 반환하기 할때 원하는 인스턴스 변수를 제외하고 보낼 수도 있고, 응답용 DTO를 만들어서 응답을 다른 객체로 convert한 후에 보낼 수도 있을 것이다. 어노테이션을 이용한 방법은 추후에 다루어볼것이고, 오늘 다루어 볼 것은 응답용 DTO를 만들어서 응답을 보내는 예제이다. 

 

만약 응답용 DTO를 만들어서 내가 응답으로 내보내고 싶은 정보만 인스턴스변수로 set해서 보내면 될것이다. 하지만 여기서 아주 노가다가 있을 것이다. 그것은 따로 Util 용 Method로 빼서 일일이 set,get하는 방법일 것이다. 만약 한두개의 인스턴스 변수라면 상관없지만 만약 응답으로 내보낼 인스턴수 변수가 아주 많다면 이만한 노가다성 작업도 없을 것이다. 오늘 여기서 소개해줄 해결사가 바로 ModelMapper라는 객체이다. 바로 예제 코드로 들어가겠다.

 

오늘 만들어볼 예제는 이전 포스팅에서 다루어봤던 미완성 GenericController의 응용이다. 자세한 설명은 밑의 링크에서 참고하면 될듯하다.

▶︎▶︎▶︎2019/03/22 - [Spring] - Spring - Springboot GenericController(제네릭컨트롤러), 컨트롤러 추상화

 

1
2
3
4
5
        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>2.3.0</version>
        </dependency>
cs

ModelMapper를 사용하기 위해서 의존성을 추가해준다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class CommonBean {
    
    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}
cs

사용할 ModelMapper클래스를 빈으로 등록해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface DomainMapper {
    
    public <D,E> D convertToDomain(E source,Class<extends D> classLiteral);
}
 
@Component
public class DomainMapperImpl implements DomainMapper{
    
    private ModelMapper modelMapper;
    
    public DomainMapperImpl(ModelMapper modelMapper) {
        this.modelMapper=modelMapper;
    }
    
    /*
     * 공통 매퍼
     */
    @Override
    public <D, E> D convertToDomain(E source, Class<extends D> classLiteral) {
        return modelMapper.map(source, classLiteral);
    }
 
}
cs

그리고 ModelMapper를 이용하여 추후에 많은 유틸메소드(도메인 클래스의 인스턴스를 조작하기 위함)를 만들 가능성이 있을 수도 있기 때문에 따로 유틸로 클래스를 만들어서 해당 클래스내에서 ModelMapper를 활용할 것이다. 여기서는 따로 유틸메소드는 없고 하나의 메소드만 있다. 이것은 엔티티클래스와 매핑할 도메인클래스의 클래스리터럴(Domain.class)를 매개변수로 받아서 ModelMapper.map(엔티티클래스,도메인클래스리터럴) 메소드에 전달한다. 메소드의 반환값으로는 도메인클래스를 반환한다. 여기서 도메인클래스 필드에 엔티티클래스의 필드를 매핑할때는 필드명으로 비교를 한다. 아래에서도 다시 설명하겠지만, 도메인 클래스를 만들때는 엔티티 클래스에서 Http Response로 보내고 싶은 데이터의 필드의 이름과 동일하게 도메인클래스의 필드명으로 만들어줘야하는 것이다.

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
/**
 * BaseEntity 추상클래스
 * 해당 추상클래스를 상속할때 @Tablegenerator는 상속하는 클래스에서 정의해야함
 * 또한 id field의 칼럼속성도 필요할때에 재정의해야함
 * @author yun-yeoseong
 *
 */
@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
@Setter
@Getter
@EqualsAndHashCode(of="id")
public abstract class BaseEntity<extends BaseEntity<?>> implements Comparable<T>{
    
    @Id
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    private long id;
    
    @JsonProperty(access=Access.READ_ONLY)
    @Column(name="CREATED_DATE",nullable=false,updatable=false)
    @CreatedDate
    private LocalDateTime createdDate;
    
    @JsonProperty(access=Access.READ_ONLY)
    @Column(name="UPDATED_DATE",nullable=false)
    @LastModifiedDate
    private LocalDateTime modifiedDate;
    
//    @Column(name="CREATED_BY",updatable=false/*,nullable=false*/)
//    @CreatedBy
    
//    @Column(name="UPDATED_BY",nullable=false)
//    @LastModifiedBy
 
    @Override
    public int compareTo(T o) {
        if(this == o) return 0;
        return Long.compare(this.getId(), o.getId());
    }
 
}
 
/**
 * Main Category Entity
 * @author yun-yeoseong
 *
 */
@Entity
@Table(name="MAIN_CATEGORY"
        ,indexes=@Index(columnList="MAIN_CATEGORY_NAME",unique=false))
@AttributeOverride(name = "id",column = @Column(name = "MAIN_CATEGORY_ID"))
@TableGenerator(name="SEQ_GENERATOR",table="TB_SEQUENCE",
                pkColumnName="SEQ_NAME",pkColumnValue="MAIN_CATEGORY_SEQ",allocationSize=1)
@Getter
@Setter
@ToString
public class MainCategoryEntity extends BaseEntity<MainCategoryEntity> implements Serializable{
    
    private static final long serialVersionUID = 5609501385523526749L;
    
    @NotNull
    @Column(name="MAIN_CATEGORY_NAME",nullable=false)
    private String mainCategoryName;
    
}
cs

이번 예제에서 사용할 엔티티 클래스이다. 우선 공통 엔티티 필드들은 BaseEntity라는 추상클래스로 뺐다. 그리고 정의할 엔티티클래스에서 해당 BaseEntity를 상속하여 정의하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@Setter
@ToString
public class MainCategoryDTO implements Serializable{
 
    private static final long serialVersionUID = 3340033210827354955L;
    
    private Long id;
    
    private String mainCategoryName;
    
}
cs

이번 예제에서 사용할 도메인클래스이다. 엔티티의 생성일과 수정일은 굳이 반환할 필요가 없기때문에 도메인클래스 필드에서 제외하였다. 그러면 엔티티 클래스에서 생성일, 수정일을 제외하고 클라이언트에게 값이 반환될 것이다. 여기서 중요한 것은 앞에서도 한번 이야기 했지만, ModelMapper는 매핑에 사용할 필드들을 이름으로 비교하기 때문에 엔티티클래스에서 사용하는 필드이름과 도메인 클래스에서 매핑할 필드이름이 동일해야한다는 것이다. 이점은 꼭 기억해야 할 것 같다.

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
 * 
 * GenericController - 컨트롤러의 공통 기능 추상화(CRUD)
 * 재정의가 필요한 기능은 @Override해서 사용할것
 * 
 * @author yun-yeoseong
 *
 * @param <E> Entity Type
 * @param <D> Domain Type
 * @param <ID> Entity key type
 */
@Slf4j
public abstract class GenericController<E,D,ID> {
    
    private JpaRepository<E, ID> repository;
    
    private DomainMapper domainMapper;
    
    private Class<D> dtoClass ;
    
    @SuppressWarnings("unchecked")
    public GenericController(JpaRepository<E, ID> repository,DomainMapper domainMapper) {
        
        this.repository = repository;
        this.domainMapper = domainMapper;
        
        /*
         * 타입추론로직
         */
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[1];
        
        if (type instanceof ParameterizedType) {
            this.dtoClass = (Class<D>) ((ParameterizedType) type).getRawType();
        } else {
            this.dtoClass = (Class<D>) type;
        }
 
    }
    
    /*
     * 엔티티아이디로 조회
     */
    @GetMapping("/{id}")
    public  ResponseEntity<D> select(@PathVariable ID id) {
        log.info("GenericController.select - {}",id);
        E e = repository.findById(id).get();
        D d = domainMapper.convertToDomain(e, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.OK);
    }
    
    /*
     * 리스트 조회
     */
    @GetMapping
    public ResponseEntity<List<D>> list(){
        log.info("GenericController.list");
        List<E> lists = repository.findAll();
        List<D> response = lists.stream().map(e->domainMapper.convertToDomain(e, dtoClass)).collect(Collectors.toList());
        return new ResponseEntity<List<D>>(response, HttpStatus.OK);
    }
    
    /*
     * 엔티티 생성
     */
    @Transactional
    @PostMapping
    public ResponseEntity<D> create(@RequestBody E e) {
        log.info("GenericController.create - {}",e.toString());
        log.info("dtoClass type = {}",dtoClass.getName());
        E created = repository.save(e);
        D d = domainMapper.convertToDomain(created, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.CREATED);
    }
    
    /*
     * 엔티티 수정
     */
    @Transactional
    @PutMapping("/{id}")
    public ResponseEntity<D> update(@RequestBody E t) {
        log.info("GenericController.update - {}",t.toString());
        E updated = repository.save(t);
        D d = domainMapper.convertToDomain(updated, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.CREATED);
    }
    
    /*
     * 엔티티 삭제
     */
    @SuppressWarnings("rawtypes")
    @Transactional
    @DeleteMapping("/{id}")
    public ResponseEntity<?> delete(@PathVariable ID id) {
        log.info("GenericController.delete - {}",id);
        repository.deleteById(id);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}
 
cs

오늘 예제로 살펴볼 GenericController이다. 주석에 나와있는 데로 E,D,ID 제네릭타입은 순서대로 엔티티클래스타입,도메인클래스타입,엔티티클래스 ID타입이다. 그리고 위에서 만든 DomainMapper라는 클래스를 이용하여 사용자 요청에 대한 결과값을 조작하여 도메인클래스로 변경하여 리턴하고 있다.(엔티티에서 반환할 데이터만 DTO로 정의해 반환함) 그리고 하나 더 설명할 것은 이전 포스팅에서 다루었던 제네릭 컨트롤러와는 다르게 제네릭 타입으로 도메인클래스 타입을 받을 수 있게 하나 선언하였고, 해당 도메인 클래스의 타입을 추론하는 로직을 생성자에 한번 넣어주었다. 왜냐하면 도메인 클래스는 직접적으로 실체를 매개변수로 받고 있지 않기 때문에 타입을 추론하여 DomainMapper의 메소드의 매개변수로 들어가야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@RestController
@RequestMapping("/category/main")
public class MainCategoryController extends GenericController<MainCategoryEntity, MainCategoryDTO , Long>{
    
    private MainCategoryService mainCategoryService;
 
    public MainCategoryController(JpaRepository<MainCategoryEntity, Long> repository, DomainMapper domainMapper,
            MainCategoryService mainCategoryService) {
        super(repository, domainMapper);
        this.mainCategoryService = mainCategoryService;
    }
    
}
 
cs

마지막으로 제네릭 컨트롤러를 상속한 컨트롤러이다. 단순 CRUD 메소드가 추상화 되어있기 때문에 굉장히 깔끔한 코드가 되었다. 이 예제 코드를 실행시켜보면 반환 객체로 DTO가 나가는 것을 확인할 수 있다. 

 

여기까지 ModelMapper를 다루어봤다. 굉장히 노가다성 작업을 줄여주는 좋은 놈인것 같다. 글을 읽고 혹시 틀린점이 있다면 꼭 지적해주었음 좋겠다.

posted by 여성게
:
Middleware/Redis 2019. 3. 1. 12:55

Springboot,Redis - Springboot Redis Nodes Cluster !(레디스 클러스터)



이전 포스팅에서는 Redis Server들의 고가용성을 위해 Redis Sentinel을 구성하여 Master-Slave 관계의 구성을 해보았습니다. 


▶︎▶︎▶︎Redis Sentinel 구성


Sentinel을 구성하여 Redis Server들의 고가용성을 키워주는 방법 이외에도 사실 Redis는 Cluster라는 좋은 기능을 지원해줍니다.

그럼 Sentinel은 무엇이고 Redis Cluster는 다른 것인가? 대답은 엄연히 다른 기능입니다. 

간단히 비교하면 Sentinel는 Master-Slave관계를

구성합니다.(Redis Server 끼리 구성을 갖춤). 하지만 Redis Server 이외에 Sentinel 인스턴스를 띄워주어야합니다. 그런 Sentinel 인스턴스들은

Redis Server들을 모니터링하고 고가용성을 위한 적당한 처리를 해줍니다. 그리고 Redis Server끼리의 데이터 동기화도 마춰줍니다. 이말은,

모든 Redis Server는 모두 같은 데이터들을 가지고 있는 것이죠.

하지만 Cluster를 이용하면 각 Redis Server들은 자신만의 HashSlot을 할당 받게 됩니다. 그리고 Cluster도 Master-Slave 관계를

구성하게 됩니다. 이말은 무엇이냐? 대략 16000개의 데이터 바구니를 나누어가지는 Redis Server들은 Master가 됩니다. Sentinel과는

다르게 하나의 마스터만 갖는 것이 아닙니다. 그리고 각 마스터에 대한 Slave 서버를 가지게 되는 것입니다. 더 자세한 사항은 아래 링크를 참조해주세요.


▶︎▶︎▶︎Cluster&Sentinel 그리고 Redis





이제는 Redis Cluster 구성을 해보겠습니다. 오늘 구성해볼 아키텍쳐입니다.

혹시나 Redis를 설치와 간단한 사용법에 대해 모르신다면 아래링크를 참조해주세요.


▶︎▶︎▶︎Redis 설치와 사용법



3개의 Master와 3개의 Slave 입니다.(편의상 Redis 폴더의 루트 == $REDIS)


$REDIS 위치에 cluster라는 폴더를 하나 구성해줍니다. 


그리고 해당 $REDIS/redis.conf를 cluster 폴더에 6개를 복사해줍니다.(redis-cluster1~6.conf)



이제 각 redis-cluster 설정파일을 수정할 것입니다. 이번에 할 설정은 간단한 설정입니다. 프러덕환경에서는

더 세부적인 설정이 필요할 수 있습니다.


이번예제는 동일한 서버에 6개의 port를 나누어 진행합니다. 만약 서로 다른 서버에 구성을 하시기 위해서는

적절히 인스턴스들을 나누어주시고 각 서버에 대해 포트 개방이 필요합니다.



redis-cluster1.conf - port:6379


설정은 직관적으로 어떠한 설정에 대한 것인지 알수 있습니다. 해당 인스턴스의 포트는 6379를 사용하고

클러스터를 사용하겠다. 그리고 해당 인스턴스가 클러스터에 대한 정보를 남기기위해 nodes.conf를 사용한다.

또한 타임아웃은 5초로 하고 모든 데이터는 영속하기 위해 항상 write마다 기록한다 라는 설정입니다.(데이터 유실방지)


나머지 인스턴스들의 설정도 port와 cluster-config-file의 설정만 구분하고 동일하게 작성합니다.


ex) port 6380, cluster-config-file nodes2.conf


설정 파일작성이 끝나셨으면 6개의 터미널을 띄워줍니다.


>cd src

>./redis-server ../cluster/redis-clusterN.conf 


총 6개의 레디스 인스턴스를 실행시킵니다.


그리고 하나 추가적으로 작업을 해주어야할 것이 있습니다. 실행되고 있는 인스턴스에 대해

명시적으로 클러스터 구성을 생성해주는 작업입니다. 이 과정은 버젼에 따라 총 2가지의 방법이 있습니다.


1
2
3
redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 \
127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 \
--cluster-replicas 1
cs


$REDIS/src 의 redis-cli를 이용한 방법입니다. 클러스터 구성에 참여하는 인스턴스 정보를 모두 입력하고 마지막에 replicas 속성을

명시해줍니다. 마지막 속성은 마스터에 대한 슬레이브를 몇개를 둘것인가 라는 설정입니다.


1
2
./redis-trib.rb create --replicas 1 127.0.0.1:6379 127.0.0.1:6380 \
127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384
cs


동일한 속성에 대한 redis-trib.rb를 이용한 클러스터 구성방법입니다.


저는 첫번째 방법을 이용하였습니다. 명령어를 탁 치는 순간 3개는 마스터 3개는 슬레이브 노드를 임의로 

선택해 이렇게 클러스터를 구성하겠습니까? 라는 질문에 yes||no로 답변해주어야합니다. yes를 입력합니다.


이제는 클러스터 구성이 잘 되었는지 확인해볼까요?



잘 구성이 되었습니다 ! 여기서 한가지 집고 넘어가야 할 것이 있습니다. Redis Cluster 사용을 위해서는 그에 맞는 클라이언트가 필요합니다. 저는

그 클라이언트를 Springboot를 이용하여 구성해보았습니다. springboot의 Spring Redis 프로젝트를 생성해줍니다!



1
2
3
4
#Redis Cluster Config(마스터노드의 리스트)
spring.redis.cluster.nodes=127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381
클러스터 노드간의 리다이렉션 숫자를 제한.
spring.redis.cluster.max-redirects=
cs


application.propeties 파일입니다. 클러스터에 참여하는 노드들을 나열해줍니다.


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
/**
 * Redis Cluster Config
 * @author yun-yeoseong
 *
 */
@Component
@ConfigurationProperties(prefix = "spring.redis.cluster")
public class RedisClusterConfigurationProperties {
    
    /**
     * spring.redis.cluster.nodes[0]=127.0.0.1:6379
     * spring.redis.cluster.nodes[1]=127.0.0.1:6380
     * spring.redis.cluster.nodes[2]=127.0.0.1:6381
     */
    List<String> nodes;
 
    public List<String> getNodes() {
        return nodes;
    }
 
    public void setNodes(List<String> nodes) {
        this.nodes = nodes;
    }
    
    
}
cs


properties에 나열한 노드들의 정보를 얻기위한 빈을 하나 띄워줍니다. 물론 @Value로 직접 주입시켜주어도 상관없습니다. 해당 방법은 Spring Redis Document에 나온데로 진행하고 있는 중입니다.


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
/**
 * Redis Configuration
 * @author yun-yeoseong
 *
 */
@Configuration
public class RedisConfig {
    
    
    /**
     * Redis Cluster 구성 설정
     */
    @Autowired
    private RedisClusterConfigurationProperties clusterProperties;
    
    /**
     * JedisPool관련 설정
     * @return
     */
    @Bean
    public JedisPoolConfig jedisPoolConfig() {
        return new JedisPoolConfig();
    }
    
    
    /**
     * Redis Cluster 구성 설정
     */
    @Bean
    public RedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig) {
        return new JedisConnectionFactory(new RedisClusterConfiguration(clusterProperties.getNodes()),jedisPoolConfig);
    }
    
    /**
     * RedisTemplate관련 설정
     * 
     * -Thread-safety Bean
     * @param jedisConnectionConfig - RedisTemplate에 설정할 JedisConnectionConfig
     * @return
     */
    @Bean(name="redisTemplate")
    public RedisTemplate redisTemplateConfig(JedisConnectionFactory jedisConnectionConfig) {
        
        RedisTemplate redisTemplate = new RedisTemplate<>();
 
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(jedisConnectionConfig);
        
        return redisTemplate;
        
    }
    
    /**
     * 문자열 중심 편의 RedisTemplate
     * 
     * @param jedisConnectionConfig
     * @return
     */
    @Bean(name="stringRedisTemplate")
    public StringRedisTemplate stringRedisTemplate(JedisConnectionFactory jedisConnectionConfig) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(jedisConnectionConfig);
        
        return stringRedisTemplate;
    }
    
}
 
cs


Redis Config를 위한 자바클래스입니다. 이제 정말로 잘되는지 확인해볼까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTest {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    @Test
    public void testDataHandling() {
        
        redisTemplate.getConnectionFactory().getConnection().info().toString();
        
        String key = "yeoseong";
        String value = "yoon";
        redisTemplate.opsForValue().set(key, value);
        String returnValue = (String) redisTemplate.opsForValue().get(key);
        
        System.out.println(value);
    }
    
}
 
cs


결과값으로 "yoon"이라는 값을 얻어옵니다. 그러면 진짜로 클러스터된 노드들에서 얻어왔는지 확인해봐야겠습니다.



6379 포트의 인스턴스로 해당 값을 얻어오려고 하니 실제로는 6381에 해당 데이터가 있어 리다이렉트 됬다라는 로그와 함께 

결과 데이터를 얻어왔습니다. 여기까지 Redis Cluster 구성이었습니다. 부족한 부분이 많아 틀린 부분이 있다면 댓글 부탁드립니다!!


posted by 여성게
:
Middleware/Redis 2019. 2. 27. 23:56

Redis - Sentinel 이란? 설정방법! Redis 고가용성을 위한 방법




이전 포스팅에서 Redis Server Replication 구성방법에 대해 알아봤습니다. 이번 포스팅은

Redis 고가용성을 위한 Sentinel 기능에 대해 알아보려고 합니다. 어떻게 보면 조금더 완벽한 클러스터링을

구성한다고 생각하시면 됩니다. 


만약 Redis 설치 및 설정 방법을 모르신다면 아래 링크를 통해 참조하시고 오셔도 좋을 것같습니다.

▶︎▶︎▶︎Redis 설치 및 설정, 간단한 사용법!


우선 Sentinel에 대한 기능에 대해 알아보겠습니다.


1) 모니터링

2) 알림기능

3) 페일오버

4) 환경 구성 프로바이더


이러한 기능을 제공해줍니다.



오늘 예제로 구성해볼 이미지입니다.



구성이 이해가 가십니까? 간단하게 설명을 하면 Master&Slave는 이전 포스팅과 동일하게 3개의 노드를 띄웁니다.

그리고 Sentinel도 동일하게 3개의 노드를 띄웁니다(3개의 의미가 있음). 이런 구성에서 Sentinel은 마스터를 지속적으로 

모니터링합니다. 그리고 장애가 있을시에 적당한 프로세스를 거칩니다. 여기서 Sentinel의 노드를 3개를 띄웠는데요. 이것은

의미가 있는 숫자입니다. 이전에 포스팅 중 Zookeeper관련된 글에서도 동일한 정책을 따랐는데요. 


"바로 홀수단위로 띄운다"


입니다. 이것은 만약 네트워크의 잠깐의 오버타임때문에 마스터가 죽었다고 생각하는 하나의 Sentinel이 있을 수 있습니다.

그럼 이것이 진짜로 죽은 거라고 판단을 해야할까요? 우리는 보통 선거를 하게되면 과반수의 원칙에 따르게 됩니다. 여기서도

동일하게 과반수의 원칙을 따르게 되는 겁니다. 과반수 이상의 Sentinel이 "OK" 해야 비로소 그 마스터 노드는

죽은 것이고, 그때서야 Slave 노드에서 마스터 노드를 선출하게 되는 것입니다. 그렇기 때문에 Sentinel은

3개이상의 홀수 인스턴스를 띄운다 원칙을 지켜주셔야합니다.


우리가 구성할 실제 구성도입니다.



진행하기 앞서, $REDIS(Redis 폴더 root)에 있는 sentinel.conf 파일을 2개 복사해줍니다.(총 3개의 Sentinel 설정파일 구성)

그리고 아래 설정을 port만 각기 분리해주고 나머지 설정을 동일하게 작성해줍니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 11001.conf ~ 11003.conf
 
# 센티널이 실행될 포트입니다. (이부분은 포트별로 다르게 설정)
port 11001
# 센티널이 감시할 레디스 Master 인스턴스 정보를 넣어줍니다.
# sentinel monitor mymaster <redis master host> <redis master port> <quorum>
sentinel monitor mymaster 127.0.0.1 10000 2
# 센티널이 Master 인스턴스에 접속하기 위한 패스워드를 넣어줍니다.
sentinel auth-pass mymaster foobared
# 센티널이 Master 인스턴스와 접속이 끊겼다는 것을 알기 위한 최소한의 시간입니다.
sentinel down-after-milliseconds mymaster 30000
# 페일오버 작업 시간의 타임오버 시간을 정합니다.
# 기본값은 3분입니다.
sentinel failover-timeout mymaster 180000
# 이 값은 Master로부터 동기화 할 수 있는 slave의 개수를 지정합니다.
# 값이 클수록 Master에 부하가 가중됩니다.
# 값이 1이라면 Slave는 한대씩 Master와 동기화를 진행합니다.
sentinel parallel-syncs mymaster 1
cs


여기서 하나 설명할 것이 있다면 설정중 quorum 입니다. 이것은 의사결정에 필요한 최소 Sentinel 노드수라고 생각하시면 됩니다.

즉, 지금 구성에서는 딱 과반수가되는 2개의 Sentinel이 동의하면 의사결정이 진행이 되는 것입니다.


SDOWN vs ODOWN

More advanced concepts이라는 페이지에서 SDOWN과 ODOWN이라는 단어가 나옵니다. SDOWN은 Subjectively Down condition의 축약어이고 ODOWN은 Objectively Down condition의 축약어입니다.

SDOWN은 센티널 인스턴스가 Master와 접속이 끊긴 경우 주관적인 다운 상태로 바뀝니다. 이것은 잠시 네트워크 순단 등으로 인해 일시적인 현상일 수 있으므로 우선 SDOWN 상태가 됩니다.

그러나 SDOWN 상태인 센티널들이 많아진다면 이는 ODOWN 상태(quorum), 즉 객관적인 다운 상태로 바뀝니다. 이때부터 실질적인 페일오버(failover) 작업이 시작됩니다.



위의 설정을 모두 완료하셨다면 이전 포스팅에서 진행한 redis.conf를 조금 변경해야합니다.


Redis Server 설정 파일에 마스터,슬래이브 관계없이 모두


masterauth yourpassword

requirepass yourpassword


두개의 설정을 3개의 redis conf에 설정해줍니다. 이유는 슬래이브 노드가 마스터 노드로 선출될 수도 있기에

모든 슬래이브는 require pass 설정을 가져야하고 마스터 노드도 슬래이브 노드가 될수 있기 때문에

masterauth설정을 해주어야합니다.


이제 실행을 해봅니다. Redis Server들은 이전 포스팅에서 진행한 데로 실행해주시면 됩니다.


>$REDIS/src/redis-sentinel ../sentinel.conf

>$REDIS/src/redis-sentinel ../sentinel2.conf

>$REDIS/src/redis-sentinel ../sentinel3.conf


명령으로 모든 센티널들을 실행시킵니다.



로그를 하나하나 설명안해도 읽어보시면 어떠한 로그인지 직관적으로 이해가갑니다.



여기에서 마스터 노드를 shutdown 시켜봅니다. 그리고 마스터 선출에 시간이 걸리니 대략

30초 정도 기다려봅니다. 아니? 슬래이브 노드가 마스터노드로 선출됬습니다.



기존에 마스터였던 6379는 슬래이브로, 기존에 슬래이브였던 6381이 마스터로 선출되었습니다.

위의 정보를 출력하기 위해서는 


>./redis-cli -p port -a password

>info


로 접속하셔야합니다. 패스워드를 작성안하시면 해당 명령어를 사용하실수 없습니다.


여기까지 Redis Server 고가용성을 위한 Sentinel 구성이었습니다. production 환경에서는 반드시

위와같이 장애에 대응할 수 있는 서버 구성을 가져야합니다. 


다음 포스팅은 실제 Redis를 Springboot에서 추상화된 객체로 사용해보는 예제를 진행 해볼 것입니다.

posted by 여성게
:
Web/Spring Cloud 2019. 2. 24. 00:20

Spring Cloud - Eureka를 이용한 마이크로서비스 

동적등록&탐색&부하분산처리


스프링 클라우드 유레카는 넷플릭스 OSS에서 유래됐다. 

자가 등록, 동적 탐색 및 부하 분산에 주로 사용되며, 부하 분산을 위해 내부적으로 리본을 사용한다.

마이크로서비스의 장점 중 하나인 동적인 서비스 증설 및 축소를 유레카를 이용하면

아주 쉽게 가능하다.




위의 그림과 같이 사용자의 사용이 급격하게 많아졌다고 가정해보자.

그렇다면 위와 같이 서비스 인스턴스를 증설할 것이다. 

여기에서 유레카를 사용한다면 마이크로서비스 인스턴스를 하나 추가하면

자가 등록을 통해 유레카서버에 자신의 서비스를 등록한다.

그러면 동적으로 추가된 인스턴스를 탐색할 수 있게 되고 내부적으로 리본에 의해

같은 인스턴스 4개가 부하 분산(로드밸런싱) 처리가 될 것이다.


만약 유레카와 같은 것을 사용하지 않았다면? 개발자가 수동으로 전부다 등록해야하고 

그렇게 함으로써 추가된 인스턴스만 배포하는 것이 아니라, 관련된 다른 인스턴스까지 추가로 

재배포가 필요할 수도 있을 것이다.


위의 구성에 대해서 간단히 설명하자면 유레카는 서버와 클라이언트 컴포넌트로 이루어져있다.

서버 컴포넌트는 모든 마이크로서비스가 자신의 가용성을 등록하는 레지스트리이다.

등록되는 정보는 일반적으로 서비스 ID&URL이 포함된다.

마이크로서비스 인스턴스는 유레카 클라이언트를 이용해서 자기 자신의 가용성을 유레카 서버의 레지스트리에 

등록한다. 등록된 마이크로서비스를 호출해서 사용하는 컴포넌트도 유레카 클라이언트를 이용해서 

필요한 서비스를 탐색한다.


마이크로서비스가 시작되면 유레카 서버에 접근해 서비스 ID&URL 등의 정보를 등록하고 자신이

기동되었다는 것을 알린다.(통신은 모두 REST) 일단 등록이 되면 유레카 서버의 레지스트리에 

30초 간격으로 ping을 날리면서 자신의 status가 정상이다라는 것을 알린다.

만약 이 ping요청이 제대로 이루어지지 않는다면 유레카서버는 서비스가 죽은 것으로 

판단하여 레지스트리에서 제거한다.


유레카 클라이언트는 서비스의 정보를 받기 위하여 매번 유레카 서버에서 요청을 보내지않고

한번 받으면 로컬캐시에 저장을 해둔다. 그리고 기본 30초마다 계속 서버에 요청을 보내서

서비스의 목록을 들여다보며 변경이 있다면 로컬캐시에 저장된 것을 갱신시킨다.

(로컬캐시와 서버에 있는 서비스 정보를 비교해차이가 있는 것을 가져오는 Delta Updates 방식으로 갱신)



예제로 만들어볼 소스는 우선 Spring Cloud Config를 이용할 것이다.

만약 스프링 클라우드 컨피그에 대한 개념을 모른다면 아래 링크를 통해 한번 보고와도 좋을 것같다.


▶︎▶︎▶︎Spring Cloud Config






우선 유레카 서버로 이용할 스프링 부트 프로젝트를 생성한다.


Cloud Config>Config Client

Cloud Discovery>Eureka Server

Ops>Actuator


를 체크하여 프로젝트를 생성해준다.




1
2
3
4
spring.application.name=eureka
spring.profiles.active=server1
server.port=8889
spring.cloud.config.uri=http://localhost:8888
cs



spring.application.name=eureka,spring.profiles.active=server1는 

클라우드 컨피그에서 가져올 프로퍼티 파일명을 뜻한다.

> eureka-server1.properties

나머지설정은 위의 클라우드 컨피그 링크에서 참조하면 될 것같다.



유레카 서버는 Standard alone과 cluster mode 모두가 가능하다. 하지만

이번 예제에서는 Standard alone mode로 진행할 것이다.




1
2
3
4
spring.application.name=eureka-server1
eureka.client.serviceUrl.defaultZone=http://localhost:8899/eureka/
eureka.client.registerWithEureka=false
eureka.client.fetchRegistry=false
cs



위의 설정정보는 git에 저장된 eureka-server1.properties에 작성될 설정정보이다.

유레카서버는 서버임과 동시에 클라이언트가 될수 있다. 즉, 유레카서버도 결국은 유레카 클라이언트로

동작하는 것이다.(유레카 서버가 여러대일때, peer 관계에 있는 유레카서버의 서비스 목록을 가져오기 위하여

자신의 클라이언트를 이용해서 가져온다. 하지만 지금은 일단 클라이언트들과 동일한 동작이 계속 시도되지 않도록 false로 한것이다.) 

eureka.client.serviceUrl.defaultZone 설정으로 Zone을 지정해준다.

그리고 eureka.client.registerWithEureka=false로 자기자신을 서비스로 등록하지 않는다.

마지막으로 eureka.client.fetchRegistry=false로 마이크로서비스인스턴스 목록을 로컬에 캐시할 것인지의

여부로 등록한다. 하지만 여기서 유레카서버는 동적 서비스 탐색등의 목적으로

사용되지는 않음으로 밑의 두개의 설정은 false로 등록한다.(즉,Standard alone이면 두개다 false)

만약 registerWithEureka를 true로 동작하면 자기 자신에게 계속 health check 요청 및 다른 유레카 클라이언트가 보내는 요청을

자기스스로에게 보내게 될것이다.



1
2
3
4
5
6
7
8
9
@EnableEurekaServer
@SpringBootApplication
public class EurekaserverApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(EurekaserverApplication.class, args);
    }
 
}
cs


@EnableEurekaServer 어노테이션으로 자기자신이 유레카서버임을 명시한다.

http://localhost:8889 로 접속하면 유레카 관리페이지가 나온다.

현재는 아무런 서비스도 등록되어 있지않은 상태이다.



나머지 유레카 클라이언트들의 코드는 편의상 클라우드 컨피그를 이용하지 않았다.



1
2
3
server.port=8070
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/
spring.application.name=eureka-call-client
cs


위의 설정파일은 마이크로서비스 인스턴스들을 호출할 하나의 클라이언트 설정이다. 유레카클라이언트는 defaultZone 속성값이 같은

다른 유레카 클라이언트와 동료관계를 형성하므로, 해당 애플리케이션의 defaultZone설정으로 유레카서버와 동일하게 작성한다.

그 다음 spring.application.name 설정은 유레카서버에 등록될 서비스이름이다.

유레카서버에게 동적서비스 등록을 하고,

동적탐색의 대상이 되는 어떠한 서비스들을 호출하기 위한 애플리케이션도 유레카 클라이언트이어야한다.



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
@EnableDiscoveryClient
@SpringBootApplication
public class Eurekaclient3Application {
 
    
    public static void main(String[] args) {
        SpringApplication.run(Eurekaclient3Application.class, args);
    }
    
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    
    @RestController
    class EurekaClientController{
        
        @Autowired
        RestTemplate restTemplate;
        
        @GetMapping("/eureka/client")
        public String eurekaClient() {
            
            String result = restTemplate.getForObject("http://eurekaclient/eureka/client"String.class);
            
            return result;
        }
    }
}
cs


위의 소스를 설명하자면 우선 애플리케이션이 유레카 클라이언트임을 @EnableDiscoveryClient 어노테이션으로 명시한다.

그리고 우리가 많이 사용하던 RestTemplate을 빈으로 등록할때 @LoadBalanced 어노테이션을 등록하여

Ribbon에 의해 로드벨런싱할 RestTemplate임을 명시한다.(@LoadBalanced 때문에 서비스로 등록된 마이크로서비스 인스턴스 등을 호출할때

라운드로빈 방식으로 분산으로 요청이 가게된다.)


그런데 RestTemplate을 사용하는 메소드 안의 URL정보가 조금 특이하다. eurekaclient? 우리는 로컬환경이고

따로 호스트를 등록하지도 않았는데, localhost가 아니고 다른 DNS명으로 호출하고 있다.

해답은 다음 과정에 나오게 된다.


다음은 서비스로 등록될 마이크로서비스 인스턴스 애플리케이션 2개이다.(2개의 애플리케이션 코드는 동일하고 설정정보만 조금 다르니, 소스코드는

하나의 애플리케이션만 명시한다.)



1
2
3
server.port=8090
spring.application.name=eurekaclient
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/
cs


1
2
3
server.port=8080
spring.application.name=eurekaclient
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/
cs


두개의 마이크로서비스 인스턴스의 설정정보이다. 다른 것은 서버포트 하나뿐이다. 그리고 해당 애플리케이션들은

같은 애플리케이션은 증설한 상황이다. 즉, 서비스 이용자 입장에서는 같은 인스턴스인 것이다. 그렇기 때문에

spring.application.name을 eurekaclient로 동일하게 등록한다. 어? 이건 이 인스턴스들을

호출한 클라이언트에서 RestTemplate의 메소드의 DNS였는데? 맞다. 그것이다.


즉, 유레카서버에 등록한 서비스 이름으로 RestTemplate 요청을 보내는 것이다. 그런 다음 해당 서비스 이름으로

서비스가 등록되어있는지 확인하고 있다면 Ribbon이 로드밸런싱(라운드로빈 방식) 해줘서 요청이 가게된다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@EnableDiscoveryClient
@SpringBootApplication
public class EurekaclientApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(EurekaclientApplication.class, args);
    }
    
    @RestController
    class EurekaClientController{
        
        @GetMapping("/eureka/client")
        public String eurekaClient() {
            return "eureka client - 1";
        }
    }
}
cs


마이크로서비스 인스턴스의 소스이다. 애플리케이션을 하나더 생성하여 위의 소스에서 반환값만 수정하자.


그런다음 유레카 관리자 페이지를 들어가보자.



총 3개의 서비스가 등록되어 있는 것을 볼 수 있다.(Eureka-call-client(1),EurekaClient(2))


마지막으로 postman 툴이나 curl로 Eureka-call-client의 "/eureka/client"를 호출해보자


계속해서 반환되는 값이 "eureka client - 1" , "eureka client - 2" 로 번갈아가면서

반환될것이다. 지금은 로컬에서 나자신 혼자만 요청을 보내니 라운드로빈방식으로 각각 번갈아가면서

한번씩 호출된다.


이렇게 독립모드형 유레카서버,클라이언트 구성을 해보았다.


마지막으로 간단한 유레카서버,클라이언트 용어 및 부트설정 설명이다.



1
2
3
4
5
6
7
8
9
10
#<Eureka 등장 용어 정리>
#    <Eureka 행동 관련>
#        Service Registration: 서비스가 자기 자신의 정보를 Eureka에 등록하는 행동
#        Service Registry: 서비스가 스스로 등록한 정보들의 목록, 가용한 서비스들의 위치 정보로 갱신됨
#        Service Discovery: 서비스 클라이언트가 요청을 보내고자 하는 대상의 정보를 Service Registry를 통해 발견하는 과정
#    <Eureka 구성 요소 관련>
#        Eureka Client: 서비스들의 위치 정보를 알아내기 위해 Eureka에 질의하는 서비스를 가리킴 (like Service consumer)
#        Eureka Service: Eureka Client에 의해 발견의 대상이 되도록 Eureka에 등록을 요청한 서비스를 가리킴 (like Service provider)
#        Eureka Server: Eureka Service가 자기 자신을 등록(Service Registration)하는 서버이자 Eureka Client가 가용한 서비스 목록(Service Registry)을 요청하는 서버
#        Eureka Instance: Eureka에 등록되어 목록에서 조회 가능한 Eureka Service를 의미
cs



일단 이번 포스팅은 간단하게 유레카의 사용법을 익혀봤다. 다음 포스팅에서는 더 다양한 유레카 설정과 유레카 서버를 클러스터구성으로

예제를 진행할것이다. 이번에는 대략적인 유레카의 사용법을 익히는 것으로 간다.

posted by 여성게
:
Web/Spring Cloud 2019. 2. 23. 12:46

Spring Cloud - Spring Cloud Config(스프링 클라우드 컨피그)


Spring cloud Config(스프링 클라우드 컨피그) 서버는 애플리케이션과 서비스의 모든 환경설정 속성 정보를 저장하고, 

조회하고 관리할 수 있게 해주는 외부화된 환경설정 서버다. 스프링 컨피그는 환경설정 정보의 버전 관리 기능도 지원한다. 

환경설정 속성 정보를 애플리케이션 배포 패키지에서 분리해 외부화하고 외부 소스에서 설정 정보를 읽어노는 방법이다.


위의 그림과 같이 스프링 클라우드 컨피그 서버가 모든 마이크로서비스의 환경설정정보를 가지고 있고,

설정 배포의 작업을 진행한다. 하지만 항상 서버에 접근해서 설정정보를 가져오지는 않고,

첫 애플리케이션 구동 단계에서 설정정보를 가져와 로컬에 캐시를 해둔다. 그리고 만약 컨피그 서버에서

설정정보의 변경이 이루어 진다면 모든 마이크로서비스에 변경 사항을 전파하고, 모든 마이크로서비스는

변경사항을 로컬캐시에 반영한다. 컨피그 서버는 개발 환경별 프로파일 기능도 제공한다.


이번 포스팅은 스프링 클라우드 컨비그 서버의 환경설정 정보를 GitHub에 저장하고 원격에서 설정정보를

불러올 것이다. (SVN 등을 이용해도 무관)




만약 Git 사용법이 익숙하지 않다면 밑의 링크에서 Git사용법을 익히고 와도 좋다.


▶︎▶︎▶︎GitHub - 간단한 Git사용법(로컬 레포지토리,원격 레포지토리)

▶︎▶︎▶︎GitHub - Git 사용법 2 (branch, checkout, reset 등)

▶︎▶︎▶︎Github - eclipse(이클립스)와 local repository(로컬레포지토리) 연동





스프링부트프로젝트를 생성할 때, 위 이미지와 같이 Config Server & Actuator를 체크해준다.


기존에 기본으로 생성됬던 application.properties를 bootstrap.properties로 변경해준다.

스프링 클라우드 컨피그는 application.properties가 아닌 bootstrap.properties를 이용한다.



1
2
3
4
server.port=8888
spring.cloud.config.server.git.uri=https://github.com/yeoseong/spring-cloud-configserver.git
management.security.enabled=false
management.endpoint.env.enabled=true
cs



bootstrap.properties를 위와같이 작성한다. 간단히 설명하면

컨피그 서버의 포트는 8888, 환경설정 파일을 관리하는 원격 Git을 서버는 ~.git이라는 것이다.

나머지는 액츄에이터에 대한 설정파일이다.



1
2
3
4
5
6
7
8
9
@EnableConfigServer
@SpringBootApplication
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
 
}
cs


@EnableConfigServer 어노테이션을 붙여서 해당 애플리케이션은 컨피그서버임을 명시해준다.


그 다음은 해당 컨피그 서버를 이용하는 클라이언트 작성이다.


새로운 스프링부트 프로젝트를 생성하고, Config Client와 Actuator,WEB를 체크해준 후 위의 과정과 동일하게

application.properties -> bootstrap.properties로 바꿔준다.



1
2
3
spring.application.name=configclient
spring.profiles.active=dev
spring.cloud.config.uri=http://localhost:8888
cs



해당 애플리케이션 이름은 configclient고, 프로파일은 dev이며, 설정정보를 받아올 컨피그 서버의 주소는 ~:8888이라는 뜻이다.

이 설정들은 다 의미가 있고 중요한 설정이다. 우선 애플리케이션 이름은 깃에 저장될 프로퍼티 파일의 이름이고, 프로파일은 해당 프로퍼티의

프로파일이라는 뜻이다. 즉, 깃허브에는 {name}-{profile}.properties라는 환경설정 파일이 저장되어 있고,

이 설정파일을 불러오기위한 설정이라고 생각하면된다. 

애플리케이션 별로 환경설정 파일을 분리하고, 한 애플리케이션에 대한 설정이지만 프로파일 별로 설정파일을 유지할 수도 있는 것이다.




이제 깃허브에 configclient-dev.properties라는 파일을 작성하고 예제로 설정파일에 밑의 이미지와 같은 내용을 기입한다.






마지막으로 예제 애플리케이션을 작성한다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootApplication
public class ConfigclientApplication implements CommandLineRunner{
    
    @Value("${application.service.name}")
    private String serviceName;
    
    @Override
    public void run(String... args) throws Exception {
        // TODO Auto-generated method stub
        
        System.out.println(serviceName);
        
        
    }
 
    public static void main(String[] args) {
        SpringApplication.run(ConfigclientApplication.class, args);
    }
 
}
cs



해당 예제는 CommandLineRunner를 이용하여 애플리케이션 구동시점에 간단히 콘솔에 

프로퍼티에 작성한 내용을 출력하였다.


▶︎▶︎▶︎CommandLineRunner란?


이제 애플리케이션을 구동해보자, 아마 Console에 "configclient2"라는 문자열이 찍혔을 것이다.


그 다음은 만약 환경설정 파일이 변경이 되었다면


해당 인스턴스에게 refresh요청을 보낸다.

파라미터는 필요없다.

>http://localhost:8080/refresh(POST)



1
2
3
4
5
6
7
8
9
10
11
12
    @RefreshScope
    @RestController
    class ConfigClientController {
        
        @Value("${application.service.name}")
        private String serviceName;
        
        @GetMapping("/config")
        public String config() {
            return serviceName;
        }
    }
cs


하지만 이미 주입된 프로퍼티값을 변경하기 위해서는 @RefreshScope라는

어노테이션을 붙여준다. 이렇게하면 애플리케이션 재시작 없이 환경설정 값을 바꿔줄 수 있다.


하지만 여기에서 마이크로서비스가 여러개이고, 관리되는 환경설정이 여러개라면 일일이 

위의 요청을 보내야 할까? 아니다.

>http://localhost:8080/bus/refresh(POST)


하나의 인스턴스에게 위의 요청을 한번만 보내면 모든 인스턴스에게 환경설정 변경정보가 전파된다.


만약 위의 요청을 사용하려면

management.security.enabled=false

설정을 꼭 해줘야한다.


설정파일을 적절한 값으로 변경하고 refresh 한후에 해당 컨트롤러에 다시 요청을 보내보자.

아마 변경된 프로퍼티를 읽어서 반환해줄 것이다.

posted by 여성게
:
Web/Spring 2019. 2. 21. 22:14

Springboot - CommandLineRunner(커맨드라인러너)



Springboot에서 서버 구동 시점에 초기화작업으로 무엇인가를 넣고 싶다면 사용할 수 있는 방법 중 하나가 

CommandLineRunner인터페이스를 상속받는 것이다.


@SpringBootApplication 어노테이션이 붙어있는 부트구동 클래스에 CommandLineRunner를 implements

하고 run(String... strings)를 Override한다.

그리고 해당 run()에 애플리케이션의 초기작업(빈등록 과정 등등)이후 실행할 로직을 작성해주면 된다.




예제 코드는 항공편 예약서비스 예제인데, 애플리케이션 구동 시점에 예약가능한

비행편을 알아보기위하여 검색 마이크로서비스 애플리케이션에 초기데이터를 

삽입하는 코드이다.



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
@SpringBootApplication
public class Application implements CommandLineRunner {
    private static final Logger logger = LoggerFactory.getLogger(Application.class);
    
    @Autowired
    private FlightRepository flightRepository;
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    @Override
    public void run(String... strings) throws Exception {
        List<Flight> flights = new ArrayList<>();
        flights.add(new Flight("BF100""SEA","SFO","22-JAN-18",new Fares("100""USD"),new Inventory(100)));
        flights.add(new Flight("BF101""NYC","SFO","22-JAN-18",new Fares("101""USD"),new Inventory(100)));
        flights.add(new Flight("BF105""NYC","SFO","22-JAN-18",new Fares("105""USD"),new Inventory(100)));
        flights.add(new Flight("BF106""NYC","SFO","22-JAN-18",new Fares("106""USD"),new Inventory(100)));
        flights.add(new Flight("BF102""CHI","SFO","22-JAN-18",new Fares("102""USD"),new Inventory(100)));
        flights.add(new Flight("BF103""HOU","SFO","22-JAN-18",new Fares("103""USD"),new Inventory(100)));
        flights.add(new Flight("BF104""LAX","SFO","22-JAN-18",new Fares("104""USD"),new Inventory(100)));
        
        flightRepository.saveAll(flights);
        
        logger.info("Looking to load flights...");
        for (Flight flight : flightRepository.findByOriginAndDestinationAndFlightDate("NYC""SFO""22-JAN-18")) {
            logger.info(flight.toString());
        }
    }
     
}
cs


만약 애플리케이션 구동 순서에 선후가 존재한다면 후행 애플리케이션에 위와같이 run()을 오버라이드하여

선행 애플리케이션에 Health Check등을 하는 로직을 넣어도 괜찮은 방법이 될 것 같다.

posted by 여성게
:
Middleware/Kafka&RabbitMQ 2019. 2. 18. 00:39

Spring - Rabbitmq를 이용한 비동기 메시징 서비스

     -리액티브 마이크로서비스



RabbitMQ에 대해

Mac OS 환경에서 작성되었습니다.


오늘은 간단히 Spring boot + Rabbitmq를 이용한 비동기 메시징 서비스를 구현해볼 것이다. 

일단 이 포스팅을 진행하는 이유는 요즘 시대에는 일체형 애플리케이션이 작은 서비스 단위인 마이크로서비스 단위로 나누어 

서비스 단위로 배포하는 아키텍쳐가 대세인듯하다. 

이 말은 즉슨, 아주 큰 애플리케이션이 작은 서비스 단위(마이크로서비스)로 나뉘어 각각 단독적으로 독립적으로 실행가능한 상태로 배포가 된다. 

이런 경우 마이크로서비스끼리의 통신은 RESTful한 통신도 있지만 메시지 큐와 같은 서비스를 이용하여 비동기적으로 통신하기도 한다.

그리고 이 구조를 발행구독구조라고 한다.


위의 사진과 같이 두 마이크로서비스 애플리케이션이 외부의 큐로 연결되는 구조인 것이다.

간단히 설명하면 Sender라는 하나의 애플리케이션에서 큐로 발신하면 

Receiver라는 애플리케이션이 해당 큐에 대한 이벤트를 수신하여

로직을 처리하게 되는 것이다.





외부의 큐도 어찌됬는 하나의 데몬으로 떠있는 애플리케이션 같은 것이다.

Rabbitmq를 다운로드한다.


▶︎▶︎▶︎래빗엠큐다운로드



Standalone MacOS binary를 클릭하여 받는다.




Rabbitmq 관리자 페이지 플러그인을 활성화하는 명령이다.



관리자 페이지에 들어갈 계정을 만들어준다.


만약 계정을 만드는 데 밑의 에러가 발생한다면 


Error:

{:undef, [{:crypto, :hash, [:sha256, <<94, 223, 167, 31, 97, 108, 105, 118, 101>>], []}, {:rabbit_password, :hash, 2, [file: 'src/rabbit_password.erl', line: 34]}, {:rabbit_auth_backend_internal, :add_user_sans_validation, 3, [file: 'src/rabbit_auth_backend_internal.erl', line: 252]}, {:rpc, :"-handle_call_call/6-fun-0-", 5, [file: 'rpc.erl', line: 197]}]}


>brew install openssl 로 인스톨하면 된다.



Rabbitmq를 실행시켜 준다. 그리고 localhost:15672로 접속한 후에 방금 전에 만들었던 계정으로

로그인한다.




위의 사진에 Set permisstion 버튼을 클릭하여 해당 계정으로 큐에서 읽거나 쓰는 권한을 부여한다.


여기까지 간단한 Rabbitmq의 설정이 완료되었다. 이것보다 더 많은 설정 요소들이 존재하겠지만,

예제로 간단하게 이정도로만 설정한다.


참고: 관리자페이지 포트 : 15672

애플리케이션(amqp) :5672

클러스터링 : 25672





소스 예제 회원가입 이후에 외부 큐에 가입 메시지를 날리면(Sender) 수신애플리케이션에서 가입 회원의 이메일을 커맨드에 뿌려준다.(Receiver)



<Sender>


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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package org.rvslab.chapter3;
 
import java.util.Optional;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
 
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitMessagingTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
 
import reactor.core.publisher.Mono;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
 
 
@SpringBootApplication
@EnableSwagger2
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
    
    @Bean
    CommandLineRunner init(CustomerRespository customerRepository) {
            return (evt) ->  {
                              customerRepository.save(new Customer("Adam","adam@boot.com"));
                              customerRepository.save(new Customer("John","john@boot.com"));
                              customerRepository.save(new Customer("Smith","smith@boot.com"));
                              customerRepository.save(new Customer("Edgar","edgar@boot.com"));
                              customerRepository.save(new Customer("Martin","martin@boot.com"));
                              customerRepository.save(new Customer("Tom","tom@boot.com"));
                              customerRepository.save(new Customer("Sean","sean@boot.com"));
            };
    }
    
 
}
 
 
@RestController
class CustomerController{
        
        CustomerRegistrar customerRegistrar;
        
        @Autowired
        CustomerController(CustomerRegistrar customerRegistrar){
                this.customerRegistrar = customerRegistrar;
        }
        
        @RequestMapping( path="/register", method = RequestMethod.POST)
        Mono<Customer> register(@RequestBody Customer customer){                
                return customerRegistrar.register(customer);     
        }
}
 
@Component
@Lazy
class CustomerRegistrar {
        
        CustomerRespository customerRespository;
        Sender sender;
        
        @Autowired
        CustomerRegistrar(CustomerRespository customerRespository, Sender sender){
                this.customerRespository = customerRespository;
                this.sender = sender;
        }
        
        
        public Mono<Customer> registerMono(Mono<Customer> monoCustomer){
                monoCustomer.doOnNext(customer -> {
                        if(customerRespository.findByName(customer.getName()).isPresent())
                                System.out.println("Duplicate Customer");
                        else {
                                customerRespository.save(customer);
                                //sender.send(customer.getEmail());             
                        }
                }).subscribe();
                return monoCustomer;
        }
 
        // ideally repository will return a Mono object
        public Mono<Customer> register(Customer customer){
                        if(customerRespository.findByName(customer.getName()).isPresent())
                                System.out.println("Duplicate Customer. No Action required");
                        else {
                                customerRespository.save(customer);
                                //외부 큐에게 메시지전송
                                sender.send(customer.getEmail());       
                                System.out.println("Rabbitmq send :::: "+customer.toString());
                        }
                return Mono.just(customer);
        }
}
 
 
//외부 큐와 연결하기 위한 Sender를 빈으로 등록한다.
//또 빈으로 Queue객체를 등록해준다. 해당 문자열로 Receiver쪽에서도 동일하게 받아야한다.
//RabbitMessagingTemplate으로 외부 큐에게 메시지를 전송한다.
@Component
@Lazy
class Sender {
        
        RabbitMessagingTemplate template;
        
        @Autowired
        Sender(RabbitMessagingTemplate template){
                this.template = template;
        }
 
        @Bean
        Queue queue() {
                return new Queue("CustomerQ"false);
        }
        
        public void send(String message){
                template.convertAndSend("CustomerQ", message);
                System.out.println("Ready to send message but suppressed "+ message);
                 
        }
}
 
//repository does not support Reactive. Ideally this should use reactive repository
@RepositoryRestResource
@Lazy
interface CustomerRespository extends JpaRepository <Customer,Long>{
        Optional<Customer> findByName(@Param("name"String name);
}
 
 
//Entity class
@Entity
class Customer{
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
        private String name;
        private String email;
        
        public Customer (){}
 
        
        public Customer(String name, String email) {
                super();
                this.name = name;
                this.email = email;
        }
 
 
        public Long getId() {
                return id;
        }
 
        public void setId(Long id) {
                this.id = id;
        }
 
        public String getName() {
                return name;
        }
 
        public void setName(String name) {
                this.name = name;
        }
 
        public String getEmail() {
                return email;
        }
 
        public void setEmail(String email) {
                this.email = email;
        }
 
        @Override
        public String toString() {
                return "Customer [id=" + id + ", name=" + name + ", email=" + email + "]";
        }
        
         
        
}
cs


  1. management.security.enabled=false
  2.  
  3. spring.rabbitmq.host=localhost
  4. spring.rabbitmq.port=5672
  5. spring.rabbitmq.username=yeoseong
  6. spring.rabbitmq.password=yeoseong


<Receiver>


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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package org.rvslab.chapter3;
 
import java.util.Optional;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
 
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitMessagingTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
 
import reactor.core.publisher.Mono;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
 
 
@SpringBootApplication
@EnableSwagger2
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
    
    @Bean
    CommandLineRunner init(CustomerRespository customerRepository) {
            return (evt) ->  {
                              customerRepository.save(new Customer("Adam","adam@boot.com"));
                              customerRepository.save(new Customer("John","john@boot.com"));
                              customerRepository.save(new Customer("Smith","smith@boot.com"));
                              customerRepository.save(new Customer("Edgar","edgar@boot.com"));
                              customerRepository.save(new Customer("Martin","martin@boot.com"));
                              customerRepository.save(new Customer("Tom","tom@boot.com"));
                              customerRepository.save(new Customer("Sean","sean@boot.com"));
            };
    }
    
 
}
 
 
@RestController
class CustomerController{
        
        CustomerRegistrar customerRegistrar;
        
        @Autowired
        CustomerController(CustomerRegistrar customerRegistrar){
                this.customerRegistrar = customerRegistrar;
        }
        
        @RequestMapping( path="/register", method = RequestMethod.POST)
        Mono<Customer> register(@RequestBody Customer customer){                
                return customerRegistrar.register(customer);     
        }
}
 
@Component
@Lazy
class CustomerRegistrar {
        
        CustomerRespository customerRespository;
        Sender sender;
        
        @Autowired
        CustomerRegistrar(CustomerRespository customerRespository, Sender sender){
                this.customerRespository = customerRespository;
                this.sender = sender;
        }
        
        
        public Mono<Customer> registerMono(Mono<Customer> monoCustomer){
                monoCustomer.doOnNext(customer -> {
                        if(customerRespository.findByName(customer.getName()).isPresent())
                                System.out.println("Duplicate Customer");
                        else {
                                customerRespository.save(customer);
                                //sender.send(customer.getEmail());             
                        }
                }).subscribe();
                return monoCustomer;
        }
 
        // ideally repository will return a Mono object
        public Mono<Customer> register(Customer customer){
                        if(customerRespository.findByName(customer.getName()).isPresent())
                                System.out.println("Duplicate Customer. No Action required");
                        else {
                                customerRespository.save(customer);
                                //외부 큐에게 메시지전송
                                sender.send(customer.getEmail());       
                                System.out.println("Rabbitmq send :::: "+customer.toString());
                        }
                return Mono.just(customer);
        }
}
 
 
//외부 큐와 연결하기 위한 Sender를 빈으로 등록한다.
//또 빈으로 Queue객체를 등록해준다. 해당 문자열로 Receiver쪽에서도 동일하게 받아야한다.
//RabbitMessagingTemplate으로 외부 큐에게 메시지를 전송한다.
@Component
@Lazy
class Sender {
        
        RabbitMessagingTemplate template;
        
        @Autowired
        Sender(RabbitMessagingTemplate template){
                this.template = template;
        }
 
        @Bean
        Queue queue() {
                return new Queue("CustomerQ"false);
        }
        
        public void send(String message){
                template.convertAndSend("CustomerQ", message);
                System.out.println("Ready to send message but suppressed "+ message);
                 
        }
}
 
//repository does not support Reactive. Ideally this should use reactive repository
@RepositoryRestResource
@Lazy
interface CustomerRespository extends JpaRepository <Customer,Long>{
        Optional<Customer> findByName(@Param("name"String name);
}
 
 
//Entity class
@Entity
class Customer{
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
        private String name;
        private String email;
        
        public Customer (){}
 
        
        public Customer(String name, String email) {
                super();
                this.name = name;
                this.email = email;
        }
 
 
        public Long getId() {
                return id;
        }
 
        public void setId(Long id) {
                this.id = id;
        }
 
        public String getName() {
                return name;
        }
 
        public void setName(String name) {
                this.name = name;
        }
 
        public String getEmail() {
                return email;
        }
 
        public void setEmail(String email) {
                this.email = email;
        }
 
        @Override
        public String toString() {
                return "Customer [id=" + id + ", name=" + name + ", email=" + email + "]";
        }
        
         
        
}
cs


  1. server.port=8090
  2.  
  3. spring.rabbitmq.host=localhost
  4. spring.rabbitmq.port=5672
  5. spring.rabbitmq.username=yeoseong
  6. spring.rabbitmq.password=yeoseong
  7.  
  8. spring.mail.host=localhost
  9. spring.mail.port=2525


부트 프로젝트생성할 때 의존성 설정 부분에서 I/O>AMQP를 선택해준다.



posted by 여성게
: