리팩토링 - yi5oyu/writemd GitHub Wiki
UserService.deleteUserData(), ChatService.deleteAllSessions()
개수에 따라 여러번의 개별 쿼리 실행
\\ UserService.deleteUserData()
@Transactional
public void deleteUserData(Long userId) {
Users user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
// 채팅 및 세션 삭제
List<Notes> userNotes = noteRepository.findByUsers_Id(userId);
for (Notes note : userNotes) {
List<Sessions> sessions = sessionRepository.findByNotes_id(note.getId());
sessionRepository.deleteAll(sessions);
}
// 메모 삭제
List<Memos> userMemos = memoRepository.findByUsers_Id(userId);
memoRepository.deleteAll(userMemos);
// API 키 삭제
List<APIs> userApis = apiRepository.findByUsersId(userId);
apiRepository.deleteAll(userApis);
// 템플릿 삭제
List<Folders> userFolders = folderRepository.findByUsers(user);
folderRepository.deleteAll(userFolders);
// 노트 삭제
noteRepository.deleteAll(userNotes);
}
배치 처리: 모든 데이터를 한 번에 처리
// QueryDSL
// UserRepositoryCustom
public interface UserRepositoryCustom {
Optional<Long> findIdByGithubId(String githubId);
Optional<String> findPrincipalNameByGithubId(String githubId);
void deleteUserDataBatch(Long userId);
}
// UserRepositoryCustomImpl
@Override
public void deleteUserDataBatch(Long userId) {
// 순서대로 삭제
queryFactory.delete(qChats)
.where(qChats.sessions.notes.users.id.eq(userId))
.execute();
queryFactory.delete(qSessions)
.where(qSessions.notes.users.id.eq(userId))
.execute();
queryFactory.delete(qTemplates)
.where(qTemplates.folders.users.id.eq(userId))
.execute();
queryFactory.delete(qFolders)
.where(qFolders.users.id.eq(userId))
.execute();
queryFactory.delete(qTexts)
.where(qTexts.notes.users.id.eq(userId))
.execute();
queryFactory.delete(qNotes)
.where(qNotes.users.id.eq(userId))
.execute();
queryFactory.delete(qMemos)
.where(qMemos.users.id.eq(userId))
.execute();
queryFactory.delete(qAPIs)
.where(qAPIs.users.id.eq(userId))
.execute();
queryFactory.delete(qUsers)
.where(qUsers.id.eq(userId))
.execute();
}
// UserService.deleteUserData()
@Transactional
public void deleteUserData(Long userId) {
// 모든 데이터 삭제
userRepository.deleteUserDataBatch(userId);
}
TemplateService.getTemplates()
사용자의 템플릿 목록을 조회할 때, 폴더 수에 비례하여 데이터베이스 쿼리가 증가하는 N+1 문제
// Folders 엔티티
@Entity
public class Folders {
@OneToMany(mappedBy = "folders",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.EAGER)
private List<Templates> templates = new ArrayList<>();
}
// getTemplates 메소드
@Transactional(readOnly = true)
public List<FolderDTO> getTemplates(Long userId) {
// 사용자 조회 쿼리
Users user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
// 폴더 조회 쿼리
List<Folders> userFolders = folderRepository.findByUsers(user);
List<FolderDTO> folderDTOs = new ArrayList<>();
for (Folders folder : userFolders) {
List<TemplateDTO> templateDTOs = new ArrayList<>();
// template의 수에 따라 추가 쿼리 발생
for (Templates template : folder.getTemplates()) {
...
}
}
return folderDTOs;
}
JOIN FETCH 사용(1개 쿼리로 모든 데이터 조회)
// QueryDSL
@Override
public List<Folders> findByUsersWithTemplates(Users user) {
return queryFactory
.selectFrom(qFolders)
.leftJoin(qFolders.templates, qTemplates).fetchJoin()
.where(qFolders.users.eq(user))
.fetch();
}
//
@Transactional(readOnly = true)
public List<FolderDTO> getTemplates(Long userId) {
// 사용자 조회 쿼리
Users user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
// 폴더와 템플릿 1개 쿼리로 모두 조회
List<Folders> userFolders = folderRepository.findByUsersWithTemplates(user);
List<FolderDTO> folderDTOs = new ArrayList<>();
for (Folders folder : userFolders) {
List<TemplateDTO> templateDTOs = new ArrayList<>();
// 이미 로드된 데이터로 추가 쿼리 없이 처리
for (Templates template : folder.getTemplates()) {
...
}
}
return folderDTOs;
}
새 사용자 등록 시마다 템플릿 JSON 파일을 디스크에서 읽어오는 병목
// 매번 JSON 파일 읽기
@Transactional
public Users saveUser(String githubId, ...) {
if (existingUser.isEmpty()) {
try {
Resource myResource = new ClassPathResource("data/template.json");
myTemplates = objectMapper.readValue(myResource.getInputStream(), ...);
} catch (IOException e) {
myTemplates = Collections.emptyList();
}
try {
Resource resource = new ClassPathResource("data/git_template.json");
gitTemplates = objectMapper.readValue(resource.getInputStream(), ...);
} catch (IOException e) {
gitTemplates = Collections.emptyList();
}
}
}
애플리케이션 시작시 JSON 데이터 캐싱처리
// 캐시 초기화
@Component
@RequiredArgsConstructor
public class CacheInitializer {
@EventListener(ApplicationReadyEvent.class)
public void initializeCache() {
loadAndCacheTemplateData("template-data", "my-templates", "data/template.json");
loadAndCacheTemplateData("template-data", "git-templates", "data/git_template.json");
}
}
// 캐싱 서비스
@Service
public class CachingDataService {
@Cacheable(value = "template-data", key = "'my-templates'")
public List<Map<String, String>> getMyTemplates() {
return Collections.emptyList();
}
}
// 캐시에서 조회
public Users saveUser(String githubId, ...) {
if (existingUser.isEmpty()) {
List<Map<String, String>> myTemplates = cachingDataService.getMyTemplates();
List<Map<String, String>> gitTemplates = cachingDataService.getGitTemplates();
}
}
API 호출 시마다 사용자 정보를 DB에서 조회하는 반복 쿼리
// 여러 메소드에서 DB에서 사용자 조회
public NoteDTO createNote(String githubId, String noteName) {
Users user = userRepository.findByGithubId(githubId)
.orElseThrow(() -> new RuntimeException("유저를 찾을 수 없습니다."));
}
public UserDTO userInfo(String githubId) {
Users user = userRepository.findByGithubId(githubId)
.orElseThrow(() -> new RuntimeException("유저 찾을 수 없음"));
}
public Memos saveMemo(String githubId, String text, Long memoId) {
Users user = userRepository.findByGithubId(githubId)
.orElseThrow(() -> new RuntimeException("유저 찾을 수 없음"));
}
...
사용자 정보 캐싱
@Service
public class CachingDataService {
@Cacheable(value = "user", key = "#githubId")
public UserDTO findUserByGithubId(String githubId) {
Users user = userRepository.findByGithubId(githubId)
.orElseThrow(() -> {
return new RuntimeException("유저 찾을 수 없음: " + githubId);
});
return UserDTO.builder()
.userId(user.getId())
.githubId(user.getGithubId())
.name(user.getName())
.avatarUrl(user.getAvatarUrl())
.htmlUrl(user.getHtmlUrl())
.build();
}
}
// 캐시에서 사용자 조회(DB 호출 감소)
public NoteDTO createNote(String githubId, String noteName) {
Users user = cachingDataService.findUserByGithubId(githubId);
}
...
AI 채팅 요청 시마다 사용자의 API 키를 DB에서 조회
// 매번 DB에서 API 키 조회
public String sendChatMessage(Long userId, Long apiId, String message)
APIs apiKey = apiRepository.findById(apiId)
.orElseThrow(() -> new RuntimeException("API 키를 찾을 수 없습니다."));
String apiKeyValue = apiKey.getApiKey();
// AI 서비스 호출...
}
API 키 캐싱
@Service
@RequiredArgsConstructor
public class CachingDataService {
private final ApiRepository apiRepository;
@Cacheable(value = "api-key", key = "#userId + ':' + #apiId")
public APIDTO findApiKey(Long userId, Long apiId) {
Optional<APIs> apiEntity = apiRepository.findById(apiId);
if (apiEntity.isPresent()) {
APIs api = apiEntity.get();
return APIDTO.builder()
.apiId(api.getId())
.aiModel(api.getAiModel())
.apiKey(api.getApiKey())
.build();
}
return null;
}
}
// 캐시에서 API 키 조회
@Service
@RequiredArgsConstructor
public class ChatService {
private final CachingDataService cachingDataService;
public String sendChatMessage(Long userId, Long apiId, String message) {
APIDTO apiKey = cachingDataService.findApiKey(userId, apiId);
if (apiKey != null) {
String apiKeyValue = apiKey.getApiKey();
// AI 서비스 호출...
}
return "응답";
}
}