개발환경

  • MacBook Air (M1, 2020)
  • OpenJDK 11
  • IntelliJ IDEA Community Edition
  • Spring Boot 2.7.3
  • MySQL Workbench 8.0.28


기간

  • 2022.8.15 ~


주제

  • 오늘의 소비에 대한 기억과 감정을 기록으로 남기고 나중에 되돌아보는 시간이 있었으면 좋겠다는 생각에 진행하게 된 프로젝트이다.
  • 새로 산 물건의 사진을 찍고 그에 대한 감상을 함께 글로 남길 수 있는 기능을 메인으로 제작할 것이다.


진행상황

MemberRepository

  • Repository에서 로그인 요청이 들어온 아이디가 존재하는지 확인하는 메서드를 만들었다.
package august.soil.repository;

import august.soil.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;

    public Optional<Member> findByLoginId(String loginId) {
        return Optional.ofNullable(em.createQuery("select m from Member m where m.loginId = :loginId", Member.class)
                .setParameter("loginId", loginId)
                .getSingleResult());
    }
}

LoginService

  • Service 계층에서는 방금 만든 로그인 아이디 검색 메서드의 결과로 반환된 회원 정보에서 요청받은 비밀번호와 일치하는지 확인한다.
package august.soil.service;

import august.soil.domain.Member;
import august.soil.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LoginService {

    private final MemberRepository memberRepository;

    /**
     * 로그인 처리
     * @param loginId
     * @param password
     * @return null - 로그인 실패
     */
    public Member login(String loginId, String password) {
        return memberRepository.findByLoginId(loginId)
                .filter(m -> m.getPassword().equals(password))
                .orElse(null);
    }
}
  • 회원 정보의 비밀번호가 요청받은 비밀번호와 일치하면 회원 객체를 반환하고 그렇지 않으면 null을 반환한다.
  • 자바 8 이상에서 제공되는 스트림을 사용해… 간결하고 있어보이는 코드 작성
  • 처음엔 스트림이 어렵게 느껴졌는데 쓰다 보니까 편하다. 스트림에 익숙해지는 건 람다식 작성에 얼마나 익숙해지느냐가 좌우하는 것 같다.

로그인 / 로그아웃 처리

  • 이제 POST 매핑으로 로그인 처리를 해 줄 것인데, 먼저 REST API 요청에 대한 응답을 반환할 DTO 클래스를 만든다.

LoginMemberResponse

package august.soil.web.response;

import lombok.AllArgsConstructor;
import lombok.Data;

import javax.validation.constraints.NotEmpty;

@Data
@AllArgsConstructor
public class LoginMemberResponse {

    @NotEmpty
    private int resultCode; // 성공: 1 / 실패 : -1
    @NotEmpty
    private String msg;
}
  • 로그인 요청에 대한 성공 여부를 반환할 것이다. 응답 받은 클라이언트쪽에서 조건 처리하기 편하게 결과값을 정수형으로 저장하고, 상세 메시지를 함께 리턴하도록 했다.
  • 로그아웃 요청시에도 이걸 사용해 같은 응답을 할 것이다.

LoginMemberRequest

package august.soil.web.request;

import lombok.Data;

import javax.validation.constraints.NotEmpty;

@Data
public class LoginMemberRequest {

    @NotEmpty
    private String loginId;
    @NotEmpty
    private String password;
}
  • POST 매핑이기 때문에 바디에서 요청된 JSON 데이터를 변환해 담아올 클래스를 생성했다.

SessionConst

package august.soil.web;

public interface SessionConst {
    String LOGIN_MEMBER = "loginMember";
}
  • 세션의 식별자를 하드코딩하면 수정하기 불편하니까 상수 클래스를 만들었다.

LoginApiController

  • 이제 컨트롤러에서 요청에 대한 응답을 처리할 메서드를 만들어 준다. 로그인 정보는 세션쿠키에 저장하는 방식을 사용했다.
package august.soil.web.api;

import august.soil.domain.Member;
import august.soil.request.LoginMemberRequest;
import august.soil.response.LoginMemberResponse;
import august.soil.response.LogoutMemberResponse;
import august.soil.service.LoginService;
import august.soil.web.SessionConst;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;

@Slf4j
@RestController
@RequiredArgsConstructor
public class LoginApiController {

    private final LoginService loginService;

    @PostMapping("/login")
    public LoginMemberResponse login(
                @RequestBody @Valid LoginMemberRequest param, 
                HttpServletResponse response, 
                HttpServletRequest request) {
        
        Member loginMember = loginService.login(param.getLoginId(), param.getPassword());
        if (loginMember == null) {
            return new LoginMemberResponse(-1, "존재하지 않는 회원이거나 비밀번호가 일치하지 않습니다.");
        } else {
            // 로그인 성공 처리
            // 세션이 있으면 세션 반환, 없으면 신규 세션 생성
            HttpSession session = request.getSession();
            // 세션에 로그인 회원 정보 보관
            session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

            return new LoginMemberResponse(1, "로그인 성공");
        }
    }

    @PostMapping("/logout")
    public LoginMemberResponse logout(HttpServletRequest request) {
        HttpSession session = request.getSession(false); // 로그아웃인데 세션 없을 때 생성 안 함
        if (session != null) {
            session.invalidate(); // 세션 정보가 있으면 정보 삭제
        }
        return new LoginMemberResponse(1, "로그아웃 성공");
    }
}
  • 로그인이 가능한(DB 테이블에 일치하는 회원 정보가 있는) 사용자일 때만 세션을 생성하고 회원 정보를 보관한다.
  • 로그아웃 처리는 요청 파라미터는 없이 세션 정보가 있다면 삭제하도록 했다. request에서 세션을 얻어오는 부분에서 인자값을 넣지 않으면 디폴트가 true이기 때문에 세션이 없는 경우 자동 생성이 된다. 근데 로그아웃하는데 세션이 새로 생성되면 이상하잖아? 그래서 인자값으로 false를 넣어 로그아웃이 호출되었지만 세션 정보가 없는 경우에는 생성하지 않도록 했다.

요청 / 응답 결과

// 요청)
{
    "loginId" : "member1",
    "password" : "1234"
}

// 응답 - 로그인 성공 시)
{
    "resultCode": 1,
    "msg": "로그인 성공"
}

// 응답 - 로그인 실패 시)
{
    "resultCode": -1,
    "msg": "존재하지 않는 회원이거나 비밀번호가 일치하지 않습니다."
}

// 로그아웃)
{
    "resultCode": 1,
    "msg": "로그아웃 성공"
}
  • 포스트맨 테스트 결과 잘 나온다~~!!

  • 참고로 포스트맨에서 쿠키 생성 결과도 볼 수 있어서 편했다. 나도 편해서 쓰지만 많이 쓰는 이유가 있었네~~


정리

  • 예전에 스프링 레거시 타입으로 프로젝트를 할 때에 HttpServletRequest를 사용해서 세션을 저장하려고 한땀한땀 코딩하던 기억이 난다… 그 때엔 막연하게 로그인 정보는 세션에 저장해야 한다고 배우면서 했었는데, 이번에 다시 강의를 들으면서 공부하면서 해 보니까 세션에 저장되는 원리를 알 수 있어 좋았다. 그리고 코드를 효율적으로 작성하는 법도 알려줘서 좋았다.


참고