Skip to content
This repository has been archived by the owner on Jul 29, 2024. It is now read-only.

[BE] feat: 글, 카테고리 저장 로직에 named lock 적용 #582

Merged
merged 8 commits into from
Oct 19, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.donggle.backend.application.service.category;

import org.donggle.backend.application.service.request.CategoryAddRequest;
import org.donggle.backend.infrastructure.concurrent.LockRepository;
import org.springframework.stereotype.Service;

@Service
public class CategoryFacadeService {
private final CategoryService categoryService;
private final LockRepository lockRepository;

public CategoryFacadeService(final CategoryService categoryService, final LockRepository lockRepository) {
this.categoryService = categoryService;
this.lockRepository = lockRepository;
}

public Long addCategory(final Long memberId, final CategoryAddRequest request) {
return lockRepository.executeWithLock(
memberId.toString(),
() -> categoryService.addCategory(memberId, request));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import org.donggle.backend.application.repository.CategoryRepository;
import org.donggle.backend.application.repository.MemberRepository;
import org.donggle.backend.application.repository.WritingRepository;
import org.donggle.backend.application.service.concurrent.NoConcurrentExecution;
import org.donggle.backend.application.service.request.CategoryAddRequest;
import org.donggle.backend.application.service.request.CategoryModifyRequest;
import org.donggle.backend.domain.category.Category;
Expand Down Expand Up @@ -46,7 +45,6 @@ public class CategoryService {
private final CategoryRepository categoryRepository;
private final WritingRepository writingRepository;

@NoConcurrentExecution
public Long addCategory(final Long memberId, final CategoryAddRequest request) {
final Member findMember = findMember(memberId);
final CategoryName categoryName = new CategoryName(request.categoryName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public ConcurrentAccessException() {
}

public int getErrorCode() {
return HttpStatus.CONFLICT.value();
return HttpStatus.BAD_REQUEST.value();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.donggle.backend.application.service.writing;

import lombok.RequiredArgsConstructor;
import org.donggle.backend.application.service.concurrent.NoConcurrentExecution;
import org.donggle.backend.application.service.parse.NotionParseService;
import org.donggle.backend.application.service.request.MarkdownUploadRequest;
import org.donggle.backend.application.service.request.NotionUploadRequest;
Expand All @@ -16,6 +15,7 @@
import org.donggle.backend.exception.business.InvalidFileFormatException;
import org.donggle.backend.infrastructure.client.notion.NotionApiClient;
import org.donggle.backend.infrastructure.client.notion.dto.response.NotionBlockNodeResponse;
import org.donggle.backend.infrastructure.concurrent.LockRepository;
import org.donggle.backend.ui.response.WritingHomeResponse;
import org.donggle.backend.ui.response.WritingListWithCategoryResponse;
import org.donggle.backend.ui.response.WritingPropertiesResponse;
Expand All @@ -38,8 +38,8 @@ public class WritingFacadeService {
private final NotionParseService notionParser;
private final MarkDownParser markDownParser;
private final HtmlRenderer htmlRenderer;
private final LockRepository lockRepository;

@NoConcurrentExecution
public Long uploadMarkDownFile(final Long memberId, final MarkdownUploadRequest request) throws IOException {
final String originalFilename = request.file().getOriginalFilename();
if (!Objects.requireNonNull(originalFilename).endsWith(MD_FORMAT)) {
Expand All @@ -48,10 +48,11 @@ public Long uploadMarkDownFile(final Long memberId, final MarkdownUploadRequest
final String originalFileText = new String(request.file().getBytes(), StandardCharsets.UTF_8);
final List<Block> blocks = markDownParser.parse(originalFileText);

return writingService.saveByFile(memberId, request.categoryId(), originalFilename, blocks);
return lockRepository.executeWithLock(
memberId.toString(),
() -> writingService.saveByFile(memberId, request.categoryId(), originalFilename, blocks));
}

@NoConcurrentExecution
public Long uploadNotionPage(final Long memberId, final NotionUploadRequest request) {
final NotionApiClient notionApiService = new NotionApiClient();
final MemberCategoryNotionInfo memberCategoryNotionInfo = writingService.getMemberCategoryNotionInfo(memberId, request.categoryId());
Expand All @@ -63,7 +64,9 @@ public Long uploadNotionPage(final Long memberId, final NotionUploadRequest requ
final String title = notionApiService.findTitle(parentBlockNode);
final List<Block> blocks = notionParser.parseBody(bodyBlockNodes);
final Writing writing = Writing.of(member, new Title(title), category, blocks);
return writingService.saveAndGetWriting(category, writing).getId();
return lockRepository.executeWithLock(
memberId.toString(),
() -> writingService.saveAndGetWriting(category, writing).getId());
}

public WritingResponse findWriting(final Long memberId, final Long writingId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.donggle.backend.infrastructure.concurrent;

import org.donggle.backend.application.service.concurrent.ConcurrentAccessException;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.function.Supplier;

@Repository
public class LockRepository {
private static final String GET_LOCK = "SELECT GET_LOCK(?,1)";
private static final String RELEASE_LOCK = "SELECT RELEASE_LOCK(?)";
private static final String EXCEPTION_MESSAGE = "LOCK 을 수행하는 중에 오류가 발생하였습니다.";

private final DataSource dataSource;

public LockRepository(final DataSource dataSource) {
this.dataSource = dataSource;
}

public <T> T executeWithLock(final String memberId, final Supplier<T> supplier) {
try (final Connection connection = dataSource.getConnection()) {
try {
getLock(connection, memberId);
return supplier.get();
} finally {
releaseLock(connection, memberId);
}
} catch (final SQLException | ConcurrentAccessException e) {
throw new RuntimeException(e.getMessage(), e);
}
}

public void getLock(final Connection connection, final String memberId) throws SQLException {
try (final PreparedStatement preparedStatement = connection.prepareStatement(GET_LOCK)) {
preparedStatement.setString(1, memberId);

checkResult(preparedStatement);
}
}

public void releaseLock(final Connection connection, final String memberId) throws SQLException {
try (final PreparedStatement preparedStatement = connection.prepareStatement(RELEASE_LOCK)) {
preparedStatement.setString(1, memberId);

checkResult(preparedStatement);
}
}

private void checkResult(final PreparedStatement preparedStatement) throws SQLException {
try (final ResultSet resultSet = preparedStatement.executeQuery()) {
if (!resultSet.next()) {
throw new RuntimeException(EXCEPTION_MESSAGE);
}
final int result = resultSet.getInt(1);
if (result != 1) {
throw new RuntimeException(EXCEPTION_MESSAGE);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.donggle.backend.application.service.category.CategoryFacadeService;
import org.donggle.backend.application.service.category.CategoryService;
import org.donggle.backend.application.service.request.CategoryAddRequest;
import org.donggle.backend.application.service.request.CategoryModifyRequest;
Expand All @@ -24,14 +25,15 @@
@RequiredArgsConstructor
@RequestMapping("/categories")
public class CategoryController {
private final CategoryFacadeService categoryFacadeService;
private final CategoryService categoryService;

@PostMapping
public ResponseEntity<Void> categoryAdd(
@AuthenticationPrincipal final Long memberId,
@Valid @RequestBody final CategoryAddRequest request
) {
final Long categoryId = categoryService.addCategory(memberId, request);
final Long categoryId = categoryFacadeService.addCategory(memberId, request);
return ResponseEntity.created(URI.create("/categories/" + categoryId)).build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.donggle.backend.application.service.category;

import org.donggle.backend.application.repository.CategoryRepository;
import org.donggle.backend.application.repository.MemberRepository;
import org.donggle.backend.application.service.request.CategoryAddRequest;
import org.donggle.backend.domain.category.Category;
import org.donggle.backend.domain.category.CategoryName;
import org.donggle.backend.domain.member.Member;
import org.donggle.backend.domain.member.MemberName;
import org.donggle.backend.domain.oauth.SocialType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class CategoryServiceTest {
@Autowired
private CategoryFacadeService categoryFacadeService;
@Autowired
private MemberRepository memberRepository;
@Autowired
private CategoryRepository categoryRepository;
private Member member;
private Category basicCategory;

@BeforeEach
void setUp() {
//given
member = memberRepository.save(Member.of(
new MemberName("테스트 멤버"),
1234L,
SocialType.KAKAO
));

basicCategory = categoryRepository.save(Category.of(new CategoryName("기본"), member));
}

@Test
void testAddCategoryConcurrency() throws InterruptedException {
final int threads = 100;
final ExecutorService executorService = Executors.newFixedThreadPool(20);
final CountDownLatch latch = new CountDownLatch(threads);

for (int i = 0; i < threads; i++) {
final int finalI = i;
executorService.submit(() -> {
try {
final CategoryAddRequest request = new CategoryAddRequest("TestCategory" + finalI);
final Long categoryId = categoryFacadeService.addCategory(member.getId(), request);
} finally {
latch.countDown();
}
});
}

latch.await();

final Category category = categoryRepository.findLastCategoryByMemberId(member.getId()).get();
final int size = categoryRepository.findAllByMemberId(member.getId()).size();
assertThat(size).isEqualTo(101);
}
}