TIL - 0520

스프링부트

로그인 구현하기

  • 로그인 구현 전에 알아야할 개념 : HTTP, 쿠키와 세션
    • HTTP : 문서를 주고받기위해 고안된 프로토콜로 1회 통신 후 소켓 연결을 끊어버림(이전 통신 정보, 상태를 알 수 없음)
    • 쿠키와 세션 : HTTP의 단점을 채우기위한 방법
    • 쿠키 : 로컬에 클라이언트 상태 정보를 그대로 기록한 데이터 파일, 만료 시간 정보까지 있음
    • 세션 : 쿠키를 사용하지만 담는 값이 다름, 서버에서 발급해준 ID값을 담음, 웹서버에 접속한 이후로 브라우저를 종료할 때까지 해당 세션ID로 요청을 보내면 같은 요청자로 봄(상태 유지기술)
    • 쿠키와 세션 차이 : 쿠키는 상태 정보를 로컬에서 보관했다가 통신 때마다 건네주지만 세션은 서버프로그램이 발급한 ID값만 통신 때마다 줌(중요 데이터를 누가 보관하고 있냐의 차이)
  • 세션을 사용한 로그인 기능 개발
    • UserController login URI 맵핑 메소드 -> 데이터베이스 저장된 정보와 체크(회원가입 정보 유무, 비밀번호 맞는지 체크) -> 세션 세팅
    • JPA Repository 쿼리 메소드 만들어둬야함 : 이름 컨벤션이 있음 - findBy컬럼명(타입 컬럼명)
/* UserController.java */
@PostMapping("login")
public String login(String userId, String passwd, HttpSession session) {
	if (HttpSessionUtils.isLogin(session) {
		return "redirect:/";			
	}
	
	Optional<User> maybeUser = userRepo.findByUserId(userId);
	if (!maybeUser.isPresent() || maybeUser.filter(userInfo -> userInfo.match(passwd)).isPresent()) {
		return "/users/loginFail";
	}
	session.setAttribute(HttpSessionUtils.USER_SESSION_KEY, maybeUser.get());
	return "redirect:/";
}


/* UserRepository */
public interface UserRepository extends CrudRepository<User, Long> {
	Optional<User> findByUserId(String userId);
}
  • 로직은 해당 역할을 맡은 객체가 처리하도록 함 : User 정보 변경, User에 메세지를 보냄
    • 정보를 빼와서 비교하는 로직을 만드는게 아니라 User 객체에 비교할 정보를 담아 요청 메세지를 보냄 : 결과만 필요함
    • 객체지향으로 짜야하는데, 위임하지않고 한 곳에서 모두 처리를 하려고 한다면 유지보수를 어렵게하고, 코드 가독성을 떨어뜨리는 일
@Entity
public class User {
    .
    .
    .
    
    public boolean isMatch(String inputPasswd) {
    	if (inputPasswd == null) {
    		return false;
    	}    
		return this.passwd.eqauls(inputPasswd);
    }
    
    public boolean isMatch(Long id) {
    	if (id == null) {
    		return false;
    	}
    	return this.id.equals(id);
    }
}

  • 방어코드 만들기 : 동작하지않아야하는 상황을 예측하고 백엔드에서 처리해두는 것이 안전하기때문에 필수적으로 방어코드를 만들어야함
    • 예상되는 예외 상황 : 로그인 하지않은 상태(세션ID 발급X), form 전송 시 action 임의로 고쳐 다른 유저 정보를 변경하려는 행위
    • 예외에 대한 처리를 아직 공부하지않은 상태라 우선 예외 던지고, 처리하지않아 에러가 발생하도록 했음 : 다음 공부 대상
/* UserController.java */
@PutMapping("/{id}")
public String update(@PathVariable("id") Long id, String currentPasswd, User updateInfo, HttpSession session) {
	validateRequest(session, id);
	
	User user = user.findById(id).get();
	user.changeInfo(currentPasswd, updateInfo); // 여기도 set/get 호출해서 정보를 변경하는게 아니라 위임하면 됨
	userRepo.save(user);
	return "redirect:/users/" + id;
}

private void validateRequest(HttpSession session, Long pathId) throws IllegalArgumentException {
	if (!HttpSessionUtils.isLogin(session)) {
		throw new IllegalArgumentException("required Login");
	}

	if (!HttpSessionUtils.getUserFromSession(session, pathId).isPresent()) {
            throw new IllegalArgumentException("request to change another user's information.");
	}
}
  • 중복코드 없애기 : 세션 정보가 필요한 메소드에서 값을 추출하려는 코드가 중복됨 -> 메소드 분리 -> 분리 후 컨트롤러에서 역할 분리할 수 있는지 체크 -> 역할 분리해서 중복코드 줄이기
    • HttpSessionUtils : HttpSession 관련 처리만 맡아서 하는 역할
    • 처음부터 역할분리가 어려우면 메소드 추출로 중복코드 없애기부터 해본 다음, 현재 위치한 객체의 역할과 분리할 수 있는지 생각해보기
/* 일반 자바 클래스 */
public class HttpSessionUtils {
	public static final String USER_SESSION_KEY = "sessionUser";
	
	public static boolean isLogin(HttpSession session) {
		return session.getAttribute(USER_SESSION_KEY) != null;
	}
	
	public static Optional<User> getUserFromSession(HttpSession session) {
        if (!isLogin(session)) {
            return Optional.empty();
        }
        User user = (User) session.getAttribute(USER_SESSION_KEY);
        return Optional.of(user);
    }

    public static Optional<User> getUserFromSession(HttpSession session, Long pathId) {
        User sessionUser = getUserFromSession(session).get();
        if (isMatchRequestUserAndSessionUser(sessionUser, pathId)) {
            return Optional.of(sessionUser);
        }
        return Optional.empty();
    }

    private static boolean isMatchRequestUserAndSessionUser(User sessionUser, Long pathId) {
        return sessionUser.isMatch(pathId);
    }
}

hibernate 쿼리 실행 보이기

  • 내부적으로 쿼리 작업을 실행하는데, 추상화되어있어서 개발자는 보기가 어려움 : application.properties에서 볼 수 있도록 설정하면 됨
    • JPA 콜 될 때마다 쿼리문이 실행되는 것으로 로그에 찍힘
/* application.properties */
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true