본문 바로가기

Server Programming/Spring Boot Full-Stack Programming

[스프링 풀스택 클론 코딩 - 회원가입] (1-20) 가입 일자 데이터의 변경이 DB에 반영되지 않는 버그

반응형

가입 일자 데이터의 변경이 DB에 반영되지 않는 버그

: 영속성 컨텍스트

-> 서비스 계층으로의 위임


AccountController의 checkEmailToken 메서드의 변경

:AccountService는 @Transactional을 클래스 전체로 옮기고,

조회만 하는 메서드의 경우 @Transactional (readOnly = true) 처리

또한, completeSignUp 메서드의 생성자를 새로 정의해서,

 

-> 컨트롤러에서 하던 내용들을 기본생성자를 통해 처리하고, 로그인 처리까지 서비스에서 맡는다.

 

변경 전

	//이메일 인증 확인 -> 이메일, 토큰 값 둘다 일치해야함 -> 화면에 전달할 모델도 파라미터로 전송
	@GetMapping("/check-email-token")
	public String checkEmailToken(String token, String email, Model model) {
		Account account = accountRepository.findByEmail(email);
		String view = "account/checked-email";
		//같은 링크를 리턴하지만, 담은 정보는 각각 다르다. 
		//해당 뷰에서 error를 담아서 왔을 경우와 setEmailVerified가 true를 담아서 왔을 때를 구분해서 처리
		
		//리파지토리를 도메인 계층으로 생각해, 레이어계층에서의 리파지토리 참조를 자유롭게 한다.
		//반대로, 레이어 계층인, 서비스나 컨트롤러를 도메인 계층에서 참조하는 것은 지양한다.
		//1단계에서 이메일의 존재여부를 판단
		//2단계에서는 해당하는 이메일의 토큰값 동일 여부를 확인한다.
		if(account==null) {
			//해당 이메일이 존재하지 않을 때 오류 발생
			//정보제공을 줄이기 위해 -> 서버단에서만 wrong.email이라는 것을 명시하고, 리턴은 하지않는다.
			model.addAttribute("error", "wrong.email");
			return view;
		}
		
		//토큰이 같지 않다면-> 하나의 메서드로
		if(!account.isValidToken(token)) {
			model.addAttribute("error", "wrong.token");
			return view;
		}
		
		//트랜잭션 적용을 위해 completeSignUp,login 메서드 accountService로 이동
		accountService.completeSignUp(account);
		
		model.addAttribute("numberOfUser", accountRepository.count());
		model.addAttribute("nickname", account.getNickname());
		return view;
	}

 

 

 

변경 후

//이메일 인증 확인 -> 이메일, 토큰 값 둘다 일치해야함 -> 화면에 전달할 모델도 파라미터로 전송
	@GetMapping("/check-email-token")
	public String checkEmailToken(String token, String email, Model model) {
		Account account = accountRepository.findByEmail(email);
		String view = "account/checked-email";
		//같은 링크를 리턴하지만, 담은 정보는 각각 다르다. 
		//해당 뷰에서 error를 담아서 왔을 경우와 setEmailVerified가 true를 담아서 왔을 때를 구분해서 처리
		
		//리파지토리를 도메인 계층으로 생각해, 레이어계층에서의 리파지토리 참조를 자유롭게 한다.
		//반대로, 레이어 계층인, 서비스나 컨트롤러를 도메인 계층에서 참조하는 것은 지양한다.
		//1단계에서 이메일의 존재여부를 판단
		//2단계에서는 해당하는 이메일의 토큰값 동일 여부를 확인한다.
		if(account==null) {
			//해당 이메일이 존재하지 않을 때 오류 발생
			//정보제공을 줄이기 위해 -> 서버단에서만 wrong.email이라는 것을 명시하고, 리턴은 하지않는다.
			model.addAttribute("error", "wrong.email");
			return view;
		}
		
		//토큰이 같지 않다면-> 하나의 메서드로
		if(!account.isValidToken(token)) {
			model.addAttribute("error", "wrong.token");
			return view;
		}
		
		account.completeSignUp();
        	accountService.login(account);
		
		model.addAttribute("numberOfUser", accountRepository.count());
		model.addAttribute("nickname", account.getNickname());
		return view;
	}

 

 

 

 

AccountService

package com.demo.account;

import java.util.List;

import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.demo.domain.Account;

import lombok.RequiredArgsConstructor;

@Service
// Open EntityManager (또는 Session) In View 필터
@Transactional
@RequiredArgsConstructor
public class AccountService implements UserDetailsService {

	private final AccountRepository accountRepository;
	// 이메일 발송 관련 빈 의존성 주입
	private final JavaMailSender javaMailSender;
	// 패스워드 인코딩
	private final PasswordEncoder passwordEncoder;
	// 정석적인 로그인 확인
	// private final AuthenticationManager authenticationManager;

	// 메서드별 적용 보다 클래스 전체로 트랜잭션
	public Account processNewAccount(SignUpForm signUpForm) {
		// 1. 회원정보 저장
		// 2. 이메일 토큰 만들기
		// 3. 이메일 전송
		Account newAccount = saveNewAccount(signUpForm);

		// 회원가입 이메일 발송 -> starter-mail 이용 -> mailsender // 일단 가짜 객체에 저장해 콘솔에 출력
		// 계정인증을 위한 토큰값 생성 -> 유효 아이디를 이용해 랜덤 생성
		newAccount.generateEmailCheckToken();
		// emailCehckToken=null
		// 저장 후 토큰 생성 -> 함수안에서만 트랜잭션이 존재하므로, DB에 싱크오류 ->@Transactional 필요
		// 상태 종료시 DB에 싱크를 해주기 위함

		sendSignUpConfirmEmail(newAccount);

		// TODO 회원 가입 처리 valid에 안걸리면 회원가입
		return newAccount;
		// 로그인 처리 위해 -> 회원정보를 리턴하도록 변경 후 컨트롤러에서 받는다.
	}

	private Account saveNewAccount(SignUpForm signUpForm) {
		// form을 이용해 account에 저장
		Account account = Account.builder().email(signUpForm.getEmail()).nickname(signUpForm.getNickname())
				// .password(signUpForm.getPassword()) // TODO encoding (해시 암호화) 필요함
				.password(passwordEncoder.encode(signUpForm.getPassword())) // 패스워드 인코딩 수행
				// .emailVerified(false) //검증은 아직 이루어지지 않았으므로 null이 들어가 있어서 불필요
				.studyCreatedByWeb(true).studyEnrollmentResultByWeb(true).studyUpdatedByWeb(true)
				// 웹으로 알림을 알리는 옵션은 켜둔 상태로 저장 -> 나머지 기본값들은 모두 false
				.build();

		// 해당 account 생성 -> repository에 저장
		// Account newAccount = accountRepository.save(account);

		// return newAccount;

		return accountRepository.save(account);
	}

	// 재전송 기능을 사용하기위해 private -> public으로 변경
	public void sendSignUpConfirmEmail(Account newAccount) {
		// 관련 객체 정의 후 -> 빈 정의 후 발송
		SimpleMailMessage mailMessage = new SimpleMailMessage();

		// 받는 주소
		mailMessage.setTo(newAccount.getEmail());
		// 제목 작성
		mailMessage.setSubject("Demo, 회원 가입 인증");
		// 본문 작성 -> 가입인증 링크 만들어 제공 -> 토큰값을 생성해 가입인증 진행
		mailMessage.setText(
				"/check-email-token?token=" + newAccount.getEmailCheckToken() + "&email=" + newAccount.getEmail());
		// 메일 발송 ->
		javaMailSender.send(mailMessage);
		// 만약에 발송을 하지않는다면, test에서 오류를 내뿜게된다.
	}

	public void completeSignUp(Account account) {
		account.completeSignUp();
		login(account);
	}

	public void login(Account account) {
		// TODO Auto-generated method stub

		// 스프링 시큐리티에서 컨텍스트 홀더라는 기능을 이용해 간편하게 로그인
		// 토큰을 이용해 로그인 -> 토큰은, 닉네임, 패스워드, 권한으로 이루어져 있다.
		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(new UserAccount(account),
				// principal 객체로 변환
				account.getPassword(), List.of(new SimpleGrantedAuthority("ROLE USER")));
		// 권한 목록을 받아서 ROLE USER로 설정

//		SecurityContext context =SecurityContextHolder.getContext();
//		context.setAuthentication(token);
		SecurityContextHolder.getContext().setAuthentication(token);
		// 해당 토큰을 인증에 사용 ->정석적으로는 form인증할때 username, password를 이용해 AuthenticationManger를
		// 통해 인증

//		//정석적인 코드 -> 실제 비밀번호를 접근해야 하므로 사용하기 어렵다.
//		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
//		Authentication authentication = authenticationManager.authenticate(token);
//		SecurityContext context = SecurityContextHolder.getContext();
//		context.setAuthentication(authentication);
	}

	
	//기본적으로 write를 안쓰고 읽기만 하게해서 메모리 사용량을 최적화
	@Transactional(readOnly = true)	
	@Override
	public UserDetails loadUserByUsername(String emailOrNickname) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		// 이메일 또는 닉네임을 이용한 로그인 처리를 위해 필요한 인터페이스 작성
		Account account = accountRepository.findByEmail(emailOrNickname);
		if (account == null) {
			account = accountRepository.findByNickname(emailOrNickname);
		}
		if (account == null) {
			throw new UsernameNotFoundException(emailOrNickname);
			// 닉네임이나 이메일이 잘못되었다.
		}
		// principal에 해당하는 객체 리턴
		return new UserAccount(account);
		// 자동으로 빈을 가져다 사용한다.
	}
}
반응형