AsyncRestTemplate의 콜백 헬과 중복 작업 문제

해당 포스팅은 토비님의 토비의 봄 TV 10회 스프링 리액티브 프로그래밍 (6) AsyncRestTemplate의 콜백 헬과 중복 작업 문제 라이브 코딩을 보며 따라했던 실습 내용을 바탕으로 정리한 글입니다.

실습 코드들은 IntelliJ를 이용해 SpringBoot 2.1.3.RELEASE 버전 기반으로 프로젝트를 생성 후(web, lombok 포함) 진행했습니다.

이번에는 포스팅에서는 지난번 ListenableFuture를 사용하면서 발생한 콜백헬을 어떻게 개선할지에 대해서 이야기합니다. ListenableFuture를 Wrapping 하는 Completion이라는 클래스를 만들어, chainable하게 사용할 수 있는 방식으로 코드를 만들어봅니다.
콜백헬의 문제로는 에러를 처리하는 코드가 중복이 된다는 것도 있는데, 이 부분도 해결해봅니다.

Completion 클래스 추가

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
@SpringBootApplication
@EnableAsync
@Slf4j
public class StudyApplication {

@RestController
public static class MyController {
AsyncRestTemplate rt = new AsyncRestTemplate(new Netty4ClientHttpRequestFactory(new NioEventLoopGroup(1)));

@Autowired
MyService myService;

static final String URL1 = "http://localhost:8081/service?req={req}";
static final String URL2 = "http://localhost:8081/service2?req={req}";

@GetMapping("/rest")
public DeferredResult<String> rest(int idx) {
DeferredResult<String> dr = new DeferredResult<>();

Completion
.from(rt.getForEntity(URL1, String.class, "hello" + idx))
.andAccept(s -> dr.setResult(s.getBody()));

/*
ListenableFuture<ResponseEntity<String>> f1 = rt.getForEntity("http://localhost:8081/service?req={req}", String.class, "hello" + idx);
f1.addCallback(s -> {
ListenableFuture<ResponseEntity<String>> f2 = rt.getForEntity("http://localhost:8081/service2?req={req}", String.class, s.getBody());
f2.addCallback(s2 -> {
ListenableFuture<String> f3 = myService.work(s2.getBody());
f3.addCallback(s3 -> {
dr.setResult(s3);
}, e -> {
dr.setErrorResult(e.getMessage());
});
}, e -> {
dr.setErrorResult(e.getMessage());
});
}, e -> {
dr.setErrorResult(e.getMessage());
});
*/

return dr;
}
}

public static class Completion {

Consumer<ResponseEntity<String>> con;

Completion next;

public Completion() {
}

public Completion(Consumer<ResponseEntity<String>> con) {
this.con = con;
}

public static Completion from(ListenableFuture<ResponseEntity<String>> lf) {
Completion c = new Completion();
lf.addCallback(s -> {
c.complete(s);
}, e -> {
c.error(e);
});
return c;
}

public void andAccept(Consumer<ResponseEntity<String>> con) {
Completion c = new Completion(con);
this.next = c;
}

void complete(ResponseEntity<String> s) {
if (next != null) next.run(s);
}

private void run(ResponseEntity<String> value) {
if (con != null) con.accept(value);
}

private void error(Throwable e) {
}
}

@Service
public static class MyService {
@Async
public ListenableFuture<String> work(String req) {
return new AsyncResult<>(req + "/asyncwork");
}
}

@Bean
public ThreadPoolTaskExecutor myThreadPool() {
ThreadPoolTaskExecutor te = new ThreadPoolTaskExecutor();
te.setCorePoolSize(1);
te.setMaxPoolSize(1);
te.initialize();
return te;
}

public static void main(String[] args) {
SpringApplication.run(StudyApplication.class, args);
}
}

andApply 메서드 추가

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
@SpringBootApplication
@EnableAsync
@Slf4j
public class StudyApplication {

@RestController
public static class MyController {
AsyncRestTemplate rt = new AsyncRestTemplate(new Netty4ClientHttpRequestFactory(new NioEventLoopGroup(1)));

@Autowired
MyService myService;

static final String URL1 = "http://localhost:8081/service?req={req}";
static final String URL2 = "http://localhost:8081/service2?req={req}";

@GetMapping("/rest")
public DeferredResult<String> rest(int idx) {
DeferredResult<String> dr = new DeferredResult<>();

Completion
.from(rt.getForEntity(URL1, String.class, "hello" + idx))
.andApply(s -> rt.getForEntity(URL2, String.class, s.getBody()))
.andAccept(s -> dr.setResult(s.getBody()));

/*
ListenableFuture<ResponseEntity<String>> f1 = rt.getForEntity("http://localhost:8081/service?req={req}", String.class, "hello" + idx);
f1.addCallback(s -> {
ListenableFuture<ResponseEntity<String>> f2 = rt.getForEntity("http://localhost:8081/service2?req={req}", String.class, s.getBody());
f2.addCallback(s2 -> {
ListenableFuture<String> f3 = myService.work(s2.getBody());
f3.addCallback(s3 -> {
dr.setResult(s3);
}, e -> {
dr.setErrorResult(e.getMessage());
});
}, e -> {
dr.setErrorResult(e.getMessage());
});
}, e -> {
dr.setErrorResult(e.getMessage());
});
*/

return dr;
}
}

public static class Completion {

Consumer<ResponseEntity<String>> con;

Function<ResponseEntity<String>, ListenableFuture<ResponseEntity<String>>> fn;

Completion next;

public Completion() {
}

public Completion(Consumer<ResponseEntity<String>> con) {
this.con = con;
}

public Completion(Function<ResponseEntity<String>, ListenableFuture<ResponseEntity<String>>> fn) {
this.fn = fn;
}

public static Completion from(ListenableFuture<ResponseEntity<String>> lf) {
Completion c = new Completion();
lf.addCallback(s -> {
c.complete(s);
}, e -> {
c.error(e);
});
return c;
}

public Completion andApply(Function<ResponseEntity<String>, ListenableFuture<ResponseEntity<String>>> fn) {
Completion c = new Completion(fn);
this.next = c;
return c;
}

public void andAccept(Consumer<ResponseEntity<String>> con) {
Completion c = new Completion(con);
this.next = c;
}

void complete(ResponseEntity<String> s) {
if (next != null) next.run(s);
}

private void run(ResponseEntity<String> value) {
if (con != null) con.accept(value);
else if (fn != null) {
ListenableFuture<ResponseEntity<String>> lf = fn.apply(value);
lf.addCallback(s -> complete(s), e -> error(e));
}
}

private void error(Throwable e) {
}
}

@Service
public static class MyService {
@Async
public ListenableFuture<String> work(String req) {
return new AsyncResult<>(req + "/asyncwork");
}
}

@Bean
public ThreadPoolTaskExecutor myThreadPool() {
ThreadPoolTaskExecutor te = new ThreadPoolTaskExecutor();
te.setCorePoolSize(1);
te.setMaxPoolSize(1);
te.initialize();
return te;
}

public static void main(String[] args) {
SpringApplication.run(StudyApplication.class, args);
}
}

AcceptCompletion, AsyncCompletion 클래스 추가

Completion을 결과를 받아서 사용만 하고 끝나는 Accept 처리를 하는 Completion과, 결과를 받아서 또 다른 비동기 작업을 수행하고 그 결과를 반환하는 Apply 용 Completion으로 분리합니다.

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
@SpringBootApplication
@EnableAsync
@Slf4j
public class StudyApplication {

@RestController
public static class MyController {
AsyncRestTemplate rt = new AsyncRestTemplate(new Netty4ClientHttpRequestFactory(new NioEventLoopGroup(1)));

@Autowired
MyService myService;

static final String URL1 = "http://localhost:8081/service?req={req}";
static final String URL2 = "http://localhost:8081/service2?req={req}";

@GetMapping("/rest")
public DeferredResult<String> rest(int idx) {
DeferredResult<String> dr = new DeferredResult<>();

Completion
.from(rt.getForEntity(URL1, String.class, "hello" + idx))
.andApply(s -> rt.getForEntity(URL2, String.class, s.getBody()))
.andAccept(s -> dr.setResult(s.getBody()));

/*
ListenableFuture<ResponseEntity<String>> f1 = rt.getForEntity("http://localhost:8081/service?req={req}", String.class, "hello" + idx);
f1.addCallback(s -> {
ListenableFuture<ResponseEntity<String>> f2 = rt.getForEntity("http://localhost:8081/service2?req={req}", String.class, s.getBody());
f2.addCallback(s2 -> {
ListenableFuture<String> f3 = myService.work(s2.getBody());
f3.addCallback(s3 -> {
dr.setResult(s3);
}, e -> {
dr.setErrorResult(e.getMessage());
});
}, e -> {
dr.setErrorResult(e.getMessage());
});
}, e -> {
dr.setErrorResult(e.getMessage());
});
*/

return dr;
}
}

public static class AcceptCompletion extends Completion {
Consumer<ResponseEntity<String>> con;

public AcceptCompletion(Consumer<ResponseEntity<String>> con) {
this.con = con;
}

@Override
public void run(ResponseEntity<String> value) {
con.accept(value);
}
}

public static class AsyncCompletion extends Completion {
Function<ResponseEntity<String>, ListenableFuture<ResponseEntity<String>>> fn;

public AsyncCompletion(Function<ResponseEntity<String>, ListenableFuture<ResponseEntity<String>>> fn) {
this.fn = fn;
}

@Override
public void run(ResponseEntity<String> value) {
ListenableFuture<ResponseEntity<String>> lf = fn.apply(value);
lf.addCallback(s -> complete(s), e -> error(e));
}
}

public static class Completion {
Completion next;

public static Completion from(ListenableFuture<ResponseEntity<String>> lf) {
Completion c = new Completion();
lf.addCallback(s -> {
c.complete(s);
}, e -> {
c.error(e);
});
return c;
}

public Completion andApply(Function<ResponseEntity<String>, ListenableFuture<ResponseEntity<String>>> fn) {
Completion c = new AsyncCompletion(fn);
this.next = c;
return c;
}

public void andAccept(Consumer<ResponseEntity<String>> con) {
Completion c = new AcceptCompletion(con);
this.next = c;
}

public void complete(ResponseEntity<String> s) {
if (next != null) next.run(s);
}

public void run(ResponseEntity<String> value) {
}

public void error(Throwable e) {
}
}

@Service
public static class MyService {
@Async
public ListenableFuture<String> work(String req) {
return new AsyncResult<>(req + "/asyncwork");
}
}

@Bean
public ThreadPoolTaskExecutor myThreadPool() {
ThreadPoolTaskExecutor te = new ThreadPoolTaskExecutor();
te.setCorePoolSize(1);
te.setMaxPoolSize(1);
te.initialize();
return te;
}

public static void main(String[] args) {
SpringApplication.run(StudyApplication.class, args);
}
}

ErrorCompletion 클래스 추가

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
@SpringBootApplication
@EnableAsync
@Slf4j
public class StudyApplication {

@RestController
public static class MyController {
AsyncRestTemplate rt = new AsyncRestTemplate(new Netty4ClientHttpRequestFactory(new NioEventLoopGroup(1)));

@Autowired
MyService myService;

static final String URL1 = "http://localhost:8081/service?req={req}";
static final String URL2 = "http://localhost:8081/service2?req={req}";

@GetMapping("/rest")
public DeferredResult<String> rest(int idx) {
DeferredResult<String> dr = new DeferredResult<>();

Completion
.from(rt.getForEntity(URL1, String.class, "hello" + idx))
.andApply(s -> rt.getForEntity(URL2, String.class, s.getBody()))
.andError(e -> dr.setErrorResult(e))
.andAccept(s -> dr.setResult(s.getBody()));

/*
ListenableFuture<ResponseEntity<String>> f1 = rt.getForEntity("http://localhost:8081/service?req={req}", String.class, "hello" + idx);
f1.addCallback(s -> {
ListenableFuture<ResponseEntity<String>> f2 = rt.getForEntity("http://localhost:8081/service2?req={req}", String.class, s.getBody());
f2.addCallback(s2 -> {
ListenableFuture<String> f3 = myService.work(s2.getBody());
f3.addCallback(s3 -> {
dr.setResult(s3);
}, e -> {
dr.setErrorResult(e.getMessage());
});
}, e -> {
dr.setErrorResult(e.getMessage());
});
}, e -> {
dr.setErrorResult(e.getMessage());
});
*/

return dr;
}
}

public static class AcceptCompletion extends Completion {
Consumer<ResponseEntity<String>> con;

public AcceptCompletion(Consumer<ResponseEntity<String>> con) {
this.con = con;
}

@Override
public void run(ResponseEntity<String> value) {
con.accept(value);
}
}

public static class ErrorCompletion extends Completion {
Consumer<Throwable> econ;

public ErrorCompletion(Consumer<Throwable> econ) {
this.econ = econ;
}

@Override
public void run(ResponseEntity<String> value) {
if (next != null) {
next.run(value);
}
}

@Override
public void error(Throwable e) {
econ.accept(e);
}
}

public static class AsyncCompletion extends Completion {
Function<ResponseEntity<String>, ListenableFuture<ResponseEntity<String>>> fn;

public AsyncCompletion(Function<ResponseEntity<String>, ListenableFuture<ResponseEntity<String>>> fn) {
this.fn = fn;
}

@Override
public void run(ResponseEntity<String> value) {
ListenableFuture<ResponseEntity<String>> lf = fn.apply(value);
lf.addCallback(s -> complete(s), e -> error(e));
}
}

public static class Completion {
Completion next;

public static Completion from(ListenableFuture<ResponseEntity<String>> lf) {
Completion c = new Completion();
lf.addCallback(s -> {
c.complete(s);
}, e -> {
c.error(e);
});
return c;
}

public Completion andApply(Function<ResponseEntity<String>, ListenableFuture<ResponseEntity<String>>> fn) {
Completion c = new AsyncCompletion(fn);
this.next = c;
return c;
}

public Completion andError(Consumer<Throwable> econ) {
Completion c = new ErrorCompletion(econ);
this.next = c;
return c;
}

public void andAccept(Consumer<ResponseEntity<String>> con) {
Completion c = new AcceptCompletion(con);
this.next = c;
}

public void complete(ResponseEntity<String> s) {
if (next != null) next.run(s);
}

public void run(ResponseEntity<String> value) {
}

public void error(Throwable e) {
if (next != null) next.error(e);
}
}

@Service
public static class MyService {
@Async
public ListenableFuture<String> work(String req) {
return new AsyncResult<>(req + "/asyncwork");
}
}

@Bean
public ThreadPoolTaskExecutor myThreadPool() {
ThreadPoolTaskExecutor te = new ThreadPoolTaskExecutor();
te.setCorePoolSize(1);
te.setMaxPoolSize(1);
te.initialize();
return te;
}

public static void main(String[] args) {
SpringApplication.run(StudyApplication.class, args);
}
}

Generic 적용

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
@SpringBootApplication
@EnableAsync
@Slf4j
public class StudyApplication {

@RestController
public static class MyController {
AsyncRestTemplate rt = new AsyncRestTemplate(new Netty4ClientHttpRequestFactory(new NioEventLoopGroup(1)));

@Autowired
MyService myService;

static final String URL1 = "http://localhost:8081/service?req={req}";
static final String URL2 = "http://localhost:8081/service2?req={req}";

@GetMapping("/rest")
public DeferredResult<String> rest(int idx) {
DeferredResult<String> dr = new DeferredResult<>();

Completion
.from(rt.getForEntity(URL1, String.class, "hello" + idx))
.andApply(s -> rt.getForEntity(URL2, String.class, s.getBody()))
.andApply(s -> myService.work(s.getBody()))
.andError(e -> dr.setErrorResult(e.toString()))
.andAccept(s -> dr.setResult(s));

/*
ListenableFuture<ResponseEntity<String>> f1 = rt.getForEntity("http://localhost:8081/service?req={req}", String.class, "hello" + idx);
f1.addCallback(s -> {
ListenableFuture<ResponseEntity<String>> f2 = rt.getForEntity("http://localhost:8081/service2?req={req}", String.class, s.getBody());
f2.addCallback(s2 -> {
ListenableFuture<String> f3 = myService.work(s2.getBody());
f3.addCallback(s3 -> {
dr.setResult(s3);
}, e -> {
dr.setErrorResult(e.getMessage());
});
}, e -> {
dr.setErrorResult(e.getMessage());
});
}, e -> {
dr.setErrorResult(e.getMessage());
});
*/

return dr;
}
}

public static class AcceptCompletion<S> extends Completion<S, Void> {
Consumer<S> con;

public AcceptCompletion(Consumer<S> con) {
this.con = con;
}

@Override
public void run(S value) {
con.accept(value);
}
}

public static class ErrorCompletion<T> extends Completion<T, T> {
Consumer<Throwable> econ;

public ErrorCompletion(Consumer<Throwable> econ) {
this.econ = econ;
}

@Override
public void run(T value) {
if (next != null) {
next.run(value);
}
}

@Override
public void error(Throwable e) {
econ.accept(e);
}
}

public static class AsyncCompletion<S, T> extends Completion<S, T> {
Function<S, ListenableFuture<T>> fn;

public AsyncCompletion(Function<S, ListenableFuture<T>> fn) {
this.fn = fn;
}

@Override
public void run(S value) {
ListenableFuture<T> lf = fn.apply(value);
lf.addCallback(s -> complete(s), e -> error(e));
}
}

// S는 넘어온 파라미터, T는 결과
public static class Completion<S, T> {
Completion next;

public static <S, T> Completion<S, T> from(ListenableFuture<T> lf) {
Completion<S, T> c = new Completion<>();
lf.addCallback(s -> {
c.complete(s);
}, e -> {
c.error(e);
});
return c;
}

public <V> Completion<T, V> andApply(Function<T, ListenableFuture<V>> fn) {
Completion<T, V> c = new AsyncCompletion<>(fn);
this.next = c;
return c;
}

public Completion<T, T> andError(Consumer<Throwable> econ) {
Completion<T, T> c = new ErrorCompletion<>(econ);
this.next = c;
return c;
}

public void andAccept(Consumer<T> con) {
Completion<T, Void> c = new AcceptCompletion<>(con);
this.next = c;
}

public void complete(T s) {
if (next != null) next.run(s);
}

public void run(S value) {
}

public void error(Throwable e) {
if (next != null) next.error(e);
}
}

@Service
public static class MyService {
@Async
public ListenableFuture<String> work(String req) {
return new AsyncResult<>(req + "/asyncwork");
}
}

@Bean
public ThreadPoolTaskExecutor myThreadPool() {
ThreadPoolTaskExecutor te = new ThreadPoolTaskExecutor();
te.setCorePoolSize(1);
te.setMaxPoolSize(1);
te.initialize();
return te;
}

public static void main(String[] args) {
SpringApplication.run(StudyApplication.class, args);
}
}
# Async, Java

댓글

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×