본문 바로가기
BackEnd/Project

[Board] Ch02. 로그인 페이지 만들기

by 개발 Blog 2024. 8. 8.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

변경사항

로그인 페이지를 만들기에 앞서, 기존 코드의 수정사항과 추가된 코드에 대한 리뷰를 먼저 진행한다.

 

변경사항: ERD 반영

새로운 UserAccount 도메인을 반영하여 ERD를 업데이트하였다.

변경사항: AuditingFields 클래스 변경

  • 변경 내용: AuditingFields 클래스를 추상 클래스로 변경
  • 목적: 엔티티에서 상속을 통해 사용하기 위함
  • 설명: AuditingFields 클래스는 엔티티의 생성일시, 생성자, 수정일시, 수정자를 자동으로 관리하기 위한 클래스이다. 이 클래스를 추상 클래스로 변경하여 엔티티 클래스들이 이를 상속받아 사용하도록 하였다.
package org.example.projectboard.domain;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.ToString;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.format.annotation.DateTimeFormat;

import java.time.LocalDateTime;

@Getter
@ToString
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class AuditingFields { // 추상 클래스 변경

    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt; // 생성일시

    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    @CreatedBy
    @Column(nullable = false, updatable = false, length = 100)
    private String createdBy; // 생성자

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime modifiedAt; // 수정일시

    @LastModifiedBy
    @Column(nullable = false, length = 100)
    private String modifiedBy; // 수정자
}

 

추가사항: 회원 계정 도메인 구현

  • 추가 내용: UserAccount 도메인 생성 및 ERD 반영
  • 목적: 회원 계정을 관리하기 위한 도메인 생성
  • 설명: UserAccount 클래스는 사용자 계정 정보를 관리하는 엔티티이다. MySQL 예약어와의 충돌을 피하기 위해 필드명을 주의하여 지정하였다. 이 클래스는 AuditingFields 클래스를 상속받아 생성일시, 생성자, 수정일시, 수정자를 자동으로 관리한다.
package org.example.projectboard.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.Objects;

@Getter
@ToString
@Table(indexes = {
        @Index(columnList = "userId"),
        @Index(columnList = "email", unique = true),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy")
})
@Entity
public class UserAccount extends AuditingFields {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Setter @Column(nullable = false, length = 50) private String userId;
    @Setter @Column(nullable = false) private String userPassword;

    @Setter @Column(length = 100) private String email;
    @Setter @Column(length = 100) private String nickname;
    @Setter private String memo;

    protected UserAccount() {}

    private UserAccount(String userId, String userPassword, String email, String nickname, String memo) {
        this.userId = userId;
        this.userPassword = userPassword;
        this.email = email;
        this.nickname = nickname;
        this.memo = memo;
    }

    public static UserAccount of(String userId, String userPassword, String email, String nickname, String memo) {
        return new UserAccount(userId, userPassword, email, nickname, memo);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof UserAccount userAccount)) return false;
        return id != null && id.equals(userAccount.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

 

추가사항: 회원 계정 레포지토리 생성

  • 추가 내용: UserAccountRepository 인터페이스 생성
  • 목적: 회원 계정 데이터를 데이터베이스에서 관리하기 위함
  • 설명: UserAccountRepository 인터페이스는 JpaRepository를 상속받아 UserAccount 엔티티에 대한 CRUD 연산을 처리할 수 있도록 한다.
package org.example.projectboard.repository;

import org.example.projectboard.domain.UserAccount;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserAccountRepository extends JpaRepository<UserAccount, Long> {
}

 

변경사항: 테스트 데이터 추가

  • 변경 내용: 테스트 데이터를 위한 SQL 파일 수정
  • 목적: 테스트용 계정 데이터를 데이터베이스에 삽입하기 위함
  • 설명: 테스트 데이터를 삽입하는 SQL 파일에 임의의 계정 데이터를 추가하였다. 패스워드가 노출되는 방식이므로 개선이 필요하다.
-- 테스트 계정
-- TODO: 테스트용이지만 비밀번호가 노출된 데이터 세팅. 개선하는 것이 좋을 지 고민해 보자.
insert into user_account (user_id, user_password, nickname, email, memo, created_at, created_by, modified_at, modified_by) values
    ('eunchan', 'asdf1234', 'Eunchan', 'eunchan@mail.com', 'I am Eunchan.', now(), 'eunchan', now(), 'eunchan');

 

 변경사항: 회원 관련 API 테스트

  • 변경 내용: 회원 관련 API가 제공되지 않는지 확인하는 테스트 추가
  • 목적: 회원 계정 정보가 API로 노출되지 않도록 하기 위함
  • 설명: 모든 HTTP 메소드에 대해 회원 계정 API가 404 Not Found를 반환하는지 확인하는 테스트를 추가하였다.
@Disabled("Spring Data REST 통합테스트는 불필요하므로 제외시킴")
@DisplayName("Data REST -API 테스트")
@Transactional
@AutoConfigureMockMvc
@SpringBootTest
public class DataRestTest {
    private final MockMvc mvc;

    // ...

    @DisplayName("[api] 회원 관련 API 는 일체 제공하지 않는다.")
    @Test
    void givenNothing_whenRequestingUserAccounts_thenThrowsException() throws Exception {
        // Given

        // When & Then
        mvc.perform(get("/api/userAccounts")).andExpect(status().isNotFound());
        mvc.perform(post("/api/userAccounts")).andExpect(status().isNotFound());
        mvc.perform(put("/api/userAccounts")).andExpect(status().isNotFound());
        mvc.perform(patch("/api/userAccounts")).andExpect(status().isNotFound());
        mvc.perform(delete("/api/userAccounts")).andExpect(status().isNotFound());
        mvc.perform(head("/api/userAccounts")).andExpect(status().isNotFound());
    }
}

 

로그인 페이지 만들기

로그인 페이지를 구현하는 과정을 단계별로 설명한다.

 

1. 의존성 추가

로그인 기능을 구현하기 위해 필요한 의존성을 추가한다. Spring Security와 Thymeleaf Extras를 사용하여 뷰를 간편하게 구성한다.

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
  • spring-boot-starter-security
    • Spring Security 기능을 사용할 수 있게 하는 의존성이다.
  • thymeleaf-extras-springsecurity6
    • Thymeleaf 템플릿 엔진과 Spring Security를 통합하여, 뷰에서 보안 관련 기능을 간편하게 사용할 수 있게 해준다.

2. SecurityConfig 클래스 생성

현재 상태에서는 아래 사진과 같이 아무 작업도 할 수 없으므로, Spring Security 설정을 위한 SecurityConfig 클래스를 생성한다.

package org.example.projectboard.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;


@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
                .formLogin(Customizer.withDefaults()) // Deprecated된 formLogin() 메서드 대신 사용
                .build();
    }
}
  • @Configuration
    • 이 클래스가 Spring 설정 클래스임을 나타낸다.
  • securityFilterChain(HttpSecurity http)
    • Spring Security의 필터 체인을 설정하는 메서드다.
  • authorizeHttpRequests
    • 모든 HTTP 요청을 허용한다.
  • formLogin(Customizer.withDefaults())
    • 기본 로그인 폼 설정을 적용한다.

이제 정상적으로 로그인 페이지에 접근할 수 있다.

 

3. 로그인 페이지 테스트 추가

로그인 페이지가 정상적으로 호출되는지 테스트를 추가한다.

package org.example.projectboard.controller;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@DisplayName("View 컨트롤러 - 인증")
@WebMvcTest
public class AuthControllerTest {
    private final MockMvc mvc;

    AuthControllerTest(@Autowired MockMvc mvc) {
        this.mvc = mvc;
    }

    @DisplayName("[view][GET] 로그인 페이지 - 정상 호출")
    @Test
    public void givenNothing_whenTryingToLogin_thenReturnsLogInView() throws Exception {
        // given

        // when & then
        mvc.perform(get("/login"))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML));
    }
}
  • @WebMvcTest
    • MVC 테스트를 위한 어노테이션으로, Spring MVC 컴포넌트만 로드한다.
  • @Import(SecurityConfig.class)
    • 테스트에 필요한 SecurityConfig 설정을 임포트 한다.
  • MockMvc
    • Spring MVC 테스트를 위한 클래스로, HTTP 요청 및 응답 테스트를 수행할 수 있다.
  • mvc.perform(get("/login"))
    • /login URL에 GET 요청을 보낸다.
  • andExpect(status().isOk())
    • HTTP 상태 코드 200(OK)이 반환되는지 확인한다.
  • andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
    • 응답의 컨텐츠 타입이 text/html인지 확인한다.
  • andExpect(view().name("login"))
    • 반환된 뷰의 이름이 login인지 확인한다.

모든 테스트가 정상적으로 통과했음을 확인한다.

 

프로젝트 변경 사항을 GitHub에 푸시하고 프로젝트 보드에서 해당 작업을 완료로 표시한다.

 

정리

1. Spring Security 설정

  • SecurityConfig 클래스를 통해 기본적인 보안 설정을 구성하였다.

2. Thymeleaf 통합

  • Thymeleaf Extras를 활용하여 뷰에서 보안 기능을 간편하게 통합하였다.

3. 테스트 작성

  •  MockMvc를 활용하여 로그인 페이지 및 게시글 페이지에 대한 테스트를 작성하고 성공적으로 통과하였다.

4. 프로젝트 관리

  •  GitHub 프로젝트 보드를 활용하여 작업 진행 상황을 체계적으로 관리하였다.

이번 로그인 기능을 구현하면서 Spring Security의 기본적인 설정과 Thymeleaf를 활용한 뷰 통합을 경험할 수 있었다.

로그인 페이지를 통해 사용자 인증 절차를 구현하고, 이를 테스트를 통해 검증함으로써 애플리케이션의 보안 기능을 강화했다.