본문 바로가기
부트캠프 개발일기/Pre-Project

86일차: Pre-Project Day 6 (회원가입 기능 구현, 암호화)

by shyun00 2023. 6. 16.

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