티스토리 뷰
프로젝트 소개
이번에 새내기를 위한 커뮤니티를 만드는 프로젝트를 진행하게 됐다.
스프링을 배우고 난 후 처음으로 하는 프로젝트라 효율적인 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을 통해 데이터가 잘 넘어가는지 확인해주었다.
'Spring' 카테고리의 다른 글
스프링부트 커뮤니티 만들기 #3 - 로그아웃, 회원탈퇴 (1) | 2024.02.27 |
---|---|
스프링부트 커뮤니티 만들기 #2 - 예외처리 (1) | 2024.02.09 |
스프링 부트 자바 백엔드 개발 입문 트러블 슈팅 모음집 (0) | 2024.01.20 |
스프링 부트 자바 백엔드 개발 입문 (1) | 2024.01.14 |
[SPRING]스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 섹션7 (AOP) (0) | 2024.01.01 |
- Total
- Today
- Yesterday
- 프론트엔드
- elasticsearch
- 자바
- SQLD
- 스프링부트
- 지연로딩
- 백준
- 스프링
- 회원탈퇴
- 로깅
- 북마크
- SQL
- 비영속
- 백준 파이썬
- 준영속
- 파이썬
- JPA
- 인텔리제이
- 커뮤니티
- DP
- 스프링 북마크
- 웹MVC
- 스프링 커뮤니티
- SQL 레벨업
- 다이나믹 프로그래밍
- 로그아웃
- 자바 스프링
- EnumType.ORDINAL
- 영속
- 웹 MVC
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |