사이먼's 코딩노트
[SpringBoot] REST API (3) 본문
[REST API 적용]
- 저번 'REST API (2)' 포스팅에 이어서 REST API를 적용하기 위한 코드를 작성해봅시다.
- 전체 코드와 패키지 구조는 깃허브 리포지터리 주소를 통해 참고 부탁드립니다.
- 리포지터리 URL 주소 : https://github.com/psm817/jwt_review
[로그인 시 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인 것을 확인할 수 있다.
[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에 출력이 되는 것을 확인할 수 있다.
[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());
}
반응형
'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 |