사이먼's 코딩노트

[SpringBoot] REST API (3) 본문

Java/SpringBoot

[SpringBoot] REST API (3)

simonpark817 2024. 6. 16. 16:32

[REST API 적용]

  • 저번 'REST API (2)' 포스팅에 이어서 REST API를 적용하기 위한 코드를 작성해봅시다.
  • 전체 코드와 패키지 구조는 깃허브 리포지터리 주소를 통해 참고 부탁드립니다.
  • 리포지터리 URL 주소 : https://github.com/psm817/jwt_review
 

GitHub - psm817/jwt_review

Contribute to psm817/jwt_review development by creating an account on GitHub.

github.com

 

[로그인 시 JwtProvider에 의해 토큰 생성]

  • MemberController.java 클래스에서 login을 수행할 때 테스트 용으로 Token을 발급하기 위해서 아래와 같이 코드를 수정한다.
  • SpringBoot가 아닌 일반 Spring을 사용할 때는 HttpServletResponse를 통해 응답을 해줘야 하고, 매개변수에 작성한 이유는 응답상태의 코드나 헤더 내용을 설정하기 위해서이다.
  • 현재 작성된 코드는 테스트용으로 헤더에 내용을 "Authentication", "JWT Token"으로 설정하였다.
@PostMapping("/login")
public Member login(@Valid @RequestBody LoginRequest loginRequest, HttpServletResponse resp) {
    // 테스트용
    resp.addHeader("Authentication", "JWT Token");

    return memberService.findByUsername(loginRequest.getUsername()).orElse(null);
}

 

  • 그 다음은 테스트 MemberController.java 클래스에서 아래와 같이 코드를 작성하면 위에서 실제 MemberController.java의 login() 메서드에서 addHeader()로 헤더에 추가한 "Authentication"을 테스트에서 가져와서 "JWT Token" 이라는 문자열이 출력되는 것을 확인할 수 있다.
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@ActiveProfiles("test")
class MemberControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    @DisplayName("POST /member/login 은 로그인 처리 URL 이다.")
    void t1() throws Exception {
        // When
        ResultActions resultActions = mvc
                .perform(
                        post("/member/login")
                                .content("""
                                        {
                                            "username": "user1",
                                            "password": "1234"
                                        }
                                        """.stripIndent())
                                .contentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8))
                )
                .andDo(print());

        // Then
        resultActions
                .andExpect(status().is2xxSuccessful());

        MvcResult mvcResult = resultActions.andReturn();

        MockHttpServletResponse response = mvcResult.getResponse();

        String authentication = response.getHeader("Authentication");
        System.out.println("authentication : " + authentication);

        assertThat(authentication).isNotEmpty();
    }
}

테스트 성공

 

  • 테스트가 완료되었다면 이번에는 실제로 access Token을 발급해봅시다.
  • 아래 코드는 MemberController.java에 작성된 코드이다.
@PostMapping("/login")
    public Member login(@Valid @RequestBody LoginRequest loginRequest, HttpServletResponse resp) {
//        // 테스트용
//        resp.addHeader("Authentication", "JWT Token");

        String accessToken = memberService.genAccessToken(loginRequest.getUsername(), loginRequest.getPassword());
        resp.addHeader("Authentication", accessToken);

        return memberService.findByUsername(loginRequest.getUsername()).orElse(null);
    }

 

  • 아래는 MemberService.java 클래스에 작성된 코드이다.
  • JwtProvider.java 클래스에 이미 jwt 토큰 발급과 관련한 genToken() 메서드를 구현해 놨기 때문에 유효기간이 1년짜리인 토큰을 발급한다.
  • return 값에 genToken() 메서드의 인자인 member.toClaims() 는 로그인한 사용자의 정보 조각이라고 생각하면 좋다.
public String genAccessToken(String username, String password) {
    Member member = findByUsername(username).orElse(null);

    if(member == null) return null;

    if(!passwordEncoder.matches((password), member.getPassword())) {
        return null;
    }

    // 유효기간 1년
    return jwtProvider.genToken(member.toClaims(), 60 * 60 * 24 * 365);
}

 

  • 여기까지 작성을 마쳤다면 아까 테스트의 MemberController.java로 돌아가서 t1() 메서드를 다시 실행하면 이제 실제로 발급된 토큰이 출력되는 것을 확인할 수 있고, JWT Debugger 홈페이지를 통해서도 확인해보면 우리가 작성한 id가 user1인 것을 확인할 수 있다.

access Token 발급 성공

 

[RsData & LoginResponse 도입]

  • 실제 토큰을 발급했을 때 토큰만 보내주는 것 보다는 RsData를 도입해서 resultCode, msg, data 형식에 맞게끔 데이터를 보내주는 것이 좋기 때문에 아래와 같이 RsData.java 클래스를 하나 생성해서 코드를 작성한다.
  • 해당 코드를 작성하면 데이터를 형식에 맞게 가공해주고, 성공했다면 "S-"로 resultCode가 시작되게 된다.
@Getter
@AllArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class RsData<T> {
    private String resultCode;
    private String msg;
    private T data;

    public static <T> RsData<T> of(String resultCode, String msg, T data) {
        return new RsData<>(resultCode, msg, data);
    }

    public static <T> RsData<T> of(String resultCode, String msg) {
        return of(resultCode, msg, null);
    }

    public boolean isSuccess() {
        return resultCode.startsWith("S-");
    }

    public boolean isFail() {
        return !isSuccess();
    }

    public Optional<RsData<T>> optional() {
        return Optional.of(this);
    }

    public <T> RsData<T> newDataOf(T data) {
        return new RsData<T>(getResultCode(), getMsg(), data);
    }
}

 

  • 그 다음 MemberController.java 클래스에서 응답하는 LoginResponse 이너 클래스를 추가하고, RsData 규격에 맞도록 login() 메서드의 return 값을 변경해준다.
@Getter
    @AllArgsConstructor
    public static class LoginResponse {
        private final String accessToken;
    }

    @PostMapping("/login")
    public RsData<LoginResponse> login(@Valid @RequestBody LoginRequest loginRequest, HttpServletResponse resp) {
//        // 테스트용
//        resp.addHeader("Authentication", "JWT Token");

        String accessToken = memberService.genAccessToken(loginRequest.getUsername(), loginRequest.getPassword());
        resp.addHeader("Authentication", accessToken);

        return RsData.of(
                "S-1",
                "access Token 생성",
                new LoginResponse(accessToken)
        );
    }

 

  • 여기까지 작성을 마쳤다면 다시 테스트의 MemberController.java로 돌아가서 t1() 메서드를 실행하면 이번에는 access token 데이터 값만 출력되지 않고 RsData 형식에 맞도록 body에 출력이 되는 것을 확인할 수 있다.

RsData 형식에 맞도록 변경 성공

 

[API 관리를 위한 버전관리]

  • 현재 Mapping 주소가 /member/login을 통해 로그인을 시도하고 있다.
  • API 요청은 예를 들어, 서비스 이름 service.com인 곳에서 service.com/api/v1/member/login로 요청을 하고 v1과 같이 버전 관리를 하기도 한다.
  • 이번에는 이 버전 관리를 위해서 몇 가지 수정을 해봅시다.
  • 아래는 MemberController.java 클래스에서 어노테이션을 수정한 모습이다.
  • produces는 서버가 클라이언트에게 반환하는 데이터 타입을 명시하고, consumes는 클라이언트가 서버에게 보내는 데이터 타입을 명시한다.
@RestController
@RequestMapping(value = "/api/v1/member", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE)
@RequiredArgsConstructor

 

[로그인한 회원의 정보를 보여주기 위한 /member/me 생성]

  • 현재 PostMapping으로 구현된 /api/v1/member/login은 토큰을 발급하는 역할을 한다면, 이번에는 /api/v1/member/me라는 주소를 통해 발급된 토큰을 확인하도록 메서드를 구현해봅시다.
  • 아래는 MemberController.java 클래스에 추가된 코드이다.
@Getter
@AllArgsConstructor
public static class MeResponse {
    private final Member member;
}

@GetMapping(value = "/me", consumes = ALL_VALUE)
public RsData<MeResponse> me() {
    Member member = memberService.findByUsername("user1").get();

    return RsData.of(
            "S-2",
            "확인 성공",
            new MeResponse(member)
    );
}

 

  • 마지막으로 테스트 MemberController.java 클래스로 이동해서 아래와 같이 t2() 메서드에 토큰 발급이 성공적으로 이루어졌는지 확인하는 코드를 작성하면 작성한 대로 resultCode, msg, 사용자 id, username, email 정보가 출력되는 것을 확인할 수 있다.
@Test
@DisplayName("GET /member/me => 내 정보를 확인하는 URL임")
void t2() throws Exception {

    // When
    ResultActions resultActions = mvc
            .perform(
                    get("/api/v1/member/me")
            )
                    .andDo(print());

    // Then
    resultActions
            .andExpect(status().is2xxSuccessful())
            .andExpect(jsonPath("$.resultCode").value("S-2"))
            .andExpect(jsonPath("$.msg").exists())
            .andExpect(jsonPath("$.data.member.id").exists())
            .andExpect(jsonPath("$.data.member.username").exists());
}

access Token 발급 확인 성공

 

 

 

반응형

'Java > SpringBoot' 카테고리의 다른 글

[SpringBoot] 멀티 채팅방 (1)  (2) 2024.06.19
[SpringBoot] REST API (4)  (0) 2024.06.19
[SpringBoot] REST API (2)  (0) 2024.06.16
[SpringBoot] REST API (1)  (0) 2024.06.16
[SpringBoot] JWT 토큰 발급  (0) 2024.06.15