API 서버와 웹소켓 서버를 구분한 아키텍처로 구현했지만 하나의 모듈 내에서 전부 구현되어 하나의 포트에서 동작하는 한계가 있었습니다. 같은 프로젝트를 두개 이상의 서버로 실행시키면서 제한적인 기능만 활용하는 모양이 되었고 이러한 구조는 서버를 분리하면 할수록 매우 비효율적으로 동작하게 되었습니다.
결국 현재 프로젝트 구조는 제가 의도했던 완벽한 서버의 분리가 젼혀 안된 구조였고 이를 각각의 프로세스로 동작하도록 나눠줄 필요가 있다고 판단했습니다.
api서버와 웹소켓 서버를 각각 구현한 두개의 프로젝트로 나눠서 구현한다면 위에서 말한 문제점은 해결된다고 볼 수도 있겠지만 두 서버에서 공통적으로 사용되는 코드를 반복 생성해야 되고 IDE도 여러개로 띄워놔야 하는 또 다른 문제점들을 발생시킬 수 있습니다.
이는 완벽한 해결방법이라 생각되지 않았고 하나의 프로젝트에서 각 서버의 기능을 구현한 모듈을 분리할 수 있는 방법을 찾아보기로 했습니다.
Spring에서는 하나의 프로젝트 내에서 여러개의 모듈을 생성해 독립적으로 프로세스를 동작시킬 수 있습니다.
멀티 모듈 구조로 현재 프로젝트를 나눠본다면 다음과 같이 나눠볼 수 있습니다.
이렇게 각각의 모듈에 해당하는 코드를 분리시키고 공통적으로 사용되는 클래스에 대해선 core 모듈에 구성함으로써 공유되도록 프로젝트를 재구성해보기로 했습니다.
자세한 멀티 모듈 구성 방법은 링크를 참고하세요.
위 사진처럼 세개의 모듈을 생성한 모습을 볼 수 있습니다.
buildscript {
ext {
springBootVersion = '2.7.1'
}
repositories {
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
classpath "io.spring.gradle:dependency-management-plugin:1.0.11.RELEASE"
}
}
// 하위 모든 프로젝트 공통 세팅
subprojects {
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group 'org.example'
version '1.0-SNAPSHOT'
sourceCompatibility = '11'
compileJava.options.encoding = 'UTF-8'
repositories {
mavenCentral()
}
// 하위 모듈에서 공통으로 사용하는 의존성도 추가할 수 있습니다. 다만 모듈별 확실한 구분을 위해 미사용합니다.
dependencies {
}
test {
useJUnitPlatform()
}
}
project(':football-core') {
bootJar { enabled = false } // main()으로 실행시키지 않을 모듈
jar { enabled = true } // 하지만 jar 파일이 만들어여야만 다른 모듈에서 의존 가능
}
project(':football-api-server') {
bootJar { enabled = true }
jar { enabled = false }
}
project(':football-websocket-server') {
bootJar { enabled = true }
jar { enabled = false }
}
루트 프로젝트의 build.gradle 파일에선 모든 모듈에서 사용될 공통 세팅을 할 수 있고 의존성도 추가할 수 있습니다. 다만 각 모듈의 확실한 분리를 위해 해당 파일에선 최대한 선언하지 않고 각 모듈들의 build.gradle 파일에서 의존성을 추가하는 방향으로 구성했습니다.
dependencies {
// MySql
implementation 'mysql:mysql-connector-java'
// Flyway
implementation 'org.flywaydb:flyway-core:5.2.4'
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Valid
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// JUnit5
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
// etc
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
implementation project(':football-core')
}
dependencies {
// Web Socket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// JUnit5
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
// etc
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
implementation project(':football-core')
}
dependencies {
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// JUnit5
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
// etc
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}
위 모듈별 의존성이 추가된 라이브러리만 확인해본다면 websocket 모듈에는 웹소켓에 대한 기능만 수행할 수 있도록 구성했고, 그 외 DB, Redis와 연동된 작업은 Api 서버가 담당하도록 분리시켰고 Spring Security에 대한 구성 클래스만 core 모듈에 위치시켜 다른 두 모듈에서 같은 설정을 가지고 공유할 수 있도록 구성했습니다.
common 모듈의 저주라는 말이 있듯이, core 모듈에 너무 많은 코드나 의존성이 추가되어 있는 것은 위험할 수 있습니다.
그래서 추후에는 core 모듈에 구성된 Spring Security에 대한 책임도 다른 모듈로 분리시켜 core 모듈 내에는 순수 자바 코드에 대한 공유만 이뤄질 수 있도록 리팩토링 할 예정입니다.
이처럼 Scale Out을 고려한 설계를 통해 Api 서버와 Websocket 서버에 대한 분리를 고려했고 이를 실제로 독립적인 프로세스로 동작할 수 있도록 멀티 모듈을 적용시켰습니다. 멀티 모듈 구조를 통해 서버 확장에 대한 실제 코드 구성이 가능하도록 할 수 있었으며 추가적으로 공통된 코드도 효율적으로 관리할 수 있는 구조로 변경할 수 있었습니다.