Backend/Spring Framework

[Spring Boot] 서비스에서 로그인한 유저에 대한 의존성을 줄이는 방법에 대해

mopil 2023. 2. 17. 03:09
반응형

필자는 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();
    }
}

*여기서 Account 객체는 유저를 뜻한다.

AccountService

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

Security의 ThreadLocal에서 로그인한 사용자 정보값을 받아와서 바로 객체로 가져오는 메소드이다.

 

ArchiveService

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

아카이브 서비스라는 가상의 서비스에서 로그인한 사용자가 아카이브를 생성하는 로직을 생각해 보자.

로그인한 사용자 정보가 필요하면 이런 식으로 로그인한 사용자 정보를 가져오곤 했다.

 

# 문제점

위와 같은 방식은 두 가지 문제점을 가진다.

 

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);
    }
}

위 코드는 더미 아카이브를 넣는 코드이다.

스프링 부트가 실행되면 자동으로 수행되도록 설정하여 초기값을 세팅하는 역할을 한다. 이게 필요한 이유는, 개발 단계에서 프론트가 더미데이터를 기반으로 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에 의존하고 있어서, 단위테스트를 작성하기가 까다롭다.

 

 

# 해결방법

그래서 필자가 고안한 방법은 archiveCreate 메소드 처럼, 로그인한 사용자 정보가 필요한 메소드 내부에 getCurrentAccount에 대한 의존성을 줄이는 방법이다.

즉, 컨트롤러 단에서 이를 수행하고 서비스 파라미터로 해당 로그인 유저 객체를 넘겨주는 방식으로 구성했다.

 

ArchiveController

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

 

ArchiveService

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

 

# 다른 문제

뭔가 성공할 것 같지만, 이 방식은 다른 문제점이 존재한다.

 

만약 이 상태에서 새롭게 만들어진 아카이브를 매핑되어 있는 유저 객체에도 추가하는 로직이 있다고 가정해 보자.

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

 

이때 no Session 예외가 발생한다.

 

failed to lazily initialize a collection of role: plub.plubserver.domain.account.model.Account.archiveList, could not initialize proxy - no Session

 

이는 외부에서 전달받은 account 객체 새로운 트랜잭션이 수행되면서 (createArchive) account에 대한 기존 영속성이 사라졌기 때문이다. (영속성 컨텍스트에서 관리하지 않게 됨)

그래서 보통 1 on 다는 지연로딩을 설정하는데, 지연로딩을 불러올 수 없어서 발생하는 문제였다.

 

# 최종 해결

지연로딩을 즉시로딩을 바꿔도 되지만, 다른 방법을 찾고 싶었다.

 

여러 번 고민을 한 결과 전달받은 유저 객체를 다시 영속성 컨텍스트로 불러오는 방법을 생각했다.

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

이렇게 하면 no Session 문제를 해결할 수 있으며, getCurrentAccount에 대한 의존성을 느슨하게 유지할 수 있다.

(물론 accountService에 대한 의존성이 추가되긴 했지만... 기존보다는 유연한 구조이다. 왜냐하면 loginAccount를 외부에서 마음대로 변경해서 넘겨줄 수 있기 때문)

 

 

현재로서 생각한 방법은 이 정도인 것 같다. 뭔가 더 좋은 아이디어가 떠오르면 개선해 봐야겠다.

반응형