Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/dev-CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
DEV_SERVER_URL: ${{ secrets.DEV_SERVER_URL }}
DEV_ACTUATOR_PORT: ${{ secrets.DEV_ACTUATOR_PORT }}
DEV_ACTUATOR_PATH: ${{ secrets.DEV_ACTUATOR_PATH }}
DEV_SLACK_WEBHOOK_URL: ${{ secrets.DEV_SLACK_WEBHOOK_URL }}
run: |
cd ./src/main/resources
envsubst < application-dev.yml > application-dev.tmp.yml && mv application-dev.tmp.yml application-dev.yml
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/prod-CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
PROD_SERVER_URL: ${{ secrets.PROD_SERVER_URL }}
PROD_ACTUATOR_PORT: ${{ secrets.PROD_ACTUATOR_PORT }}
PROD_ACTUATOR_PATH: ${{ secrets.PROD_ACTUATOR_PATH }}
PROD_SLACK_WEBHOOK_URL: ${{ secrets.PROD_SLACK_WEBHOOK_URL }}
run: |
cd ./src/main/resources
envsubst < application-prod.yml > application-prod.tmp.yml && mv application-prod.tmp.yml application-prod.yml
Expand Down
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ dependencies {

// AOP
implementation 'org.springframework.boot:spring-boot-starter-aop'

// Slack Webhook
implementation 'com.slack.api:slack-api-client:1.45.3'

// @ConfigurationProperties auto bean register
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}

jar {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/beat/BeatApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
Expand All @@ -14,6 +15,7 @@
@EnableScheduling
@EnableAsync
@EnableAspectJAutoProxy
@ConfigurationPropertiesScan
@ImportAutoConfiguration({FeignAutoConfiguration.class})
public class BeatApplication {

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/beat/domain/member/api/MemberApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import com.beat.domain.member.dto.AccessTokenGenerateResponse;
import com.beat.domain.member.dto.MemberLoginResponse;
import com.beat.domain.member.application.dto.response.AccessTokenGenerateResponse;
import com.beat.domain.member.application.dto.response.MemberLoginResponse;
import com.beat.global.auth.annotation.CurrentMember;
import com.beat.global.auth.client.dto.MemberLoginRequest;
import com.beat.global.common.dto.ErrorResponse;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

import com.beat.domain.member.application.AuthenticationService;
import com.beat.domain.member.application.SocialLoginService;
import com.beat.domain.member.dto.AccessTokenGenerateResponse;
import com.beat.domain.member.dto.LoginSuccessResponse;
import com.beat.domain.member.dto.MemberLoginResponse;
import com.beat.domain.member.application.dto.response.AccessTokenGenerateResponse;
import com.beat.domain.member.application.dto.response.LoginSuccessResponse;
import com.beat.domain.member.application.dto.response.MemberLoginResponse;
import com.beat.domain.member.exception.MemberSuccessCode;
import com.beat.global.auth.annotation.CurrentMember;
import com.beat.global.auth.client.dto.MemberLoginRequest;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.beat.domain.member.application;

import com.beat.domain.member.dto.AccessTokenGenerateResponse;
import com.beat.domain.member.dto.LoginSuccessResponse;
import com.beat.domain.member.application.dto.response.AccessTokenGenerateResponse;
import com.beat.domain.member.application.dto.response.LoginSuccessResponse;
import com.beat.domain.user.domain.Role;
import com.beat.domain.user.domain.Users;
import com.beat.global.auth.client.dto.MemberInfoResponse;
Expand Down Expand Up @@ -30,7 +30,6 @@
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private static final String BEARER_PREFIX = "Bearer ";
private final JwtTokenProvider jwtTokenProvider;
private final TokenService tokenService;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.beat.domain.member.application;

import com.beat.domain.member.application.dto.event.MemberRegisteredEvent;
import com.beat.domain.member.dao.MemberRepository;
import com.beat.domain.member.domain.Member;
import com.beat.domain.user.dao.UserRepository;
Expand All @@ -10,6 +11,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -18,6 +20,7 @@
@RequiredArgsConstructor
public class MemberRegistrationService {

private final ApplicationEventPublisher eventPublisher;
private final UserRepository userRepository;
private final MemberRepository memberRepository;

Expand All @@ -41,9 +44,10 @@ public Long registerMemberWithUserInfo(final MemberInfoResponse memberInfoRespon
);

memberRepository.save(member);

log.info("Member registered with memberId: {}, role: {}", member.getId(), users.getRole());

eventPublisher.publishEvent(new MemberRegisteredEvent(member.getNickname()));

return member.getId();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,18 @@ public Member findMemberBySocialIdAndSocialType(final Long socialId, final Socia
.orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND));
}

@Override
@Transactional
public void deleteUser(final Long id) {
Users users = userRepository.findById(id)
.orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND));

userRepository.delete(users);
}
}

@Override
@Transactional(readOnly = true)
public long countMembers() {
return memberRepository.count();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

import com.beat.domain.member.domain.Member;
import com.beat.domain.member.domain.SocialType;
import com.beat.domain.member.dto.LoginSuccessResponse;
import com.beat.domain.member.application.dto.response.LoginSuccessResponse;
import com.beat.domain.member.exception.MemberErrorCode;
import com.beat.domain.member.port.in.MemberUseCase;
import com.beat.domain.user.domain.Users;
import com.beat.domain.user.port.in.UserUseCase;
import com.beat.global.auth.client.application.KakaoSocialService;
import com.beat.global.auth.client.application.SocialService;
import com.beat.global.auth.client.dto.MemberInfoResponse;
Expand Down Expand Up @@ -116,4 +115,4 @@ private Long findOrRegisterMember(final MemberInfoResponse memberInfoResponse) {

return memberRegistrationService.registerMemberWithUserInfo(memberInfoResponse);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.beat.domain.member.application.dto.event;

public record MemberRegisteredEvent(
String nickname
) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.beat.domain.member.dto;
package com.beat.domain.member.application.dto.response;

public record AccessTokenGenerateResponse(
String accessToken
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.beat.domain.member.dto;
package com.beat.domain.member.application.dto.response;

public record LoginSuccessResponse(
String accessToken,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.beat.domain.member.dto;
package com.beat.domain.member.application.dto.response;

public record MemberLoginResponse(
String accessToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public interface MemberUseCase {
Member findMemberBySocialIdAndSocialType(Long socialId, SocialType socialType);

void deleteUser(Long id);

long countMembers();
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ protected void doFilterInternal(@NonNull HttpServletRequest request,
final String token = getJwtFromRequest(request);

if (!StringUtils.hasText(token)) {
log.info("JWT Token not found in request header. Assuming guest access or public API request.");
log.debug("JWT Token not found in request header. Assuming guest access or public API request.");
filterChain.doFilter(request, response);
return;
}
Expand Down Expand Up @@ -109,4 +109,4 @@ private String getJwtFromRequest(HttpServletRequest request) {
}
return null;
}
}
}
40 changes: 40 additions & 0 deletions src/main/java/com/beat/global/common/config/AsyncThreadConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.beat.global.common.config;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.concurrent.DelegatingSecurityContextExecutor;

import com.beat.global.common.handler.GlobalAsyncExceptionHandler;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableAsync
@RequiredArgsConstructor
public class AsyncThreadConfig implements AsyncConfigurer {
private final ThreadPoolProperties threadPoolProperties;

@Override
@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(threadPoolProperties.getCoreSize());
executor.setThreadNamePrefix(threadPoolProperties.getThreadNamePrefix());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return new DelegatingSecurityContextExecutor(executor.getThreadPoolExecutor());
}

@Override
@Bean
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new GlobalAsyncExceptionHandler();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.beat.global.common.config;

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

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@ConfigurationProperties(prefix = "thread-pool")
@RequiredArgsConstructor
@Getter
public class ThreadPoolProperties {
private final int coreSize;
private final String threadNamePrefix;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.beat.global.common.handler;

import java.lang.reflect.Method;
import java.util.Arrays;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class GlobalAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

@Override
public void handleUncaughtException(@NonNull Throwable ex, @NonNull Method method, @Nullable Object... params) {
if (params == null) {
log.error("비동기 작업 중 예외 발생! Method: [{}], Params: [null], Exception: [{}]",
method.getName(), ex.getMessage(), ex);
return;
}

String paramValues = Arrays.toString(params);

log.error("비동기 작업 중 예외 발생! Method: [{}], Params: [{}], Exception: [{}]",
method.getName(), paramValues, ex.getMessage(), ex);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.beat.global.external.notification.slack.application;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.Map;

@FeignClient(name = "slackClient", url = "${slack.webhook.url}")
public interface SlackClient {

@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
void sendMessage(@RequestBody Map<String, String> payload);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.beat.global.external.notification.slack.application;

import java.util.Map;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class SlackService {

private final SlackClient slackClient;

public void sendMessage(Map<String, String> payload) {
slackClient.sendMessage(payload);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.beat.global.external.notification.slack.event;

import java.util.HashMap;
import java.util.Map;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import com.beat.domain.member.application.dto.event.MemberRegisteredEvent;
import com.beat.domain.member.port.in.MemberUseCase;
import com.beat.global.external.notification.slack.application.SlackService;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class MemberRegisteredEventListener {
private static final String TEXT_KEY = "text";
private static final String WELCOME_MESSAGE = "번째 유저가 회원가입했띠예 🎉🎉 - ";
private static final String SLACK_TRANSFER_ERROR = "Slack 전송 실패";

private final MemberUseCase memberUseCase;
private final SlackService slackService;

@Async("taskExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendSlackNotification(MemberRegisteredEvent event) {
Map<String, String> payload = new HashMap<>();
payload.put(TEXT_KEY, memberUseCase.countMembers() + WELCOME_MESSAGE + event.nickname());

try {
slackService.sendMessage(payload);
} catch (Exception e) {
throw new RuntimeException(SLACK_TRANSFER_ERROR);
}
}
}
8 changes: 8 additions & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,11 @@ springdoc:
operations-sorter: alpha
display-request-duration: true
urls-primary-name: general

slack:
webhook:
url: ${DEV_SLACK_WEBHOOK_URL}

thread-pool:
core-size: 2
thread-name-prefix: executor-
8 changes: 8 additions & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,11 @@ springdoc:
operations-sorter: alpha
display-request-duration: true
urls-primary-name: general

slack:
webhook:
url: ${PROD_SLACK_WEBHOOK_URL}

thread-pool:
core-size: 2
thread-name-prefix: executor-