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 Cloud 2019. 8. 25. 18:38

 

2019/02/24 - [Web/Spring Cloud] - Spring Cloud - Eureka를 이용한 마이크로서비스 동적등록&탐색&부하분산처리

 

Spring Cloud - Eureka를 이용한 마이크로서비스 동적등록&탐색&부하분산처리

Spring Cloud - Eureka를 이용한 마이크로서비스 동적등록&탐색&부하분산처리 스프링 클라우드 유레카는 넷플릭스 OSS에서 유래됐다. 자가 등록, 동적 탐색 및 부하 분산에 주로 사용되며, 부하 분산을 위해 내부..

coding-start.tistory.com

우리는 이전 포스팅들에서 Spring Cloud를 다루어보면서 동적인 서비스 등록과 서버사이드 로드밸런싱에 중대한 역할을 하게 되는 Eureka에 대해 다루어 봤었다. 이러한 유레카를 이용하여 우리는 애플리케이션의 무중단 배포도 가능하다. 새로운 애플리케이션을 올리고 이전 버전의 애플리케이션을 죽이는 단순한 과정에서 우리는 중요한 개념을 생각해야 한다. 만약 이전 버전의 애플리케이션이 사용자의 요청을 받아 처리 중이라면? 그냥 애플리케이션을 죽이면 처리중인 요청을 끝까지 처리하지 못하고 데이터 유실이 발생할 것이다. 이럴때 우리는 우아하게 종료할 수 있는 방안이 필요하다. 예를 들면, Apache에서도 프로세스를 재시작하는 명령에 restart / graceful 명령이 존재한다. 전자는 단순히 stop&start이고 후자는 받은 요청을 모두 처리하고 종료하게 된다. 이러한 기능을 Spring boot는 어떻게 제공할까?

 

Actuator를 사용하면 된다. 스프링 액츄에이터는 다양하게 실행 중인 애플리케이션의 모니터링 정보 및 유용한 기능을 제공한다. 이중 shutdown 기능이 있는데, 액츄에이터의 shutdown은 graceful 하게 shutdown을 시켜준다 ! 즉, 데이터 유실 없이 안전하고 우아한 애플리케이션 종료를 제공한다.

posted by 여성게
:
Web/Spring 2019. 8. 17. 19:31

 

스프링에서 빈을 생성할 때, 기본 전략은 모든 빈이 싱글톤으로 생성된다. 즉, 어디에서든지 빈을 주입받는 다면 동일한 빈을 주입받는 것을 보장한다. 하지만 필요에 따라서 빈 주입마다 새로운 빈을 생성해야할 필요가 있을 경우도 있다. 이럴 경우에는 빈 생성시 Scope를 prototype으로 주면 빈 주입마다 새로운 인스턴스가 생성되는 것을 보장한다. 하지만 프로토타입 빈을 사용할 경우 주의해야 할 상황이 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class ABean {
    
    @Autowired
    private BBean b;
 
    public void bMethod() {
        b.print();
    }
    
}
 
@Component
@Scope("prototype")
public class BBean {
    
    public void print() {
        System.out.println("BBean !");
    }
}
cs

 

이런식으로 사용한다면 어떻게 될까? 이것은 사실 프로토타입을 쓰나마나이다. 싱글톤으로 생성된 A빈에 프로토타입 B빈을 주입해봤자 A빈은 더이상 생성되지 않기 때문에 항상 동일한 B빈을 사용하게 되는 것이다.

 

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
@Component
public class ABean {
    
    @Autowired
    private BBean b;
    
    public void print() {
        System.out.println(b.hashCode());
    }
    
}
 
@Component
@Scope("prototype")
public class BBean {
    
}
 
@Component
public class CBean {
    
    @Autowired
    private ABean a;
    
    public void print() {
        a.print();
    }
}
 
@Component
public class DBean {
    
    @Autowired
    private ABean a;
    
    public void print() {
        a.print();
    }
}
 
@SpringBootApplication
public class CircularReferenceApplication implements CommandLineRunner{
    
    @Autowired private CBean c;
    @Autowired private DBean d;
 
    public static void main(String[] args) {
        SpringApplication.run(CircularReferenceApplication.class, args);
    }
 
    @Override
    public void run(String... args) throws Exception {
        c.print();
        d.print();
    }
    
}
cs

 

위 코드를 실행시켜보자. 프로토타입 빈으로 등록된 B빈이지만 항상 어디서든 동일한 해시코드를 반환한다. 즉, 프로토타입빈을 사용하기 위해서는 아래와 같이 사용하자.

 

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
@Service
public class BeanUtil implements ApplicationContextAware {
 
    private static ApplicationContext context;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
 
    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }
}
 
@Component
public class ABean {
    
    public void print() {
        BBean b = BeanUtil.getBean(BBean.class);
        System.out.println(b.hashCode());
    }
    
}
 
cs

 

ApplicationContext 객체에서 직접 빈을 가져와서 메소드 내부에서 사용하도록 하자. 이제는 매번 다른 B빈의 해시코드를 반환할 것이다.

 

2019/02/25 - [Web/Spring] - Spring - ApplicationContext,ApplicationContextAware, 빈이 아닌 객체에 빈주입할때!

 

Spring - ApplicationContext,ApplicationContextAware, 빈이 아닌 객체에 빈주입할때!

Spring - ApplicationContext,ApplicationContextAware, 빈이 아닌 객체에 빈주입할때! @Autuwired,@Inject 등의 어노테이션으로 의존주입을 하기 위해서는 해당 객체가 빈으로 등록되어 있어야만 가능하다. 사실..

coding-start.tistory.com

 

posted by 여성게
:
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. 5. 30. 10:32

오늘 포스팅할 내용은 웹프로그래밍에서 아주 자주 쓰이는 내용입니다. 바로 JSON->Object 혹은 Object->JSON 컨버팅하는 라이브러리 소개입니다. 우선은 스프링에서는 기본적으로 ObjectMapper라는 라이브러리를 사용하여 컨버팅 작업을 하는데, 해당 라이브러리 이외에 Gson이라는 라이브러리를 이용할 수도 있습니다.

posted by 여성게
:
Web/Maven 2019. 4. 30. 11:55

 

오늘 다루어볼 포스팅 내용은 Maven Multi Module을 이용한 Spring Boot Project 만들기입니다. 우선 Maven Multi Module 프로젝트란 하나의 부모 Maven Project를 생성하고 그 밑으로 자식 Maven Module들을 가지는 구조입니다. 부모의 pom.xml에 공통적인 의존 라이브러리를 넣어주면 다른 자식 Maven Module에서는 그대로 사용이 가능합니다. 또한 JPA관련된 모든 소스코드를 common이라는 Maven Module로 만들어서 다른 Maven Module에서 사용하여 공통적인 중복코드를 줄일 수도 있습니다. 바로 예제로 들어가겠습니다.

 

Maven Multi Project

 

모든 예제는 Eclipse + Mac OS 기반으로 작성되었습니다.

 

 

 

오늘 구성해볼 프로젝트 구조입니다. 나중에 OAuth2.0 포스팅에서 다루어 봤던 예제를 다시 정리하여 올릴 소스를 정리할겸 구조를 잡아보려고합니다. 간단히 프로젝트 구조에 대해 설명하자면 oauth2라는 Maven Project를 생성할 겁니다. 이것이 바로 부모 Maven Project가 됩니다. 그리고 그 하위 자식 Maven Moduleauthorizationserver,resourceserver,client 세개의 모듈을 만들겁니다. 그리고 마지막으로 공통적으로 사용할 JPA관련 프로젝트를 common이라는 공통 Maven Module로 만들어 모든 자식 모듈에서 import하여 사용하도록 할 것입니다. common이라는 공통 JPA 프로젝트를 만드는 이유는 만약 3개의 자식 Maven Module들이 같은 DB를 공유하고 있다면 중복되는 코드를 대폭 줄일 수 있습니다. 바로 예제로 들어가겠습니다.

 

첫번째로 Package Explorer에서 오른쪽 클릭하여 New를 선택합니다. 그리고 Maven Project를 클릭합니다.

 

 

Create a simple project를 클릭하고 Next 버튼을 누릅니다.

 

 

 

Group IdArtifact Id를 입력해주고 Packagingpom으로 바꾸어준후 Finish를 클릭합니다. 잘 생성이 되었다면 oauth2프로젝트가 생성이 되고 그 밑으로는 src 폴더와 pom.xml이 생성되어 있을 겁니다. src폴더는 삭제해줍니다.

 

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.spring.security</groupId>
    <artifactId>oauth2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
        <module>authorizationserver</module>
        <module>resourceserver</module>
        <module>client</module>
    <module>common</module>
  </modules>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <repositories>
        <!-- 오라클 저장소 -->
        <repository>
            <id>codelds</id>
            <url>https://code.lds.org/nexus/content/groups/main-repo</url>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
          <groupId>org.webjars</groupId>
          <artifactId>bootstrap</artifactId>
          <version>3.3.5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc6</artifactId>
            <version>11.2.0.3</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ucp</artifactId>
            <version>11.2.0.3</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
cs

 

oauth2 프로젝트에 위의 pom.xml 코드를 삽입해줍니다. 그러면 이후에 생성되는 Maven Module들은 위의 라이브러리 설정들을 상속받게 됩니다.oauth2라는 부모 프로젝트를 오른쪽 클릭하여 New를 클릭하고 Maven Module을 클릭해줍니다.

 

 

 

 

Next 를 클릭합니다.

 

 

 

생성할 Module 이름을 작성한 후에 Next를 누릅니다.

 

 

 

현재 select 되어있는 Artifact Id를 누른 후에 Next 버튼을 누릅니다.

 

 

적당한 Group Id를 정해줄 수 있지만 저는 부모 Maven Project 꺼를 default로 사용하였습니다.

 

 

나머지 모든 Module도 동일하게 생성해줍니다. 아마 위와 같은 구조로 생성이 되어있을 겁니다. 하지만 우리가 생각하는 Spring Boot 프로젝트와는 거의 유사하지만 없는 것들이 존재합니다. src/main/resources 입니다. 프로젝트를 오른쪽클릭하여 Project Bulid Path를 누르고 source tab에서 add Folder를 해줍니다. 그리고 main을 클릭하고 새로운 resources 폴더를 생성합니다. 그리고 resourcesbulid 경로를 바꿔줍니다.

 

 

src/main/resources output folderedit해줍니다.

 

 

target/classes를 경로로 잡습니다. 그러면 이제 이 프로젝트는 bulid 이후 해당 경로로 리소스들을 떨궈줄것입니다.

 

 

Excluded**로 설정할겁니다. 그래야 해당 디렉토리로 폴더를 생성하면 패키지처럼 보이지 않을겁니다.

 

 

마지막으로 Spring Boot Main 클래스처럼 @SpringApplication 어노테이션을 달고 main method에 run method를 달아줍니다.

 

여기까지 Maven Multi Module Spring boot Project 만들기였습니다.

'Web > Maven' 카테고리의 다른 글

Apache Maven이란?(아파치 메이븐)  (0) 2019.04.04
메이븐 멀티프로젝트(maven multi module) & SVN  (0) 2018.09.30
posted by 여성게
:
Web/Spring 2019. 4. 2. 21:57

Spring boot - Redis를 이용한 HttpSession


오늘의 포스팅은 Spring boot 환경에서 Redis를 이용한 HttpSession 사용법입니다. 무슨 말이냐? 일반 Springframework와는 다르게 Spring boot 환경에서는 그냥 HttpSession을 사용하는 것이 아니고, Redis와 같은 in-memory DB 혹은 RDB(JDBC),MongoDB와 같은 외부 저장소를 이용하여 HttpSession을 이용합니다. 어떻게 보면 단점이라고 볼 수 있지만, 다른 한편으로는 장점?도 존재합니다. 일반 war 형태의 배포인 Dynamic Web은 같은 애플리케이션을 여러개 띄울 경우 세션 공유를 위하여 WAS단에서 Session Clustering 설정이 들어갑니다. 물론 WAS 설정에 익숙한 분들이라면 별 문제 없이 설정가능하지만, WAS설정 등에 미숙하다면 확실함 없이 구글링을 통하여 막 찾아서 설정을 할 것입니다. 물론 나쁘다는 것은 아닙니다. 벤치마킹 또한 하나의 전략이니까요. 하지만 Spring boot의 경우 Session Cluster를 위하여 별도의 설정은 필요하지 않습니다. 이중화를 위한 같은 애플리케이션 여러개가 HttpSession을 위한 같은 저장소만 바라보면 됩니다. 어떻게 보면 설정이 하나 추가된 것이긴 하지만 익숙한 application.properties등에 설정을 하니, 자동완성도 되고... 실수할 일도 줄고, 디버깅을 통해 테스트도 가능합니다. 크게 중요한 이야기는 아니므로 바로 예제를 들어가겠습니다.



테스트 환경

  • Spring boot 2.1.3.RELEASE(App1,App2)
  • Redis 5.0.3 Cluster(Master-6379,6380,6381 Slave-6382,6383,6384)

만약 Redis Cluster환경을 구성한 이유는, 프로덕 환경에서는 Redis 한대로는 위험부담이 있기때문에 고가용성을 위하여 클러스터 환경으로 테스트를 진행하였습니다. 한대가 죽어도 서비스되게 하기 위해서이죠. 만약 한대로 하시고 싶다면 한대로 진행하셔도 됩니다. 하지만 클러스터환경을 구성하고 싶지만 환경구성에 대해 잘 모르시는 분은 아래 링크를 참조하여 구성하시길 바랍니다.


▶︎▶︎▶︎2019/03/01 - [Redis] - Springboot,Redis - Springboot Redis Nodes Cluster !(레디스 클러스터)

▶︎▶︎▶︎2019/02/28 - [Redis] - Redis - Cluster & Sentinel 차이점 및 Redis에 대해


애플리케이션은 총 2대를 준비하였고, 한대를 클라이언트 진입점인 API G/W, 한대를 서비스 애플리케이션이라고 가정하고 테스트를 진행하였습니다. 즉, 두 애플리케이션이 하나의 세션을 공유할 수 있을까라는 궁금즘을 해결하기 위한 구성이라고 보시면 됩니다. 사실 같은 애플리케이션을 이중화 구성을 한 것이 아니고 별도의 2개의 애플리케이션끼리 세션을 공유해도 되는지는 아직 의문입니다. 하지만 다른 애플리케이션끼리도 HttpSession을 공유할 수 있다면 많은 이점이 있을 것같아서 진행한 테스트입니다.


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
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
cs


두개의 애플리케이션에 동일하게 의존 라이브러리를 추가해줍니다. 저는 부트 프로젝트 생성시 Web을 체크하였고, 나머지 위에 4개는 수동으로 추가해주었습니다.


1
2
3
spring.session.store-type=redis
spring.redis.cluster.nodes=127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381
 
cs


application.properties입니다. 이 또한 두개의 애플리케이션에 동일하게 넣어줍니다. 간단히 설정에 대해 설명하면 spring.session.store-type=redis는 HttpSession 데이터를 위한 저장소를 Redis를 이용하겠다는 설정입니다. Redis 말고도 MongoDB,JDBC등이 있습니다. 두번째 spring.redis.cluster.nodes=~설정은 저장소로 사용할 Redis의 클러스터 노드 리스트(마스터)를 넣어줍니다. 


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
     */
    private List<String> nodes;
 
    public List<String> getNodes() {
        return nodes;
    }
 
    public void setNodes(List<String> nodes) {
        this.nodes = nodes;
    }
    
    
}
cs


Redis 설정을 위하여 클러스터 노드리스트 값을 application.proerties에서 읽어올 빈입니다.


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
@Configuration
public class RedisConfig {
    /**
     * Redis Cluster 구성 설정
     */
    @Autowired
    private RedisClusterConfigurationProperties clusterProperties;
 
    /**
     * JedisPool관련 설정
     * @return
     */
    @Bean
    public JedisPoolConfig jedisPoolConfig() {
        return new JedisPoolConfig();
    }
    
    /**
     * Redis Cluster 구성 설정 - Cluster 구성
     */
    @Bean
    public RedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig) {
        return new JedisConnectionFactory(new RedisClusterConfiguration(clusterProperties.getNodes()),jedisPoolConfig);
    }
        
}
cs


JedisPoolConfig 및 RedisConnectionFacotry 빈입니다. 아주 작동만 할 수 있는 기본입니다. 추후에는 적절히 설정값을 넣어서 성능 튜닝이 필요합니다.


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
@Slf4j
@EnableRedisHttpSession
@RestController
@SpringBootApplication
public class SessionWebTest1Application {
 
    public static void main(String[] args) {
        SpringApplication.run(SessionWebTest1Application.class, args);
    }
    
    @GetMapping("/request")
    public String getCookie(HttpSession session) {
        String sessionKey = session.getId();
        session.setAttribute("ID""yeoseong_yoon");
        log.info("set userId = {}","yeoseong_yoon");
        
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders header = new HttpHeaders();
        header.add("Cookie""SESSION="+redisSessionId);
        HttpEntity<String> requestEntity = new HttpEntity<>(null, header);
        
        ResponseEntity<String> cookieValue = restTemplate.exchange("http://localhost:8090/request",HttpMethod.GET ,requestEntity ,String.class);
        return "server1_sessionKey : "+session.getId()+"<br>server2_sessionKey : "+cookieValue.getBody();
    }
    
}
 
cs


App1의 클래스입니다. 우선 로그를 찍기위해 lombok 어노테이션을 사용하였고, Redis를 이용한 HttpSession 사용을 위해 @EnableRedisHttpSession 어노테이션을 선언하였습니다. 여기서 조금 특이한 점은 RestTemplate 요청에 SESSION이라는 쿠키값을 하나 포함시켜 보내는 것입니다. 잘 생각해보면 일반 웹프로젝트에서는 세션객체의 식별을 위해 JSESSIONID라는 쿠키값을 이용합니다. 이것과 동일한 용도로 Redis HttpSession은 SESSION이라는 쿠키값을 이용하여 자신의 HttpSession 객체를 식별합니다. 즉, App2에서도 동일한 HttpSession객체 사용을 위하여 SESSION 쿠키값을 보내는 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@EnableRedisHttpSession
@RestController
@SpringBootApplication
public class SessionWebTest2Application {
 
    public static void main(String[] args) {
        SpringApplication.run(SessionWebTest2Application.class, args);
    }
    
    @GetMapping("/request")
    public String getCookie(HttpSession session) {
        log.info("get userId = {}",session.getAttribute("ID"));
        System.out.println(session.getAttribute("ID"));
        System.out.println(session.getId());
        return session.getId();
    }
    
}
cs

App2번의 클래스입니다. App1에서 보낸 요청을 받기위한 컨트롤러가 존재합니다. 결과값으로는 HttpSession의 Id값을 리턴합니다. 그리고 App1의 컨트롤러에서는 App2번이 보낸 세션 아이디와 자신의 세션아이디를 리턴합니다. 


브라우저에서 요청한 최종 결과입니다. 두 애플리케이션의 HttpSession ID 값이 동일합니다.


각각 애플리케이션의 로그입니다. App1번에서 yeoseong_yoon이라는 데이터를 세션에 추가하였고, 해당 데이터를 App2번에서 잘 가져오는 것을 볼 수 있습니다.



마지막으로 Redis 클라이언트 명령어를 이용해 진짜 Redis에 세션관련 데이터가 들어가있는지 확인해보니 잘 들어가있습니다. (Redis serialization 설정을 적절히 맞추지 않아 yeoseong_yoon이라는 데이터 앞에 알 수 없게 인코딩된 데이터가 있내요..) 


여기까지 Spring boot환경에서 Redis를 이용한 HttpSession 사용방법이었습니다. 혹시 틀린점이 있다면 코멘트 부탁드립니다.

posted by 여성게
:
인프라/Web Server & WAS 2018. 9. 12. 21:21

tomcat WAS에 spring(spring boot) 여러개의 war파일 배포(여러개 context)




Mac OS 기준에서 작성되었습니다.(tomcat이 설치되었다는 가정)


하나의 웹사이트가 여러개의 war파일로 되어있고, 그러한 war파일들의 통신으로 이루어지는 웹사이트일 경우의 was 배포입니다.(각 war의 was 포트를 다르게 가져갈 경우)


만약 배포할 프로젝트가 spring boot project라면 pom.xml에서 embbed was를 사용하지 않는 설정을 넣어주어야합니다.


<dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

            <exclusions>

                <exclusion>

                    <groupId>org.springframework.boot</groupId>

                    <artifactId>spring-boot-starter-tomcat</artifactId>

                </exclusion>

            </exclusions>

        </dependency>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-tomcat</artifactId>

            <scope>provided</scope>

        </dependency>


->임베디드 WAS를 사용하지 않는다.





tomcat이 깔린 디렉토리에 가면 webapps라는 폴더가 있습니다. 그 폴더 밑에 배포할 war 파일들을 넣어줍니다. 그리고 tomcat 디렉토리에 conf 폴더에 server.xml을 수정해줍니다.


<Service name="Catalina">

  <Connector port="8038” protocol="HTTP/1.1" 

   maxThreads="150" connectionTimeout="20000" 

   redirectPort="8443" />

  <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

  <Engine name="Catalina" defaultHost="localhost">
   <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
    resourceName="UserDatabase"/>

   <Host name="localhost"  appBase="webapps"
    unpackWARs="true" autoDeploy="true"
    xmlValidation="false" xmlNamespaceAware="false">

   <Context path=“/context1” docBase=“ contextName1” />
   </Host>

  </Engine>
 </Service>



<Service name="Catalina2">

  <Connector port=“8028” protocol="HTTP/1.1" 

     maxThreads="150" connectionTimeout="20000" 

     redirectPort="8443" />

  <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

  

  <Engine name="Catalina" defaultHost="localhost">

   <Realm className="org.apache.catalina.realm.UserDatabaseRealm"

     resourceName="UserDatabase"/>

 

   <Host name="localhost"  appBase=“webapps

    unpackWARs="true" autoDeploy="true"

    xmlValidation="false" xmlNamespaceAware="false">

   <Context path=“/context2” docBase=“contextName2” />

   </Host>

  </Engine>

 </Service>


<Service name="Catalina3”>

  <Connector port=“8018” protocol="HTTP/1.1" 

     maxThreads="150" connectionTimeout="20000" 

     redirectPort="8443" />

  <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

  

  <Engine name="Catalina" defaultHost="localhost">

   <Realm className="org.apache.catalina.realm.UserDatabaseRealm"

     resourceName="UserDatabase"/>

 

   <Host name="localhost"  appBase=“webapps

    unpackWARs="true" autoDeploy="true"

    xmlValidation="false" xmlNamespaceAware="false">

   <Context path=“/context3” docBase=“ contextName3” />

   </Host>

  </Engine>

 </Service>


이렇게 서비스를 3개를 등록해주고 각 서비스마다 WAS가 넘겨주는 포트를 다르게줍니다. 이러면 하나의 WAS에 서비스가 3개가 올라가게 됩니다.


그리고 tomcat의 bin 폴더의 ./startUp.sh를 실행하면 war파일이 풀리면서 was에 배포가 되게 됩니다. 만약 하나의 war를 배포한다면? 해당 프로젝트의 서비스를 하나만 등록하면 됩니다.


posted by 여성게
: