ํ์ : PM(1) / Design(1) / Frontend(2) / Backend(3)
๊ธฐ๊ฐ : 2024.03 ~ 2025.03
๋งํฌ : https://github.com/M-ung/MoodBuddy_Server
์๋น์ค ๋ด์ฉ : ์ฌ์ฉ์๊ฐ ์์ฑํ ์ผ๊ธฐ๋ฅผ ๋ฐํ์ผ๋ก ๊ฐ์ ๋ถ์ํ๋ ์น ์๋น์ค
์ํต : GitHub, Slack, Notion, Discord
์ฐ๋ฆฌ ์๋น์ค "MoodBuddy" ๋ ์ฌ์ฉ์ ํผ์๋ง์ ๊ณต๊ฐ์ด๊ณ ๋ค๋ฅธ ์ฌ์ฉ์๊ฐ ์ ๊ทผํ ์ ์๋ ๊ณต๊ฐ์ด๊ธฐ ๋๋ฌธ์ ๋์์ฑ์ ํฌ๊ฒ ๊ณ ๋ คํ์ง ์์๋ค. ๊ทธ ์ด์ ๋ ๋ง์ฝ ์ผ๊ธฐ ์์ , ์ผ๊ธฐ ์ญ์ , ๋ถ๋งํฌ, ๋ฑ์ ๊ธฐ๋ฅ์ ํ๋์ ์ผ๊ธฐ๋ฅผ ๊ฐ์ง๊ณ ๋์์ ํธ์ถํ ์ผ์ด ์๊ธฐ ๋๋ฌธ์ด๋ค.
๊ทธ๋์ ๋์์ฑ์ ๊ณ ๋ คํ์ง ์๊ณ ๊ธฐ๋ฅ ๊ตฌํ์ ๋ชฐ๋ํ๋ค.
ํ.์ง.๋ง ๋ง์ฝ์ '๋์์ ์ฌ๋ฌ ๊ธฐ๊ธฐ๋ก ์ผ๊ธฐ์ ์ ๊ทผํ๋ค๋ฉด..?' ๋ผ๋ ์๊ฐ์ ํ๋ค. ์๋ฅผ ๋ค์ด ์ฌ์ฉ์๊ฐ ๋ ธํธ๋ถ, ํธ๋ํฐ์ผ๋ก ๋์์ ์ผ๊ธฐ ์์ ๋๋ ์ญ์ ๋ฅผ ํ๋ค๋ฉด ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ ๋๋ค.
ํ์ง๋ง ์ ๊ฐ์ ๊ฒฝ์ฐ๋ ์ ๋ง ์ ๋ง ์ ๋ง ์ ์ ์ผ์ด๋ผ๊ณ ์๊ฐํ๋ค. ๊ทธ๋๋ ๊ฐ๋ฐ์๋ผ๋ฉด ์ ๊ฐ์ ๊ฒฝ์ฐ๋ ์๊ฐํด์ ๊ฐ๋ฐ์ ํด์ผ ํ๋ค๊ณ ์๊ฐํ๊ธฐ ๋๋ฌธ์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ๋ก ํ๋ค.
์์์ ๋งํ๋ฏ์ด ์ ๊ฐ์ ๊ฒฝ์ฐ๋ ์์ฃผ ์ผ์ด๋ ์ผ์ด ์๋๋ผ๊ณ ํ๋จํ์๊ธฐ์ ์ต๋ํ ๊ฐ๋จํ๊ณ ๊ฐ๋ณ๊ฒ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ๋ก ํ๋ค. ๊ทธ๋์ ์ ํํ ๋ฐฉ์์ "๋๊ด์ ๋ฝ"์ด๋ค.
๊ทธ ์ด์ ๋ ์๋์ ๊ฐ๋ค.
๋๊ด์ ๋ฝ์ ๋์ ์์ ์ด ์ ์ ๊ฒฝ์ฐ ํจ์จ์ ์ด๋ค.
ํธ๋์ญ์
์ถฉ๋์ด ๋ฐ์ํ ๋๋ง ๊ฐ์งํ์ฌ ์ฑ๋ฅ ์ ํ๊ฐ ์ ๋ค.
๋น๊ด์ ๋ฝ๋ณด๋ค ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฝ์ ์ต์ํํ์ฌ ๋ถํ๋ฅผ ์ค์ธ๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์์ฃผ ๋ฐ์ํ์ง ์๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ ํฉํ๋ค๊ณ ํ๋จํ๋ค.
๋จผ์ ์ผ๊ธฐ(Diary) ์ํฐํฐ๋ฐ version
์ ์ถ๊ฐํด ์ฃผ์๋ค.
๐ Diary.java
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "diary")
public class Diary extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "date", nullable = false)
private LocalDate date;
@Column(name = "content", nullable = false, columnDefinition = "text")
private String content;
@Enumerated(EnumType.STRING)
@Column(name = "weather", nullable = false)
private DiaryWeather weather;
@Enumerated(EnumType.STRING)
@Column(name = "emotion")
private DiaryEmotion emotion;
@Enumerated(EnumType.STRING)
@Column(name = "subject")
private DiarySubject subject;
@Column(name = "summary", columnDefinition = "varchar(255)")
private String summary;
@Column(name = "user_id", nullable = false, columnDefinition = "bigint")
private Long userId;
@Column(name = "book_mark")
private Boolean bookMark;
@Enumerated(EnumType.STRING)
@Column(name = "font")
private DiaryFont font;
@Enumerated(EnumType.STRING)
@Column(name = "font_size")
private DiaryFontSize fontSize;
@Column(name = "thumbnail", columnDefinition = "text")
private String thumbnail;
@Enumerated(EnumType.STRING)
@Column(name = "mood_buddy_status")
private MoodBuddyStatus moodBuddyStatus;
@Version
@Column(name = "version", nullable = false)
private Long version;
}
๋ค์์ผ๋ก ์ผ๊ธฐ ์์ ๊ณผ ์ผ๊ธฐ ์ญ์ ๊ธฐ๋ฅ์ ์๋์ ๊ฐ์ด version ์ถฉ๋์ด ์ผ์ด๋๋์ง ๊ฐ์งํ ์ ์๋๋ก ๊ตฌํํด ์ฃผ์๋ค.
๐ DiaryServiceImpl.java
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class DiaryServiceImpl implements DiaryService {
private final DiaryRepository diaryRepository;
@Override
@Transactional
public Long updateDiary(final Long userId, DiaryReqUpdateDTO requestDTO) {
try {
var findDiary = findDiaryById(userId, requestDTO.diaryId());
findDiary.updateDiary(requestDTO);
return findDiary.getId();
} catch (ObjectOptimisticLockingFailureException ex) {
throw new DiaryConcurrentUpdateException(ErrorCode.DIARY_CONCURRENT_UPDATE);
}
}
@Override
@Transactional
public LocalDate deleteDiary(final Long userId, final Long diaryId) {
try {
final var findDiary = findDiaryById(userId, diaryId);
findDiary.updateMoodBuddyStatus(MoodBuddyStatus.DIS_ACTIVE);
return findDiary.getDate();
} catch (OptimisticLockException ex) {
throw new DiaryConcurrentUpdateException(ErrorCode.DIARY_CONCURRENT_DELETE);
}
}
}
๐ DiaryRepository.java
public interface DiaryRepository extends JpaRepository<Diary, Long>, DiaryRepositoryCustom {
boolean existsByUserIdAndDate(Long userId, LocalDate date);
@Lock(LockModeType.OPTIMISTIC)
Optional<Diary> findByUserIdAndIdAndMoodBuddyStatus(final Long userId, final Long diaryId, MoodBuddyStatus moodBuddyStatus);
}
๋ ์ ์ผ๊ธฐ ์์ ๊ณผ ์ผ๊ธฐ ์ญ์ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ฉด์ ์ถ๊ฐ์ ์ผ๋ก ๋ ๊ณ ๋ฏผ์ด "์ผ๊ธฐ ์ ์ฅ" ๋ํ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ํ์๊ฐ ์๋ค๊ณ ์๊ฐํ๋ค. ๊ทธ ์ด์ ๋ ์ฐ๋ฆฌ "MoodBuddy" ์๋น์ค๋ ํ๋ฃจ์ ํ ๋ฒ ์ผ๊ธฐ๋ฅผ ์์ฑํ ์ ์๊ธฐ ๋๋ฌธ์ ํ๋์ ๋ ์ง์ ์ผ๊ธฐ๊ฐ 2๋ฒ ์ด์ ์์ฑ๋๋ฉด ์ ๋๊ธฐ ๋๋ฌธ์ด๋ค. ๊ทธ๋์ ์ด ๋ฌธ์ ๋ ํ ์ด๋ธ์ ์ ์ฝ ์กฐ๊ฑด์ ๊ฑธ์ด์ ๊ฐ๋จํ ํด๊ฒฐํ๋ค.
@Table(name = "diary",
uniqueConstraints = @UniqueConstraint(columnNames = {"userId", "date"}))
๋ฌธ์ ํด๊ฒฐ๋ก ์๋์ ๊ฐ์ ๊ฒฐ๊ณผ๋ฅผ ์ป์๋ค.
1. ํฌ๋ฐํ ๋์์ฑ ์ถฉ๋ ์ํฉ์ ๊ณ ๋ คํด ๋๊ด์ ๋ฝ์ผ๋ก ์ฒ๋ฆฌ.
2. 10๋ ๊ธฐ๊ธฐ ๊ธฐ์ค, ๋์์ ์์ฒญ ์ค 1๋ ์ฑ๊ณต, 9๋ ์คํจ.
๊ทธ๋ฆฌ๊ณ ์ผ๊ธฐ ๊ด๋ฆฌ ์๋๋ฆฌ์ค๋ ์๋์ ๊ฐ๋ค.
๐ ์ผ๊ธฐ ๊ด๋ฆฌ ์๋๋ฆฌ์ค
๊ทธ๋ผ ์ ์๋๋ฆฌ์ค๋๋ก ํ ์คํธ๋ฅผ ์งํํด ๋ณด๊ฒ ๋ค.
@Test
@DisplayName("์ผ๊ธฐ ์ ์ฅํ ๋ ๋์์ฑ ํ
์คํธ")
public void ์ผ๊ธฐ_์ ์ฅํ _๋_๋์์ฑ_ํ
์คํธ() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failureCount = new AtomicInteger(0);
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.submit(() -> {
try {
diaryService.saveDiary(1L, saveRequestDTO);
successCount.incrementAndGet();
} catch (Exception e) {
failureCount.incrementAndGet();
e.printStackTrace();
} finally{
latch.countDown();
}
});
}
executorService.shutdown();
latch.await();
assertThat(successCount.get()).isEqualTo(1);
assertThat(failureCount.get()).isEqualTo(THREAD_COUNT - 1);
}
@Test
@DisplayName("์ผ๊ธฐ ์์ ํ ๋ ๋์์ฑ ํ
์คํธ")
public void ์ผ๊ธฐ_์์ ํ _๋_๋์์ฑ_ํ
์คํธ() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failureCount = new AtomicInteger(0);
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.submit(() -> {
try {
diaryService.updateDiary(1L, updateRequestDTO);
successCount.incrementAndGet();
} catch (Exception e) {
failureCount.incrementAndGet();
e.printStackTrace();
} finally{
latch.countDown();
}
});
}
executorService.shutdown();
latch.await();
Optional<Diary> findDiary = diaryRepository.findById(diaryId);
assertThat(findDiary.get().getTitle()).isEqualTo(updateRequestDTO.diaryTitle());
assertThat(successCount.get()).isEqualTo(1);
assertThat(failureCount.get()).isEqualTo(THREAD_COUNT - 1);
}
@Test
@DisplayName("์ผ๊ธฐ ์ญ์ ํ ๋ ๋์์ฑ ํ
์คํธ")
public void ์ผ๊ธฐ_์ญ์ ํ _๋_๋์์ฑ_ํ
์คํธ () throws InterruptedException {
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failureCount = new AtomicInteger(0);
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.submit(() -> {
try {
diaryService.deleteDiary(1L, diaryId);
successCount.incrementAndGet();
} catch (Exception e) {
failureCount.incrementAndGet();
e.printStackTrace();
} finally{
latch.countDown();
}
});
}
executorService.shutdown();
latch.await();
Optional<Diary> findDiary = diaryRepository.findById(diaryId);
assertThat(findDiary.get().getMoodBuddyStatus()).isEqualTo(MoodBuddyStatus.DIS_ACTIVE);
assertThat(successCount.get()).isEqualTo(1);
assertThat(failureCount.get()).isEqualTo(THREAD_COUNT - 1);
}
ํ ์คํธ ๋ชจ๋ ํต๊ณผํ๋ ๊ฑธ ๋ณผ ์ ์๋ค.
์ด๋ฒ ๊ฒฝํ์ ํตํด ๋์์ฑ ๋ฌธ์ ๋ ์์ํ์ง ๋ชปํ ๊ณณ์์๋ ๋ฐ์ํ ์ ์๋ค๋ ์ ์ ๋ค์ ํ๋ฒ ๊นจ๋ฌ์๋ค. ์ฒ์์๋ ์ฌ์ฉ์๊ฐ ๊ฐ์ ์ผ๊ธฐ๋ฅผ ์ฌ๋ฌ ๊ธฐ๊ธฐ๋ฅผ ๊ฐ์ง๊ณ ๋์์ ์์ ํ๊ฑฐ๋ ์ญ์ ํ๋ ์ผ์ด ๊ทนํ ๋๋ฌผ๋ค๊ณ ์๊ฐํ์ง๋ง, ๊ฐ๋ฐ์๋ก์ ๊ทนํ ๋๋ฌธ ๊ฒฝ์ฐ๋ผ๋ ๋๋นํ๋ ๊ฒ์ด ์ข์ ์๋น์ค์ ๊ธฐ๋ณธ์ด๋ผ๋ ์ ์ ๋๋ผ๊ณ ๋ฐฐ์ธ ์ ์์๋ค.
ํนํ ๋๊ด์ ๋ฝ์ ์ ์ฉํ๋ฉด์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ๋จ์ํ๊ณ ๊ฐ๋ณ๊ฒ ํด๊ฒฐํ ์ ์์๊ณ , JPA์ @Version
๊ธฐ๋ฅ๊ณผ @UniqueConstraint
๋ฅผ ํ์ฉํด ๋ถํ์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฝ์ ์ต์ํํ๋ฉด์๋ ์์ ์ ์ธ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ ์ ์์๋ค.
์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ฉด์ ์ฌ๋ฌ ๋์์ฑ ๋ฌธ์ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์๊ฒ ๋์์ง๋ง, ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ์๋น์ค์ ํน์ฑ์ ๋ง์ถฐ ์ ์คํ๊ฒ ์ ํํด์ผ ํ๋ค๋ ์ ๋ ๊นจ๋ฌ์๋ค.
๋ง์ฝ ์ผ๊ธฐ์ ๋์ ์์ ๊ณผ ์ญ์ ๊ฐ ์ฆ๊ฑฐ๋ ์ฌ์ฉ์๊ฐ ๋ค๋ฅธ ์ฌ์ฉ์์ ์ผ๊ธฐ๋ฅผ ๊ณต์ ํ๋ ์๋น์ค์๋ค๋ฉด ๋๊ด์ ๋ฝ๋ณด๋ค๋ ๋น๊ด์ ๋ฝ์ด๋ ๋ถ์ฐ ๋ฝ์ ๊ณ ๋ คํ์ ๊ฒ์ด๋ค.
์ด๋ฒ ๊ฒฝํ์ ํตํด ์๋น์ค์ ํน์ง์ ๋ฐ๋ผ ์ ์ ํ ๋์์ฑ ์ ์ด ๋ฐฉ๋ฒ์ ์ ์ฉํ๋ ๊ฒ์ด ์ค์ํ๋ฉฐ, ์ฑ๋ฅ๊ณผ ์์ ์ฑ์ ๋ชจ๋ ๊ณ ๋ คํด์ผ ํ๋ค๋ ์ ์ ๋ชธ์ ๋ฐฐ์ธ ์ ์์๋ค.