ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • REST와 자원 그리고 시행착오
    Server 2020. 8. 24. 20:47


    우테코 미션 중 지하철 노선도 구현(즐겨찾기)를 하며 발생한 에피소드에 대한 글

     

     

     

     

    지하철 노선도 구현 미션은 총 3단계로 구성된다. 
    노선도 자체를 구현하기. 그리고 경로를 탐색하는 기능을 추가하기, 마지막으로 사용자 관리와 즐겨찾기 기능 추가.
    이 중 마지막 미션에서 사용자 관리를 추가하며 고민이 시작되었다.

    사용자 회원가입과 로그인 기능은 잘 구현했다. 문제는 로그인 후 사용자 정보를 확인하는 API에 있었다. 로그인이 완료된 후에는 브라우져가 토큰을 가지고 있다가 해당 토큰을 이용하여 인가가 필요한 요청에 토큰을 함께 보내 MethodArgumentResolver를 이용하여 유효성 검사를 한다. 여기서 인가가 필요한 요청에 대한 응답으로 사용자 정보를 반환한다. 아래 코드를 살펴보자.

     

    @RequestMapping("/members")
    public class MemberController {
    	private final MemberService memberService;
    
    	...
    
    	@GetMapping("/{id}") // 문제의 메서드 => 이미 인증된 사용자에 대해 사용자 자신의 정보라는 자원을 돌려준다.
    	public ResponseEntity<MemberResponse> getMemberOfMineBasic(@PathVariable Long id, @LoginMember Member member) {
    		member.validateId(id);
    
    		return ResponseEntity.ok().body(MemberResponse.of(member));
        }
        
    	@NoValidate  
    	@PostMapping("/login")
    	public ResponseEntity<TokenResponse> login(@RequestBody @Valid LoginRequest param, HttpSession session) {
    		Member member = memberService.loginWithForm(param.getEmail(), param.getPassword());
    
    		String token = memberService.createToken(param);
    		session.setAttribute("loginMemberEmail", param.getEmail());
    
    		return ResponseEntity.ok()
    				.location(URI.create("/members/" + member.getId()))
    				.body(new TokenResponse(token, "bearer"));
    	}
        
        ...
    }

     

    이게 무슨 문제가 있느냐라 생각하실테니 수정하기 전에 제공받은 스켈레톤 코드(아래)에는 이런 클래스가 있었다.

     

    @RestController
    public class LoginMemberController { // LoginMember를 따로 관리하는 컨트롤러 
        private MemberService memberService;
    
        public LoginMemberController(MemberService memberService) {
            this.memberService = memberService;
        }
    
        @PostMapping("/oauth/token")
        public ResponseEntity<TokenResponse> login(@RequestBody LoginRequest param) {
            String token = memberService.createToken(param);
            return ResponseEntity.ok().body(new TokenResponse(token, "bearer"));
        }
    
        @PostMapping("/login") // 로그인하는 메서드
        public ResponseEntity login(@RequestParam Map<String, String> paramMap, HttpSession session) {
            String email = paramMap.get("email");
            String password = paramMap.get("password");
            if (!memberService.loginWithForm(email, password)) {
                throw new InvalidAuthenticationException("올바르지 않은 이메일과 비밀번호 입력");
            }
    
            session.setAttribute("loginMemberEmail", email);
    
            return ResponseEntity.ok().build();
        }
    
        @GetMapping({"/me/basic", "/me/session", "/me/bearer"}) // 바로 위에서 언급한 문제 코드와 같은 일을 하는 메서드
        public ResponseEntity<MemberResponse> getMemberOfMineBasic(@LoginMember Member member) {
            return ResponseEntity.ok().body(MemberResponse.of(member));
        }
    }

     

    바로 사용자 로그인에 관한 컨트롤러!
    사용자 로그인을 시키기도 하고 로그인한 사용자에 대한 정보를 반환해주기도 하는 컨트롤러가 따로 존재했다. 

     

     


     

    그럼 여기서 어떤 문제가 있다는걸까? 

    당시 REST에 대해 공부하던 나와 유안은 한가지 자원에 대해서는 하나의 일관된 api로 접근해야한다는 생각을 했다. 즉 사용자를 접근할 때에는 ~/members로 시작하는 uri로 접근해야만 한다고 생각했다. 그 이유로는 REST에서는 uri를 통해 자원을 특정지어야 한다고 배웠기 때문이다. 그렇기에 스켈레톤 코드처럼 로그인한 멤버 자신을 가리키는 ~/me라는 uri를 사용해서는 안된다고 생각했다. 생각의 결과로 우리는 LoginMemberController를 없에고 MemberController에 사용자(member)와 관련된 모든 api를 두고 ~/members로 시작하는 uri로 관리했다. ~/me와 같은 경우는 모두 로그인한 사용자 자신의 식별자를 담아 ~/members/{memberId}와 같은 uri를 통해 자원을 접근하도록 수정했다.

     

    그래 무척 RESTful 하다!

    그런데 귀찮다. 로그인한 사용자의 식별자(id)를 따로 관리해줘야 한다는 점이 말이다. 그리고 다른 문제점도 있다. 사용자 본인으로 로그인하지 않은 경우에도 사용자의 정보를 접근하고 싶을 경우가 있을 수 있다. 예를 들어 사용자 정보를 관리하는 페이지에서 특정 사용자 정보를 확인하고 싶을 경우다. 이 때 관리자 계정을 가진 사용자는 로그인 되어있지 않은 사용자의 정보에 대해 접근해야한다. 이런 상황에서 사용자 정보에 대해 생각해보니 자원의 성격이 나뉜다는 생각이 들었다. 사용자 정보 자체로그인 한 후 자신의 사용자 정보. 원점이다. 레거시(스켈레톤 코드)가 LoginMemberController와 ~/me를 사용한 데에는 이런 이유가 있었던걸까?

     

    검색하라, 확실해질 때 까지

    레거시 코드를 접한 직후 불완전한 지식 아닌 검색이나 질문 통해 얻은 증명된 지식으로 프로그래밍을 했다면 미션 진행이 더 수월했을 것이다. (물론 온라인 상의 정보는 잘 판별해서 습득해야한다) 

     

    ---- 질문은 /me 와  같은 url을 사용하여 로그인한 사용자 정보를 처리해도 되는가에 대한 이야기 ----

    It's up to you.All the approaches are perfectly fine from a REST perspective.

    According to Roy Thomas Fielding's dissertation*,any information that can be named can be a resource:

    5.2.1.1 Resources and Resource Identifiers

    The key abstraction of information in REST is a resource. Any information that can be named can be a resource: a document or image, a temporal service (e.g. "today's weather in Los Angeles"), a collection of other resources, a non-virtual object (e.g. a person), and so on. In other words, any concept that might be the target of an author's hypertext reference must fit within the definition of a resource. A resource is a conceptual mapping to a set of entities, not the entity that corresponds to the mapping at any particular point in time. [...]

    When using/me,/users/me,/users/myself,/users/currentand similars, you have a locator for theauthenticated userand it will always identify theconceptof anauthenticated user, regardless of which user is authenticated.

    For more flexibility, you also can support/users/{username}.

    By the way, a similar situation was addressed inIs using magic (me/self) resource identifiers going against REST principles?

    관련 내용에 대한 스택오버플로우

     

    맹목적으로 글을 따를 필요는 없지만 생각해보지 못했던 REST에 대한 지식을 전달해줬다. 그리고 실제로 이렇게 사용한다는 점도 알게 되었다. 이름 지을 수 있다면 어떠한 정보든 자원이 될 수 있다니. 단순하게 사용자 정보가 아니라 인증된 사용자 정보라는 자원이라 부를 수 있다는 점이 확실해졌다. 

     

    이번 글은 프로그래밍 이론이나 기술적인 정보를 제공하기 위해 작성하기 보다는 시행착오 과정을 담았다. 모르는 부분은 확실하게 알고 넘어가자. 공부하는 데에 드는 시간이 추후 수정하는 데에 걸리는 시간보다 적을테니. 또한 유연하게 생각하자. 사용자 정보에 대해 맹목적으로 하나의 자원이라고 생각했기에 시행착오를 겪었다. 의심하고 다시 확인해보자.
    기존의 레거시 코드가 있다면, 의구심이 든다면 찝찝하게 넘어가지 말고 왜 그렇게 코드를 짰는지를 파악하고 접근하자. 코드를 짠 사람은 당시 최선의 아웃풋을 냈을테니. 혹은 내가 모르는 지식이 숨어있을테니.

    댓글

Toneyparky Blog