본문 바로가기

백엔드/Makit

세션을 통한 회원가입

 

회원가입 기능을 구현할 때 가장 고려해야했던 것은 다음과 같다. 

1. 각 항목을 다른 페이지에서 입력 받기 때문에 각 항목을 세션으로 저장 후 회원가입 시 한번에 db에 저장시키기

2. password 보안성을 위한 단방향 암호화 

 

 

먼저 기본적으로 설계한 구조는 다음과 같다.

 

 

컨트롤러, 서비스, entity, repository는 기본적 요소이니 제외하고

DTO, Validator 등을 추가적으로 사용하고자 했다. 

 

DTO를 사용한 이유는 다음과 같다. 

1. 클라이언트 요청에서 수신하는 데이터와 응답 데이터를 캡슐화

2. 클라이언트와 서버 간의 데이터 구조를 명확히 정의하므로, 예상치 못한 데이터 변화를 방지

3. 요청 데이터가 복잡한 경우(ex) JSON), DTO를 사용하면 코드 가독성과 유지보수성이 향상

4. 요청 데이터에 대해 강력한 타입 검사를 수행

5. 향후 확장성

 

사용한 DTO 예시

 

package com.example.makit.signup.DTO;

import lombok.Data;

import java.util.List;

@Data
public class SignupRequestDTO {
    private String password;           // 사용자가 입력한 비밀번호
    private String confirmPassword;    // 사용자가 입력한 비밀번호 확인
    private boolean showPassword;      // 비밀번호 보기/숨기기
    private String nickname;
    private boolean isNicknameValid;   // 닉네임 유효성 검사 결과

    private String phoneNumber;

    private List<String> selectedFields;  // 사용자가 선택한 분야 '이름' 리스트
    private List<String> selectedGenres;  // 사용자가 선택한 장르 '이름' 리스트
}

 

 

 

 

Validator를 사용한 이유는 다음과 같다 1. 데이터 유효성 검증 로직을 컨트롤러 외부로 분리 -> 재사용 가능(단일책임원리(SRP)준수)2. 협업 시 코드 가독성

 

사용한 Validator 예시

package com.example.makit.signup.Validator;

import java.util.regex.Pattern;

public class PasswordValidator {

    //형식 정규식
    private static final String PASSWORD_REGEX =
            "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,16}$";

    public static boolean isValidPassword(String password) {
        return password != null && Pattern.matches(PASSWORD_REGEX, password);
    }
}

 

위와 같이 정규식을 통해 비밀번호 양식을 정의하고 밑에는 비밀번호의 유효성을 검증하는 메서드를 정의하여 컨트롤러의 개입없이 유효성을 검증 할 수 있도록 하였다. 

 

 

작성한 코드들은 다음과 같다 

 

 

package com.example.makit.signup.Service;

import com.example.makit.signup.DTO.SignupRequestDTO;
import com.example.makit.signup.Entity.*;
import com.example.makit.signup.Repository.*;
import com.example.makit.signup.Validator.NicknameValidator;
import com.example.makit.signup.Validator.PasswordValidator;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import jakarta.servlet.http.HttpSession;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class SignupService {

    //password part

    // 비밀번호 암호화를 위한 BCryptPasswordEncoder 객체 생성
    private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    // HttpSession 객체 주입 받기 (세션 관리)
    private final HttpSession session;

    // 엔티티별 리포지토리
    private final UserRepository userRepository;
    private final FieldRepository fieldRepository;
    private final GenreRepository genreRepository;
    private final UserFieldRepository userFieldRepository;
    private final UserGenreRepository userGenreRepository;

    // 생성자 주입을 통해 HttpSession 객체를 받을 수 있도록 설정
    public SignupService(HttpSession session, UserRepository userRepository, FieldRepository fieldRepository,
                         GenreRepository genreRepository, UserFieldRepository userFieldRepository,
                         UserGenreRepository userGenreRepository) {
        this.session = session;
        this.userRepository = userRepository;
        this.fieldRepository = fieldRepository;
        this.genreRepository = genreRepository;
        this.userFieldRepository = userFieldRepository;
        this.userGenreRepository = userGenreRepository;
    }

    // 비밀번호 유효성 검사 및 비밀번호 해싱
    @Transactional
    public PasswordValidationResponse validatePasswords(SignupRequestDTO request) {
        String password = request.getPassword();
        String confirmPassword = request.getConfirmPassword();

        PasswordValidationResponse response = new PasswordValidationResponse();

        // 비밀번호 형식 검증. figma에 따른 메세지 반환
        if (!PasswordValidator.isValidPassword(password)) {
            response.setIsPasswordValid(false); // 비밀번호 유효성 실패
            response.setPasswordErrorMessage("비밀번호는 8~16자의 영문 대/소문자, 숫자, 특수문자를 포함해야 합니다.");
        } else {
            response.setIsPasswordValid(true); // 비밀번호 유효성 성공
        }

        // 비밀번호 일치 여부 검증
        if (!password.equals(confirmPassword)) {
            response.setIsPasswordMatch(false); // 비밀번호 불일치
            response.setPasswordMatchErrorMessage("다시 입력해주세요.");
        } else {
            response.setIsPasswordMatch(true); // 비밀번호 일치
        }

        // 비밀번호가 유효하고, 비밀번호 확인이 일치하면 비밀번호 해싱 및 세션에 저장
        if (response.isPasswordValid() && response.isPasswordMatch()) {
            // 비밀번호 해싱
            String hashedPassword = passwordEncoder.encode(password);

            // 세션에 해싱된 비밀번호 저장 (임시 저장)
            session.setAttribute("hashedPassword", hashedPassword);

            // 위에서 isPasswordValid, isPasswordMatch가 모두 true일 때 닉네임 페이지로 넘어갈 수 있다는 의미
            // endpoint로 redirection 및 session의 password값 저장 유무를 통해 페이지 이동 가능
            response.setRedirectToNicknamePage(true);
        }

        return response;
    }


    //nickname part

    public boolean validateAndSaveNickname(String nickname) {
        if (NicknameValidator.isValidNickname(nickname)) {
            // 유효하다면 세션에 저장
            session.setAttribute("nickname", nickname);
            return true;
        }
        return false;
    }


    //phone number part
    // 전화번호 유효성 검사 및 세션 저장
    public boolean validateAndSavePhoneNumber(String phoneNumber) {
        // 유효성 검사: 숫자만 허용 + 길이 제한 (10~11자리)
        if (phoneNumber != null && phoneNumber.matches("\\d{10,11}")) {
            // 유효하다면 세션에 저장
            session.setAttribute("phoneNumber", phoneNumber);
            return true;
        }
        return false;
    }

    // 회원가입 완료 메서드
    @Transactional
    public boolean completeSignup(List<String> selectedFields, List<String> selectedGenres) {
        // 1. 세션에서 사용자 정보 가져오기
        String email = (String) session.getAttribute("email");
        String hashedPassword = (String) session.getAttribute("hashedPassword");
        String nickname = (String) session.getAttribute("nickname");
        String phoneNumber = (String) session.getAttribute("phoneNumber");

        // 2. 세션에 저장된 정보가 모두 존재하는지 확인
        if (email == null || hashedPassword == null || nickname == null || phoneNumber == null) {
            throw new IllegalStateException("세션에 저장된 회원 정보가 부족합니다.");
        }

        // 3. 사용자 엔티티 생성 및 저장
        UserEntity user = new UserEntity();
        user.setEmail(email);
        user.setPassword(hashedPassword);
        user.setNickname(nickname);
        user.setPhoneNumber(phoneNumber);
        userRepository.save(user);

        // 4. 분야 정보 저장 (이름으로 조회)
        for (String fieldName : selectedFields) {
            FieldEntity field = fieldRepository.findByFieldName(fieldName)
                    .orElseThrow(() -> new IllegalArgumentException("잘못된 분야 이름: " + fieldName));
            UserField userField = new UserField();
            userField.setUser(user);
            userField.setField(field);
            userFieldRepository.save(userField);
        }

        // 5. 장르 정보 저장 (이름으로 조회)
        for (String genreName : selectedGenres) {
            GenreEntity genre = genreRepository.findByGenreName(genreName)
                    .orElseThrow(() -> new IllegalArgumentException("잘못된 장르 이름: " + genreName));
            UserGenre userGenre = new UserGenre();
            userGenre.setUser(user);
            userGenre.setGenre(genre);
            userGenreRepository.save(userGenre);
        }

        return true; // 회원가입 완료
    }
}

 

 개발명세서에 해당하는 규칙들을 적용해두었고, 비밀번호는 세션에 저장 시 해싱을 통해 저장하였다. 저장 전, Validator에 정의한 validation 규칙을 거쳐 저장 가능 여부를 체크하도록 하였다. 또한, 가능한 항목들은 모두 세션에 저장함으로써 추후에 회원가입 시 세션에 저장된 정보들을 한번에 불러서 DB에 저장할 수 있도록 하였다.Bycrpt를 사용하는데 Bycrpt를 사용한 이유는 다음과 같다. 1. salt 사용 및 강력한 암호화 2. 스프링부트와 용이한 호환성 -> 개발 편리 

 

위 sevice에 정의된 로직을 토대로 컨트롤러에서 mapping을 통해 client 요청을 처리할 수 있도록 구현하였다. 

 

'백엔드 > Makit' 카테고리의 다른 글

회원가입 시 이메일 전송을 통한 유효성 검사  (0) 2024.11.17