이번 강의에서는 회원 로그인 기능을 구현했다.
이전까지는 회원가입과 API Key 발급 구조를 만들었고,
이번 로그인은 단순 인증을 넘어서 API Key를 클라이언트에게 전달하는 역할을 한다.
로그인 기능 역시 테스트 → 구현 순서로 진행했다.
@Test
@DisplayName("로그인")
void t2() throws Exception {
ResultActions resultActions = mvc
.perform(
post("/api/v1/members/login")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"username": "user1",
"password": "1234"
}
""".stripIndent())
)
.andDo(print());
Member member = memberService.findByUsername("user1").get();
resultActions
.andExpect(handler().handlerType(ApiV1MemberController.class))
.andExpect(handler().methodName("login"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").value("200-1"))
.andExpect(jsonPath("$.msg").value("%s님 환영합니다.".formatted(member.getNickname())))
.andExpect(jsonPath("$.data").exists())
.andExpect(jsonPath("$.data.item.id").value(member.getId()))
.andExpect(jsonPath("$.data.item.name").value(member.getName()))
.andExpect(jsonPath("$.data.apiKey").value(member.getApiKey()));
}
👉 로그인 성공 시
record MemberLoginReqBody(
@NotBlank
@Size(min = 2, max = 30)
String username,
@NotBlank
@Size(min = 2, max = 30)
String password
) {
}
record MemberLoginResBody(
MemberDto item,
String apiKey
) {
}
MemberDto는 회원 정보를 표현하기 위한 공용 DTO다.
public record MemberDto(
int id,
LocalDateTime createDate,
LocalDateTime modifyDate,
String name
) {
}
👉 그래서 보안에 민감한 apiKey는 포함하지 않았다.
맞다.
하지만 차이는 “항상 보내느냐 / 필요한 순간에만 보내느냐” 다.
📌 DTO는 API의 의도를 표현하는 계약서다.
“이 응답은 로그인 전용이다”를 구조로 드러내는 것이 중요하다.
@PostMapping("/login")
public RsData<MemberLoginResBody> login(
@Valid @RequestBody MemberLoginReqBody reqBody
) {
Member member = memberService.findByUsername(reqBody.username())
.orElseThrow(() -> new ServiceException("401-1", "존재하지 않는 아이디입니다."));
if (!member.getPassword().equals(reqBody.password())) {
throw new ServiceException("401-2", "비밀번호가 일치하지 않습니다.");
}
return new RsData<>(
"200-1",
"%s님 환영합니다.".formatted(member.getName()),
new MemberLoginResBody(
new MemberDto(member),
member.getApiKey()
)
);
}