본문 바로가기
프로젝트/Recipository

[Dev] 22.12.05. 로그인 사용자 정보 얻기

by 규글 2022. 12. 5.

 이번 게시글에서는 게시글 작성에 대한 내용을 종합하여 기록할 것이다. 제목이나 내용은 작성하면 그만이지만, 작성자는 직접 기입하는 것이 아니다. 이에 대한 작업을 잠시 소개하고서 종합적인 내용을 기록하겠다.

 

 

작성자 정보

 이전의 국비 과정에서 작성자의 정보를 불러오는 방법은 로그인 시 session scope에 저장한 로그인 email의 정보를 가져오는 것이었다. 하지만 이번에는 Spring Security를 사용해서 login processing 했고, 당연하게도 필자가 session scope에 따로 저장한 정보는 존재하지 않는다.

 

 이에 따라 Spring Security에서 로그인 한 사용자의 정보를 가져오는 방법이 필요했는데, 이 방법들을 언급한 블로그들이 있어서 그 내용을 살펴보았다.[각주:1] [각주:2] [각주:3] [각주:4] 이들은 모두 한 블로그에 작성된 내용을 기반으로 하는 것 같으나, 해당 게시글로의 접근이 불가능하여 그것을 참고해 작성했다는 게시글을 가져온 것이다. 방법은 다음과 같다.

 

  • Bean에서 사용자 정보 얻기
  • Controller에서 사용자 정보 얻기
  • @AuthenticationPrincipal annotation
  • Adapter Class

 

Bean에서 사용자 정보 얻기

    @PostMapping("/user/write")
    public ResponseEntity<Recipe> write(@ModelAttribute RecipeDto recipeDto,
                                        @RequestPart MultipartFile imageFile,
                                        Principal principal){    
                                        
        SpUser principal = (SpUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

        String username = principal.getUsername();
        String password = principal.getPassword();
        System.out.println(username);
        System.out.println(password);
        
        return ResponseEntity.ok().body(recipeDto.toEntity());
    }

 첫 번째 방법은 전역에 선언된 SecurityContextHolder 를 이용해서 가져오는 방법이다. 가장 간단한 방법이라고 할 수 있겠다. 필자는 UserDetails를 implements 한 SpUser를 사용하였으나, UserDetails를 그대로 사용해도 동일하게 값을 얻을 수 있다. 위 이미지가 도움이 될 것 같아서 다시 가져왔다.

 

Controller에서 사용자 정보 얻기

@RestController
public class RecipeController {
(...)

    @PostMapping("/user/write")
    public ResponseEntity<Recipe> write(@ModelAttribute RecipeDto recipeDto,
                                        @RequestPart MultipartFile imageFile,
                                        Principal principal){

        String username = principal.getName();

        return ResponseEntity.ok().body(recipeDto.toEntity());
    }

 @Controller가 명시된 Bean에서는 method의 인자로 Principal 을 전달받을 수 있다. 이렇게 전달받은 Principal을 통해서 로그인한 사용자의 name을 얻는 방법이 있다. 이 Principal은 SecurityContextHolder의 Principal과 달라서 username을 얻는 것이 아니다. 이 방법의 단점은 username 정보밖에 얻을 수 없다는 것이다.

 

    @PostMapping("/user/write")
    public ResponseEntity<Recipe> write(@ModelAttribute RecipeDto recipeDto,
                                        @RequestPart MultipartFile imageFile,
                                        Authentication authentication){

        SpUser principal = (SpUser) authentication.getPrincipal();
        String username = principal.getUsername();
        String password = principal.getPassword();

        return ResponseEntity.ok().body(recipeDto.toEntity());
    }

 또한 @Controller가 명시된 Bean에서는 Controller의 인자로 Authentication을 받을 수도 있는데, 방식은 첫 번째 SecurityContextHolder를 통해 얻는 과정과 거의 동일하다고 할 수 있다.

 

 

@AuthenticationPrincipal annotation

    @PostMapping("/user/write")
    public ResponseEntity<Recipe> write(@ModelAttribute RecipeDto recipeDto,
                                        @RequestPart MultipartFile imageFile,
                                        @AuthenticationPrincipal SpUser spUser){

        String username = spUser.getUsername();
        String password = spUser.getPassword();

        System.out.println(username);
        System.out.println(password);

        return ResponseEntity.ok().body(recipeDto.toEntity());
    }

 Spring Security 3.2 부터는 annotation을 사용하여 로그인 한 사용자의 객체를 인자에 주입할 수 있게 되었다고 한다. 만약 UserDetails 혹은 그것을 구현한 객체가 존재한다면 annotation을 명시하고 해당 객체를 작성하여 로그인 한 사용자에 대한 정보를 얻을 수 있다.

 

Adapter Class

 지금까지의 내용을 참조했다고 언급한 블로그 중에 세 군데에서 Adapter class를 만들어 사용할 것을 언급하고 있다. 이유는 도메인 객체가 특정 기술에 종속되지 않도록 개발해야하기 때문이라고 말한다. 이는 마치 국비 과정을 한 번 리펙토링 해보는 과정에서의 HttpServlet에 종속되지 않도록 Service 단에서 분리한 것과 비슷한 맥락일 것으로 생각한다.

 세 블로그 모두 이전 code와의 비교가 없어 Account가 어떻게 생겼는지 등을 알 수 없지만, 이 블로그[각주:5]의 게시글에 따르면 기존 login process에서는 User에 넣어 return 하거나 UserDetails를 implements한 Account를 return 하여 사용했던 것으로 보인다.

 

 문제는 이렇게 개발해야 하는 필요성을 아직 인지하지 못하고 있다는 것이다. 특정 기술에 종속되지 않아야한다는 점에는 공감하지만, 지금 이 시점에서 이렇게 개발하는 것이 어떤 의미에서 특정 기술에 종속되지 않는 것인지 깨닫지 못하고 있다.

 

 나름대로 이해해보기 위해서 그림을 그려보았다. 검정색은 기존 필자의 작업, 붉은색은 찾아낸 블로그의 작업을 표기했다. 먼저 블로그의 내용부터 이해해보겠다. UserRepository와 data를 주고 받는 Account Entity가 있다. 이때 로그인 과정의 loadUserByUsername method에서 Account 자체를 return 하기 위해서는 method의 return type이 UserDetails이기 때문에 Account가 UserDetails interface를 implements 한 것이어야만 한다. 하지만 이렇게 된다면 Account는 UserRepository와 data를 주고 받는 역할에 더하여 로그인 과정에서 UserDetails의 역할도 수행하게 되는 것이다. 필자의 작업이 딱 그 모양새이다.

 하지만 Account는 data를 주고 받는 그 자체로 두면서 UserDetails를 구축하기 위해서는 UserDetails와 Account 사이에 UserDetails의 역할을 할 수 있는 UserAdapter를 두고, 이 adapter 객체를 로그인 과정의 loadUserByUsername method에서 return 하도록 한다면 다음과 같이 말할 수 있을 것 같다.

  • Account는 그대로 UserRepository와 data를 주고 받는 역할을 고수한다.
  • UserAdapter는 UserDetails를 implements한 User를 extends 하여 UserDetails와 Account 사이에서 data를 중개하는 역할을 새롭게 하게 된다.
  • Account가 UserDetails를 implements 한 꼴로 하는 것이 꼭 나쁜 것은 아니지만, 보다 객체 지향적으로 구성하기 위해서는 그보다 둘 사이에 Adapter 객체를 형성하는 것이 좋다.

 

 잘 이해한 것인지 모르겠지만, 어느 정도 납득을 하고 있다. 해당 작업을 게시글 작성 기능을 마무리 한 후에 이어서 작업해보겠다.

 

Reference

댓글