Skip to content

Commit

Permalink
Merge branch 'feature/33'
Browse files Browse the repository at this point in the history
# Conflicts:
#	bp-app-api/build.gradle
#	bp-app-api/src/main/java/com/beautify_project/bp_app_api/bean/ImageRepositoryBean.java
#	bp-app-api/src/main/java/com/beautify_project/bp_app_api/bean/JasyptConfigBean.java
#	bp-app-api/src/main/java/com/beautify_project/bp_app_api/config/IOBoundAsyncThreadPoolConfiguration.java
#	bp-app-api/src/main/java/com/beautify_project/bp_app_api/config/JasyptConfigBean.java
#	bp-app-api/src/main/java/com/beautify_project/bp_app_api/config/JasyptConfiguration.java
#	bp-app-api/src/main/java/com/beautify_project/bp_app_api/config/properties/NaverCloudPlatformObjectStorageConfig.java
#	bp-app-api/src/main/java/com/beautify_project/bp_app_api/controller/ShopController.java
#	bp-app-api/src/main/java/com/beautify_project/bp_app_api/repository/image/NaverCloudPlatformObjectStorageRepository.java
#	bp-app-api/src/main/java/com/beautify_project/bp_app_api/service/ShopLikeService.java
#	bp-app-api/src/main/java/com/beautify_project/bp_app_api/service/ShopService.java
#	bp-app-api/src/main/resources/application-local.yml
#	bp-app-api/src/main/resources/application-test.yml
#	bp-app-api/src/test/java/com/beautify_project/bp_app_api/integration/ShopServiceRepositoryIntegrationTest.java
#	bp-domain-mysql/src/main/java/com/beautify_project/bp_mysql/entity/BaseEntity.java
#	bp-domain-mysql/src/main/java/com/beautify_project/bp_mysql/entity/CustomEntityListener.java
#	bp-domain-mysql/src/main/java/com/beautify_project/bp_mysql/repository/ShopLikeRepositoryCustom.java
#	bp-domain-mysql/src/main/java/com/beautify_project/bp_mysql/repository/ShopLikeRepositoryImpl.java
#	bp-domain-mysql/src/main/java/com/beautify_project/bp_mysql/repository/ShopRepository.java
#	bp-utils/src/main/java/com/beautify_project/bp_utils/UUIDGenerator.java
#	bp-utils/src/main/java/com/beautify_project/bp_utils/Validator.java
#	settings.gradle
  • Loading branch information
sssukho committed Jan 15, 2025
2 parents 168df3b + 7177308 commit 42ed1b2
Show file tree
Hide file tree
Showing 62 changed files with 3,424 additions and 2,374 deletions.
5 changes: 2 additions & 3 deletions bp-app-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.kafka:spring-kafka'
implementation 'org.apache.commons:commons-lang3:3.14.0'
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.apache.commons:commons-lang3:3.14.0'

runtimeOnly 'com.mysql:mysql-connector-j:9.1.0'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.beautify_project.bp_app_api.config;

import com.beautify_project.bp_app_api.config.properties.AsyncThreadPoolConfigurationProperties;
import com.beautify_project.bp_utils.Validator;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -12,38 +15,61 @@
@Slf4j
@Configuration
@EnableAsync
public class IOBoundAsyncThreadPoolConfiguration implements AsyncConfigurer{
@RequiredArgsConstructor
public class IOBoundAsyncThreadPoolConfiguration implements AsyncConfigurer {

private static final int CORE_COUNT = Runtime.getRuntime().availableProcessors();
private static final int IO_THREAD_POOL_SIZE = CORE_COUNT * 2;
private static final int IO_THREAD_POOL_MAX_SIZE = CORE_COUNT * 4;
private static final int IO_THREAD_POOL_QUEUE_CAPACITY = 100;
private static final int IO_THREAD_KEEP_ALIVE_SECONDS = 60;
private static final int IO_THREAD_AWAIT_TERMINATION_SECONDS = 60;
private static final String IO_THREAD_NAME_PREFIX = "IO-Async-Executor";
private static final int DEFAULT_IO_THREAD_POOL_SIZE = CORE_COUNT * 2;
private static final int DEFAULT_IO_THREAD_POOL_MAX_SIZE = CORE_COUNT * 4;
private static final int DEFAULT_IO_THREAD_POOL_QUEUE_CAPACITY = 100;
private static final int DEFAULT_IO_THREAD_KEEP_ALIVE_SECONDS = 60;
private static final int DEFAULT_IO_THREAD_AWAIT_TERMINATION_SECONDS = 60;
private static final String DEFAULT_IO_THREAD_NAME_PREFIX = "IO-Async-Executor";

private final AsyncThreadPoolConfigurationProperties properties;

@Bean("ioBoundExecutor")
@Override
public Executor getAsyncExecutor() {

int corePoolSize = DEFAULT_IO_THREAD_POOL_SIZE;
if (!Validator.isNullOrZero(properties.corePoolSize())) {
corePoolSize = properties.corePoolSize();
}

int maxPoolSize = DEFAULT_IO_THREAD_POOL_MAX_SIZE;
if (!Validator.isNullOrZero(properties.maxPoolSize())) {
maxPoolSize = properties.maxPoolSize();
}

int queueCapacity = DEFAULT_IO_THREAD_POOL_QUEUE_CAPACITY;
if (!Validator.isNullOrZero(properties.queueCapacity())) {
queueCapacity = properties.queueCapacity();
}

String threadNamePrefix = DEFAULT_IO_THREAD_NAME_PREFIX;
if (!Validator.isEmptyOrBlank(properties.threadNamePrefix())) {
threadNamePrefix = properties.threadNamePrefix();
}

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(IO_THREAD_POOL_SIZE);
executor.setMaxPoolSize(IO_THREAD_POOL_MAX_SIZE);
executor.setQueueCapacity(IO_THREAD_POOL_QUEUE_CAPACITY);
executor.setKeepAliveSeconds(IO_THREAD_KEEP_ALIVE_SECONDS);
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(DEFAULT_IO_THREAD_KEEP_ALIVE_SECONDS);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(IO_THREAD_AWAIT_TERMINATION_SECONDS);
executor.setThreadNamePrefix(IO_THREAD_NAME_PREFIX);
executor.setAwaitTerminationSeconds(DEFAULT_IO_THREAD_AWAIT_TERMINATION_SECONDS);
executor.setThreadNamePrefix(threadNamePrefix);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // thread 실행 불가능할 때 호출한 스레드에서 실행
logging();
return executor;
}

private void logging() {
private static void logging() {
log.debug("CORE_COUNT: {}", CORE_COUNT);
log.debug("IO_THREAD_POOL_SIZE: {}", IO_THREAD_POOL_SIZE);
log.debug("IO_THREAD_POOL_MAX_SIZE: {}", IO_THREAD_POOL_MAX_SIZE);
log.debug("IO_THREAD_POOL_QUEUE_CAPACITY: {}", IO_THREAD_POOL_QUEUE_CAPACITY);
log.debug("IO_THREAD_KEEP_ALIVE_SECONDS: {}", IO_THREAD_KEEP_ALIVE_SECONDS);
log.debug("IO_THREAD_POOL_SIZE: {}", DEFAULT_IO_THREAD_POOL_SIZE);
log.debug("IO_THREAD_POOL_MAX_SIZE: {}", DEFAULT_IO_THREAD_POOL_MAX_SIZE);
log.debug("IO_THREAD_POOL_QUEUE_CAPACITY: {}", DEFAULT_IO_THREAD_POOL_QUEUE_CAPACITY);
log.debug("IO_THREAD_KEEP_ALIVE_SECONDS: {}", DEFAULT_IO_THREAD_KEEP_ALIVE_SECONDS);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.beautify_project.bp_app_api.config;

import com.beautify_project.bp_app_api.config.properties.KafkaProducerConfigProperties;
import com.beautify_project.bp_app_api.dto.event.ShopLikeCancelEvent;
import com.beautify_project.bp_app_api.dto.event.ShopLikeEvent;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.serializer.JsonSerializer;

@EnableKafka
@Configuration
@RequiredArgsConstructor
public class KafkaProducerConfig {

private final KafkaProducerConfigProperties configProperties;

@Bean
public Map<String, Object> producerConfig() {
return Map.of(
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, configProperties.getBroker(),
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class,
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
}

@Bean(name = "ShopLikeEventProducerFactory")
public ProducerFactory<String, ShopLikeEvent> shopLikeEventProducerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfig());
}

@Bean(name = "ShopLikeEventKafkaTemplate")
public KafkaTemplate<String, ShopLikeEvent> shopLikeEventKafkaTemplate() {
return new KafkaTemplate<>(shopLikeEventProducerFactory());
}

@Bean(name = "ShopLikeCancelEventProducerFactory")
public ProducerFactory<String, ShopLikeCancelEvent> shopLikeCancelEventProducerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfig());
}

@Bean(name = "ShopLikeCancelEventKafkaTemplate")
public KafkaTemplate<String, ShopLikeCancelEvent> shopLikeCancelEventKafkaTemplate() {
return new KafkaTemplate<>(shopLikeCancelEventProducerFactory());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.beautify_project.bp_app_api.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "thread-pool.async" )
public record AsyncThreadPoolConfigurationProperties(Integer corePoolSize, Integer maxPoolSize,
Integer queueCapacity, String threadNamePrefix) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.beautify_project.bp_app_api.config.properties;

import com.beautify_project.bp_app_api.exception.BpCustomException;
import com.beautify_project.bp_app_api.response.ErrorResponseMessage.ErrorCode;
import com.beautify_project.bp_utils.Validator;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "kafka.producer")
@Slf4j
@Getter
public class KafkaProducerConfigProperties {

private String broker;
private Topic topic;

public void setBroker(final String broker) {
if (Validator.isEmptyOrBlank(broker)) {
throw new BpCustomException("broker 설정값이 올바르지 않습니다.", ErrorCode.IS001);
}
this.broker = broker;
}

public void setTopic(final Topic topic) {
if (topic == null) {
throw new BpCustomException("topic 설정값이 올바르지 않습니다.", ErrorCode.IS001);
}
this.topic = topic;
}

@Getter
public static class Topic {
private String shopLikeEvent;
private String shopLikeCancelEvent;
private String signUpCertificationMailEvent;

public void setShopLikeEvent(final String shopLikeEvent) {
if (Validator.isEmptyOrBlank(shopLikeEvent)) {
throw new BpCustomException("shop-like-event 설정값이 올바르지 않습니다.", ErrorCode.IS001);
}
this.shopLikeEvent = shopLikeEvent;
}

public void setShopLikeCancelEvent(final String shopLikeCancelEvent) {
if (Validator.isEmptyOrBlank(shopLikeCancelEvent)) {
throw new BpCustomException("shop-like-cancel-event 설정값이 올바르지 않습니다.", ErrorCode.IS001);
}
this.shopLikeCancelEvent = shopLikeCancelEvent;
}

public void setSignUpCertificationMailEvent(final String signUpCertificationMailEvent) {
if (Validator.isEmptyOrBlank(signUpCertificationMailEvent)) {
throw new BpCustomException("sign-up-certification-mail 설정값이 올바르지 않습니다.",
ErrorCode.IS001);
}
this.signUpCertificationMailEvent = signUpCertificationMailEvent;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.beautify_project.bp_app_api.controller;

import com.beautify_project.bp_app_api.dto.event.ShopLikeCancelEvent;
import com.beautify_project.bp_app_api.dto.event.ShopLikeEvent;
import com.beautify_project.bp_app_api.request.shop.ShopListFindRequestParameters;
import com.beautify_project.bp_app_api.request.shop.ShopRegistrationRequest;
import com.beautify_project.bp_app_api.response.ResponseMessage;
Expand All @@ -8,6 +10,7 @@
import com.beautify_project.bp_app_api.service.ShopService;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand Down Expand Up @@ -56,22 +59,38 @@ public ResponseMessage findShopList(@RequestParam(name = "type") final String se
// TODO: 샵 삭제 구현

/**
* Shop 좋아요
* Shop 좋아요 이벤트 producer
*/
@PostMapping("/v1/shops/likes/{id}")
@ResponseStatus(code = HttpStatus.NO_CONTENT)
@ResponseStatus(code = HttpStatus.OK)
public void likeShop(@PathVariable(value = "id") @NotBlank final String shopId) {
// TODO: spring security 통해서 토큰 넘겨주는 방식으로 개선 필요
shopService.likeShop(shopId, "[email protected]");
shopService.produceShopLikeEvent(shopId, "[email protected]");
}

/**
* Shop 좋아요 취소
* Shop 좋아요 이벤트 처리 (consumer 는 bp-kafka-consumer 에서 처리)
*/
@DeleteMapping("/v1/shops/likes/{id}")
@PostMapping("/v1/shops/likes")
@ResponseStatus(code = HttpStatus.NO_CONTENT)
public void batchShopLikes(@Valid @RequestBody final List<ShopLikeEvent> shopLikeEvents) {
shopService.batchShopLikes(shopLikeEvents);
}

/**
* Shop 좋아요 취소 이벤트 producer
*/
@DeleteMapping("/v1/shops/likes/{id}")
@ResponseStatus(code = HttpStatus.OK)
public void cancelLikeShop(@PathVariable(value = "id") @NotBlank final String shopId) {
// TODO: spring security 통해서 토큰 넘겨주는 방식으로 개선 필요
shopService.cancelLikeShop(shopId, "[email protected]");
shopService.produceShopLikeCancelEvent(shopId, "[email protected]");
}

/**
* Shop 좋아요 취소 이벤트 처리 (consumer 는 bp-kafka-consumer 에서 처리)
*/
@DeleteMapping("/v1/shops/likes")
@ResponseStatus(code = HttpStatus.NO_CONTENT)
public void batchShopLikeCancel(@Valid @RequestBody final List<ShopLikeCancelEvent> shopLikeCancelEvents) {
shopService.batchShopLikesCancel(shopLikeCancelEvents);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.beautify_project.bp_app_api.dto.event;


import com.beautify_project.bp_utils.Validator;

public record ShopLikeCancelEvent(String shopId, String memberEmail) {

public ShopLikeCancelEvent(final String shopId, final String memberEmail) {
validate(shopId, memberEmail);
this.shopId = shopId;
this.memberEmail = memberEmail;
}

private void validate(final String shopId, final String memberEmail) {
if (Validator.isEmptyOrBlank(shopId)) {
throw new UnsupportedOperationException("shopId 파라미터는 필수값 입니다.");
}

if (Validator.isEmptyOrBlank(memberEmail)) {
throw new UnsupportedOperationException("memberEmail 파라미터는 필수값 입니다.");
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.beautify_project.bp_app_api.dto.event;

import com.beautify_project.bp_utils.Validator;

public record ShopLikeEvent (String shopId, String memberEmail){

public ShopLikeEvent(final String shopId, final String memberEmail) {
validate(shopId, memberEmail);
this.shopId = shopId;
this.memberEmail = memberEmail;
}

private void validate(final String shopId, final String memberEmail) {

if (Validator.isEmptyOrBlank(shopId)) {
throw new UnsupportedOperationException("shopId 파라미터는 필수값 입니다.");
}

if (Validator.isEmptyOrBlank(memberEmail)) {
throw new UnsupportedOperationException("memberEmail 파라미터는 필수값 입니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.beautify_project.bp_app_api.producer;

import com.beautify_project.bp_app_api.config.properties.KafkaProducerConfigProperties;
import com.beautify_project.bp_app_api.dto.event.ShopLikeCancelEvent;
import com.beautify_project.bp_app_api.dto.event.ShopLikeEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

@Component
@Slf4j
@RequiredArgsConstructor
public class KafkaEventProducer {

private final KafkaTemplate<String, ShopLikeEvent> shopLikeEventKafkaTemplate;
private final KafkaTemplate<String, ShopLikeCancelEvent> shopLikeCancelEventKafkaTemplate;
private final KafkaProducerConfigProperties configProperties;

public void publishShopLikeEvent(final ShopLikeEvent event) {
shopLikeEventKafkaTemplate.send(configProperties.getTopic().getShopLikeEvent(), event);
}

public void publishShopLikeCancelEvent(final ShopLikeCancelEvent event) {
shopLikeCancelEventKafkaTemplate.send(configProperties.getTopic().getShopLikeCancelEvent(), event);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ public enum ErrorCode {
AL001(HttpStatus.BAD_REQUEST, "AL001", "이미 좋아요를 누른 샵 입니다."),
AL002(HttpStatus.BAD_REQUEST, "AL002", "좋아요를 하지 않은 샵 입니다."),

SL001(HttpStatus.BAD_REQUEST, "SL001", "등록되지 않은 샵 혹은 이미 좋아요를 누른 샵 입니다."),
SL002(HttpStatus.BAD_REQUEST, "SL002", "등록되지 않은 샵 혹은 이미 좋아요 취소를 누른 샵 입니다."),

II001(HttpStatus.NOT_FOUND, "II001", "유효하지 않은 식별자 입니다."),
II002(HttpStatus.BAD_REQUEST, "II002", "이미 처리된 식별자 입니다."),

Expand Down
Loading

0 comments on commit 42ed1b2

Please sign in to comment.