Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cbde362
refactor: update application configuration files for logging and envi…
jbh010204 Aug 7, 2025
fe516dc
refactor: update application configuration files for logging and envi…
jbh010204 Aug 7, 2025
b9e1687
refactor: add default value for Discord webhook URL and prevent notif…
jbh010204 Aug 8, 2025
bc7d613
MOSU-267 feat: cache 적용할 도메인들 enum으로 정의
jbh010204 Aug 9, 2025
40f61d0
MOSU-267 feat: LocalCacheManager 구현
jbh010204 Aug 9, 2025
c5be4ad
MOSU-267 fix: 변수명 오타 수정
jbh010204 Aug 10, 2025
a0fd7db
MOSU-267 feat: 공지 도메인 캐싱 적용
jbh010204 Aug 10, 2025
febbe32
refactor: remove unnecessary @NotBlank annotation from PhoneNumberPat…
jbh010204 Aug 10, 2025
e784335
Merge branch 'develop' into refactor/mosu-267
jbh010204 Aug 10, 2025
50915bc
Merge pull request #269 from mosu-dev/refactor/mosu-267
jbh010204 Aug 10, 2025
24f6e2e
Merge branch 'feat/mosu-249' of https://github.com/mosu-dev/mosu-serv…
polyglot-k Aug 10, 2025
03357a6
feat: reorganize application configuration files and add base profile
polyglot-k Aug 10, 2025
affed22
feat: update deployment scripts to use base environment variables and…
polyglot-k Aug 10, 2025
7f0bf73
feat: implement updateInquiry method for modifying existing inquiries
polyglot-k Aug 10, 2025
3dbbf21
feat: initialize DiscordNotifier with webhook URL and prevent notific…
polyglot-k Aug 10, 2025
da95311
feat: update DiscordNotifier to use blank check for webhook URL confi…
polyglot-k Aug 10, 2025
3f0a973
feat: increase title size limit in InquiryUpdateRequest to 300 charac…
polyglot-k Aug 10, 2025
08a58cd
feat: update logging configuration to specify log file path in applic…
polyglot-k Aug 10, 2025
5e01c1f
Merge pull request #257 from mosu-dev/feat/mosu-249
polyglot-k Aug 10, 2025
867e96d
feat: use sudo for Docker build and push commands in self-deploy YAML
polyglot-k Aug 10, 2025
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
11 changes: 9 additions & 2 deletions .github/workflows/docker-depoly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,16 @@ jobs:
script: |
cd /home/ubuntu/mosu

echo "${{ secrets.ENV_BLUE }}" > .env.blue
echo "${{ secrets.ENV_GREEN }}" > .env.green
echo "${{ secrets.ENV_BASE }}" > .env
echo "${{ secrets.ENV_BASE }}" > .env.blue
echo "${{ secrets.ENV_BASE }}" > .env.green

echo "${{ secrets.ENV }}" >> .env
echo "${{ secrets.ENV }}" >> .env.blue
echo "${{ secrets.ENV }}" >> .env.green

echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env
echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env.blue
echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env.green

./deploy.sh
16 changes: 12 additions & 4 deletions .github/workflows/self-depoly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,27 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build Docker image
run: docker build -t kangtaehyun1107/mosu-server:${{ github.sha }} .
run: sudo docker build -t kangtaehyun1107/mosu-server:${{ github.sha }} .
working-directory:
- name: Push Docker image
run: docker push kangtaehyun1107/mosu-server:${{ github.sha }}
run: sudo docker push kangtaehyun1107/mosu-server:${{ github.sha }}

- name: Deploy via SSH
run: |
cd ~/mosu-server

echo "${{ secrets.TEST_ENV_BLUE }}" > .env.blue
echo "${{ secrets.TEST_ENV_GREEN }}" > .env.green
echo "${{ secrets.ENV_BASE }}" > .env
echo "${{ secrets.ENV_BASE }}" > .env.blue
echo "${{ secrets.ENV_BASE }}" > .env.green

echo "${{ secrets.ENV_TEST }}" >> .env
echo "${{ secrets.ENV_TEST }}" >> .env.blue
echo "${{ secrets.ENV_TEST }}" >> .env.green

echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env
echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env.blue
echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env.green

sudo docker stop $(sudo docker ps -aq) || true
sudo docker rm $(sudo docker ps -aq) || true
echo "Stopping all containers..."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package life.mosu.mosuserver.application.caffeine;

import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import life.mosu.mosuserver.domain.caffeine.CacheGroup;
import life.mosu.mosuserver.domain.caffeine.CacheType;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@EnableCaching
@Configuration
public class LocalCacheConfig {

@Bean
public LocalCacheManager localCacheManager() {
List<Cache> caches = Arrays.stream(CacheGroup.values())
.filter(g -> g.getCacheType() == CacheType.LOCAL
|| g.getCacheType() == CacheType.COMPOSITE)
.map(g -> new CaffeineCache(
g.getCacheName(),
Caffeine.newBuilder()
.recordStats()
.expireAfterWrite(g.getExpiredAfterWrite())
.build()
)).collect(Collectors.toList());

return new LocalCacheManager(caches);
}

@Bean
@Primary
public CacheManager appCacheManager(LocalCacheManager localCacheManager) {
return localCacheManager;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package life.mosu.mosuserver.application.caffeine;

import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;

public class LocalCacheManager implements CacheManager, UpdatableCacheManager {

private final List<Cache> caches;
private Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
private volatile Set<String> cacheNames = Collections.emptySet();

public LocalCacheManager(List<Cache> caches) {
this.caches = (caches != null) ? caches : Collections.emptyList();
}

@PostConstruct
public void init() {
Set<String> cacheNamesSet = new LinkedHashSet<>(caches.size());
Map<String, Cache> cacheMapTemp = new ConcurrentHashMap<>(16);

for (Cache cache : caches) {
String name = cache.getName();
cacheNamesSet.add(name);
cacheMapTemp.put(name, cache);
}
this.cacheMap = cacheMapTemp;
this.cacheNames = cacheNamesSet;
}

@Override
@Nullable
public Cache getCache(String name) {
return cacheMap.get(name);
}

@Override
public Collection<String> getCacheNames() {
return cacheNames;
}

@Override
public void putIfAbsent(Cache cache, String key, Object value) {
Cache localCache = getCache(cache.getName());
if (localCache != null) {
localCache.putIfAbsent(key, value);
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package life.mosu.mosuserver.application.caffeine;

import org.springframework.cache.Cache;

public interface UpdatableCacheManager {

void putIfAbsent(Cache cache, String key, Object value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
import life.mosu.mosuserver.domain.inquiry.entity.InquiryJpaEntity;
import life.mosu.mosuserver.domain.inquiry.entity.InquiryStatus;
import life.mosu.mosuserver.domain.inquiry.repository.InquiryJpaRepository;
import life.mosu.mosuserver.domain.inquiryAnswer.repository.InquiryAnswerJpaRepository;
import life.mosu.mosuserver.domain.user.entity.UserJpaEntity;
import life.mosu.mosuserver.domain.user.entity.UserRole;
import life.mosu.mosuserver.domain.user.repository.UserJpaRepository;
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorCode;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryCreateRequest;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryDetailResponse;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryDetailResponse.InquiryAnswerDetailResponse;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryResponse;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryUpdateRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
Expand All @@ -26,11 +25,9 @@
@RequiredArgsConstructor
public class InquiryService {

private final UserJpaRepository userJpaRepository;
private final InquiryAttachmentService inquiryAttachmentService;
private final InquiryJpaRepository inquiryJpaRepository;
private final InquiryAnswerService inquiryAnswerService;
private final InquiryAnswerJpaRepository inquiryAnswerJpaRepository;

@Transactional
public void createInquiry(UserJpaEntity user, InquiryCreateRequest request) {
Expand Down Expand Up @@ -74,6 +71,17 @@ public void deleteInquiry(UserJpaEntity user, Long postId) {
inquiryJpaRepository.delete(inquiry);
}

@Transactional
public void updateInquiry(UserJpaEntity user, InquiryUpdateRequest request, Long postId) {
InquiryJpaEntity inquiry = getInquiry(postId);
hasPermission(inquiry.getUserId(), user);

inquiry.update(request.title(), request.content(), user.getName());
inquiryJpaRepository.save(inquiry);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The explicit call to inquiryJpaRepository.save(inquiry) is redundant here. Since the updateInquiry method is annotated with @Transactional, the inquiry entity fetched by getInquiry is in a managed state. Any changes made to it (like in the inquiry.update(...) call) will be automatically detected and persisted to the database when the transaction commits. Removing this line will make the code cleaner and rely on the standard behavior of Spring Data JPA.


inquiryAttachmentService.updateAttachment(request.attachments(), inquiry);
}

private InquiryDetailResponse toInquiryDetailResponse(InquiryJpaEntity inquiry) {
InquiryAnswerDetailResponse answer = inquiryAnswerService.getInquiryAnswerDetail(
inquiry.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import life.mosu.mosuserver.presentation.notice.dto.NoticeUpdateRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand All @@ -28,12 +30,15 @@ public class NoticeService {
private final NoticeJpaRepository noticeJpaRepository;
private final NoticeAttachmentService attachmentService;

@CacheEvict(cacheNames = "notice", allEntries = true)
@Transactional
public void createNotice(NoticeCreateRequest request, UserJpaEntity user) {
NoticeJpaEntity noticeEntity = noticeJpaRepository.save(request.toEntity(user));
attachmentService.createAttachment(request.attachments(), noticeEntity);
}

@Cacheable(cacheNames = "notice",
key = "'page=' + #page + ',size=' + #size")
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List<NoticeResponse> getNotices(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("id"));
Expand All @@ -44,19 +49,22 @@ public List<NoticeResponse> getNotices(int page, int size) {
.toList();
}

@Cacheable(cacheNames = "notice", key = "#noticeId")
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public NoticeDetailResponse getNoticeDetail(Long noticeId) {
NoticeJpaEntity notice = getNotice(noticeId);

return toNoticeDetailResponse(notice);
}

@CacheEvict(cacheNames = "notice", allEntries = true)
@Transactional
public void deleteNotice(Long noticeId) {
NoticeJpaEntity noticeEntity = getNotice(noticeId);
noticeJpaRepository.delete(noticeEntity);
}

@CacheEvict(cacheNames = "notice", allEntries = true)
@Transactional
public void updateNotice(Long noticeId, NoticeUpdateRequest request, UserJpaEntity user) {
NoticeJpaEntity noticeEntity = getNotice(noticeId);
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package life.mosu.mosuserver.domain.caffeine;

import java.time.Duration;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum CacheGroup {

NOTICE(
"notice",
Duration.ofMinutes(10),
CacheType.LOCAL
),

INQUIRY(
"inquiry",
Duration.ofMinutes(10),
CacheType.GLOBAL
),

COMPOSITE_ALL(
"composite",
Duration.ofMinutes(10),
CacheType.COMPOSITE
);


private final String cacheName;
private final Duration expiredAfterWrite;
private final CacheType cacheType;
}
13 changes: 13 additions & 0 deletions src/main/java/life/mosu/mosuserver/domain/caffeine/CacheType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package life.mosu.mosuserver.domain.caffeine;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public enum CacheType {
LOCAL("로컬 타입만 적용"),
GLOBAL("분산 캐시만 적용"),
COMPOSITE("로컬 + 분산 캐시 모두 적용");

private final String type;

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,10 @@ public void updateStatusToComplete() {
public void updateStatusToPending() {
this.status = InquiryStatus.PENDING;
}

public void update(final String title, final String content, final String author) {
this.title = title;
this.content = content;
this.author = author;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -13,7 +12,6 @@
regexp = "^01[016789]-\\d{3,4}-\\d{4}$",
message = "전화번호 형식은 010-XXXX-XXXX 이어야 합니다."
)
@NotBlank
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import org.springframework.context.annotation.Configuration;

@Configuration
public class CaffeineCacheConfig {
public class CaffeineFilterCacheConfig {

@Bean
public Cache<String, RequestCounter> ipRequestCountsCache(IpRateLimitingProperties properties) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package life.mosu.mosuserver.infra.notify;

import jakarta.annotation.PostConstruct;
import java.util.Map;
import life.mosu.mosuserver.infra.notify.dto.discord.DiscordExceptionNotifyEventRequest;
import lombok.RequiredArgsConstructor;
Expand All @@ -20,15 +21,25 @@ public class DiscordNotifier implements NotifyClientAdapter<DiscordExceptionNoti

private final WebClient webClient;

@Value("${discord.base-url}")
private String DISCORD_WEBHOOK_URL;
@Value("${discord.base-url:}")
private String discordWebhookUrl;

@PostConstruct
void init() {
log.info("DiscordNotifier 초기화, Webhook URL: {}", discordWebhookUrl);
}

@Override
public void send(DiscordExceptionNotifyEventRequest request) {
if (discordWebhookUrl.isBlank()) {
log.debug("Discord Webhook URL 미설정, 알림 전송하지 않습니다.");
return;
}

String message = request.getMessage();

webClient.post()
.uri(DISCORD_WEBHOOK_URL)
.uri(discordWebhookUrl)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(Map.of("content", message))
.retrieve()
Expand Down
Loading