저는 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를 자유롭게 변경하여 전달할 수 있기 때문입니다)
지금까지는 그렇게 생각합니다. 더 좋은 아이디어가 있으면 개선하도록 노력하겠습니다.
