Service IoC 등록하기 및 DI
package shop.mtcoding.blog.user; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @RequiredArgsConstructor @Service //IoC 등록하기 public class UserService { private final UserJPARepository userJPARepository; }
서비스는 레파지토리한테 의존한다. 그럼 컨트롤러는 이제 서비스에 의존할 듯
비즈니스 로직을 서비스에게 처리하라고 넘겨줌
UserService 시작!
package shop.mtcoding.blog.user; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service public class UserService { private final UserJPARepository userJPARepository; @Transactional public void 회원가입(UserRequest.JoinDTO requestDTO) { userJPARepository.save(requestDTO.toEntity()); } }
한 번에 여러 개를 호출하는 서비스에서 @Transactional을 붙인다!
원래 레파지토리에 붙이는게 아니다!!
(그때는… 서비스가 없어서 누구라도 트랜젝션 관리를 해야했기에 붙였던 것임)
이해를 돕기 위해 메소드 이름을 한글로 진행
이젠 try-catch는 별로 추천하지 않는다.
save메소드를 내가 만든게 아니라서 못 건다. JPARepository 꺼다... save는 안에서 터지는 오류 유형이 많다. (ex.중복임? 아이디 틀림? 아이디 김?) 이런걸 이제 ... 명확하게 해줘야함. 서비스에서 명확하게 해야할 게 뭘까? 후보 1. 유효성 검사 -> 컨트롤러 책임. 컨트롤러에서 if처리 후보 2. 중복 체크 -> 중복인지 아닌지 DB를 조회해봐야 아는 것.(DB연결 필요) DB연결이 필요하기 때문에 서비스에서 실행
왜 추천하지 않는가?
1. 비즈니스 로직과 분리해야 함! 예외 처리 로직을 try-catch 블록을 통해 서비스나 컨트롤러에 직접 구현하게 되면, 코드가 복잡해지고, 비즈니스 로직과 예외 처리 로직이 혼재되어 코드의 가독성과 유지보수성이 떨어지기 때문. 2. 트랜잭션 관리! 비즈니스 로직 중간에 예외가 발생하면, 스프링이 자동으로 해당 트랜잭션을 롤백한다. 즉, try-catch 블록을 사용하여 예외를 직접 처리하게 되면, 예외 발생 시 명시적으로 롤백 처리를 해줘야 하기 때문에 코드가 길어지고 복잡해진다.
회원가입
[ UserJPARepository 수정 ]
package shop.mtcoding.blog.user; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.Optional; //자동 컴퍼넌트 스캔이 된다 public interface UserJPARepository extends JpaRepository<User, Integer> { //@Query("select u from User u where u.username = :username and u.password = :password") //추상 메소드 생성 Optional<User> findByUsernameAndPassword(@Param("username") String username, @Param("password") String password); Optional<User> findByUsername(@Param("username") String username); }
단 건 조회할 때 null이 발생할 수 있는 것은 Optional을 붙여라
하는 김에 BoardJPARepository도 바꿔줌
package shop.mtcoding.blog.board; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.Optional; public interface BoardJPARepository extends JpaRepository<Board, Integer> { @Query("select b from Board b join fetch b.user u where b.id = :id") Optional<Board> findByIdJoinUser(@Param("id") int id); }
[ UserService ] → 회원가입 예외(오류)처리 하기!!
package shop.mtcoding.blog.user; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import shop.mtcoding.blog._core.errs.exception.Exception400; import java.util.Optional; @RequiredArgsConstructor @Service public class UserService { private final UserJPARepository userJPARepository; @Transactional public void 회원가입(UserRequest.JoinDTO requestDTO) { // 1. 유효성 검사 -> 컨트롤러 책임 x // 2. 유저네임 중복검사 (서비스 체크) - DB연결 필요 Optional<User> userOP = userJPARepository.findByUsername(requestDTO.getUsername()); //isPresent가 있으면 비정상 if (userOP.isPresent()) { throw new Exception400("중복된 유저네임입니다."); } //아닌 경우 정상적으로 저장 userJPARepository.save(requestDTO.toEntity()); } }
중복된 아이디 (ex. ssar) 이 들어오면 무조건 팅겨나간다.
happy가 들어오면 없는 아이디기 때문에 userJPARepository.save(requestDTO.toEntity());로 넘어감
[ UserController ]
@RequiredArgsConstructor @Controller public class UserController { private final UserService userService; private final UserRepository userRepository; private final HttpSession session; @PostMapping("/join") public String join(UserRequest.JoinDTO requestDTO) { userService.회원가입(requestDTO); return "redirect:/"; }
컨트롤러를 이렇게 깔끔하게 유지 가능!
원래 코드
@PostMapping("/join") public String join(UserRequest.JoinDTO requestDTO) { try { userRepository.save(requestDTO.toEntity()); } catch (DataIntegrityViolationException e) { throw new Exception400("동일한 유저네임이 존재합니다."); } // session.setAttribute("sessionUser", sessionUser); return "redirect:/"; }
[ 화면 확인 ]
중복된 유저네임으로는 회원가입 되지 않고, 중복되지 않으면 잘 됨
로그인 (+orElseThrow)
[ UserService ]
@RequiredArgsConstructor @Service public class UserService { private final UserJPARepository userJPARepository; //조회라 트랜젝션 안 붙여도 됨! public User 로그인(UserRequest.LoginDTO requestDTO) { //나중에 해시 비교하는 이런 코드 여기에 들어옴 User sessionUser = userJPARepository.findByUsernameAndPassword(requestDTO.getUsername(), requestDTO.getPassword()) .orElseThrow(() -> new Exception401("인증되지 않았습니다")); return sessionUser; }
리턴값이 옵셔널이니까... 이렇게 orElseThrow로 사용할 수도 있다! 내가 ssar , 1234를넣으면 옵셔널에 값이 들어감. (존재하니까) 만약, ssar. 12345를 넣으면 옵셔널에 null 이 들어감. (존재x) orElseThrow는 내부에 람다식을 넣어서 사용하는데, Optional 객체가 null이 아닌 값을 포함하고 있으면 그 값을 반환하고, Optional 객체가 비어있다면 (즉, 값이 null이라면) 사용자가 제공한 예외를 던진다! -> Optional을 사용하면 메서드의 리턴 값이 null일 가능성이 있는 경우에도 안전하게 처리가능!
isPresent 써도 됨! 그러나 orElseThrow를 적으면 코드가 많이 줄어든다.
orElseThrow 자체가 throw를 던져주기 때문에 throw new Exception400 이렇게 적지 않아도 된다!
[ UserController ]
@PostMapping("/login") public String login(UserRequest.LoginDTO requestDTO) { User sessionUser = userService.로그인(requestDTO); session.setAttribute("sessionUser", sessionUser); return "redirect:/"; }
이렇게 해도 되긴 함
세션을 서비스에서 받아와서 컨트롤러에 넘기는 것…
그러나 이후엔 제이슨 토큰으로 바꿀거라서.. 로그인은 순수하게 두는 게 좋다…
서비스 단위 테스트 할 때, 세션 메모리는 안 뜨기 때문에 안 넘기는게 좋다…
근데 왜 서비스에 넘긴 코드도 하신거지?
세션을 넘기면 테스트가 안되는 구나~ 라는 걸 경험해보라고 ^^!
[ 화면 확인 ]
틀려봄. 아이디, 비밀번호를 알맞게 넣으면 로그인 되는 것도 확인!
회원가입에서는 .orElseThrow를 못 쓰나요? → 네
회원가입은 '이미 존재하는 경우'에 throw가 날아가야하기 때문에 orElseThrow를 못씀. orElseThrow는 Optional 객체가 비어있을 때, 즉 값이 없을 때 예외를 던지는데... 값이 있는데 어떻게 던지나??
회원 조회 (회원 정보 수정 폼?)
[ UserService 버전 1 ]
@RequiredArgsConstructor @Service public class UserService { private final UserJPARepository userJPARepository; public User 회원조회(int id) { return userJPARepository.findById(id) .orElseThrow(() -> new Exception404("회원정보를 찾을 수 없습니다.")); }
바로 return 하는 경우
[ UserService 버전 2 ]
@RequiredArgsConstructor @Service public class UserService { private final UserJPARepository userJPARepository; public User 회원조회(int id) { User user = userJPARepository.findById(id) .orElseThrow(() -> new Exception404("회원정보를 찾을 수 없습니다.")); return user; }
return user로 하는 경우
[ userController ]
@GetMapping("/user/update-form") public String updateForm(HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); User user = userService.회원조회(sessionUser.getId()); //User user = userRepository.findById(id); //이렇게 간단한건 굳이 서비스에서 안땡기고 레파지토리에서 땡겨도 됨. request.setAttribute("user", user); return "user/update-form"; }
회원 정보 수정 (update / 더티 체킹)
[ UserService ]
@RequiredArgsConstructor @Service public class UserService { private final UserJPARepository userJPARepository; @Transactional public User 회원수정(int id, UserRequest.UpdateDTO requestDTO) { User user = userJPARepository.findById(id) .orElseThrow(() -> new Exception404("회원정보를 찾을 수 없습니다.")); user.setPassword(requestDTO.getPassword()); user.setEmail(requestDTO.getEmail()); return user; } //더티체킹
이게 바로 더티 체킹!
userJPARepository.save(user) 이걸 사용해도 됨. (머지ver)
근데 더티체킹 쪽이 좋은 것 같음
[ userController ]
@PostMapping("/user/update") public String update(UserRequest.UpdateDTO requestDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); User newSessionUser = userService.회원수정(sessionUser.getId(), requestDTO); session.setAttribute("sessionUser", newSessionUser); return "redirect:/"; }
현재 로그인한 사용자의 정보를 세션에서 가져오고, 회원수정 메소드를 호출하여 실제로 사용자 정보를 업데이트 한다. 이후, 업데이트된 사용자 정보를 다시 세션에 저장
sessionUser.getId()를 인자로 받는 이유
현재 로그인한 사용자의 고유 식별자(ID)를 기반으로 해당 사용자의 정보를 업데이트하기 위함
TIP!
전부 옮겼으면 이제 지워도 됨!
단순히 조회만 하는 것들은 레파지토리에서 그냥 바로 땡겨서 쓸 수 있음.(개인 호불호)
서비스에서는 이것 외에도 할 게 많다.
LogRepository.save() 해서 로그도 남기고… 등등등 많다.
Share article