[스프링, 스프링부트] Spring test - 스프링 시큐리티의 authentication객체를 어떻게 사용해야 할까?

2023. 8. 22. 00:51·Spring/테스트 코드
반응형

Controller 테스트를 하면서 시큐리티의 authentication객체를 매게변수로 받아서 사용하도록 작성한 메서드에서 많은 고민을 하게되었다.  왜냐하면 테스트를 위해 이 객체를 주입해줘야 했기 때문이다. 그래서 어떻게 해결할지 고민을 했다.

 

 

1. 먼저 커스텀 Annotation을 만들어서 사용해보자는 결심이 들었다.


1-1. config 패키지에 annotation 패키지를 생성한다.

1-2. annotation패키지에 아래의 클래스들을 생성한다

  • InjectAuthentication - 어노테이션 클래스
/**
 * 시큐리티 authentication을 주입하는 커스텀 annotation
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface InjectAuthentication {
}
  • AOP를 설정할 AuthenticationAspect 클래스
@Aspect
@Component
public class AuthenticationAspect {

    @Around("@annotation(com.jinan.profile.config.annotation.InjectAuthentication)")
    public Object injectAuthentication(ProceedingJoinPoint joinPoint) throws Throwable {
        // 인증 객체 가져오기
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // ThreadLocal을 사용하여 현재 스레드에 인증 객체 저장
        CurrentUserContextHolder.set(authentication);

        // 메서드를 실행한다.
        Object result = joinPoint.proceed();

        // 메서드 실행 후 ThreadLocal에서 인증 객체 제거
        CurrentUserContextHolder.clear();

        return result;
    }
}
  • ThreadLocal을 처리할 CurrentUserContextHolder 클래스 생성
/**
 * CurrentUserContextHolder라는 ThreadLocal을 사용하여 현재 스레드에 인증 객체를 저장하고, 이를 메서드 내부에서 사용할 수 있도록 한다.
 */
public class CurrentUserContextHolder {

    private static final ThreadLocal<Authentication> context = new ThreadLocal<>();

    public static void set(Authentication authentication) {
        context.set(authentication);
    }

    public static Authentication get() {
        return context.get();
    }

    public static void clear() {
        context.remove();
    }
}

1-3. 커스텀 어노테이션 사용방법

  • 컨트롤러를 작성하자
// 상단에 이렇게 만들어준 어노테이션을 적는다.
@InjectAuthentication
@PostMapping("/createBoard")
public String createBoard(@RequestBody BoardRequest request) {

    // 현재 인증된 사용자의 loginId 가져오기 - 어노테이션이 동작해서 세팅된 값을 가져온다.
    Authentication authentication = CurrentUserContextHolder.get();
    String loginId = authentication.getName();

    // 사용자 로그인id를 사용하여 사용자의 전체 정보 가져오기 (예: 서비스 또는 리포지토리에서)
    User user = Optional.ofNullable(userService.findByLoginId(loginId))
            .map(User::of)
            .orElseThrow(() -> new ProfileApplicationException(ErrorCode.USER_NOT_FOUND));

    // 사용자 정보를 request에 추가
    request.setUser(user);

    boardService.createBoard(request);
    return "redirect:/board/list";
}

1-4. 결론

  • 이렇게 사용하는게 대체 뭐가 좋은건지 모르겠다. (어노테이션을 적용시키는 방법은 실패라고 생각됨)

  • 그냥 매게변수안에 Authentication authentication이나 Principal principal을 적어서 꺼내서 사용하는게 낫지않을까? 라는 생각이 들었다.

  • 아직 내가 잘 몰라서 그런것일수도있어서 다른 방법을 찾아보기 시작했다. 나는 컨트롤러 메서드가 매게변수에 시큐리티의 authentication 객체를 받아서 사용하도록 하고싶지가 않았다.

 


 

2. 해결방안


2-1. SecurityContextHolder를 직접 컨트롤러에서 사용하는 것은 좋은 설계 방식은 아니다.

  • Controller는 주로 요청(request)을 처리하고 응답(response)을 반환하는 역할에 집중하게 설계하는 것이 좋다. SecurityContextHolder와 같은 보안 관련 로직을 컨트롤러에 직접 넣는 것은 관심사의 분리 원칙에 위배될 수 있다.

2-2. 일반적인 설계 원칙에 따르면:

  1. 컨트롤러는 주로 사용자의 요청을 처리하고 응답을 반환하는 역할에 집중해야 한다.

  2. 서비스 계층은 비즈니스 로직 및 데이터 처리를 담당한다.

  3. 도메인 모델은 비즈니스 로직의 핵심이며, 데이터와 관련된 규칙 및 연산을 포함한다.

  4. 리포지토리는 데이터의 CRUD 작업을 담당한다.
  • 따라서, SecurityContextHolder를 사용하여 현재 사용자를 가져오는 로직은 서비스 또는 유틸리티 클래스로 추출하는 것이 좋다. 이렇게 하면 컨트롤러는 보안 관련 로직에 대해 알 필요가 없으며, 요청 처리에만 집중할 수 있다.

2-3. 사용자를 가져오는 클래스 생성

@RequiredArgsConstructor
@Service
public class SecurityService {

    private final UserRepository userRepository;

    /**
     * 지금 접속중인 유저의 이름을 꺼내온다.
     */
    public String getCurrentUsername() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            return authentication.getName();
        }
        return null;
    }

    /**
     * 유저이름으로 유저를 꺼낸다.
     */
    public User getCurrentUser() {
        String username = getCurrentUsername();

        if (username != null) {
            return userRepository.findUserByLoginId(username)
                    .orElseThrow(() -> new ProfileApplicationException(ErrorCode.USER_NOT_FOUND));
        }

        return null;
    }
}

2-4. 컨트롤러에 적용

  • securityService.getCurrentUsername(); 이렇게 시큐리티 유틸 클래스를 만들고 가져다 사용하도록 변경했다.
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/board")
@Controller
public class BoardController {

    private final SecurityService securityService;
    
    /**
     * Action - 게시글 저장기능
     * request로 받아온 데이터를 db에 저장한다.
     */
    @PostMapping("/createBoard")
    public String createBoard(@RequestBody BoardRequest request) {

        String loginId = securityService.getCurrentUsername();

        User user = Optional.ofNullable(userService.findByLoginId(loginId))
                .map(User::of)
                .orElseThrow(() -> new ProfileApplicationException(ErrorCode.USER_NOT_FOUND));

        request.setUser(user);
        boardService.createBoard(request);

        return "redirect:/board/list";
    }
    
}

 

 

결론적으로 나는 서비스 클래스를 따로 만들어서 의존성을 주입해서 사용하는 방식을 선택했다. 이렇게 함으로써 테스트 코드를 짜는것이 많이 편해졌다.





2023.08.17 - [Spring 테스트코드/기초 지식] - Spring test - 코드 작성중 만난 트러블 슈팅(서비스)




 

Spring test - 코드 작성중 만난 트러블 슈팅(서비스)

코딩은 글쓰기라고 생각한다. 꾸준히 기록하며 내 개발 실력을 키울것이다.

curiousjinan.tistory.com

 

반응형

'Spring > 테스트 코드' 카테고리의 다른 글

주니어 개발자의 테스트 코드 이해하기  (2) 2023.12.22
[스프링, 스프링 부트] Spring test - when()에서 발생한 에러  (0) 2023.08.22
[스프링, 스프링부트] Spring test - 테스트 코드의 기초(5) [CRUD 테스트]  (0) 2023.08.09
[스프링, 스프링 부트] Spring test - 테스트 코드의 기초(4) [mock 테스트]  (0) 2023.08.09
[스프링, 스프링 부트] Spring test - 테스트 코드의 기초(3) [Mockito.when() 메서드]  (0) 2023.08.09
'Spring/테스트 코드' 카테고리의 다른 글
  • 주니어 개발자의 테스트 코드 이해하기
  • [스프링, 스프링 부트] Spring test - when()에서 발생한 에러
  • [스프링, 스프링부트] Spring test - 테스트 코드의 기초(5) [CRUD 테스트]
  • [스프링, 스프링 부트] Spring test - 테스트 코드의 기초(4) [mock 테스트]
Stark97
Stark97
문의사항 또는 커피챗 요청은 링크드인 메신저를 보내주세요! : https://www.linkedin.com/in/writedev/
  • Stark97
    오늘도 개발중입니다
    Stark97
  • 전체
    오늘
    어제
    • 분류 전체보기 (240)
      • 개발지식 (20)
        • 스레드(Thread) (8)
        • WEB, DB, GIT (3)
        • 디자인패턴 (8)
      • JAVA (21)
      • Spring (88)
        • Spring 기초 지식 (35)
        • Spring 설정 (6)
        • JPA (7)
        • Spring Security (17)
        • Spring에서 Java 활용하기 (8)
        • 테스트 코드 (15)
      • 아키텍처 (5)
      • MSA (14)
      • DDD (7)
      • gRPC (9)
      • Apache Kafka (18)
      • DevOps (23)
        • nGrinder (4)
        • Docker (1)
        • k8s (1)
        • 테라폼(Terraform) (12)
      • AWS (32)
        • ECS, ECR (14)
        • EC2 (2)
        • CodePipeline, CICD (8)
        • SNS, SQS (5)
        • RDS (2)
      • notion&obsidian (3)
  • 링크

    • notion기록
    • 깃허브
    • 링크드인
  • hELLO· Designed By정상우.v4.10.0
Stark97
[스프링, 스프링부트] Spring test - 스프링 시큐리티의 authentication객체를 어떻게 사용해야 할까?
상단으로

티스토리툴바