본문 바로가기

Heute lerne ich/Java

[스프링부트 회원 프로젝트] 로그인

https://youtu.be/W-XKXKLvV_8?si=D_mlosxCSp5tjjOH

이 글은, 코딩레시피 님의 <스프링 부트 쉽게 해보기> 영상을 참조하였으며, 해당 영상을 공부하며 겪었던 오류들과 관련 지식들을 정리했습니다.

 

로그인 페이지 만들기에 앞서, DTO와 Entity의 차이에 대해 알아보자.


00. DTO & Entity

- DTO는 Data Transfer Obejct, 데이터 전송 객체로, 주로 데이터 전송에 사용된다. 클라이언트와 서버, 또는 서비스 간에 데이터를 전달할 때 사용되며 주로 데이터 전송에 필요한 최소한의 필드를 포함한다.

- Entity는 데이터베이스에서 영속적으로 저장되는 데이터의 표현이다. 보통 ORM(Object-Relational Mapping)기술을 사용하여 데이터베이스의 테이블과 매핑된다.

 

간단히 말해, DTO는 데이터의 전송을 위한 표현이고, Entity는 비즈니스 논리를 적용하고, 데이터베이스와의 상호 작용을 관리한다.

 

이 둘을 분리하여 MVC 패턴을 적용하면, 코드의 가독성과 유지보수를 용이하게 할 수 있다. 또, 순환 참조를 예방할 수 있으며 보안 강화의 장점을 갖는다. 따라서 직접 Entity를 반환하지 말고, DTO를 통해 변환하여 반환해야 한다.

 

Controller는 DTO의 형태로 데이터를 받아 Service에 전달한다.

Service는 받은 DTO를 Entity로 변환하고, 작업 수행 후 Entity를 Repository에 전달한다.

Repository는 받은 Entity를 영속화한다.

이때 영속화란 데이터를 영구적으로 저장하고 지속시키는 것으로, 데이터 저장, 접근 및 검색, 갱신 및 삭제 등을 포함한다.

 


01. login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
<form action="/member/login" method="post">
    이메일 : <input type="text" name="memberEmail"> <br>
    비밀번호 : <input type="text" name="memberPassword"> <br>
    <input type="submit" value="로그인">
</form>
</body>
</html>

 

먼저 로그인 페이지를 간단하게 작성한다.

 


02. MemberController

@GetMapping("member/login")
    public String loginForm(){
        return "login";
    }

@PostMapping("member/login")
    public String login(@ModelAttribute MemberDTO memberDTO, HttpSession session){
        MemberDTO loginResult = memberService.login(memberDTO);
        if (loginResult != null){
            // login 성공
            session.setAttribute("loginEmail", loginResult.getMemberEmail());
            // session 저장
            return "main";
        }else{
            // login 실패
            return "login";
        }
    }

 

1. GET방식으로 member/login에 접근했을 때 login 페이지를 반환한다.

2. POST방식으로 member/login에 접근했을 때, memberDTO를 받아 Service에 넘기고, 결과값을 받는다.

이때 결과값이 null이 아니라면 로그인이 성공한 것이므로, 간단한 session(사용자의 이메일)을 저장하고, main 페이지를 반환한다.

결과값이 null이라면 로그인이 실패한 것이므로, 다시 login 페이지를 반환한다.

 

main.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>main</title>
</head>
<body>
    session 값 확인: <p th:text="${session.loginEmail}"></p>
</body>
</html>

 

서버 사이드 렌더링을 위해 thymeleaf를 사용한다.

서버 사이드에서 동작하여 동적 콘텐츠를 생성하고 페이지를 렌더링한다.

매번 다른 session값을 정적 콘텐츠인 html이 표현할 수 없기 때문에 thymeleaf가 사용된다.

 

session 값이 잘 저장됐는지 확인하기 위해 main 페이지에 session값을 출력한다.

로그인 성공시, main 페이지로 이동하며 session 값이 출력되는 것을 확인할 수 있다.

 


02. MemberService / MemberRepository

MemberService

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    public void save(MemberDTO memberDTO){
        // 1. dto -> entity 변환
        // 2. repository의 save 메서드 호출
        MemberEntity memberEntity = MemberEntity.toMemberEntity(memberDTO);
        memberRepository.save(memberEntity);
    }

    public MemberDTO login(MemberDTO memberDTO) {
        /*
            1. 회원이 입력한 이메일로 DB에서 조회함
            2. DB에서 조회한 비밀번호와 사용자가 입력한 비밀번호가 일치하는지 판단
         */
         
        Optional<MemberEntity> byMemberEmail = memberRepository.findByMemberEmail(memberDTO.getMemberEmail());
        if (byMemberEmail.isPresent()){
            // 조회 결과가 있다 (해당 이메일을 가진 회원 정보가 있다)
            MemberEntity memberEntity = byMemberEmail.get();
            if (memberEntity.getMemberPassword().equals(memberDTO.getMemberPassword())){ // == 대신 equals를 반드시 써야 함
                // 비밀번호 일치
                // entity -> dto 변환 후 리턴
                return MemberDTO.toMemberDTO(memberEntity);
            }else{
                // 비밀번호 불일치 (로그인 실패)
                return null;
            }
        }else{
            // 조회 결과가 없다
            return null;
        }
    }
}

 

memberService에 login 메서드를 추가한다. 

 

save(회원가입)에서는 dto -> entity 변환하여 repository에 전달한다.

login은 이메일을 DB에서 조회하여, 조회 결과가 있다면 entity를 가져온다. 이때 entity -> dto로 변환하여 service에 전달한다.

만약 비밀번호가 일치하지 않거나 조회 결과가 없으면 null을 반환한다.

 

MemberRepository

public interface MemberRepository extends JpaRepository<MemberEntity, Long> {
    // 이메일로 회원 정보 조회 (select * from member_table from where member_email=?)
    Optional<MemberEntity> findByMemberEmail(String memberEmail);
}

 

MemberRepository 인터페이스는 JpaRepository 인터페이스를 상속받았기 때문에 Spring Data JPA가 제공하는 다양한 CRUD 메소드를 상속받아 사용할 수 있다. JpaRepository에서 제공하는 메소드 외에도, 사용자가 정의한 메소드를 추가할 수 있다.

findByMemberEmail를 보고 JPA는 "findBy" 다음에 오는 부분은 검색 조건으로 알고, 파라미터로 전달되는 값과 일치하는 값을 찾는다. (자동으로 쿼리를 생성하고 실행한다.)

이처럼 메서드 명을 잘 작성하면 직접 SQL 쿼리를 작성하지 않고도 간편하게 데이터베이스와 상호 작용이 가능하다.

 

여기서 findByMemberEmail 메서드는 Optional 객체로 감싸져 있다.

Optional<MemberEntity>는 Optional을 사용하여 null을 방지한다. 즉, 조회 결과가 존재하지 않으면 Optional.empty()를 반환하고, 조회 결과가 존재하면 해당 결과를 감싼 Optional 객체를 반환한다.

 

Optional<MemberEntity> byMemberEmail = memberRepository.findByMemberEmail(memberDTO.getMemberEmail());
        if (byMemberEmail.isPresent()){
            // 조회 결과가 있다 (해당 이메일을 가진 회원 정보가 있다)
            MemberEntity memberEntity = byMemberEmail.get(); // 안의 entity 개체 가져옴
            if (memberEntity.getMemberPassword().equals(memberDTO.getMemberPassword())){
                // 비밀번호 일치
                // entity -> dto 변환 후 리턴
                return MemberDTO.toMemberDTO(memberEntity);
            }else{
                // 비밀번호 불일치 (로그인 실패)
                return null;
            }
            ...

 

다시 MebmerService의 코드를 보자.

byMemberEmail은 findByMemberEmail에서 반환한 Optional 객체일 것이다.

 

따라서 byMemberEmail.get()으로 안의 entity 개체를 한 번 꺼내고, 

패스워드 비교를 위해 memberEntity.getMemberPassword()로 안의 개체를 비교해야 한다.

이때, memberEntity.getMemberPassword()는 String이고, memberDTO.getMemberPassword 또한 String이기 때문에, == 대신 equals를 사용하여 비교해야 한다.

 


03. MemberDTO

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class MemberDTO {
    private Long id;
    private String memberEmail;
    private String memberPassword;
    private String memberName;

    public static MemberDTO toMemberDTO(MemberEntity memberEntity){
        MemberDTO memberDTO = new MemberDTO();
        memberDTO.setId(memberEntity.getId());
        memberDTO.setMemberEmail(memberEntity.getMemberEmail());
        memberDTO.setMemberName(memberEntity.getMemberName());
        memberDTO.setMemberPassword(memberEntity.getMemberPassword());
        return memberDTO;
    }
}

 

entity -> DTO 변환을 위해 DTO에서 toMemberDTO 메서드를 만든다.

Entity의 값들을 get으로 가져와 DTO에서 set한다.

 


04. application.yml

  # spring data jpa 설정
  jpa:
    database-platform: org.hibernate.dialect.MySQLDialect
    open-in-view: false
    show-sql: true
    hibernate:
      ddl-auto: update

 

ddl-auto 부분을 create에서 update로 변경한다.

create로 하면 매번 새로운 테이블을 생성하므로, update로 바꾸어 변경사항이 있을 때만 테이블을 업데이트한다.

 

 

 

아무래도 간단하게 구현하다보니 보안적으로는 취약한 점이 많은 것 같다.

천천히 바꿔나가면 될 것 같다!