[Spring Boot] 서비스에서

저는 Spring Security로 로그인을 구현할 때 현재 JWT 토큰 값으로 로그인한 사용자를 즉시 ​​호출할 수 있는 편의 메서드를 만들어 사용합니다.

public class SecurityUtils {

    public static String getCurrentAccountEmail() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication.getName() == null) {
            throw new AccountException(AccountCode.NOT_FOUND_ACCOUNT);
        }
        return authentication.getName();
    }
}

* 여기서 계정 개체는 사용자를 나타냅니다.

계정 서비스

public Account getCurrentAccount() {
    return accountRepository.findByEmail(getCurrentAccountEmail())
            .orElseThrow(() -> new AccountException(AccountCode.NOT_FOUND_ACCOUNT));
}

이 메서드는 Security의 ThreadLocal에서 로그인한 사용자의 정보 값을 검색하여 개체로 직접 가져옵니다.

아카이브 서비스

@Transactional
public ArchiveIdResponse createArchive(Long plubbingId, ArchiveRequest form) {
    Account account = accountService.getCurrentAccount();
    // 비즈니스 로직...
}

로그인한 사용자가 Archive Service라는 가상의 서비스에서 아카이브를 생성하는 논리를 고려해 봅시다.

이와 같이 로그인한 사용자 정보가 필요할 때 로그인한 사용자 정보를 조회하였다.

# 문제

위의 절차에는 두 가지 문제가 있습니다.

1. 더미 데이터를 붙여넣을 수 없습니다.

public void run(ApplicationArguments args) {
    if (archiveRepository.count() > 0) {
        log.info("(4) 아카이브가 존재하여 더미를 생성하지 않았습니다.");
        return;
    }
    Account admin1 = accountService.getAccountByEmail("admin1");
    for (int i = 0; i < 10; i++) {
        ArchiveDto.ArchiveRequest archiveRequest = new ArchiveDto.ArchiveRequest(
                "테스트 아카이브" + i,
                List.of(PLUB_MAIN_LOGO, PLUB_MAIN_LOGO, PLUB_PROFILE_TEST)
        );
        archiveService.createArchive(admin1, 1L, archiveRequest);
    }
}

위의 코드는 더미 아카이브를 삽입하는 코드입니다.

Spring Boot 실행시 자동으로 실행되도록 설정하여 초기값을 설정하는 역할을 합니다. 그 이유는 개발 단계에서 더미 데이터를 기반으로 UI-QA로 접수를 진행하기 때문입니다.

(물론 실제 데이터를 입력할 수 있는 경우에는 필요하지 않습니다.)

위 코드에서 createArchive에 문제가 있습니다. 이는 ThreadLocal에서 JWT를 기반으로 자격 증명을 가져오지 않는 관리 사용자의 getCurrentAccountEmail에서 예외가 발생하기 때문입니다.

public class SecurityUtils {

    public static String getCurrentAccountEmail() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication.getName() == null) {
            throw new AccountException(AccountCode.NOT_FOUND_ACCOUNT);
            // API요청으로 JWT를 받지 않았으므로, 컨텍스트에 Authentication 정보가 없다.
            // 즉 어드민 계정으로 스프링 부트 실행시 더미데이터를 넣으려 하면 이 부분에서 예외가 발생한다.
        }
        return authentication.getName();
    }
}

2. 테스트 코드 작성이 어렵다.

위와 비슷한 이유로 archiveCreate 메서드는 단위 테스트를 작성하기 어렵게 만드는 getCurrentAccount에 의존합니다.

# 해결

그래서 제가 개발한 방법은 다음과 같이 로그인한 사용자 정보가 필요한 메소드 내에서 getCurrentAccount에 대한 종속성을 줄이는 것입니다. B. archiveCreate 방법.

즉, 컨트롤러가 이 작업을 수행하고 적절한 로그인 사용자 개체를 서비스 매개 변수로 전달하도록 구성됩니다.

아카이브 컨트롤러

@PostMapping
public ApiResponse<ArchiveIdResponse> createArchive(
        @PathVariable Long plubbingId,
        @Valid @RequestBody ArchiveRequest archiveRequest
) {
    Account loginAccount = accountService.getCurrentAccount();
    return success(
            archiveService.createArchive(loginAccount, plubbingId, archiveRequest)
    );
}

아카이브 서비스

@Transactional
public ArchiveIdResponse createArchive(Account loginAccount, Long plubbingId, ArchiveRequest form) {
    // 비즈니스 로직...
}

# 다른 문제

뭔가 작동하는 것 같지만 이 방법에는 다른 문제가 있습니다.

새로 생성된 아카이브를 이 상태에서 매핑된 사용자 개체에 추가하는 논리가 있다고 가정해 보겠습니다.

@Transactional
public ArchiveIdResponse createArchive(Account loginAccount, Long plubbingId, ArchiveRequest form) {
    // 비즈니스 로직...
    
    // 만든 아카이브를 매핑된 유저에게도 저장
    loginAccount.addArchive(archive);
}

이 시점에서 세션 예외는 발생하지 않습니다.

역할 컬렉션 지연 초기화 실패: plub.plubserver.domain.account.model.Account.archiveList, 프록시 초기화 실패 – 세션 없음

이는 외부에서 받은 계정 객체에 대해 새로운 트랜잭션(createArchive)을 수행하면 계정의 기존 지속성이 사라지기 때문입니다. (지속성 컨텍스트로 관리되지 않음)

그래서 보통 Lazy Loading을 1로 설정하는데 Lazy Loading을 호출할 수 없는 문제였습니다.

# 최종 해상도

레이지 로딩을 인스턴트 로딩으로 전환하는 것도 가능하지만 다른 방법을 찾고 싶었습니다.

많은 고려 사항으로 인해 전달된 사용자 개체를 지속성 컨텍스트로 다시 가져오는 방법을 생각해 왔습니다.

@Transactional
public ArchiveIdResponse createArchive(Account loginAccount, Long plubbingId, ArchiveRequest form) {
    Account account = accountService.getAccount(loginAccount.getId());
    // 비즈니스 로직...
    
    // 만든 아카이브를 매핑된 유저에게도 저장
    loginAccount.addArchive(archive);
}

이것은 세션이 없는 문제를 해결하고 getCurrentAccount에 대한 종속성을 느슨하게 유지합니다.

(물론 accountService에 대한 의존성이 추가되긴 했지만.. 이전보다는 유연해졌습니다. 외부에서 loginAccount를 자유롭게 변경하여 전달할 수 있기 때문입니다)

지금까지는 그렇게 생각합니다. 더 좋은 아이디어가 있으면 개선하도록 노력하겠습니다.