Spring Initializr를 통해 JPA, Security, Web, Lombok, H2를 셋팅해두었다.
초기 셋팅을 마무리하고 각자 branch를 구분해서 맡은 부분을 작성하기 시작했다.
내가 맡은 부분은 회원정보와 관련된 것으로, 회원 가입 / 로그인 / 로그아웃을 구현하기로 했다.
회원가입 -> 로그인 -> 로그아웃 순서대로 진행 하기로 하고 구조를 잡아보았다.
우선은 H2를 사용하기로 해서 application.yml 파일에 관련 내용(콘솔사용, 테이블 생성 등)을 설정해두고 작업을 시작했다.
[회원가입 기능 구현]
1. MemberController: 클라이언트 HTTP 요청을 받는 부분이다. POST 메서드를 받는다.
회원가입이므로 엔드포인트는 /singup으로 설정하였다.
DTO 객체를 매개변수로 받고 Member <-> DTO 변환은 Mapper를 사용했다.
로그인아이디, 패스워드에 관한 유효성 조건이 아직 정해지지 않아 해당 부분은 적용하지 않았다.
현재는 저장된 member 객체를 HTTP 201 과 함께 리턴해주고있으나, Response 형태에 대해서는 프론트엔드 팀원과 협의해서
수정할 예정이다. (member 일부 정보 같이 리턴 혹은 저장된 URI 리턴 등)
@RestController
@RequestMapping("/")
public class MemberController {
private final MemberService memberService;
private final MemberMapper mapper;
public MemberController(MemberService memberService, MemberMapper mapper) {
this.memberService = memberService;
this.mapper = mapper;
}
@PostMapping("/signup")
public ResponseEntity signup(@RequestBody MemberSignupDto memberSignupDto){
// 아이디, 패스워드, 이름(닉네임), 자기소개를 입력받아서
// 패스워드는 암호화하고 데이터베이스에 저장한다. -> Service에서 수행
Member member = mapper.memberSignupDtoToMember(memberSignupDto);
Member savedMember = memberService.createMember(member);
return new ResponseEntity<>(member, HttpStatus.CREATED);
}
}
2. Member: 회원 정보를 나타내는 객체이다. @Entity로 설정하고, memberId를 식별자로 설정했다.
회원 상태를 활동중 / 휴면상태 / 탈퇴 세가지로 구분하여 나타내었다. (추후 회원 탈퇴에 적용 예정)
자기소개(personalInfo)에만 null값을 허용할 예정이며, 그 외 필드에는 @Column()을 통해 조건을 추가할 예정이다.
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
private String loginId;
private String pwd;
private String name;
private String personalInfo;
@Enumerated(EnumType.STRING)
private MemberStatus memberStatus = MemberStatus.ACTIVE;
private enum MemberStatus {
ACTIVE("Active"),
INACTIVE("Inactive"),
CLOSED("Closed");
private String memberStatus;
MemberStatus(String memberStatus) {
this.memberStatus = memberStatus;
}
public String getMemberStatus() {
return memberStatus;
}
}
}
3. MemberSignupDto: 클라이언트가 회원가입을 하기 위해 보내는 정보를 담는다. Member 필드 중 필요한 정보만 담아두었다.
@Getter
public class MemberSignupDto {
private String loginId;
private String pwd;
private String name;
private String personalInfo;
}
4. MemberMapper: MapStruct를 사용해 Mapper를 구현하였다.
implementation 'org.mapstruct:mapstruct:1.4.2.Final // 의존 라이브러리 추가
memberId는 자동으로 부여되므로 해당 부분은 제외하고 매핑하였다.
@Mapper(componentModel = "spring")
public interface MemberMapper {
Member memberSignupDtoToMember(MemberSignupDto memberSignupDto);
@Mapping(target = "memberId", ignore = true) // memberId는 자동 생성되므로 무시한다.
Member updateMemberFromSignupDto(MemberSignupDto memberSignupDto, @MappingTarget Member member);
}
5. MemberRepository: JpaRepository를 상속받아 구현하였다. 로그인용 아이디는 유일한 값이어야하므로 해당값을 검색하는 메서드를 추가하였다.
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByLoginId(String loginId);
}
6. MemberService: 직접적으로 회원가입 비즈니스 로직을 구현하는 부분이다.
@Service를 통해 서비스 클래스임을 명시하고 회원가입인 createMember() 메서드를 작성했다.
또한 로그를 통해 회원가입 시도가 제대로 되었는지, 암호화는 되었는지 확인하고싶어서 로그 관련 내용을 추가했다.
회원 정보를 받아서 -> 동일한 아이디가 있는지 확인하고(중복된 아이디 있을경우 예외 발생) -> 비밀번호를 암호화한 뒤
-> 해당 회원 정보를 저장
@Service
@Slf4j
public class MemberService {
private final MemberRepository memberRepository;
// 어떤 PasswordEncoder를 사용할지는 auth.config 파일에서 @Bean으로 등록해서 사용했다.
private final PasswordEncoder passwordEncoder;
public MemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
this.memberRepository = memberRepository;
this.passwordEncoder = passwordEncoder;
}
public Member createMember(Member member) {
log.info("# 회원가입 시작");
verifyExistsLoginId(member.getLoginId());
String encryptedPassword = passwordEncoder.encode(member.getPwd());
member.setPwd(encryptedPassword);
log.info("# 비밀번호 암호화 완료");
return memberRepository.save(member);
}
// 만약 존재하는 loginId 이면 에러 던지는 메서드(추후 에러 타입 정리 필요)
public void verifyExistsLoginId(String loginId) {
Member findMember = memberRepository.findByLoginId(loginId);
if(findMember != null) throw new RuntimeException();
}
}
7. SecurityConfiguration: 암호화를 하기 위해 필요한 설정 정보를 포함한다. 추후 로그인 / 로그아웃 관련된 내용도 추가될 예정이다.
현재 Spring Security가 적용되어있어 디폴트 로그인화면이 계속해서 나타나는 상태였다.
Postman을 통해 테스트를 하려고 해도 계속해서 인증을 해주어야해서 해당 부분을 비활성화하였다.
(cors / csrf / formLogin 모두 비활성화 해둠)
PasswordEncoder는 DelegatingPasswordEncoder로 BCrypt 알고리즘을 사용한 기본 encoder를 적용했다.
DelegatingPasswordEncoder는 사용하고자하는 암호화 알고리즘을 지정하지 않으면 Spring Security에서 권장하는
최신 암호화 알고리즘을 사용할 수 있도록 해준다.
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(withDefaults()) // cors 일부 허용하기 위한 작업(프론트엔드와 소통시 필요), CorsConfigurationSource Bean생성 필요.(아래에 구현)
.csrf().disable() // csrf 설정 비활성화 (로컬 작업용)
.formLogin().disable() // 폼로그인 비활성화
.headers().frameOptions().sameOrigin(); // H2 웹콘솔 정상적으로 쓸 수 있도록 설정
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); // 모든 출처에 대해 스크립트 기반 HTTP 통신 허용(상황에 따라 변경 가능)
configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE")); // 허용 메서드 지정
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // CorsConfigurationSource 인터페이스 구현 클래스 생성
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
[결과 확인]
아직 수정해야할 부분이 많이 남았지만, 포스트맨을 통해 요청을 보냈을 때 작동하는것을 확인할 수 있었다.
H2 콘솔을 통해 확인했을때에도 해당 내용이 잘 들어와있는것을 확인할 수 있었다.
참고자료
2023.04.11 - [부트캠프 개발일기/Spring MVC] - 부트캠프 41일차 - Spring MVC(API 계층)
2023.05.15 - [부트캠프 개발일기/Spring Security] - 부트캠프 63일차 - Spring Security (2) 웹요청 처리흐름, Filter Chain
'부트캠프 개발일기 > Pre-Project' 카테고리의 다른 글
88일차: Pre-Project Day 8-1 (JWT 검증) (0) | 2023.06.20 |
---|---|
87일차: Pre-Project Day 7(JWT 로그인 기능 구현) (0) | 2023.06.19 |
85일차: Pre-Project Day 5 (이슈 설정, 역할 분담) (0) | 2023.06.15 |
84일차: Pre-Project Day 4 (사용자 요구사항 정의서, ERD) (0) | 2023.06.14 |
83일차: Pre-Project Day 3 (프로젝트 관리) (0) | 2023.06.13 |