diff --git a/.gitignore b/.gitignore index cf4d7b59..39a600bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .DS_Store backend/src/main/resources/application-env.yml + +.env diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..99673384 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,4 @@ +FROM gradle:7.5.1-jdk17 +WORKDIR /app +COPY . /app +CMD ["gradle", "bootRun"] diff --git a/backend/build.gradle b/backend/build.gradle index 4f3f0148..9c84e92b 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -2,9 +2,23 @@ plugins { id 'java' id 'org.springframework.boot' version '3.1.2' id 'io.spring.dependency-management' version '1.1.2' + id 'com.google.cloud.tools.jib' version '3.2.1' id 'org.asciidoctor.jvm.convert' version '3.3.2' } +jib { + from { + image = "openjdk:11-jre-slim" + } + to { + image = "ohksj77/twtw-backend" + tags = ["latest"] + } + container { + jvmFlags = ["-Xms128m", "-Xmx128m"] + } +} + group = 'com.twtw' version = '0.0.1-SNAPSHOT' @@ -37,10 +51,12 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' - implementation 'org.springframework.boot:spring-boot-configuration-processor' + implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'org.springframework.boot:spring-boot-starter-reactor-netty' compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' runtimeOnly 'com.h2database:h2:1.4.200' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.projectreactor:reactor-test' diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 6e569023..948f38a7 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -3,13 +3,54 @@ version: "3" services: db: image: mariadb:10 + container_name: db ports: - 3306:3306 env_file: .env environment: TZ: Asia/Seoul + volumes: + - data_db:/var/lib/mysql networks: - backend restart: always + + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq + entrypoint: > + /bin/bash -c " + rabbitmq-server & + sleep 10 && rabbitmq-plugins enable rabbitmq_web_stomp & + tail -f /dev/null + " + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} + ports: + - 5672:5672 + - 15672:15672 + - 61613:61613 + volumes: + - data_rabbitmq:/rabbitmq + networks: + - backend + restart: always + tty: true + + redis: + image: redis:latest + container_name: redis + ports: + - 6379:6379 + networks: + - backend + restart: always + tty: true + networks: backend: + +volumes: + data_rabbitmq: null + data_db: null diff --git a/backend/src/main/java/com/twtw/backend/config/mapper/ObjectMapperConfig.java b/backend/src/main/java/com/twtw/backend/config/mapper/ObjectMapperConfig.java index 9ee426cc..27a74d37 100644 --- a/backend/src/main/java/com/twtw/backend/config/mapper/ObjectMapperConfig.java +++ b/backend/src/main/java/com/twtw/backend/config/mapper/ObjectMapperConfig.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializationFeature; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -17,6 +18,7 @@ public ObjectMapper objectMapper() { return new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true) .setPropertyNamingStrategy( new CompositePropertyNamingStrategy( PropertyNamingStrategies.SNAKE_CASE, diff --git a/backend/src/main/java/com/twtw/backend/config/properties/PropertiesConfig.java b/backend/src/main/java/com/twtw/backend/config/properties/PropertiesConfig.java index fd975e8d..ae5858a7 100644 --- a/backend/src/main/java/com/twtw/backend/config/properties/PropertiesConfig.java +++ b/backend/src/main/java/com/twtw/backend/config/properties/PropertiesConfig.java @@ -2,10 +2,17 @@ import com.twtw.backend.global.properties.KakaoProperties; import com.twtw.backend.global.properties.NaverProperties; +import com.twtw.backend.global.properties.RabbitMQProperties; +import com.twtw.backend.global.properties.RedisProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration -@EnableConfigurationProperties({NaverProperties.class, KakaoProperties.class}) +@EnableConfigurationProperties({ + NaverProperties.class, + KakaoProperties.class, + RedisProperties.class, + RabbitMQProperties.class +}) public class PropertiesConfig {} diff --git a/backend/src/main/java/com/twtw/backend/config/rabbitmq/RabbitMQConfig.java b/backend/src/main/java/com/twtw/backend/config/rabbitmq/RabbitMQConfig.java new file mode 100644 index 00000000..b5842c76 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/config/rabbitmq/RabbitMQConfig.java @@ -0,0 +1,67 @@ +package com.twtw.backend.config.rabbitmq; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.twtw.backend.global.properties.RabbitMQProperties; + +import lombok.RequiredArgsConstructor; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@EnableRabbit +@Configuration +@RequiredArgsConstructor +public class RabbitMQConfig { + + private static final String QUEUE_NAME = "map.queue"; + private static final String EXCHANGE_NAME = "map.exchange"; + private static final String ROUTING_KEY = "group.*"; + private final RabbitMQProperties rabbitMQProperties; + private final ObjectMapper objectMapper; + + @Bean + public Queue queue() { + return new Queue(QUEUE_NAME, true); + } + + @Bean + public TopicExchange topicExchange() { + return new TopicExchange(EXCHANGE_NAME); + } + + @Bean + public Binding binding(final Queue queue, final TopicExchange exchange) { + return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY); + } + + @Bean + public RabbitTemplate rabbitTemplate() { + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory()); + rabbitTemplate.setMessageConverter(jsonMessageConverter()); + rabbitTemplate.setRoutingKey(QUEUE_NAME); + return rabbitTemplate; + } + + @Bean + public ConnectionFactory connectionFactory() { + CachingConnectionFactory factory = new CachingConnectionFactory(); + factory.setHost(rabbitMQProperties.getHost()); + factory.setUsername(rabbitMQProperties.getUsername()); + factory.setPassword(rabbitMQProperties.getPassword()); + return factory; + } + + @Bean + public Jackson2JsonMessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(objectMapper); + } +} diff --git a/backend/src/main/java/com/twtw/backend/config/redis/RedisConfig.java b/backend/src/main/java/com/twtw/backend/config/redis/RedisConfig.java new file mode 100644 index 00000000..f2d1d082 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/config/redis/RedisConfig.java @@ -0,0 +1,61 @@ +package com.twtw.backend.config.redis; + +import com.twtw.backend.global.properties.RedisProperties; + +import lombok.RequiredArgsConstructor; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@Configuration +@EnableCaching +@EnableRedisRepositories +@RequiredArgsConstructor +public class RedisConfig { + private static final Long TIME_TO_LIVE = 1L; + private final RedisProperties redisProperties; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + } + + @Bean + public RedisTemplate redisTemplate() { + final RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setEnableTransactionSupport(false); + return redisTemplate; + } + + @Bean + public CacheManager cacheManager(final RedisConnectionFactory cf) { + final RedisCacheConfiguration redisCacheConfiguration = + RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer())) + .entryTtl(Duration.ofHours(TIME_TO_LIVE)); + + return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf) + .cacheDefaults(redisCacheConfiguration) + .build(); + } +} diff --git a/backend/src/main/java/com/twtw/backend/config/socket/StompConfig.java b/backend/src/main/java/com/twtw/backend/config/socket/StompConfig.java new file mode 100644 index 00000000..ba4ee749 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/config/socket/StompConfig.java @@ -0,0 +1,34 @@ +package com.twtw.backend.config.socket; + +import com.twtw.backend.global.properties.RabbitMQProperties; + +import lombok.RequiredArgsConstructor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@RequiredArgsConstructor +@EnableWebSocketMessageBroker +public class StompConfig implements WebSocketMessageBrokerConfigurer { + private final RabbitMQProperties rabbitMQProperties; + + @Override + public void registerStompEndpoints(final StompEndpointRegistry registry) { + registry.addEndpoint("/socket").setAllowedOrigins("*"); + } + + @Override + public void configureMessageBroker(final MessageBrokerRegistry registry) { + registry.enableStompBrokerRelay("/topic", "/queue", "/exchange", "/amq/queue") + .setRelayHost(rabbitMQProperties.getHost()) + .setRelayPort(61613) + .setClientPasscode(rabbitMQProperties.getPassword()) + .setClientLogin(rabbitMQProperties.getUsername()); + + registry.setApplicationDestinationPrefixes("/pub"); + } +} diff --git a/backend/src/main/java/com/twtw/backend/domain/plan/entity/PlanMember.java b/backend/src/main/java/com/twtw/backend/domain/plan/entity/PlanMember.java index 7aef3d1b..1b7b0825 100644 --- a/backend/src/main/java/com/twtw/backend/domain/plan/entity/PlanMember.java +++ b/backend/src/main/java/com/twtw/backend/domain/plan/entity/PlanMember.java @@ -20,11 +20,11 @@ public class PlanMember { @Column(columnDefinition = "BINARY(16)") private UUID id; - @JoinColumn + @JoinColumn(columnDefinition = "BINARY(16)") @ManyToOne(fetch = FetchType.LAZY) private Plan plan; - @JoinColumn + @JoinColumn(columnDefinition = "BINARY(16)") @ManyToOne(fetch = FetchType.LAZY) private Member member; diff --git a/backend/src/main/java/com/twtw/backend/global/properties/RabbitMQProperties.java b/backend/src/main/java/com/twtw/backend/global/properties/RabbitMQProperties.java new file mode 100644 index 00000000..99932093 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/global/properties/RabbitMQProperties.java @@ -0,0 +1,16 @@ +package com.twtw.backend.global.properties; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "spring.rabbitmq") +public class RabbitMQProperties { + private final String host; + private final Integer port; + private final String username; + private final String password; +} diff --git a/backend/src/main/java/com/twtw/backend/global/properties/RedisProperties.java b/backend/src/main/java/com/twtw/backend/global/properties/RedisProperties.java new file mode 100644 index 00000000..dc3e4471 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/global/properties/RedisProperties.java @@ -0,0 +1,14 @@ +package com.twtw.backend.global.properties; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "spring.data.redis") +public class RedisProperties { + private final String host; + private final Integer port; +} diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index 1b094396..a58ebe2b 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -18,3 +18,14 @@ spring: config: import: - classpath:application-env.yml + rabbitmq: + host: localhost + port: 5672 + cache: + type: redis + redis: + cache-null-values: true + data: + redis: + host: localhost + port: 6379 diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml new file mode 100644 index 00000000..7acd9a45 --- /dev/null +++ b/backend/src/main/resources/application-prod.yml @@ -0,0 +1,31 @@ +server: + servlet: + context-path: /api/v1 +spring: + datasource: + driver-class-name: org.mariadb.jdbc.Driver + url: jdbc:mariadb://${MYSQL_HOST}:${MYSQL_PORT}/${SCHEMA}?serverTimezone=Asia/Seoul + username: ${MYSQL_USER} + password: ${MYSQL_ROOT_PASSWORD} + jpa: + database: mysql + generate-ddl: true + show-sql: true + hibernate: + ddl-auto: create + main: + allow-bean-definition-overriding: true + config: + import: + - classpath:application-env.yml + rabbitmq: + host: rabbitmq + port: 5672 + cache: + type: redis + redis: + cache-null-values: true + data: + redis: + host: redis + port: 6379 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..174cc0b0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,78 @@ +version: "3" + +services: + backend: + build: + context: ./backend + container_name: backend + ports: + - 8080:8080 + networks: + - backend + depends_on: + - db + environment: + MYSQL_HOST: ${MYSQL_HOST} + MYSQL_PORT: ${MYSQL_PORT} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + SCHEMA: ${SCHEMA} + SPRING_PROFILES_ACTIVE: prod + restart: always + tty: true + + db: + image: mariadb:10 + container_name: db + ports: + - 3306:3306 + env_file: .env + environment: + TZ: Asia/Seoul + volumes: + - data_db:/var/lib/mysql + networks: + - backend + restart: always + + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq + entrypoint: > + /bin/bash -c " + rabbitmq-server & + sleep 10 && rabbitmq-plugins enable rabbitmq_web_stomp & + tail -f /dev/null + " + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} + ports: + - 5672:5672 + - 15672:15672 + - 61613:61613 + volumes: + - data_rabbitmq:/rabbitmq + networks: + - backend + restart: always + tty: true + + redis: + image: redis:latest + container_name: redis + ports: + - 6379:6379 + networks: + - backend + restart: always + tty: true + +networks: + backend: + +volumes: + data_rabbitmq: null + data_db: null