티스토리 뷰

프로젝트 소개

이번에 새내기를 위한 커뮤니티를 만드는 프로젝트를 진행하게 됐다.

 

스프링을 배우고 난 후 처음으로 하는 프로젝트라 효율적인 DB 및 JPA 사용 등 성능에 초점을 맞추긴 어렵겠지만, 그래도 최대한 열심히 이것저것 사용하며 배워보려고 한다!


DB 설계

일단 db를 설계해두면 나중에 구현하기 더 쉬울 것 같아서 ERDCloud를 사용하여 설계해주었다.


Entity 설계

일단 회원가입, 로그인 등의 멤버 로직을 만들기 위해 필요한 엔티티는 Member와 Major이다.

 

package teamFive.freshmanCommunity.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter @Setter
@AllArgsConstructor
@NoArgsConstructor
public class Major {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "majorId")
    private Long id;

    @Column
    private String majorName;
}
package teamFive.freshmanCommunity.entity;

import teamFive.freshmanCommunity.dto.SignupDto;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter @Setter
@AllArgsConstructor //모든 필드를 매개변수로 갖는 생성자 자동 생성
@NoArgsConstructor //매개변수가 아예 없는 기본 생성자 자동 생성
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "memberId")
    private Long id;

    @Column
    private String memberName;

    @Column
    private String studentId;

    @Column
    private String email;

//    @JsonIgnore //데이터의 이동에서 민감정보인 password를 숨기기 위해
    @Column
    private String password;

    //학과 정보
    @ManyToOne
    @JoinColumn(name = "majorId")
    private Major major;

 

학과와 멤버가 1:N 이므로 ManyToOne 관계로 설정해주었다. 


Controller

MemberApiController

package teamFive.freshmanCommunity.api;

import teamFive.freshmanCommunity.dto.LoginDto;
import teamFive.freshmanCommunity.dto.LoginResponseDto;
import teamFive.freshmanCommunity.dto.SignupDto;
import teamFive.freshmanCommunity.entity.Member;
import teamFive.freshmanCommunity.service.MemberService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import teamFive.freshmanCommunity.dto.LoginDto;

@RestController
@RequiredArgsConstructor
public class MemberApiController {

    private final MemberService memberService;

    //회원가입
    @PostMapping("/user/signup")
    public ResponseEntity<String> signup(@RequestBody SignupDto signupDto) {
        memberService.signup(signupDto);
        return ResponseEntity.status(HttpStatus.OK).body("회원가입 성공");
    }

    //로그인
    @PostMapping("/user/login")
    public ResponseEntity<?> login(@RequestBody LoginDto loginDto, HttpServletRequest request){
        Member member = memberService.login(loginDto);

        if (member != null) {
            // 로그인 성공
            HttpSession session = request.getSession();
            session.setAttribute("member", member);
            LoginResponseDto response = new LoginResponseDto(member.getId(), member.getMemberName(), member.getMajor());
            return ResponseEntity.status(HttpStatus.OK).body(response);
        } else{
            // 로그인 실패
//            HttpSession session = request.getSession();
//            session.removeAttribute("member");
//            session.invalidate();
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("로그인 실패");
        }
    }

 

Httpsession을 이용하여 로그인한 멤버(유저)의 상태를 유지하도록 했다. 

HttpSession은 웹 애플리케이션에서 멤버의 세션을 관리하는 데 사용된다. 멤버가 로그인에 성공하면, 멤버의 정보를 포함하는 Member 객체가 세션에 "member"라는 이름으로 저장된다. 이렇게 하면 서버가 멤버의 상태 정보를 유지할 수 있고, 세션은 멤버가 로그아웃하거나 세션이 타임아웃되어 종료될 때까지 정보를 유지하게 된다.

SignupDto

프론트에서 회원가입 정보를 받기위한 SignupDto도 만들어주었다.

package teamFive.freshmanCommunity.dto;

import teamFive.freshmanCommunity.entity.Member;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.StringUtils;

@Data
//@NoArgsConstructor
@AllArgsConstructor
@NoArgsConstructor
public class SignupDto {
    private String memberName;
    private String studentId;
    private String email;
    private String password;
    private String majorName;

    public static SignupDto createMemberDto(Member member){
        return new SignupDto(
                member.getMemberName(),
                member.getStudentId(),
                member.getEmail(),
                member.getPassword(),
                member.getMajor().getMajorName()
        );
    }

    public void encodingPassword(PasswordEncoder passwordEncoder) {
        if (StringUtils.isEmpty(password)) {
            return;
        }
        password = passwordEncoder.encode(password);
    }

}

 

회원가입할 때 비밀번호는 encodingPassword를 사용하여 password가 그대로 노출되지 않게 해주었다. 

SecurityConfig

package teamFive.freshmanCommunity;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig{

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

스프링 시큐리티를 사용하여 비밀번호를 암호화하였다. SecurityConfig 클래스에 @Configuration 애노테이션을 사용하여 스프링 설정을 정의하고, BCryptPasswordEncoder PasswordEncoder 인터페이스의 구현체로 사용하여 비밀번호를 안전하게 해싱해주었다.

 

LoginDto, LoginResponseDto

 

다음은 프론트에서 로그인 정보를 받기 위한 LoginDto와, 프론트에게 Login후에 보내줄  LoginResponseDto도 만들어주었다.

package teamFive.freshmanCommunity.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
//@NoArgsConstructor
@AllArgsConstructor
@NoArgsConstructor
public class LoginDto {
    private String email;
    private String password;
}

 

package teamFive.freshmanCommunity.dto;

import teamFive.freshmanCommunity.entity.Major;
import teamFive.freshmanCommunity.entity.Member;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
//@NoArgsConstructor
@AllArgsConstructor
@NoArgsConstructor
public class LoginResponseDto {
    private Long id;
    private String memberName;
    private Major major;

    public static LoginResponseDto loginResponseDto(Member member){
        return new LoginResponseDto(
                member.getId(),
                member.getMemberName(),
                member.getMajor().getMajorName()
        );
    }

}

 


Service

MemberService

package teamFive.freshmanCommunity.service;

import teamFive.freshmanCommunity.dto.LoginDto;
import teamFive.freshmanCommunity.dto.SignupDto;
import teamFive.freshmanCommunity.entity.Major;
import teamFive.freshmanCommunity.entity.Member;
import teamFive.freshmanCommunity.exception.DuplicateMemberException;
import teamFive.freshmanCommunity.exception.IncorrectPasswordException;
import teamFive.freshmanCommunity.exception.MemberNotFoundException;
import teamFive.freshmanCommunity.repository.MajorRepository;
import teamFive.freshmanCommunity.repository.MemberRepository;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    private final MajorRepository majorRepository;
    private final PasswordEncoder passwordEncoder;

    @Transactional
    //회원가입
    public void signup (SignupDto signupDto) {
        validateDuplicateMember(signupDto);
        signupDto.encodingPassword(passwordEncoder);
        Major major = majorRepository.findByMajorName(signupDto.getMajorName());
        if (major == null) {
            throw new IllegalStateException("전공이 존재하지 않습니다.");
        }
        //멤버 엔티티 생성
        Member member = Member.createMember(signupDto, major);
        //멤버 엔티티를 DB에 저장
        memberRepository.save(member);
//        Member signed = memberRepository.save(member);
//        //DTO로 변환 --> 필요없어서 주석처리
//        SignupDto.createMemberDto(signed);
    }

    //중복 검사
//    private void validateDuplicateMember(RegisterDto registerDto){ //실무에서는 한 번 더 최후의 방어(멀티스레드 상황 고려) -> 멤버의 네임을 유니크 제약조건으로 잡는 것을 권장
//        //EXCEPTION
//        List<Member> findMembers = memberRepository.findByEmail(registerDto.getEmail());
//        if(!findMembers.isEmpty()) {
//            throw new IllegalStateException("이미 존재하는 회원입니다.");
//        }
//    }

    private void validateDuplicateMember(SignupDto signupDto) {
        Long count = memberRepository.countByEmail(signupDto.getEmail());
        if (count > 0) {
            // 중복된 이메일이나 학번이 존재하는 경우 예외 발생
            throw new DuplicateMemberException("이미 존재하는 회원입니다.");
        }
    }

    //로그인
    public Member login (LoginDto loginDto) {
        Member member = memberRepository.findByEmail(loginDto.getEmail());
        if (member == null)
            throw new MemberNotFoundException("존재하지 않는 회원입니다.");
        if (!passwordEncoder.matches(loginDto.getPassword(), member.getPassword()))
            throw new IncorrectPasswordException("비밀번호가 맞지 않습니다.");

        member.clearPassword();
        return member;
    }

 

제일 중요한 로직을 담당하는 회원가입, 로그인 서비스를 작성해주었다. 회원가입에서는 validateDuplicateMember메서드를 통해 중복된 이메일인 경우에는 "이미 존재하는 회원입니다."라는 에러 메시지를 반환해주도록 했다.

 

로그인에서는 가입된 이메일이 아닌 경우엔 "존재하지 않는 회원입니다"를, 비밀번호가 틀린 경우에는 "비밀번호가 맞지 않습니다"라는 에러 메시지를 반환해주도록 했다. 

또한 로그인이 완료된 후에는 member.clearPassword()를 통해 password 필드의 값을 지워 비밀번호 노출 위험을 막아주었다. 

 


postman을 통한 데이터 확인

postman을 통해 데이터가 잘 넘어가는지 확인해주었다.

회원가입

 

중복 확인 로직
로그인

 

잘못된 이메일로 로그인했을 경우
비밀번호가 틀렸을 경우

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함