API 응답시간 개선 - yi5oyu/writemd GitHub Wiki

API 응답시간 측정

단일 사용자 테스트 모니터링

{54CB22D7-7AA8-4485-893B-3AACE9E67C1E}

개선 후

{7EF2AEE5-F69C-4123-A139-168486FCF824}

로그인 리다이렉트

문제 확인

초기 데이터(JSON) 파일 읽기 시간

GET [302] - REDIRECTION | 832ms(평균) 1.34s(최고) 321ms(최저)

주요 기존 코드

// UserService
    @Transactional
    public Users saveUser(String githubId, String name, String htmlUrl, String avatarUrl, String principalName) {

        Optional<Users> user = userRepository.findByGithubId(githubId);
        if (user.isPresent()) {
            Users existingUser = user.get();
            boolean updated = false;

            if (!Objects.equals(existingUser.getName(), name)) {
                existingUser.setName(name);
                updated = true;
            }
            if (!Objects.equals(existingUser.getAvatarUrl(), avatarUrl)) {
                existingUser.setAvatarUrl(avatarUrl);
                updated = true;
            }
            if (!Objects.equals(existingUser.getPrincipalName(), principalName)) {
                existingUser.setPrincipalName(principalName);
                updated = true;
            }

            return updated ? userRepository.save(existingUser) : existingUser;
        }

        // 새 유저 저장
        Users newUser = Users.builder()
            .githubId(githubId)
            .name(name)
            .htmlUrl(htmlUrl)
            .avatarUrl(avatarUrl)
            .principalName(principalName)
            .build();

        Folders myFolder = Folders.builder()
            .users(newUser)
            .title("내 템플릿")
            .build();

        Folders gitFolder = Folders.builder()
            .users(newUser)
            .title("깃 허브")
            .build();

        // JSON 파일에서 템플릿 데이터 로드
        List<Map<String, String>> myTemplates;
        List<Map<String, String>> gitTemplates;

        try {
            Resource myResource = new ClassPathResource("data/template.json");
            myTemplates = objectMapper.readValue(myResource.getInputStream(),
                new TypeReference<List<Map<String, String>>>() {
                });
        } catch (IOException e) {
            myTemplates = Collections.emptyList();
        }

        for (Map<String, String> templateData : myTemplates) {
            Templates template = Templates.builder().folders(myFolder)
                .title(templateData.getOrDefault("title", ""))
                .description(templateData.getOrDefault("description", ""))
                .content(templateData.getOrDefault("content", ""))
                .build();

            myFolder.getTemplates().add(template);
        }

        try {
            Resource resource = new ClassPathResource("data/git_template.json");
            gitTemplates = objectMapper.readValue(resource.getInputStream(),
                new TypeReference<List<Map<String, String>>>() {
                });
        } catch (IOException e) {
            gitTemplates = Collections.emptyList();
        }

        for (Map<String, String> templateData : gitTemplates) {
            Templates template = Templates.builder().folders(gitFolder)
                .title(templateData.getOrDefault("title", ""))
                .description(templateData.getOrDefault("description", ""))
                .content(templateData.getOrDefault("content", ""))
                .build();

            gitFolder.getTemplates().add(template);
        }

        newUser.getFolders().add(myFolder);
        newUser.getFolders().add(gitFolder);

        return userRepository.save(newUser);
    }

문제 해결

GET [302] - REDIRECTION | 348ms(평균) 403ms(최고) 227ms(최저)

평균 응답시간: 832ms -> 348ms 최고 응답시간: 1.34s -> 403ms 최저 응답시간: 321ms -> 227ms

  1. 초기 데이터(JSON) 파일 읽기 캐싱
  2. 트랜잭션 커밋 완료 후 유저 캐시 비동기 업데이트
// config/RedisConfig.cacheManager()
    // 자동 캐싱설정

// cache/CacheInitializer
    // 캐시 초기화, 초기 JSON 데이터 캐싱

// 유저 정보 비동기 redis 저장 
    @Async
    public void updateUserCacheAsync(String githubId, Users user) {
        Cache cache = cacheManager.getCache("user");
        if (cache != null) {
            cache.put(githubId, user);
        }
    }

// service/UserService.saveUser() 리팩토링
    @Transactional
    public Users saveUser(String githubId, String name, String htmlUrl, String avatarUrl, String principalName) {

        Optional<Users> existingUser = userRepository.findByGithubId(githubId);

        Users user = existingUser
            .map(ckUser -> {
                // 변경사항 체크 후 업데이트(기존 사용자)
                if (!Objects.equals(ckUser.getName(), name)) {
                    ckUser.setName(name);
                }
                if (!Objects.equals(ckUser.getAvatarUrl(), avatarUrl)) {
                    ckUser.setAvatarUrl(avatarUrl);
                }
                if (!Objects.equals(ckUser.getPrincipalName(), principalName)) {
                    ckUser.setPrincipalName(principalName);
                }
                return ckUser;
            })
            .orElseGet(() -> {
                // 새 유저 저장
                return Users.builder()
                    .githubId(githubId)
                    .name(name)
                    .htmlUrl(htmlUrl)
                    .avatarUrl(avatarUrl)
                    .principalName(principalName)
                    .build();
            });

        Users savedUser = userRepository.save(user);

        if (existingUser.isEmpty()) {
            // JSON 파일에서 템플릿 데이터 로드
            List<Map<String, String>> myTemplates = cachingDataService.getMyTemplates();
            List<Map<String, String>> gitTemplates = cachingDataService.getGitTemplates();

            Folders myFolder = Folders.builder()
                .users(savedUser)
                .title("내 템플릿")
                .build();

            Folders gitFolder = Folders.builder()
                .users(savedUser)
                .title("깃 허브")
                .build();

            for (Map<String, String> templateData : myTemplates) {
                Templates template = Templates.builder().folders(myFolder)
                    .title(templateData.getOrDefault("title", ""))
                    .description(templateData.getOrDefault("description", ""))
                    .content(templateData.getOrDefault("content", "")).build();

                myFolder.getTemplates().add(template);
            }

            for (Map<String, String> templateData : gitTemplates) {
                Templates template = Templates.builder().folders(gitFolder)
                    .title(templateData.getOrDefault("title", ""))
                    .description(templateData.getOrDefault("description", ""))
                    .content(templateData.getOrDefault("content", "")).build();

                gitFolder.getTemplates().add(template);
            }

            savedUser.getFolders().add(myFolder);
            savedUser.getFolders().add(gitFolder);

            savedUser = userRepository.save(savedUser);
        }

        final Users finalSavedUser = savedUser;

        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    cachingDataService.updateUserCacheAsync(githubId, finalSavedUser);

                }
            }
        );
        return savedUser;
    }

결과

노트 생성

문제 확인

사용자 조회 + 노트 생성 + 텍스트 엔티티 생성: 순차 처리, 불필요한 DB 조회 중복

POST [200] - /api/note/create/{userName} | 204ms(평균) 338ms(최고) 151ms(최저)

주요 기존 코드

// NoteService
    public NoteDTO createNote(String userName, String noteName) {
        Users user = userRepository.findByGithubId(userName)
            .orElseThrow(() -> new RuntimeException("유저를 찾을 수 없습니다."));

        Notes newNote = Notes.builder()
            .users(user)
            .noteName(noteName)
            .build();

        Notes savedNote = noteRepository.save(newNote);

        Texts text = Texts.builder()
            .notes(savedNote)
            .markdownText("")
            .build();
        textRepository.save(text);

        NoteDTO note = NoteDTO.builder()
            .noteId(savedNote.getId())
            .noteName(savedNote.getNoteName())
            .createdAt(savedNote.getCreatedAt())
            .updatedAt(savedNote.getUpdatedAt())
            .build();

        return note;
    }

문제 해결

POST [200] - /api/note/create/{userName} | 134ms(평균) 295ms(최고) 85.7ms(최저)

평균 응답시간: 204ms -> 134ms 최고 응답시간: 338ms -> 295ms 최저 응답시간: 151ms -> 85.7ms

  1. 반복적인 사용자 조회 캐싱
  2. cascade 설정으로 한 번에 저장(DB 호출 감소)
  3. 노트와 텍스트 엔티티 생성을 하나의 트랜잭션으로 관리
// NoteService.createNote() 리팩토링
    @Transactional
    public NoteDTO createNote(String githubId, String noteName) {
        Users user = cachingDataService.findUserByGithubId(githubId);

        Notes newNote = Notes.builder()
            .users(user)
            .noteName(noteName)
            .build();

        Texts text = Texts.builder()
            .notes(newNote)
            .markdownText("")
            .build();

        newNote.setTexts(text);

        Notes savedNote = noteRepository.save(newNote);

        return NoteDTO.builder()
            .noteId(savedNote.getId())
            .noteName(savedNote.getNoteName())
            .createdAt(savedNote.getCreatedAt())
            .updatedAt(savedNote.getUpdatedAt())
            .build();
    }
⚠️ **GitHub.com Fallback** ⚠️