[데브코스] Spring Boot 인증·인가(Auth) (20강) - 로그인 API 구현과 API Key 응답 설계

zuno·2026년 1월 12일

이번 강의에서는 회원 로그인 기능을 구현했다.
이전까지는 회원가입과 API Key 발급 구조를 만들었고,
이번 로그인은 단순 인증을 넘어서 API Key를 클라이언트에게 전달하는 역할을 한다.


✅ 20강에서 한 일 요약

  • 로그인 API 구현
  • 로그인 테스트 코드 작성
  • 로그인 성공 시 API Key 반환
  • DTO 설계 분리 (MemberDto vs Login 응답 DTO)

🧪 1️⃣ 로그인 테스트 코드부터 작성

로그인 기능 역시 테스트 → 구현 순서로 진행했다.

@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()));
}

👉 로그인 성공 시

  • 회원 정보
  • API Key
    가 함께 내려오는지를 검증한다.

📦 2️⃣ 로그인 요청 / 응답 DTO 설계

로그인 요청 DTO

record MemberLoginReqBody(
        @NotBlank
        @Size(min = 2, max = 30)
        String username,
        @NotBlank
        @Size(min = 2, max = 30)
        String password
) {
}

로그인 응답 DTO

record MemberLoginResBody(
        MemberDto item,
        String apiKey
) {
}

🤔 MemberDto에 apiKey를 넣지 않은 이유

MemberDto는 회원 정보를 표현하기 위한 공용 DTO다.

public record MemberDto(
        int id,
        LocalDateTime createDate,
        LocalDateTime modifyDate,
        String name
) {
}
  • 여러 API에서 재사용됨
  • 브라우저로 자주 전달됨
  • 기본적인 회원 정보만 담는 용도

👉 그래서 보안에 민감한 apiKey는 포함하지 않았다.


❗ 그런데 결국 apiKey를 응답으로 보내지 않나?

맞다.
하지만 차이는 “항상 보내느냐 / 필요한 순간에만 보내느냐” 다.

  • ❌ MemberDto에 apiKey 포함
    → 모든 회원 관련 응답에 apiKey가 섞이기 시작
  • ✅ 로그인 응답 DTO에만 apiKey 포함
    → 로그인 성공 시에만 제한적으로 노출

📌 DTO는 API의 의도를 표현하는 계약서다.
“이 응답은 로그인 전용이다”를 구조로 드러내는 것이 중요하다.


🔐 3️⃣ 로그인 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()
            )
    );
}

🔄 로그인 이후 흐름 정리

  1. 클라이언트가 username/password로 로그인
  2. 서버에서 인증
  3. 성공 시 apiKey 반환
  4. 클라이언트는 apiKey를 저장
  5. 이후 모든 요청은 apiKey 기반 인증

📌 이번 강의 핵심 요약

  • 로그인은 인증 + API Key 발급
  • apiKey는 공용 DTO에 넣지 않는다
  • 민감 정보는 필요한 순간에만, 최소 범위로
  • 테스트 코드가 구조를 명확하게 만든다

🔜 다음 단계

  • username/password 인증 ❌
  • apiKey 인증 ✅
  • 진짜 REST API 인증 구조로 진입

0개의 댓글