[Cinemagram] 회원가입 기본 구현 - (2)

2022. 11. 13. 18:45Project/시네마그램

Security세팅

dependencies 추가

  •  security
  • 로그 확인용 log4j2 → 사용시 클래스 상단에 @Slf4j 붙임
plugins {
	id 'org.springframework.boot' version '2.7.5'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
	id 'java'
}

group = 'com'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
	all {
		exclude group: 'ch.qos.logback', module: 'logback-classic'
		exclude group: 'org.apache.logging.log4j', module: 'log4j-to-slf4j'
	}
}

repositories {
	mavenCentral()
}

dependencies {
	...
	implementation 'org.springframework.boot:spring-boot-starter-security'
	...

	// log4j2
	implementation("org.springframework.boot:spring-boot-starter-log4j2")

	...
}

tasks.named('test') {
	useJUnitPlatform()
}

 

여기까지 작성하고 구동하면 아래와 같은 로그인 페이지가 뜨는 것을 볼 수 있습니다. 

이 화면은 제가 만든 것도 아니고 이 화면이 뜨도록 의도한 것도 아닌데 혹시 오류일까요...? 

아닙니다, 이는 위에서 추가한 security 라이브러리가 제 역할을 하는 것입니다.

오류가 아니라고 말씀드리는 이유는 혹시 놀라실까 봐 드리는 말씀입니다. 왜냐하면 전 너무 놀랐거든요..

implementation 'org.springframework.boot:spring-boot-starter-security'

 

이 라이브러리는 클라이언트가 서버에 들어올라고 하면 인증이 안 된 사용자는 모두 시큐리티가 가로채서 리다이렉션 하는 역할을 합니다. 

물론 고맙지만, 우리는 우리가 만들어 놓은 로그인 페이지로 가게 하고 싶으므로 시큐리티 설정을 추가적으로 해야 합니다. 

 

 

시큐리티 설정 파일

config > SecurityConfig

@EnableWebSecurity  
@Configuration      
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // super.configure(http)

        // 직접 설정
        http.authorizeRequests()
            .antMatchers("/","/user/**", "/image/**", "/subscribe/**","/comment/**").authenticated() 
            .anyRequest().permitAll() 
            .and()
            .formLogin()
            .loginPage("/auth/signin") 
            .defaultSuccessUrl("/"); 

    }
}

(1) @EnableWebSecurity

Securiry를 활성화시키는 어노테이션입니다.

 

(2) @Configuration

Spring 컨테이너에 Bean을 등록하는 어노테이션입니다. (관련 포스팅)

 

(3) 해당 코드를 주석 처리 함으로써 기존 시큐리티 기능이 전부 비활성화되었습니다.

     시큐리티 설정의 경우 http.authorizeRequests()로 직접 커스텀할 예정입니다.

super.configure(http)

 

(4) 해당 URL로 요청이 들어오면 인증이 필요하고, 그 외 모든 요청은 허용합니다.

.antMatchers("/","/user/**", "/image/**", "/subscribe/**","/comment/**").authenticated() 
.anyRequest().permitAll()

 

(5) (4)에서 언급한 인증이 필요한 경우 "/auth/signin"으로 이동하고 로그인 정상적으로 성공 시 "/"으로 이동합니다.

.loginPage("/auth/signin") 
.defaultSuccessUrl("/");

 

 

회원가입 기능 구현

로직 : 가입하기 버튼 클릭 →  회원 정보 입력 →가입 버튼 클릭 시 회원가입  

 

기존 signup.html

<form class="login__input" >
    <input type="text" name="username" placeholder="유저네임" required="required" />
    <input type="password" name="password" placeholder="패스워드" required="required" />
    <input type="email" name="email" placeholder="이메일" required="required" />
    <input type="text" name="name" placeholder="이름" required="required" />
    <button>가입</button>
</form>

 

수정 signup.html

<form class="login__input" action="/auth/signup" method="post">
    <input type="text" name="username" placeholder="유저네임" required="required" />
    <input type="password" name="password" placeholder="패스워드" required="required" />
    <input type="email" name="email" placeholder="이메일" required="required" />
    <input type="text" name="name" placeholder="이름" required="required" />
    <button>가입</button>
</form>

 

action="/auth/signup"으로 가는 거면 같은 페이지로 보내는 거 아니냐?라고 물어볼 수 있겠지만

답은 아니오입니다. 왜냐하면 이건 POST방식으로 보낼 것이기 때문입니다.

POST방식으로 보내는 이유는 직접 입력한 4건의 데이터를 DB에 INSERT 요청을 보내기 위해서입니다.

 

 

그럼 요청하고 응답을 받을 수 있는 컨트롤러를 생성해보겠습니다.

AuthController

@Controller 
public class AuthController {

    @GetMapping("/auth/signin")
    public String signinForm() {
        return "/auth/signin";
    }

    @GetMapping("/auth/signup")
    public String signupForm() {
        return "/auth/signup";
    }

    /*[ 회원가입 진행 ]
    * 회원 정보 입력 후 가입 버튼 클릭-> @PostMapping("/auth/signup") -> 리턴 /auth/signin.html
    * */
    @PostMapping("/auth/signup")
    public String signup() {
        return "/auth/signin";
    }
}

 

여기까지 작성하고 구동을 해보면 403 에러가 뜨는 것을 볼 수 있었습니다. 

 

 

CSRF 토큰

스프링 시큐리티는 기본적으로 CSRF 토큰이 활성화되어있기 때문입니다. 이 토큰은 서버에 들어온 요청이 실제 서버에서 허용한 요청이 맞는지 확인하기 위한 토큰입니다. 회원가입을 예로 들어 설명해보자면 정상적으로 페이지(signup)를 받아서 작성해 요청한 사용자인지 아니면 postman을 통해 접근 한 사용자 인지 확인하는 역할을 수행합니다. 

 

하지만 CSRF 토큰을 사용하면 나중에 자바스크립트에서 추가적으로 코드를 넣어줘야 하는 번거로움이 발생하므로 이번 프로젝트에서는 비활성화시키도록 하겠습니다.

 

 

스프링 부트에서 CSRF 해제 방법

SecurityConfig 추가

@EnableWebSecurity 
@Configuration 
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
       
        // [CSRF토큰 해제] 
        http.csrf().disable();

        http.authorizeRequests()
            .antMatchers("/","/user/**", "/image/**", "/subscribe/**","/comment/**").authenticated() 
            .anyRequest().permitAll() 
            .and()
            .formLogin()
            .loginPage("/auth/signin") 
            .defaultSuccessUrl("/"); 

    }
}

 

CSRF해제 코드를 추가하면 이제 가입 버튼 클릭 시 로그인 화면으로 이동합니다.

 

 

이제 4건의 데이터를 받고 DB에 INSERT 하는 일만 남았습니다.

먼저 데이터를 받는 것부터 진행해보겠습니다. (DTO)

 

 

 

DTO

DTO는 Data Transfer Object의 약자로 통신할 때 필요한 데이터를  담는 곳입니다.

파일명만 보아도 쉽게 이해할 수 있게

  • 요청하는 DTO의 경우 : xxxReqDto
  • 응답하는 DTO의 경우 : xxxResDto

로 이름을 짓도록 하겠습니다.

 

 

여기서 굳이~ DTO를 만들 필요가 있을까? 보아하니 User 관련 정보인 것 같은데 간단하게 User 도메인을 하나 만들고 여기저기서 가져다가 쓸 수 있게 세팅해두면 좋지 않을까?라고 생각할 수 있습니다.(그게 접니다..)

이 방법을 권장하지 않는 이유를 알아보겠습니다.

 

💡 엔티티 vs DTO 사용
지금은 토이 프로젝트이므로 요구사항이 간단하지만 실무는 복잡하기도 하고 계속해서 수정 추가됩니다.
만약 DTO를 사용하지 않고 엔티티를 여기저기서 사용한다면 결과적으로 엔티티는 점점 화면에 종속적으로 변하고 지저분해지게 됩니다. 이는 유지 보수하기 어렵게 만들게 되는데요. 

따라서 엔티티는 핵심 비즈니스 로직만 가지고 있고 화면을 위한 로직은 없어야 하고 화면이나 API 요구사항을 처리하는 용도로 DTO를 생성하는 것을 권장합니다.

[ 정리 - 항상 지켜져야 할 것 ]
API를 개발할 때 엔티티를 바로 파라미터로 받는 등 엔티티 외부 노출하지 말기
API 스펙에 맞는 DTO를 생성해서 파라미터에 넣어주기

 

이제 회원가입을 위해 데이터를 담는 SignupReqDto를 생성해보도록 하겠습니다.

@Data
public class SignupReqDto {
    private String username;
    private String password;
    private String email;
    private String name;
}

 

롬복 어노테이션 @Data
 = @toString + @getter + @setter + @RequiredArgsConstructor + @EqualsAndHashCode

 

이제 AuthController로 돌아가 파라미터로 SignupReqDto를 받아보고 데이터를 받아오는지 Logger를 통해서 확인해보도록 하겠습니다.

@Slf4j
@Controller 
public class AuthController {

    @PostMapping("/auth/signup")
    public String signup(SignupReqDto signupReqDto) {
        log.info("signupReqDto {}",signupReqDto.toString());
        return "/auth/signin";
    }
}

 

콘솔 확인

signupReqDto SignupReqDto(username=apple, password=1234, email=apple@apple.com, name=사과)

 

기입한 데이터가 Controller로 전달됨을 확인할 수 있었습니다.

 

 

 

다음은 User 모델을 만들도록 하겠습니다.

 

User모델(오브젝트)

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

import javax.persistence.*;
import java.time.LocalDateTime;

@AllArgsConstructor 
@NoArgsConstructor  
@Data
@Entity             
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    private int id;

    private String username;
    private String password;
    private String name;
    private String website;
    private String bio;
    private String email;
    private String phone;
    private String gender;

    private String profileImageUrl; // 유저 사진
    private String role;            // 권한


    // 생성 시간
    private LocalDateTime createDate;

    @PrePersist 
    public void createDate() {
        this.createDate = LocalDateTime.now();
    }

}

우선 클래스 위에 붙은 어노테이션부터 설명해보자면

 

(1) 생성자를 자동 생성해주는 롬복 어노테이션

@NoArgsConstructor
파라미터가 없는 기본 생성자를 생성

@RequiredArgsConstructor
final이나 @NonNull인 필드 값만 파라미터로 받는 생성자 생성

@AllArgsConstructor
모든 필드 값을 파라미터로 받는 생성자 생성
이 어노테이션을 사용할 경우 기본 생성자는 만들어 주지 않으므로 @NoArgsConstructor와 함께 사용

 

(2) 객체와 테이블 매핑하는 @Entity

@Entity가 붙은 클래스는 JPA가 관리합니다. 따라서 JPA를 사용해서 테이블과 매핑할 클래스는 필수로 붙여야 합니다.

 

📢 여기서 잠깐! JPA는 무엇일까요?
우리는 JPA를 사용하고 있는데 개념을 간단하게 짚고 넘어갈 필요성이 있습니다.
JPA는 Java Persistence API의 약자로 '자바 진영의 ORM 기술 표준입니다.'

....

자바 진영의 ORM 기술 표준이라... 이렇게 짧은 문장이 이해가 어렵다니 저도 처음에는 당황을 많이 했습니다.

이를 간단하게 풀어서 설명해보겠습니다.
- JPA는 자바로 데이터를 영구적으로 저장(DB) 할 수 있는 API를 제공합니다.
- ORM이란?
  - Object-relational mapping(객체 관계 매핑)으로 객체는 객체대로 설계하고
    관계형 데이터베이스는 관계형 데이터베이스대로 설계한 후 ORM 프레임워크가 중간에서 매핑합니다.
  - DB를 확인하면 테이블이 만들어져 있는 것을 확인할 수 있는데 이는 자바에서 오브젝트(User)를 만들면
    이 오브젝트를 기반으로 테이블이 생성되기 때문입니다.

 

(3) 기본 키 매핑 어노테이션

직접 할당 : @Id

자동 생성 : @GeneratedValue

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@GeneratedValue(strategy = GenerationType.IDENTITY)
기본 키 생성을 데이터베이스에게 위임하는 방식으로 id값을 따로 할당하지 않아도 데이터베이스가 자동으로 AUTO_INCREMENT를 하여 기본 키를 생성해줍니다.

 

(4) @PrePersist을 사용해서 DB에 INSERT 되기 전에 실행되어 현재 시간을 칼럼에 넣어줍니다.

private LocalDateTime createDate;

@PrePersist 
public void createDate() {
    this.createDate = LocalDateTime.now();
}

 

 

 

브라우저에서 회원정보 기입을 하고 가입 버튼을 클릭하고 DB를 확인해보면 테이블이 생성되어 있는 것을 확인할 수 있다.




 

UserReqDto에서 데이터를 받아서 User에 담기 (빌더 패턴)

UserReqDto

@Data
public class SignupReqDto {
    private String username;
    private String password;
    private String email;
    private String name;

    public User toEntity() {
        return User.builder()
                .username(username)
                .password(password)
                .email(email)
                .name(name)
                .build();
    }

}

 

여기서 빌더 패턴을 이용했습니다.

💡 빌더 패턴이란? 
객체를 생성할 때에는 생성자 패턴, 정적 메서드 패턴, 수정자 패턴, 빌더 패턴이 있습니다.
그중에서 빌더 패턴 사용을 권고하고 있습니다.

장점으로는
1. 필요한 데이터만 설정할 수 있습니다.
- 새로운 변수가 추가되어도 기존 코드에 영향을 주지 않습니다.
- 불필요한 코드의 양을 줄입니다,
- 반복적인 변경이 있을 때 유용합니다.

2. 가독성이 높은 코드입니다.
User user = User.builder()
                      .name("Pika")
                      .height(120)
                      .age(70).build();

3. 변경 가능성을 최소화할 수 있습니다.
-  Setter 패턴을 사용할 경우 변경 가능성을 열어두기 때문에 유지보수 시에 잘못된 값이 들어간 지점을
   찾기 어렵습니다.
- 변수를 final로 선언함으로써 불변성을 확보하는 것이 제일 좋습니다.
  (클래스 위 @Builder @RequiredArgsConstructor적용)
- final을 붙일 수 없는 경우라도 Setter를 넣어주지 않아도 됩니다.
  (클래스 위 @Builder @AllArgsConstructor적용)

참고: https://mangkyu.tistory.com/163

 

회원가입용 빌더 패턴을 생성했습니다. (그전에 User모델에 @Builder 임포트)
4개의 데이터를 기반으로 User 객체 만들고 .build()해서 User 리턴합니다. 

 

 

AuthController에서 파라미터로 받아 사용할 수 있습니다.

@PostMapping("/auth/signup")
public String signup(SignupReqDto signupReqDto) {
    User user = signupReqDto.toEntity();
    return "/auth/signin";
}

 

 

 

이제 마지막 단계입니다.

1. AuthController에서 AuthService를 호출하고

2. AuthService에서 userRepository.save()를 통해 DB에 INSERT 해보겠습니다.

 

 

AuthController에서 AuthService를 호출하는 방법으로 DI(의존성 주입)를 활용할 것이고 그중에서 생성자 주입을 사용할 것입니다.

 

생성자 주입

public AuthController(AuthService authService) {
        this.authService = authService;
    }

 

@Controller이 붙어 있으면 스프링이 AuthContoller를 컨테이너를 관리하는 메모리에 '객체를 생성'해서 로드하는데

객체를 생성하는 첫 번째 조건이 '생성자를 실행'하는 것입니다.

스프링은 이미 IoC에 등록한 애들 중에서 AuthService type이 있는지 확인한 후 있으면 넣어줍니다 (=의존성 주입)

따라서 만약에 @Service 주석하면 오류가 나게 됩니다.


하지만 이 방법보다 간편한 방법이 있습니다. 

[ 추천하는 방법 ]
전역 변수에 final을 걸고 Controller 클래스 상단에 @RequiredArgsConstructor를 추가로 걸어주는 방법입니다.

전역 변수에 final을 걸면 객체가 만들어질 때 초기화를 무조건 해주고
@RequiredArgsConstructor은 final 걸려있는 필드의 생성자 만들어주는 역할을 합니다. (final필드를 DI 할 때 사용)


참고 : 
https://mangkyu.tistory.com/125
https://mangkyu.tistory.com/155

 

 

AuthController

@Slf4j
@RequiredArgsConstructor 
@Controller 
public class AuthController {

    private final AuthService authService;


    @GetMapping("/auth/signin")
    public String signinForm() {
        return "/auth/signin";
    }

    @GetMapping("/auth/signup")
    public String signupForm() {
        return "/auth/signup";
    }

    @PostMapping("/auth/signup")
    public String signup(SignupReqDto signupReqDto) {
        User user = signupReqDto.toEntity();
        authService.signup(user);
        return "/auth/signin";
    }
}

user를 authService로 넘겼습니다.

 

 

AuthService

@RequiredArgsConstructor
@Service 				
public class AuthService {

    private final UserRepository userRepository;

    /* 회원가입 */
    public User signup(User user) { 
        User userEntity = userRepository.save(user); 
        return userEntity;
    }
}

(1) @Service

Ioc에 등록될 뿐만 아니라 트랜잭션 관리도 해줍니다.

 

 

(2) DB INSERT 위해 Repository DI 해줌

private final UserRepository userRepository;

 

(3) userRepository.save(*)의 리턴은 넣은 타입(*)으로 합니다.

User userEntity = userRepository.save(user);

 

(4) signup(User user)에서 User user와 User userEntity 차이점

User user : 외부 통신을 통해 받은 데이터(user)를 User 오브젝트에 담음
User userEntity : DB에 있는 데이터를 User 오브젝트에 담음

 

 

 

브라우저에서 확인 후 DB 확인

 

 

우리가 의도한 대로 DB에 잘 저장이 되었고 여기까지 회원가입 기본으로 push 했습니다. 

 

 

 

 

 

여기서  추가적으로 하나씩 쌓아 올릴 예정입니다. 

우선 회원가입은 잘 되지만 아직은 password 암호화가 안된 상태로 DB에 INSERT 된 상태입니다.

다음 포스팅에서는 password암호화부터 진행해보겠습니다.