Web/Spring 2020. 3. 3. 16:30

 

오늘 다루어볼 내용은 spring cache를 이용할때, 쉽게 놓쳐 실수 할 수 있는 @CacheEvict이다. @CacheEvict는 캐시 되어 있는 내용을 refresh, 정확히는 삭제하는 어노테이션인데 명시적으로 캐시 키값을 명시해주지 않으면 발생할 수 있는 실수 있다. 바로 예제를 살펴본다.

 

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
package com.example.cache;
 
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
 
@EnableCaching
@SpringBootApplication
@RestController
@RequiredArgsConstructor
public class CacheApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }
 
    private final CacheService cacheService;
 
    @GetMapping("/")
    public Mono<String> cacheTest(@RequestParam("id"String id) {
        return Mono.just(cacheService.cacheTest(id));
    }
 
    @GetMapping("/refresh")
    @CacheEvict(value = "testCache")
    public Mono<String> cacheRefresh() {
        return Mono.just("refresh");
    }
 
    @Component
    public static class CacheService{
        @Cacheable("testCache")
        public String cacheTest(String id) {
 
            return id+(Math.random()*100);
 
        }
    }
 
}
 
cs

 

  • 15Line - spring cache를 사용하겠다라는 어노테이션
  • 40Line - testCache라는 value로 cacheTest 메서드의 결과를 캐시한다. 하지만 여기서 쉽게 지나칠수 있는 것은 cacheTest 메서드의 String id 값이 "cacheTest"라는 캐시의 키값이 되어 들어간다. 이말은 같은 "cacheTest" value이지만 키값에 따라 데이터가 여러건 캐시될 수 있다라는 뜻이다.
  • 33Line - value가 "testCache"인 캐시를 삭제한다. 여기서 또한 쉽게 놓칠 수 있는 부분이 있다. 해당 메서드는 파라미터가 없는데, 이때 value = "testCache"이며 key가 0인 캐시 데이터를 지우게 된다. 

 

바로 요청을 날려서 결과를 보자.

 

 

계속 날려도 같은 값이 온다. 그리고 refresh 요청을 보내보자.

 

 

이후에 다시 기존 api를 호출해보자.

 

 

똑같다. 이 말은 캐시가 삭제되지 않았다는 것이다. 왜그럴까? 이유는 "testCache"인 캐시에 key가 ab인 캐시가 생성되어 있는데, 캐시는 "testCache"인 key가 ab가 아닌 캐시를 삭제하려고 하고 있기 때문이다. 해결방법은 무엇일까? 그것은 캐시를 삭제하는 메서드에도 똑같이 key값을 가지는 매개변수를 받던가 혹은 @CacheEvict(value="testCache",key="ab")를 넣어준다. 하지만 수동으로 넣는 것은 유지보수 측면으로 좋지 않으니, 캐시를 삭제할때 key값을 메서드 매개변수로 받도록 한다.

 

그리고 한가지더 방법은 @CacheEvict(value="testCache", allEntries=true)로 지워주는 것도 방법이다. 이것은 key값 상관없이 해당 value로 들어간 모든 캐시데이터를 지우는 것이다.

 

방법1)

캐시를 지우는 메서드에 캐시를 생성하는 메서드와 같이 key값이 되는 매개변수를 넣어준다.

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
package com.example.cache;
 
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
 
@EnableCaching
@SpringBootApplication
@RestController
@RequiredArgsConstructor
public class CacheApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }
 
    private final CacheService cacheService;
 
    @GetMapping("/")
    public Mono<String> cacheTest(@RequestParam("id"String id) {
        return Mono.just(cacheService.cacheTest(id));
    }
 
    @GetMapping("/refresh")
    @CacheEvict(value = "testCache")
    public Mono<String> cacheRefresh(@RequestParam("id"String id) {
        return Mono.just("refresh");
    }
 
    @Component
    public static class CacheService{
        @Cacheable("testCache")
        public String cacheTest(String id) {
 
            return id+(Math.random()*100);
 
        }
    }
 
}
 
cs

 

방법2)

해당 value로 생성된 모든 캐시 엔트리를 지운다.

 

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
package com.example.cache;
 
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
 
@EnableCaching
@SpringBootApplication
@RestController
@RequiredArgsConstructor
public class CacheApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }
 
    private final CacheService cacheService;
 
    @GetMapping("/")
    public Mono<String> cacheTest(@RequestParam("id"String id) {
        return Mono.just(cacheService.cacheTest(id));
    }
 
    @GetMapping("/refresh")
    @CacheEvict(value = "testCache", allEntries = true)
    public Mono<String> cacheRefresh() {
        return Mono.just("refresh");
    }
 
    @Component
    public static class CacheService{
        @Cacheable("testCache")
        public String cacheTest(String id) {
 
            return id+(Math.random()*100);
 
        }
    }
 
}
 
cs

 

여기까지 @CacheEvict를 사용할때 유의해야할 점이었다. 마지막으로 무거운 로직에 캐시를 무조건 해야한다는 생각은 버려야한다. 아무리 무거운 로직이라도 자주 변경되는 데이터라면 캐시하면 오히려 성능 저하로 이어진다. 올바르게 용도에 맞게 캐시를 사용하도록 하자!

posted by 여성게
:
Web/Spring 2020. 3. 1. 01:34

 

오늘 다루어볼 내용은 테스트 코드에서 DB관련된 테스트를 작성하는 방법이다. 사실 지금까지 여러가지 유닛테스트를 짜면서 로직에 대한 검증을 하긴 했지만, 데이터베이스와 관련된 테스트 작성은 조금 꺼려하기는 했다. 이유는 여러가지이지만, 귀찮은 설정들이 필요하고 사실 외부 환경에 따라 테스트가 실패할 가능성도 있기 때문이다. 하지만 계속 미룰수는 없는 법.. 오늘은 간단하게 데이터베이스관련 된 테스트를 Junit으로 만들어볼 것이다.

 

환경

  • springboot
  • mongodb
  • junit

 

다루어볼 예제는 정말 간단한 User 객체에 대한 생성,수정,조회 등을 테스트해볼 것이다.

 

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
buildscript {
    ext {
        flapdoodleVersion = "2.2.0"
    }
}
 
plugins {
    id 'org.springframework.boot' version '2.2.5.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}
 
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '13'
 
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
 
repositories {
    mavenCentral()
}
 
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'io.projectreactor:reactor-test'
    testImplementation "de.flapdoodle.embed:de.flapdoodle.embed.mongo:${flapdoodleVersion}"
-> 임베디드 몽고 의존
}
 
test {
    useJUnitPlatform()
}
 
cs

 

build.gradle 내용이다.

 

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
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class User {
 
    @JsonIgnore
    private ObjectId id;
    @NotNull
    private String username;
    @NotNull
    private Integer age;
    @NotNull
    private Address address;
    private String hobby;
 
    @Data
    @AllArgsConstructor(staticName = "of")
    @NoArgsConstructor
    public static class Address {
        @NotNull
        private String si;
        @NotNull
        private String gu;
        @NotNull
        private String dong;
    }
}
cs

 

간단한 유저 도메인 객체이다.

 

1
2
3
4
5
public interface UserRepository {
    Mono<User> findUserById(final ObjectId id);
    Mono<User> updateUser(final User user);
    Mono<User> createUser(final User user);
}
cs

 

해당 인터페이스는 DB Access 구현 방식에 구애 받지 않고 core한 영역에서 사용하기 위한 User Data Access 인터페이스이다. 나머지 코드를 보면 왜 구현 코드에 구애 받지 않는지 알 수 있다.

 

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
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
@Document("User")
public class UserData {
 
    @Id
    private ObjectId id;
    private String name;
    private Integer age;
    private AddressData userAddress;
    private String hobby;
 
    @Data
    @AllArgsConstructor(staticName = "of")
    @NoArgsConstructor
    public static class AddressData {
        private String si;
        private String gu;
        private String dong;
 
        public User.Address from() {
            return User.Address.of(
                    si,
                    gu,
                    dong
            );
        }
 
        public static UserData.AddressData to(User.Address address) {
            return UserData.AddressData.of(
                    address.getSi(),
                    address.getGu(),
                    address.getDong()
            );
        }
    }
 
    public User from() {
        return User.of(
                id,
                name,
                age,
                userAddress.from(),
                hobby
        );
    }
 
    public static UserData to(User user) {
        return UserData.of(
                user.getId(),
                user.getUsername(),
                user.getAge(),
                AddressData.to(user.getAddress()),
                user.getHobby()
        );
    }
}
cs

 

실제 DB Access를 위한 엔티티 클래스이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepository {
 
    private final UserMongoRepository repository;
 
    @Override
    public Mono<User> findUserById(final ObjectId id) {
        return repository.findById(id).map(UserData::from);
    }
 
    @Override
    public Mono<User> updateUser(final User user) {
        return repository.save(UserData.to(user)).map(UserData::from);
    }
 
    @Override
    public Mono<User> createUser(final User user) {
        return repository.save(UserData.to(user)).map(UserData::from);
    }
 
}
cs

 

실제 DB Access 구현체를 필드에 가지고 있는 UserRepository의 구현체이다. 즉, core(프레임워크에 영향을 받지 않는 비지니스 영역)한 영역에서 UserRepository를 사용할때는 구현체에 대한 자세한 정보가 필요하지 않고 DI해서 사용하면 되는 것이다. 

 

1
public interface UserMongoRepository extends ReactiveMongoRepository<UserData, ObjectId> { }
cs

 

UserRepositoryImpl가 가지고 있는 DB Access Object 구현체이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableReactiveMongoRepositories(basePackages = {"com.example.test.data.db.repositories"}, reactiveMongoTemplateRef = "userMongoTemplate")
public class MongoConfig {
 
    @Bean
    public MongoClient mongoClient() {
        return MongoClients.create("mongodb://localhost:27017");
    }
 
    @Bean
    public ReactiveMongoTemplate userMongoTemplate(MongoClient mongoClient) {
        return new ReactiveMongoTemplate(mongoClient, "USER_DB");
    }
 
}
cs

 

몽고디비 설정이다. 여기까지 실제 동작하는 코드 영역의 클래스들이다. 이러한 클래스들이 잘 동작하길 원할 것이고, 실제 테스트를 해볼 것이지만 효율적인 테스트 방법이 무엇일까? 관련 API를 만들어서 직접 호출해본다? 과연 좋은 테스트 방법인지는 모르겠다. 이러한 DB 관련 테스트 코드를 짜보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@TestConfiguration
@EnableReactiveMongoRepositories(basePackages = {
        "com.example.test.data.db.repositories"
}, reactiveMongoTemplateRef = "userTestMongoTemplate")
public class TestEmbeddedMongoConfig {
 
    @Bean(initMethod = "start", destroyMethod = "stop")
    public MongodExecutable mongodExecutable() throws IOException {
        MongodStarter starter = MongodStarter.getDefaultInstance();
 
        IMongodConfig mongoDConfig = new MongodConfigBuilder()
                .version(Version.Main.V3_4)
                .net(new Net("127.0.0.1"27017false))
                .build();
 
        return starter.prepare(mongoDConfig);
    }
 
    @Bean("userTestMongoTemplate")
    public ReactiveMongoTemplate mongoTemplate() throws IOException {
        MongoClient mongo = MongoClients.create("mongodb://localhost:27017");
        return new ReactiveMongoTemplate(mongo, "USER_TEST_DB");
    }
}
cs

 

해당 설정은 테스트 코드에서만 사용하기 위한 설정이다. 몽고디비는 임베디드 몽고를 사용할 것이다. 필자로 사실 내부적으로 정확히 어떻게 동작하는지는 자세히 보지 않았지만, 실제 특정 포트를 물고 프로세스로 뜬다. 그래서 직접 포트 번호, ip를 기입해준다. 설정들이 다양하지만 간단히 예제를 다루어보는 것임으로 간단하게 설정을 마친다.

 

다음은 실제 테스트 코드이다.

 

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
@ContextConfiguration(
        initializers = ConfigFileApplicationContextInitializer.class,
        classes = {
                TestEmbeddedMongoConfig.class,
                UserRepositoryImpl.class
        })
@ExtendWith(SpringExtension.class)
public class UserRepositoryTest {
 
    @Autowired
    private UserRepository userRepository;
 
    private User initData;
 
    @BeforeEach
    public void setup() {
        initData = userRepository.createUser(
                User.of(
                        new ObjectId(),
                        "yeoseonggae",
                        29,
                        User.Address.of("seoul""kangbuk""mia"),
                        "none"
                )
        ).block();
    }
 
    @Test
    @DisplayName("유저 데이터 생성 테스트")
    public void createUserTest() {
        //준비
        User createUser = User.of(
                new ObjectId(),
                "UserName",
                29,
                User.Address.of("seoul""kangbuk""mia"),
                "none"
        );
 
        //실행
        Mono<User> createdUser = userRepository.createUser(createUser);
 
        //단언
        StepVerifier.create(createdUser)
                .assertNext(user -> {
                    assertEquals(user.getUsername(), "UserName");
                    assertEquals(user.getAge(), 29);
                    assertEquals(user.getHobby(), "none");
                    assertEquals(user.getAddress().getSi(), "seoul");
                }).verifyComplete();
    }
 
    @Test
    @DisplayName("유저 데이터 수정 테스트")
    public void updateUserTest() {
        //준비
        initData.setUsername("여성게");
 
        //실행
        Mono<User> updatedUser = userRepository.updateUser(initData);
 
        //단언
        StepVerifier.create(updatedUser)
                .assertNext(user -> {
                    assertEquals(user.getId().toHexString(), initData.getId().toHexString());
                    assertEquals(user.getUsername(), "여성게");
                    assertEquals(user.getAge(), 29);
                    assertEquals(user.getHobby(), "none");
                    assertEquals(user.getAddress().getSi(), "seoul");
                }).verifyComplete();
    }
 
    @Test
    @DisplayName("유저 이름으로 유저 데이터 조회 테스트")
    public void findUserByNameTest() {
        //준비
        final ObjectId id = initData.getId();
 
        //실행
        Mono<User> findUser = userRepository.findUserById(id);
 
        //단언
        StepVerifier.create(findUser)
                .assertNext(user -> {
                    assertEquals(user.getUsername(), "yeoseonggae");
                    assertEquals(user.getAge(), 29);
                    assertEquals(user.getHobby(), "none");
                    assertEquals(user.getAddress().getSi(), "seoul");
                }).verifyComplete();
    }
}
cs

 

바로 위에 있는 테스트 몽고 설정을 import 한다. 그리고 우리는 UserRepository를 테스트 할것임으로 모든 빈들을 가져올 필요가 없이 테스트에 필요한 빈만 주입받아서 테스트 코드를 작성한다. 과연 결과는 어떻게 될까?(위 코드에 조금더 추가하여 실제로 데이터가 어떻게 왔다갔다하는지 콘솔로 찍었다.)

 

우선 테스트 성공 여부이다.

 

 

크아 깔끔하게 테스트를 통과하였다. 그렇다면 진짜 데이터가 왔다갔다 한것이 맞을까? 콘솔로 찍힌 로그를 확인해보자 !

 

 

유저 데이터 수정 테스트 로그이다.

 

 

유저 데이터 생성 테스트 로그이다.

 

 

마지막으로 조회 테스트 로그이다. 실제로 데이터가 잘 찍혀있다. 여기까지 간단히 몽고디비를 이용한 DB Access unit 테스트를 다루어보았다. 정말 간단한 테스트이지만, 이 예제를 잘 이용하면 실제 서비스 클래스 영역까지 사용되어서 정말 내가 개발한 클래스가 정상 작동하는지 확인해볼 수 있다.

 

혹시 코드가 필요하다면 아래 깃헙를 참고하자.

 

 

yoonyeoseong/dbTest

Contribute to yoonyeoseong/dbTest development by creating an account on GitHub.

github.com

posted by 여성게
:
Web/Spring 2020. 2. 20. 00:46

 

오늘 다루어볼 내용은 Webflux에서 사용되는 WebClient가 onCancel이 호출되는 시점이다.  이번에 애플리케이션을 개발하면서 많은 서비스가 통신을 하는데, 필자가 개발한 애플리케이션의 jaeger 로그를 보니 WebClient가 cancelled("The subscription was cancelled") 되었다는 로그가 찍혀있었다. 무슨 이유로 이러한 로그가 남는지 확인했더니, 아래와 같은 이유였다.

 

"AServer -> BServer ->CServer"

 

이렇게 3개의 서버가 통신하는 상황인데, AServer의 Client는 ReadTimeOut이 3초이고, BServer는 ReadTimeOut이 5초이다. 그런데 CServer가 응답을 주는데 4초가 걸렸다면? 

 

이런 상황에서 발생하는 것이 cancelled 상황이다. AServer는 이미 Readtimeout이 나버려서 500 에러가 발생하였고, 이제 더이상 BServer는 응답을 받아올 필요가 없어졌으므로 subscribe를 cancel을 해버리는 것이다. 간단히 위와 같은 상황을 샘플 앱으로 작성해 보았다.

 

[AServer:9090]

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
@Configuration
public class WebClientConfig {
 
    @Bean
    public WebClient.Builder webClientFactory(){
        TcpClient tcpClient = TcpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
                .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(3000, TimeUnit.MILLISECONDS)));
 
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)));
    }
 
    @Bean
    public WebClient webClient() {
        return webClientFactory().build();
    }
 
}
 
@Slf4j
@RestController
@SpringBootApplication
@RequiredArgsConstructor
public class ExamApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ExamApplication.class, args);
    }
 
    private final WebClient client;
 
    @GetMapping("/")
    public Mono<String> hello(){
        log.info("exam server accept request");
        return client.get("http://localhost:8080/")
                .retrieveBodyToMono(String.class);
}
 
 
 
cs

 

[BServer:8080]

 

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
@Configuration
public class WebClientConfig {
 
    @Bean
    public WebClient.Builder webClientFactory(){
        TcpClient tcpClient = TcpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
                .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
 
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)));
    }
 
    @Bean
    public WebClient webClient() {
        return webClientFactory().build();
    }
 
}
 
@Slf4j
@RestController
@SpringBootApplication
@RequiredArgsConstructor
public class ClientApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }
 
    private final WebServiceClient client;
 
    @GetMapping("/")
    public Mono<String> hello(){
        log.info("client server accept request");
        return client.get("http://localhost:7070/")
                .retrieveBodyToMono(String.class);
    }
 
}
 
 
cs

 

[CServer:7070]

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@SpringBootApplication
public class ServerApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class, args);
    }
 
    @GetMapping("/")
    public String hello() throws InterruptedException {
        Thread.sleep(10000);
        return "hello";
    }
 
}
cs

 

이제 "http://localhost:9090/" 으로 요청을 보내보자!

 

 

보면 AServer(exam)은 ReadTimeOut Exception이 발생하였고, subscription was cancelled라는 메시지를 남기고 있다. 혹시나 이러한 문제가 발생한 사람이 있다면.. 적절히 서버들의 WebClient 옵션을 조정해줄 필요가 있거나 혹은 비정상적으로 응답이 느린 구간이 있을 수 있다. 하지만 때때로 클라이언트 애플리케이션에서 커낵션 풀을 사용하지 않아 과도하게 로컬 포트를 많이 개방한다거나 등의 문제로 cancelled되는 경우가 있으니, 여러가지 상황을 고려해보아야 한다.

 

마지막으로 ConnectionTimeOut, ReadTimeOut, SocketTimeOut의 차이점을 모른다면 아래 링크를 확인하자.

 

2019/02/12 - [프로그래밍언어/Java&Servlet] - Java - ConnectionTimeout,ReadTimeout,SocketTimeout 차이점?

 

Java - ConnectionTimeout,ReadTimeout,SocketTimeout 차이점?

Java - ConnectionTimeout,ReadTimeout,SocketTimeout 차이점? 사실 지금까지 웹개발을 해오면서 ConnectionTimeout,ReadTimeout,SocketTimeout에 대해 대략적으로만 알고있었지 사실 정확히 설명해봐라 혹은 차이..

coding-start.tistory.com

 

posted by 여성게
:
Web/Spring 2020. 2. 3. 21:54

 

기본적으로 MongoDB는 ObjectId라는 유니크한 primary id를 갖는다. 하지만 @Id 어노테이션을 특정 Class로 매핑시키기 위한 방법은 없을까? 예를 들어 아래와 같은 상황이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Document
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DocumentData {
 
    @Id
    private CustomId id;
    private String value;
 
    @Data
    @AllArgsConstructor(staticName = "of")
    @NoArgsConstructor
    public static class CustomId implements Serializable {
        private String idPrefix;
        private String idDelemeter;
    }
}
cs

 

DocumentData라는 Collection이 있고, 해당 Collection에는 ObjectId 타입이 아닌 CustomId 오브젝트 타입의 Id를 넣고 싶은 것이고, 실제로 Id는 내부적으로 String 타입이며 idPrefix + idDelemeter 라는 스트링으로 매핑하고 싶다. 즉, CustomId("prefix", "!"); 로 생성된 오브젝트가 있고 이것이 실제로 Mongodb에 들어가면 _id : "prefix!" 인 형태로 저장이 하고 싶은 것이다. 이럴때는 어떻게 해야할까?

 

1
2
3
4
5
6
7
8
9
10
11
    @Bean
    public MongoCustomConversions customConversions() {
        return new MongoCustomConversions(Collections.singletonList(new CustomIdConverter()));
    }
 
    public static class CustomIdConverter implements Converter<DocumentData.CustomId, String> {
        @Override
        public String convert(DocumentData.CustomId source) {
            return source.getIdPrefix() + source.getIdDelemeter();
        }
    }
cs

 

위와 같이 MongoCustomConversions를 Bean으로 등록하면 특정 오브젝트 타입이 @Id로 Mapping 되어 있을 때, 구현한 Converter 내용에 따라 적절히 _id 값이 들어가게 된다. 위의 구현은 CustomId 오브젝트 타입이 @Id로 매핑되어 있을 때, idPrefix와 IdDelemeter를 조합하여 String Type의 _id 값을 만들어 낸다. 그리고 기존의 ObjectId로 매핑한 @Id도 그대로 사용가능하다.

 

1
2
3
4
5
6
7
8
public interface MongoRepositoryImpl extends ReactiveMongoRepository<DocumentData, DocumentData.CustomId> {}
 
@PostMapping("/mongo")
public Mono<DocumentData> save(){
    DocumentData data = new DocumentData(DocumentData.CustomId.of("prefix","!"), "value");
    mongoRepository.save(data);
    return mongoRepository.save(data);
}
cs

 

실제로 간단히 API를 만들어 요청을 보내보면 아래와 같이 데이터가 삽입되어 있다.

 

1
2
3
_id : "prefix!"
value : "value"
_class : "com.webflux.mongoexam.DocumentData"
cs

 

실제 필자도 내부적으로 자체적인 복잡한 ID 체계를 가져가기 위해 MongoCustomConversions를 빈으로 등록하여 사용하고 있다.

 

추가적으로 하나더 이슈가 있다. 만약에 특정 커스텀 클래스를 @Id class로 사용하려면 해당 클래스가 반드시 Serializable을 구현하는 클래스이어야한다! 이 이슈를 발견하게 된 것은 ReactiveMongoRepository의 findById를 사용할때 였다. 아이디클래스가 Serializable을 구현하지 않으면 예외를 내뱉는다.

 

 

{
    logEvent: "java.lang.ClassCastException",
    errorMsg: "class xxx cannot be cast to class java.io.Serializable (xxx is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @2c4eab42; java.io.Serializable is in module java.base of loader 'bootstrap')"
}

 

posted by 여성게
:
Web/Netty 2020. 2. 1. 14:12

 

오늘 다루어볼 포스팅 내용은 Netty의 개념과 아키텍쳐에 대한 대략적인 설명이다. Netty에 대해 알아보기 전에 AS-IS 자바의 네트워킹 동작 방식에 대해 먼저 다루어본다.

 

자바의 네트워킹

순수 자바로 네트워크 통신을 하기위해서 생긴 최초의 라이브러리는 java.net 패키지이다. 해당 소켓 라이브러리가 제공하는 방식은 블로킹 함수만 지원했다. 해당 라이브러리를 이용한 서버코드를 간단히 보면 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public void blockCall() throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        Socket clientSocket = serverSocket.accept();
        BufferedReader in = new BufferedReader(
                new InputStreamReader(clientSocket.getInputStream())
        );
 
        PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
 
        String request, response;
        while ((request = in.readLine()) != null) {
            if ("OK".equals(request)) {
                break;
            }
            response = processRequest(request);
            System.out.println(response);
        }
    }
 
    public String processRequest(String request) {
        return request + "Done";
    }
cs

 

해당 코드는 한 번에 한 연결만 처리한다. 다수의 동시 클라이언트를 관리하려면 새로운 클라이언트 Socket마다 새로운 Thread를 할당해야한다. 이런식의 블로킹 처리는 어떠한 결과를 초래하게 될 것인가? 여러 스레드가 입력이나 출력 데이터가 들어오기를 기다리며 무한정 대기 상태로 유지될 수 있고, 이것은 고로 리소스의 낭비로 이어진다. 그리고 하나의 연결당 하나의 스레드가 생성되므로, 많은 수의 클라이언트를 관리하기 위해서는 많은 수의 스레드를 생성해야 하고, 이것은 리소스 낭비는 물론 잦은 컨텍스트 스위칭에 의한 오버헤드가 발생하게 된다. 

 

이러한 문제때문에 그 다음 나온 자바의 네트워킹 통신은 자바 NIO방식이다.

 

자바 NIO

해당 방식은 네트워크 리소스 사용률을 세부적으로 제어할 수 있는 논블로킹 호출이 포함되어 있다. 논블로킹은 내부적으로 시스템의 이벤트 통지 API를 이용해 논블로킹 소켓의 집합을 등록하면 읽거나 기록할 데이터가 준비됐는지 여부를 알 수 있다. 즉, 계속해서 기다릴 필요가 없어지는 것이다.

 

 

OIO와는 달리 NIO는 채널과 소켓이 1:1 매칭되지 않는다. java.nio.channels.Selector가 논블로킹 Socket의 집합에서 입출력이 가능한 항목을 지정하기 위해 이벤트 통지 API를 이용하기 때문에, 언제든지 읽기나 쓰기 작업의 완료상태를 확인가능하다. 그렇기 때문에 채널당 하나의 쓰레드가 분배되지 않고(블록킹하면 기다리지 않아도) 필요할때마다 쓰레드에게 통지를 하므로써 필요할때만 일을 할 수 있고, 필요하지 않을때는 다른 일을 할 수 있게 한다. 이런식의 처리흐름을 도입함으로써

 

  • 적은 수의 스레드로 더 많은 연결을 처리할 수 있어 메모리 관리와 컨텍스트 스위치에 대한 오버헤드가 준다.
  • 입출력을 처리하지 않을 때는 스레드를 다른 작업에 활용할 수 있다.

 

라는 장점이 생긴다. 하지만 그에 따른 단점이 존재한다면, 순수 자바 라이브러리를 직접 사용해 애플리케이션을 제작하기에는 아주 어렵다. 특히 부하가 높은 상황에서 입출력을 안정적이고 효율적으로 처리하고 호출하는 것과 같이 까다롭고 문제 발생이 높은 일은 어려운 일이기 때문이다.

 

이러한 어렵고 까다로운 일을 대신해주기 위해 네티와 같은 네트워크 프레임워크가 존재하는 것이다.

 

Netty(네티)

네티는 위와 같이 어려운 자바의 고급 API를 내부에 숨겨 놓고, 사용자에게 비즈니스 로직에만 집중할 수 있도록 추상화한 API를 제공한다. 또한 적은양의 리소스를 소모하면서 더 많은 요청을 처리할 수 있게 설계되었으므로 확장하기에도 부담이 없다. 아래는 네티의 특징을 요약한 표이다.

 

category feature
설계 단일 API로 블로킹과 논블로킹 방식의 여러 전송 유형을 지원하며, 단순하지만 강력한 스레딩 모델을 제공한다. 
이용 편의성 JDK 1/6+을 제외한 추가 의존성이 필요 없다.
성능 코어 자바 API보다 높은 처리량과 짧은 지연시간을 갖는다. 풀링과 재사용을 통한 리소스 소비를 감소시켰고, 메모리 복사를 최소화하였다.
견고성 저속, 고속 또는 과부하 연결로 인한 OOM이 잘 발생하지 않는다.(요청을 처리하는 스레드 수가 굉장히 적기 때문) 고속 네트워크 상의 NIO 애플리케이션에서 일반적인 읽기/쓰기 비율 불균형이 발생하지 않는다.
보안 완벽한 SSL/TLS 및 StarTLS 지원. 애플릿이나 OSGi 같은 제한된 환경에서도 이용 가능.

 

비동기식 이벤트 기반 네트워킹

 

  • 네티의 논블로킹 네트워크 연결은 작업 완료를 기다릴 필요가 없다. 완전 비동기 입출력은 이 특징을 바탕으로 한 단계 더 나아간다. 비동기 메서드는 즉시 반환하며 작업이 완료되면 직접 또는 나중에 이를 통지한다. 
  • 셀렉터는 적은 수의 스레드로 여러 연결에서 이벤트를 모니터링할 수 있게 해준다

 

위의 특징들을 종합해보면 블로킹 입출력 방식을 이용할 때보다 더 많은 이벤트를 훨씬 빠르고 경제적으로 처리할 수 있다. 

 

네티의 핵심 컴포넌트들

  • Channel
  • Callback
  • Future
  • 이벤트와 핸들러

Channel은 하나 이상의 입출력 작업을 수행할 수 있는 하드웨어 장치, 파일, 네트워크 소켓, 프로그램 컴포넌트와 같은 엔티티에 대한 열린 연결을 뜻한다. 쉽게 말해 인바운드 데이터와 아웃바운드 데이터를 위한 운송수단이라고 생각하면 좋을 것 같다.

 

Callback은 간단히 말해 다른 메서드로 자신에 대한 참조를 제공할 수 있는 메서드다. 관심 대상에게 작업 완료를 알리는 가장 일반적인 방법 중 하나이다. 네티는 이러한 콜백을 내부적으로 이벤트 처리에 사용한다.

 

Future는 작업이 완료되면 애플리케이션에게 알리는 한 방법이다. 즉, 작업이 완료되는 미래의 어떤 시점에 그 결과에 접근할 수 있게 해준다. 하지만 자바의 Future는 결과를 얻기 위해서는 반드시 블로킹해야만 한다. 그래서 네티는 비동기 작업이 실행됐을 때 이용할 수 있는 자체 구현 ChannelFuture를 제공한다. 더 자세한 내용은 뒤에서 다루어본다.

 

마지막으로 네티는 이벤트가 들어오면 해당 이벤트를 핸들러 클래스의 사용자 구현 메서드로 전달하여 이벤트를 변환하며 이러한 핸들러의 체인으로 이벤트들을 처리한다. 이벤트와 핸들러 또한 뒤에서 더 자세히 다룬다.

posted by 여성게
:
Web/Spring 2019. 10. 23. 21:33

응답으로 나가는 Object에 getter가 존재하지 않아서 Response Content-type을 추론하지 못했다. 그래서 필터 단에서 임의로 DefaultMediaType으로 */*? * 로 넣어버리지만 사실 Content-Type에 wildCard는 들어가지 못한다. 즉, 예외발생!

posted by 여성게
:
Web/Gradle 2019. 10. 21. 20:47

 

이번 포스팅은 간단하게 Gradle Task를 작성하는 방법이다. 모든 경우의 Task 작성 방법을 다루지는 않지만 몇가지 예제를 다루어볼 것이다.

 

1
2
3
4
5
task hello{
    doLast{
        println 'Hello'
    }
}
cs

 

위는 간단하게 'Hello'라는 문자열을 출력하는 태스크이다. 아래 명령으로 실행시킨다.

 

1
2
3
4
5
gradle -q hello
 
result->
Hello
 
cs

 

-q 옵션 같은 경우는 로그 출력없이 결과값만 출력하는 옵션이다. 만약 -q 옵션을 뺀다면 빌드에 걸린 시간등의 로그가 찍히게 된다.

 

디폴트 태스크 정의

gradle -q 라는 명령어로 실행하는 디폴트 태스크를 정의하는 방법이다. 보통 빌드전에 clean, install 등의 작업을 기계적으로 하는 경우가 많은데, 디폴트 태스크로 정의하여 사용하면 보다 간편하다.

 

1
2
//Default Task usecase : gradle -q
defaultTasks 'bye', 'variablePrint'
cs

 

 

태스크간 의존성

다음은 태스크간 의존성을 설정하는 방법이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Task 의존성설정
task bye{
    dependsOn 'hello'
    doLast{
        println 'bye'
    }
}
 
task hello{
    doLast{
        println 'Hello'
    }
}
 
cs

 

위 태스크는 bye를 실행하기 전에 hello Task를 실행시키는 의존성을 설정하였다.

 

1
2
3
4
5
6
gradle -q bye
 
rsult->
Hello
bye
 
cs

 

태스크에서 변수사용 하기

태스크에서 사용자 정의 변수를 정의해서 사용가능하다. 결과값은 생략한다.

 

1
2
3
4
5
6
7
8
9
10
//변수사용방법
task variableTask{
    ext.myProperty = 'testProperty'
}
 
task variablePrint{
    doLast{
        println variableTask.myProperty
    }
}
cs

 

사용자 정의 메서드 사용하기

gradle은 groovy, kotlin 스크립트를 이용한 빌드툴이다. 즉, 변수선언은 물론 메서드를 정의해서 사용가능하다.

 

1
2
3
4
5
6
7
8
9
10
//메서드 사용
task methodTask{
    doLast{
        printStr('method args')
    }
}
 
String printStr(String arg){
    println arg
}
cs

 

빌드스크립트 자체에 의존성 라이브러리가 필요할 때

프로젝트가 사용하는 라이브러리는 물론, 빌드 스크립트 자체가 어떠한 외부 라이브러리가 필요할 때가 있다. 왜냐? gradle 자체는 groovy 언어로 작성하기 때문에 다른 외부 라이브러리를 사용가능하다!

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
빌드스크립트 코드 자체가 외부 라이브러리를 필요로 할때
멀티 프로젝트 일 경우 루트 build.gradle에 선언하면
모든 하위 프로젝트 build.gradle에 반영된다.
 */
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath group: 'commons-codec', name: 'commons-codec', version: '1.2'
    }
}
 
import org.apache.*
 
task encode{
    doLast{
        Base64.Encoder encoder = Base64.getEncoder()
        println encoder.encode(variableTask.myProperty.getBytes())
    }
}
cs

 

태스크 수행 순서 제어

태스크의 수행 순서를 제어하는 방법이다.

 

1
2
3
4
5
6
7
8
9
task taskX{
    println 'taskX'
}
 
task taskY{
    println 'taskY'
}
 
taskX.mustRunAfter taskY
cs

 

If문을 이용한 태스크 생략

if문을 이용하여 특정 태스크를 생략할 수 있다.

 

1
2
3
4
5
6
7
8
9
task conditionTask{
    doLast{
        println 'conditinal Task'
    }
}
 
conditionTask.onlyIf{!project.hasProperty('skip')}
 
>gradle -q -Pskip conditionTask
cs

 

태스크 비활성화

태스크를 지우고 싶진 않고, 당장은 사용하지 않아도 추후에 사용될 수도 있다면 비활성화 기능을 사용해보아도 좋을 듯하다.

 

1
2
3
4
5
6
7
task disableTask{
    doLast{
        println 'disabled'
    }
}
 
disableTask.enabled=true
cs

 

 

여기까지 간단하게 Task를 작성하는 방법을 다루어봤다. 사실 반복문등을 사용하는 것도 가능하다. 많은 태스크 예제가 있지만, 여기까지 간단하게 gradle task 작성방법의 맛을 보았다.

posted by 여성게
:
Web/Gradle 2019. 10. 20. 21:03

 

이번 포스팅은 그래들을 이용한 자바 프로젝트 구성에 대해 다루어볼 것이다. 그래들로 자바 프로젝트를 초기화 하는 방법은 이전 포스팅에 있으니 참고 바란다.

 

2019/10/20 - [Web/Gradle] - Gradle - Gradle의 기본

 

Gradle - Gradle의 기본

이번 포스팅은 Gradle에 대한 기본을 다루어볼 것이다. 사실 Gradle이 뭔지 모르는 개발자는 거의 없을 것임으로, 자세한 설명은 하지 않을 것이다. Gradle은 빌드툴이다! (마치 Maven과 같은) Gradle 내부 프로..

coding-start.tistory.com

 

자바 타입으로 그래들 프로젝트를 생성하면 아래와 같은 기본 디렉토리 구조를 가진다.

 

src

    -main

       -java

    -test

       -java

 

그래들의 자바 플러그인의 Task 의존 관계는 아래 그림과 같다.

 

 

build를 실행하게 되면 compileJava와 test Task가 실행된다. 그런데 이전 단계 Task가 실패하면 다음 단계는 진행되지 않는다. 

 

-컴파일시 인코딩 오류가 날 경우

>gradle compileJava 

 

위와 같은 명령으로 컴파일 할때 인코딩 문제가 있다면 아래와 같이 옵션 값을 넣어준다.(build.gradle)

 

compileJava.options.encoding='UTF-8'

 

혹은 gradle.properties 파일에서 그래들의 jvmargs 환경변수로 값을 추가할 수도 있다.

 

org.gradle.jvmargs=-Dfile.encoding=UTF-8

 

 

-컴파일 단게에서 테스트 클래스들을 제외하는 방법

 

1
2
3
4
5
6
7
8
sourceSets{
    main{
        java{
            srcDirs = ['src','extraSrc']
            exclude 'test/*'
        }
    }
}
cs

 

컴파일 태스크가 완료되면 build 디렉토리에 컴파일된 클래스 파일들이 생겨난다. 그런데, 이전에 생성된 파일과 중복되지 않도록 파일을 삭제한 후에 컴파일해야 하는 경우가 있다. 이럴 때는 Clean Task를 사용한다. 해당 태스크를 이용하면 빌드한 결과물이 생성되는 build 디렉토리 내용 전체가 삭제된다. 또한, clean Task는 다른 Task와 조합해서 사용할 수 있다.

 

>gradle clean

 

위 명령을 실행하면 build 디렉토리가 삭제되는 것을 볼 수 있다.

 

-의존성 관리하기

자바는 JVM을 통해 운영체제에 독립적으로 동작하고, 라이브러리를 만들때에도 운영체제별로 만들지 않고 자바 런타임에 호환되도록 만들면 되므로 타 언어에 비해서 오픈소스나 라이브러리들이 많은 편이다. 그래들은 이러한 라이브러리들을 편리하게 관리할 수 있도록 지원한다.

 

1)자바 프로젝트의 라이브러리 스코프

개발을 진행할 때 대표적으로 세 가지의 작업(컴파일, 테스트, 실행)을 한다. 각 작업에서 사용되는 라이브러리가 매우 다양하다. 그래들에서는 사용하는 라이브러리가 중복되지 않도록 스코프를 지원한다. 자바 프로젝트에서 지원하는 스코프는 dependencies 명령으로 확인할 수 있다.

 

>gradle dependencies

 

스코프별로 포함된 라이브러리가 출력되는데, 주로 사용하는 스코프는 compile, testCompile, runtime이다.

 

스코프 Task 설명
compile compileJava 컴파일 시 포함해야 할 때
runtime - 실행시점에 포함해야 할 때
testCompile compileTestJava 테스트를 위한 컴파일 시 포함해야 할때
testRuntime test 테스트를 실행시킬 때

 

2)라이브러리 추가

 

1
2
3
4
5
6
7
8
9
10
11
12
dependencies {
    // This dependency is exported to consumers, that is to say found on their compile classpath.
    api 'org.apache.commons:commons-math3:3.6.1'
 
    // This dependency is used internally, and not exposed to consumers on their own compile classpath.
    implementation 'com.google.guava:guava:28.0-jre'
 
    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'
 
    compile 'com.itextpdf:itextpdf:5.5.5'
}
cs

 

마지막에 compile scope로 라이브러리를 하나 추가하였다. 그리고 아래 Task를 실행하면 compile scope에 라이브러리가 추가된 것을 볼 수 있다.

 

>gradle dependencies

 

이제 컴파일 태스크를 실행하면 위의 의존 라이브러리를 내려받을 것이다.

 

3)패키징하기

Java 플러그인을 사용한 상태에서 build 명령을 사용하면 기본으로 JAR 형태로 팩킹된다. 그래들에서 JAR파일은 libs 디렉토리에 생성되며 기본값으로 설정되어 있다. JAR 파일은 두가지 형태가 있다.

 

  1. 실행하기 위한 형태로 배포하는 JAR
  2. 라이브러리로 사용하는 JAR

만약 라이브러리 용도로 사용하는 JAR라면 별도 설정할 것이 없지만, 이러한 JAR를 실행시키면 아래와 같은 예외 메시지가 나타날것이다.

 

"*.jar에 기본 Manifest 속성이 없습니다."

 

JAR를 gradle task로 실행시키기 위해서는 아래 설정을 build.gradle에 추가하고 run task를 수행하면 된다.

 

apply plugin: 'application'
mainClassName="package.classname"

 

>gradle run

 

gradle run 태스크를 수행하면 jar파일이 실행된다.

 

Maven 프로젝트를 Gradle 프로젝트로 변환

 

>gradle init --type pom

 

 

posted by 여성게
: